Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow deletion of the metadata files while the compiler process is running #1329

Merged
merged 1 commit into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/Microsoft.Windows.CsWin32/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public partial class Generator : IGenerator, IDisposable
private readonly StructDeclarationSyntax variableLengthInlineArrayStruct2;

private readonly Dictionary<string, IReadOnlyList<ISymbol>> findTypeSymbolIfAlreadyAvailableCache = new(StringComparer.Ordinal);
private readonly Rental<MetadataReader> metadataReader;
private readonly MetadataFile.Rental metadataReader;
private readonly GeneratorOptions options;
private readonly CSharpCompilation? compilation;
private readonly CSharpParseOptions? parseOptions;
Expand Down Expand Up @@ -85,9 +85,11 @@ public Generator(string metadataLibraryPath, Docs? docs, GeneratorOptions option
throw new ArgumentNullException(nameof(options));
}

this.MetadataIndex = MetadataIndex.Get(metadataLibraryPath, compilation?.Options.Platform);
MetadataFile metadataFile = MetadataCache.Default.GetMetadataFile(metadataLibraryPath);
this.MetadataIndex = metadataFile.GetMetadataIndex(compilation?.Options.Platform);
this.metadataReader = metadataFile.GetMetadataReader();

this.ApiDocs = docs;
this.metadataReader = MetadataIndex.GetMetadataReader(metadataLibraryPath);

this.options = options;
this.options.Validate();
Expand Down
41 changes: 41 additions & 0 deletions src/Microsoft.Windows.CsWin32/MetadataCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Windows.CsWin32;

internal class MetadataCache
{
internal static readonly MetadataCache Default = new();

private readonly Dictionary<string, MetadataFile> metadataFiles = new(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Gets a file accessor for the given path that supports many concurrent readers.
/// </summary>
/// <param name="path">The path to the .winmd file.</param>
/// <returns>The file accessor.</returns>
internal MetadataFile GetMetadataFile(string path)
{
lock (this.metadataFiles)
{
MetadataFile? metadataFile;
DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(path);
if (this.metadataFiles.TryGetValue(path, out metadataFile))
{
if (metadataFile.LastWriteTimeUtc == lastWriteTimeUtc)
{
// We already have the file, and it is still current. Happy path.
return metadataFile;
}

// Stale file. Evict from the cache.
this.metadataFiles.Remove(path);
metadataFile.Dispose();
}

// New or updated file. Re-open.
this.metadataFiles.Add(path, metadataFile = new MetadataFile(path));
return metadataFile;
}
}
}
147 changes: 147 additions & 0 deletions src/Microsoft.Windows.CsWin32/MetadataFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.IO.MemoryMappedFiles;
using System.Reflection.PortableExecutable;

namespace Microsoft.Windows.CsWin32;

[DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
internal class MetadataFile : IDisposable
{
private readonly object syncObject = new();
private readonly Stack<(PEReader PEReader, MetadataReader MDReader)> peReaders = new();
private readonly Dictionary<Platform?, MetadataIndex> indexes = new();
private int readersRentedOut;
private MemoryMappedFile file;
private bool obsolete;

internal MetadataFile(string path)
{
this.Path = path;
this.LastWriteTimeUtc = File.GetLastWriteTimeUtc(path);

// When using FileShare.Delete, the OS will allow the file to be deleted, but it does not disrupt
// our ability to read the file while our handle is open.
// The file may be recreated on disk as well, and we'll keep reading the original file until we close that handle.
// We may also open the new file while holding the old handle,
// at which point we have handles open to both versions of the file concurrently.
FileStream metadataStream = new(path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete);
this.file = MemoryMappedFile.CreateFromFile(metadataStream, mapName: null, capacity: 0, MemoryMappedFileAccess.Read, HandleInheritability.None, leaveOpen: false);
}

internal string Path { get; }

internal DateTime LastWriteTimeUtc { get; }

private string DebuggerDisplay => $"\"{this.Path}\" ({this.LastWriteTimeUtc})";

/// <summary>
/// Prepares to close the file handle and release resources as soon as all rentals have been returned.
/// </summary>
public void Dispose()
{
lock (this.syncObject)
{
this.obsolete = true;

// Drain our cache of readers (the ones that aren't currently being used).
while (this.peReaders.Count > 0)
{
this.peReaders.Pop().PEReader.Dispose();
}

// Close the file if we have no readers rented out.
if (this.readersRentedOut == 0)
{
this.file.Dispose();
}
}
}

internal Rental GetMetadataReader()
{
lock (this.syncObject)
{
if (this.obsolete)
{
throw new InvalidOperationException("This file was deleted and should no longer be used.");
}

PEReader peReader;
MetadataReader metadataReader;
if (this.peReaders.Count > 0)
{
(peReader, metadataReader) = this.peReaders.Pop();
}
else
{
peReader = new(this.file.CreateViewStream(offset: 0, size: 0, MemoryMappedFileAccess.Read));
metadataReader = peReader.GetMetadataReader();
}

this.readersRentedOut++;
return new Rental(peReader, metadataReader, this);
}
}

internal MetadataIndex GetMetadataIndex(Platform? platform)
{
lock (this.syncObject)
{
if (!this.indexes.TryGetValue(platform, out MetadataIndex? index))
{
this.indexes.Add(platform, index = new MetadataIndex(this, platform));
}

return index;
}
}

private void ReturnReader(PEReader peReader, MetadataReader mdReader)
{
lock (this.syncObject)
{
this.readersRentedOut--;
Debug.Assert(this.readersRentedOut >= 0, "Some reader was returned more than once.");

if (this.obsolete)
{
// This file has been marked as stale, so we don't want to recycle the reader.
peReader.Dispose();

// If this was the last rental to be returned, we can close the file.
if (this.readersRentedOut == 0)
{
this.file.Dispose();
}
}
else
{
// Store this in the cache for reuse later.
this.peReaders.Push((peReader, mdReader));
}
}
}

internal class Rental : IDisposable
{
private (PEReader PEReader, MetadataReader MDReader, MetadataFile File)? state;

internal Rental(PEReader peReader, MetadataReader mdReader, MetadataFile file)
{
this.state = (peReader, mdReader, file);
}

internal MetadataReader Value => this.state?.MDReader ?? throw new ObjectDisposedException(typeof(Rental).FullName);

public void Dispose()
{
if (this.state is (PEReader peReader, MetadataReader mdReader, MetadataFile file))
{
file.ReturnReader(peReader, mdReader);
this.state = null;
}
}
}
}
113 changes: 6 additions & 107 deletions src/Microsoft.Windows.CsWin32/MetadataIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.IO.MemoryMappedFiles;
using System.Reflection.PortableExecutable;

namespace Microsoft.Windows.CsWin32;

Expand All @@ -25,19 +23,7 @@ internal class MetadataIndex
{
private static readonly int MaxPooledObjectCount = Math.Max(Environment.ProcessorCount, 4);

private static readonly Action<MetadataReader, object?> ReaderRecycleDelegate = Recycle;

private static readonly Dictionary<CacheKey, MetadataIndex> Cache = new();

/// <summary>
/// A cache of metadata files read.
/// All access to this should be within a <see cref="Cache"/> lock.
/// </summary>
private static readonly Dictionary<string, MemoryMappedFile> MetadataFiles = new(StringComparer.OrdinalIgnoreCase);

private static readonly ConcurrentDictionary<string, ConcurrentBag<(PEReader, MetadataReader)>> PooledPEReaders = new(StringComparer.OrdinalIgnoreCase);

private readonly string metadataPath;
private readonly MetadataFile metadataFile;

private readonly Platform? platform;

Expand Down Expand Up @@ -72,14 +58,14 @@ internal class MetadataIndex
/// <summary>
/// Initializes a new instance of the <see cref="MetadataIndex"/> class.
/// </summary>
/// <param name="metadataPath">The path to the metadata that this index will represent.</param>
/// <param name="metadataFile">The metadata file that this index will represent.</param>
/// <param name="platform">The platform filter to apply when reading the metadata.</param>
private MetadataIndex(string metadataPath, Platform? platform)
internal MetadataIndex(MetadataFile metadataFile, Platform? platform)
{
this.metadataPath = metadataPath;
this.metadataFile = metadataFile;
this.platform = platform;

using Rental<MetadataReader> mrRental = GetMetadataReader(metadataPath);
using MetadataFile.Rental mrRental = metadataFile.GetMetadataReader();
MetadataReader mr = mrRental.Value;
this.MetadataName = Path.GetFileNameWithoutExtension(mr.GetString(mr.GetAssemblyDefinition().Name));

Expand Down Expand Up @@ -246,50 +232,7 @@ void PopulateNamespace(NamespaceDefinition ns, string? parentNamespace)

internal string CommonNamespaceDot { get; }

private string DebuggerDisplay => $"{this.metadataPath} ({this.platform})";

internal static MetadataIndex Get(string metadataPath, Platform? platform)
{
metadataPath = Path.GetFullPath(metadataPath);
CacheKey key = new(metadataPath, platform);
lock (Cache)
{
if (!Cache.TryGetValue(key, out MetadataIndex index))
{
Cache.Add(key, index = new MetadataIndex(metadataPath, platform));
}

return index;
}
}

internal static Rental<MetadataReader> GetMetadataReader(string metadataPath)
{
if (PooledPEReaders.TryGetValue(metadataPath, out ConcurrentBag<(PEReader, MetadataReader)>? pool) && pool.TryTake(out (PEReader, MetadataReader) readers))
{
return new(readers.Item2, ReaderRecycleDelegate, (readers.Item1, metadataPath));
}

PEReader peReader = new PEReader(CreateFileView(metadataPath));
return new(peReader.GetMetadataReader(), ReaderRecycleDelegate, (peReader, metadataPath));
}

internal static MemoryMappedViewStream CreateFileView(string metadataPath)
{
lock (Cache)
{
// We use a memory mapped file so that many threads can perform random access on it concurrently,
// only mapping the file into memory once.
if (!MetadataFiles.TryGetValue(metadataPath, out MemoryMappedFile? file))
{
var metadataStream = new FileStream(metadataPath, FileMode.Open, FileAccess.Read, FileShare.Read);
file = MemoryMappedFile.CreateFromFile(metadataStream, mapName: null, capacity: 0, MemoryMappedFileAccess.Read, HandleInheritability.None, leaveOpen: false);
MetadataFiles.Add(metadataPath, file);
}

return file.CreateViewStream(offset: 0, size: 0, MemoryMappedFileAccess.Read);
}
}
private string DebuggerDisplay => $"{this.metadataFile.Path} ({this.platform})";

/// <summary>
/// Attempts to translate a <see cref="TypeReferenceHandle"/> to a <see cref="TypeDefinitionHandle"/>.
Expand Down Expand Up @@ -423,21 +366,6 @@ internal bool TryGetEnumName(MetadataReader reader, string enumValueName, [NotNu
return false;
}

private static void Recycle(MetadataReader metadataReader, object? state)
{
(PEReader peReader, string metadataPath) = ((PEReader, string))state!;
ConcurrentBag<(PEReader, MetadataReader)> pool = PooledPEReaders.GetOrAdd(metadataPath, _ => new());
if (pool.Count < MaxPooledObjectCount)
{
pool.Add((peReader, metadataReader));
}
else
{
// The pool is full. Dispose of this rather than recycle it.
peReader.Dispose();
}
}

private static string CommonPrefix(IReadOnlyList<string> ss)
{
if (ss.Count == 0)
Expand Down Expand Up @@ -497,33 +425,4 @@ private static string CommonPrefix(IReadOnlyList<string> ss)
// Return null if the value was determined to be missing.
return this.enumTypeReference.HasValue && !this.enumTypeReference.Value.IsNil ? this.enumTypeReference.Value : null;
}

[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
private struct CacheKey : IEquatable<CacheKey>
{
internal CacheKey(string metadataPath, Platform? platform)
{
this.MetadataPath = metadataPath;
this.Platform = platform;
}

internal string MetadataPath { get; }

internal Platform? Platform { get; }

private string DebuggerDisplay => $"{this.MetadataPath} ({this.Platform})";

public override bool Equals(object obj) => obj is CacheKey other && this.Equals(other);

public bool Equals(CacheKey other)
{
return this.Platform == other.Platform
&& string.Equals(this.MetadataPath, other.MetadataPath, StringComparison.OrdinalIgnoreCase);
}

public override int GetHashCode()
{
return StringComparer.OrdinalIgnoreCase.GetHashCode(this.MetadataPath) + (this.Platform.HasValue ? (int)this.Platform.Value : 0);
}
}
}
Loading