File: TemporaryStorage\TemporaryStorageService.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Host;
 
/// <summary>
/// Temporarily stores text and streams in memory mapped files.
/// </summary>
#if NET
[SupportedOSPlatform("windows")]
#endif
internal sealed partial class TemporaryStorageService : ITemporaryStorageServiceInternal
{
    /// <summary>
    /// The maximum size in bytes of a single storage unit in a memory mapped file which is shared with other storage
    /// units.
    /// </summary>
    /// <remarks>
    /// <para>The value of 256k reduced the number of files dumped to separate memory mapped files by 60% compared to
    /// the next lower power-of-2 size for Roslyn.sln itself.</para>
    /// </remarks>
    /// <seealso cref="_fileReference"/>
    private const long SingleFileThreshold = 256 * 1024;
 
    /// <summary>
    /// The size in bytes of a memory mapped file created to store multiple temporary objects.
    /// </summary>
    /// <remarks>
    /// <para>This value (8mb) creates roughly 35 memory mapped files (around 300MB) to store the contents of all of
    /// Roslyn.sln a snapshot. This keeps the data safe, so that we can drop it from memory when not needed, but
    /// reconstitute the contents we originally had in the snapshot in case the original files change on disk.</para>
    /// </remarks>
    /// <seealso cref="_fileReference"/>
    private const long MultiFileBlockSize = SingleFileThreshold * 32;
 
    private readonly IWorkspaceThreadingService? _workspaceThreadingService;
    private readonly ITextFactoryService _textFactory;
 
    /// <summary>
    /// The synchronization object for accessing the memory mapped file related fields (indicated in the remarks
    /// of each field).
    /// </summary>
    /// <remarks>
    /// <para>PERF DEV NOTE: A concurrent (but complex) implementation of this type with identical semantics is
    /// available in source control history. The use of exclusive locks was not causing any measurable
    /// performance overhead even on 28-thread machines at the time this was written.</para>
    /// </remarks>
    private readonly object _gate = new();
 
    /// <summary>
    /// The most recent memory mapped file for creating multiple storage units. It will be used via bump-pointer
    /// allocation until space is no longer available in it.  Access should be synchronized on <see cref="_gate"/>
    /// </summary>
    private MemoryMappedFile? _fileReference;
 
    /// <summary>The name of the current memory mapped file for multiple storage units. Access should be synchronized on
    /// <see cref="_gate"/></summary>
    /// <seealso cref="_fileReference"/>
    private string? _name;
 
    /// <summary>The total size of the current memory mapped file for multiple storage units. Access should be
    /// synchronized on <see cref="_gate"/></summary>
    /// <seealso cref="_fileReference"/>
    private long _fileSize;
 
    /// <summary>
    /// The offset into the current memory mapped file where the next storage unit can be held. Access should be
    /// synchronized on <see cref="_gate"/>.
    /// </summary>
    /// <seealso cref="_fileReference"/>
    private long _offset;
 
    [Obsolete(MefConstruction.FactoryMethodMessage, error: true)]
    private TemporaryStorageService(IWorkspaceThreadingService? workspaceThreadingService, ITextFactoryService textFactory)
    {
        _workspaceThreadingService = workspaceThreadingService;
        _textFactory = textFactory;
    }
 
    ITemporaryStorageTextHandle ITemporaryStorageServiceInternal.WriteToTemporaryStorage(SourceText text, CancellationToken cancellationToken)
        => WriteToTemporaryStorage(text, cancellationToken);
 
    async Task<ITemporaryStorageTextHandle> ITemporaryStorageServiceInternal.WriteToTemporaryStorageAsync(SourceText text, CancellationToken cancellationToken)
        => await WriteToTemporaryStorageAsync(text, cancellationToken).ConfigureAwait(false);
 
    public TemporaryStorageTextHandle WriteToTemporaryStorage(SourceText text, CancellationToken cancellationToken)
    {
        var memoryMappedInfo = WriteToMemoryMappedFile();
        var identifier = new TemporaryStorageIdentifier(memoryMappedInfo.Name, memoryMappedInfo.Offset, memoryMappedInfo.Size);
        return new(this, memoryMappedInfo.MemoryMappedFile, identifier, text.ChecksumAlgorithm, text.Encoding, text.GetContentHash());
 
        MemoryMappedInfo WriteToMemoryMappedFile()
        {
            using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_WriteText, cancellationToken))
            {
                // the method we use to get text out of SourceText uses Unicode (2bytes per char). 
                var size = Encoding.Unicode.GetMaxByteCount(text.Length);
                var memoryMappedInfo = this.CreateTemporaryStorage(size);
 
                // Write the source text out as Unicode. We expect that to be cheap.
                using var stream = memoryMappedInfo.CreateWritableStream();
                {
                    using var writer = new StreamWriter(stream, Encoding.Unicode);
                    text.Write(writer, cancellationToken);
                }
 
                return memoryMappedInfo;
            }
        }
    }
 
    public async Task<TemporaryStorageTextHandle> WriteToTemporaryStorageAsync(SourceText text, CancellationToken cancellationToken)
    {
        if (this._workspaceThreadingService is { IsOnMainThread: true })
        {
            await Task.Yield().ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
        }
 
        return WriteToTemporaryStorage(text, cancellationToken);
    }
 
    ITemporaryStorageStreamHandle ITemporaryStorageServiceInternal.WriteToTemporaryStorage(Stream stream, CancellationToken cancellationToken)
        => WriteToTemporaryStorage(stream);
 
    public TemporaryStorageStreamHandle WriteToTemporaryStorage(Stream stream)
    {
        stream.Position = 0;
        var memoryMappedInfo = WriteToMemoryMappedFile();
        var identifier = new TemporaryStorageIdentifier(memoryMappedInfo.Name, memoryMappedInfo.Offset, memoryMappedInfo.Size);
        return new(memoryMappedInfo.MemoryMappedFile, identifier);
 
        MemoryMappedInfo WriteToMemoryMappedFile()
        {
            using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_WriteStream, CancellationToken.None))
            {
                var size = stream.Length;
                var memoryMappedInfo = this.CreateTemporaryStorage(size);
                using var viewStream = memoryMappedInfo.CreateWritableStream();
                {
                    using var pooledObject = SharedPools.ByteArray.GetPooledObject();
                    var buffer = pooledObject.Object;
                    while (true)
                    {
                        var count = stream.Read(buffer, 0, buffer.Length);
                        if (count == 0)
                            break;
 
                        viewStream.Write(buffer, 0, count);
                    }
                }
 
                return memoryMappedInfo;
            }
        }
    }
 
    internal static TemporaryStorageStreamHandle GetStreamHandle(TemporaryStorageIdentifier storageIdentifier)
    {
        Contract.ThrowIfNull(storageIdentifier.Name, $"{nameof(GetStreamHandle)} should only be called for VS on Windows (where named memory mapped files as supported)");
        var memoryMappedFile = MemoryMappedFile.OpenExisting(storageIdentifier.Name);
        return new(memoryMappedFile, storageIdentifier);
    }
 
    internal TemporaryStorageTextHandle GetTextHandle(
        TemporaryStorageIdentifier storageIdentifier,
        SourceHashAlgorithm checksumAlgorithm,
        Encoding? encoding,
        ImmutableArray<byte> contentHash)
    {
        Contract.ThrowIfNull(storageIdentifier.Name, $"{nameof(GetTextHandle)} should only be called for VS on Windows (where named memory mapped files as supported)");
        var memoryMappedFile = MemoryMappedFile.OpenExisting(storageIdentifier.Name);
        return new(this, memoryMappedFile, storageIdentifier, checksumAlgorithm, encoding, contentHash);
    }
 
    /// <summary>
    /// Allocate shared storage of a specified size.
    /// </summary>
    /// <remarks>
    /// <para>"Small" requests are fulfilled from oversized memory mapped files which support several individual
    /// storage units. Larger requests are allocated in their own memory mapped files.</para>
    /// </remarks>
    /// <param name="size">The size of the shared storage block to allocate.</param>
    /// <returns>A <see cref="MemoryMappedInfo"/> describing the allocated block.</returns>
    private MemoryMappedInfo CreateTemporaryStorage(long size)
    {
        // Larger blocks are allocated separately
        if (size >= SingleFileThreshold)
            return MemoryMappedInfo.CreateNew(CreateUniqueName(size), size: size);
 
        lock (_gate)
        {
            // Obtain a reference to the memory mapped file, creating one if necessary. If a reference counted
            // handle to a memory mapped file is obtained in this section, it must either be disposed before
            // returning or returned to the caller who will own it through the MemoryMappedInfo.
            var reference = _fileReference;
            if (reference == null || _offset + size > _fileSize)
            {
                var mapName = CreateUniqueName(MultiFileBlockSize);
 
                reference = MemoryMappedFile.CreateNew(mapName, MultiFileBlockSize);
                _fileReference = reference;
                _name = mapName;
                _fileSize = MultiFileBlockSize;
                _offset = size;
                return new MemoryMappedInfo(reference, _name, offset: 0, size: size);
            }
            else
            {
                // Reserve additional space in the existing storage location
                _offset += size;
                return new MemoryMappedInfo(reference, _name, _offset - size, size);
            }
        }
    }
 
    public static string? CreateUniqueName(long size)
    {
        // MemoryMapped files which are used by the TemporaryStorageService are present in .NET Framework (including
        // Mono) and .NET Core Windows. For non-Windows .NET Core scenarios, we return null to enable create the memory
        // mapped file (just not in a way that can be shared across processes).
        return PlatformInformation.IsWindows || PlatformInformation.IsRunningOnMono
            ? $"Roslyn Shared File: Size={size} Id={Guid.NewGuid():N}"
            : null;
    }
 
    public sealed class TemporaryStorageTextHandle(
        TemporaryStorageService storageService,
        MemoryMappedFile memoryMappedFile,
        TemporaryStorageIdentifier identifier,
        SourceHashAlgorithm checksumAlgorithm,
        Encoding? encoding,
        ImmutableArray<byte> contentHash)
        : ITemporaryStorageTextHandle
    {
        public TemporaryStorageIdentifier Identifier => identifier;
        public SourceHashAlgorithm ChecksumAlgorithm => checksumAlgorithm;
        public Encoding? Encoding => encoding;
        public ImmutableArray<byte> ContentHash => contentHash;
 
        public async Task<SourceText> ReadFromTemporaryStorageAsync(CancellationToken cancellationToken)
        {
            // There is a reason for implementing it like this: proper async implementation
            // that reads the underlying memory mapped file stream in an asynchronous fashion
            // doesn't actually work. Windows doesn't offer
            // any non-blocking way to read from a memory mapped file; the underlying memcpy
            // may block as the memory pages back in and that's something you have to live
            // with. Therefore, any implementation that attempts to use async will still
            // always be blocking at least one threadpool thread in the memcpy in the case
            // of a page fault. Therefore, if we're going to be blocking a thread, we should
            // just block one thread and do the whole thing at once vs. a fake "async"
            // implementation which will continue to requeue work back to the thread pool.
            if (storageService._workspaceThreadingService is { IsOnMainThread: true })
            {
                await Task.Yield().ConfigureAwait(false);
                cancellationToken.ThrowIfCancellationRequested();
            }
 
            return ReadFromTemporaryStorage(cancellationToken);
        }
 
        public SourceText ReadFromTemporaryStorage(CancellationToken cancellationToken)
        {
            using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_ReadText, cancellationToken))
            {
                var info = new MemoryMappedInfo(memoryMappedFile, Identifier.Name, Identifier.Offset, Identifier.Size);
                using var stream = info.CreateReadableStream();
                using var reader = CreateTextReaderFromTemporaryStorage(stream);
 
                // we pass in encoding we got from original source text even if it is null.
                return storageService._textFactory.CreateText(reader, encoding, checksumAlgorithm, cancellationToken);
            }
        }
 
        private static unsafe DirectMemoryAccessStreamReader CreateTextReaderFromTemporaryStorage(UnmanagedMemoryStream stream)
        {
            var src = (char*)stream.PositionPointer;
 
            // BOM: Unicode, little endian
            // Skip the BOM when creating the reader
            Debug.Assert(*src == 0xFEFF);
 
            return new DirectMemoryAccessStreamReader(src + 1, (int)stream.Length / sizeof(char) - 1);
        }
    }
}