| File: TemporaryStorage\TemporaryStorageService.MemoryMappedInfo.cs | Web Access |
| Project: src\roslyn\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.Diagnostics; using System.IO; using System.IO.MemoryMappedFiles; using System.Runtime; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Host; internal sealed partial class TemporaryStorageService { /// <summary> /// Our own abstraction on top of memory map file so that we can have shared views over mmf files. /// Otherwise, each view has minimum size of 64K due to requirement forced by windows. /// /// most of our view will have short lifetime, but there are cases where view might live a bit longer such as /// metadata dll shadow copy. shared view will help those cases. /// </summary> /// <remarks> /// <para>This class and its nested types have familiar APIs and predictable behavior when used in other code, but /// are non-trivial to work on.</para> /// </remarks> internal sealed class MemoryMappedInfo(MemoryMappedFile memoryMappedFile, string? name, long offset, long size) { /// <summary> /// The memory mapped file. /// </summary> public readonly MemoryMappedFile MemoryMappedFile = memoryMappedFile; /// <summary> /// A weak reference to a read-only view for the memory mapped file. /// </summary> /// <remarks> /// <para>This holds a weak counted reference to current <see cref="MemoryMappedViewAccessor"/>, which allows /// additional accessors for the same address space to be obtained up until the point when no external code is /// using it. When the memory is no longer being used by any <see cref="MemoryMappedViewUnmanagedMemoryStream"/> /// objects, the view of the memory mapped file is unmapped, making the process address space it previously /// claimed available for other purposes. If/when it is needed again, a new view is created.</para> /// /// <para>This view is read-only, so it is only used by <see cref="CreateReadableStream"/>.</para> /// </remarks> private ReferenceCountedDisposable<MemoryMappedViewAccessor>.WeakReference _weakReadAccessor; public static MemoryMappedInfo CreateNew(string? name, long size) => new(MemoryMappedFile.CreateNew(name, size), name, offset: 0, size); /// <summary> /// The name of the memory mapped file. Non null on systems that support named memory mapped files, null /// otherwise.. /// </summary> public string? Name { get; } = name; /// <summary> /// The offset into the memory mapped file of the region described by the current /// <see cref="MemoryMappedInfo"/>. /// </summary> public long Offset { get; } = offset; /// <summary> /// The size of the region of the memory mapped file described by the current /// <see cref="MemoryMappedInfo"/>. /// </summary> public long Size { get; } = size; /// <summary> /// Caller is responsible for disposing the returned stream. /// multiple call of this will not increase VM. /// </summary> public UnmanagedMemoryStream CreateReadableStream() { // Note: TryAddReference behaves according to its documentation even if the target object has been disposed. // If it returns non-null, then the object will not be disposed before the returned reference is disposed // (see comments on _memoryMappedFile and TryAddReference). var streamAccessor = _weakReadAccessor.TryAddReference(); if (streamAccessor == null) { var rawAccessor = RunWithCompactingGCFallback( static info => info.MemoryMappedFile.CreateViewAccessor(info.Offset, info.Size, MemoryMappedFileAccess.Read), this); streamAccessor = new ReferenceCountedDisposable<MemoryMappedViewAccessor>(rawAccessor); _weakReadAccessor = new ReferenceCountedDisposable<MemoryMappedViewAccessor>.WeakReference(streamAccessor); } Debug.Assert(streamAccessor.Target.CanRead); return new MemoryMappedViewUnmanagedMemoryStream(streamAccessor, Size); } /// <summary> /// Caller is responsible for disposing the returned stream. /// multiple call of this will increase VM. /// </summary> public Stream CreateWritableStream() { return RunWithCompactingGCFallback( static info => info.MemoryMappedFile.CreateViewStream(info.Offset, info.Size, MemoryMappedFileAccess.Write), this); } /// <summary> /// Run a function which may fail with an <see cref="IOException"/> if not enough memory is available to satisfy /// the request. In this case, a full compacting GC pass is forced and the function is attempted again. /// </summary> /// <remarks> /// <para><see cref="MemoryMappedFile.CreateViewAccessor(long, long, MemoryMappedFileAccess)"/> and <see /// cref="MemoryMappedFile.CreateViewStream(long, long, MemoryMappedFileAccess)"/> will use a native memory map, /// which can't trigger a GC. In this case, we'd otherwise crash with OOM, so we don't care about creating a UI /// delay with a full forced compacting GC. If it crashes the second try, it means we're legitimately out of /// resources.</para> /// </remarks> /// <typeparam name="TArg">The type of argument to pass to the callback.</typeparam> /// <typeparam name="T">The type returned by the function.</typeparam> /// <param name="function">The function to execute.</param> /// <param name="argument">The argument to pass to the function.</param> /// <returns>The value returned by <paramref name="function"/>.</returns> private static T RunWithCompactingGCFallback<TArg, T>(Func<TArg, T> function, TArg argument) { try { return function(argument); } catch (IOException) { ForceCompactingGC(); return function(argument); } } private static void ForceCompactingGC() { // repeated GC.Collect / WaitForPendingFinalizers till memory freed delta is super small, ignore the return value GC.GetTotalMemory(forceFullCollection: true); // compact the LOH GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; GC.Collect(); } private sealed unsafe class MemoryMappedViewUnmanagedMemoryStream( ReferenceCountedDisposable<MemoryMappedViewAccessor> accessor, long length) : UnmanagedMemoryStream( (byte*)accessor.Target.SafeMemoryMappedViewHandle.DangerousGetHandle() + accessor.Target.PointerOffset, length) { protected override void Dispose(bool disposing) { base.Dispose(disposing); if (disposing) accessor.Dispose(); } } } }