Skip to content

Commit 6130ce9

Browse files
committed
Allow deletion of the metadata files while the compiler process is running
Fixes #1324
1 parent 31fb791 commit 6130ce9

File tree

6 files changed

+287
-201
lines changed

6 files changed

+287
-201
lines changed

src/Microsoft.Windows.CsWin32/Generator.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public partial class Generator : IGenerator, IDisposable
3535
private readonly StructDeclarationSyntax variableLengthInlineArrayStruct2;
3636

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

88-
this.MetadataIndex = MetadataIndex.Get(metadataLibraryPath, compilation?.Options.Platform);
88+
MetadataFile metadataFile = MetadataCache.Default.GetMetadataFile(metadataLibraryPath);
89+
this.MetadataIndex = metadataFile.GetMetadataIndex(compilation?.Options.Platform);
90+
this.metadataReader = metadataFile.GetMetadataReader();
91+
8992
this.ApiDocs = docs;
90-
this.metadataReader = MetadataIndex.GetMetadataReader(metadataLibraryPath);
9193

9294
this.options = options;
9395
this.options.Validate();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace Microsoft.Windows.CsWin32;
5+
6+
internal class MetadataCache
7+
{
8+
internal static readonly MetadataCache Default = new();
9+
10+
private readonly Dictionary<string, MetadataFile> metadataFiles = new(StringComparer.OrdinalIgnoreCase);
11+
12+
/// <summary>
13+
/// Gets a file accessor for the given path that supports many concurrent readers.
14+
/// </summary>
15+
/// <param name="path">The path to the .winmd file.</param>
16+
/// <returns>The file accessor.</returns>
17+
internal MetadataFile GetMetadataFile(string path)
18+
{
19+
lock (this.metadataFiles)
20+
{
21+
MetadataFile? metadataFile;
22+
DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(path);
23+
if (this.metadataFiles.TryGetValue(path, out metadataFile))
24+
{
25+
if (metadataFile.LastWriteTimeUtc == lastWriteTimeUtc)
26+
{
27+
// We already have the file, and it is still current. Happy path.
28+
return metadataFile;
29+
}
30+
31+
// Stale file. Evict from the cache.
32+
this.metadataFiles.Remove(path);
33+
metadataFile.Dispose();
34+
}
35+
36+
// New or updated file. Re-open.
37+
this.metadataFiles.Add(path, metadataFile = new MetadataFile(path));
38+
return metadataFile;
39+
}
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.IO.MemoryMappedFiles;
5+
using System.Reflection.PortableExecutable;
6+
7+
namespace Microsoft.Windows.CsWin32;
8+
9+
[DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
10+
internal class MetadataFile : IDisposable
11+
{
12+
private readonly object syncObject = new();
13+
private readonly Stack<(PEReader PEReader, MetadataReader MDReader)> peReaders = new();
14+
private readonly Dictionary<Platform?, MetadataIndex> indexes = new();
15+
private int readersRentedOut;
16+
private MemoryMappedFile file;
17+
private bool obsolete;
18+
19+
internal MetadataFile(string path)
20+
{
21+
this.Path = path;
22+
this.LastWriteTimeUtc = File.GetLastWriteTimeUtc(path);
23+
24+
// When using FileShare.Delete, the OS will allow the file to be deleted, but it does not disrupt
25+
// our ability to read the file while our handle is open.
26+
// The file may be recreated on disk as well, and we'll keep reading the original file until we close that handle.
27+
// We may also open the new file while holding the old handle,
28+
// at which point we have handles open to both versions of the file concurrently.
29+
FileStream metadataStream = new(path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete);
30+
this.file = MemoryMappedFile.CreateFromFile(metadataStream, mapName: null, capacity: 0, MemoryMappedFileAccess.Read, HandleInheritability.None, leaveOpen: false);
31+
}
32+
33+
internal string Path { get; }
34+
35+
internal DateTime LastWriteTimeUtc { get; }
36+
37+
private string DebuggerDisplay => $"\"{this.Path}\" ({this.LastWriteTimeUtc})";
38+
39+
/// <summary>
40+
/// Prepares to close the file handle and release resources as soon as all rentals have been returned.
41+
/// </summary>
42+
public void Dispose()
43+
{
44+
lock (this.syncObject)
45+
{
46+
this.obsolete = true;
47+
48+
// Drain our cache of readers (the ones that aren't currently being used).
49+
while (this.peReaders.Count > 0)
50+
{
51+
this.peReaders.Pop().PEReader.Dispose();
52+
}
53+
54+
// Close the file if we have no readers rented out.
55+
if (this.readersRentedOut == 0)
56+
{
57+
this.file.Dispose();
58+
}
59+
}
60+
}
61+
62+
internal Rental GetMetadataReader()
63+
{
64+
lock (this.syncObject)
65+
{
66+
if (this.obsolete)
67+
{
68+
throw new InvalidOperationException("This file was deleted and should no longer be used.");
69+
}
70+
71+
PEReader peReader;
72+
MetadataReader metadataReader;
73+
if (this.peReaders.Count > 0)
74+
{
75+
(peReader, metadataReader) = this.peReaders.Pop();
76+
}
77+
else
78+
{
79+
peReader = new(this.file.CreateViewStream(offset: 0, size: 0, MemoryMappedFileAccess.Read));
80+
metadataReader = peReader.GetMetadataReader();
81+
}
82+
83+
this.readersRentedOut++;
84+
return new Rental(peReader, metadataReader, this);
85+
}
86+
}
87+
88+
internal MetadataIndex GetMetadataIndex(Platform? platform)
89+
{
90+
lock (this.syncObject)
91+
{
92+
if (!this.indexes.TryGetValue(platform, out MetadataIndex? index))
93+
{
94+
this.indexes.Add(platform, index = new MetadataIndex(this, platform));
95+
}
96+
97+
return index;
98+
}
99+
}
100+
101+
private void ReturnReader(PEReader peReader, MetadataReader mdReader)
102+
{
103+
lock (this.syncObject)
104+
{
105+
this.readersRentedOut--;
106+
Debug.Assert(this.readersRentedOut >= 0, "Some reader was returned more than once.");
107+
108+
if (this.obsolete)
109+
{
110+
// This file has been marked as stale, so we don't want to recycle the reader.
111+
peReader.Dispose();
112+
113+
// If this was the last rental to be returned, we can close the file.
114+
if (this.readersRentedOut == 0)
115+
{
116+
this.file.Dispose();
117+
}
118+
}
119+
else
120+
{
121+
// Store this in the cache for reuse later.
122+
this.peReaders.Push((peReader, mdReader));
123+
}
124+
}
125+
}
126+
127+
internal class Rental : IDisposable
128+
{
129+
private (PEReader PEReader, MetadataReader MDReader, MetadataFile File)? state;
130+
131+
internal Rental(PEReader peReader, MetadataReader mdReader, MetadataFile file)
132+
{
133+
this.state = (peReader, mdReader, file);
134+
}
135+
136+
internal MetadataReader Value => this.state?.MDReader ?? throw new ObjectDisposedException(typeof(Rental).FullName);
137+
138+
public void Dispose()
139+
{
140+
if (this.state is (PEReader peReader, MetadataReader mdReader, MetadataFile file))
141+
{
142+
file.ReturnReader(peReader, mdReader);
143+
this.state = null;
144+
}
145+
}
146+
}
147+
}

src/Microsoft.Windows.CsWin32/MetadataIndex.cs

+6-107
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33

44
using System.Collections.Concurrent;
55
using System.Collections.ObjectModel;
6-
using System.IO.MemoryMappedFiles;
7-
using System.Reflection.PortableExecutable;
86

97
namespace Microsoft.Windows.CsWin32;
108

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

28-
private static readonly Action<MetadataReader, object?> ReaderRecycleDelegate = Recycle;
29-
30-
private static readonly Dictionary<CacheKey, MetadataIndex> Cache = new();
31-
32-
/// <summary>
33-
/// A cache of metadata files read.
34-
/// All access to this should be within a <see cref="Cache"/> lock.
35-
/// </summary>
36-
private static readonly Dictionary<string, MemoryMappedFile> MetadataFiles = new(StringComparer.OrdinalIgnoreCase);
37-
38-
private static readonly ConcurrentDictionary<string, ConcurrentBag<(PEReader, MetadataReader)>> PooledPEReaders = new(StringComparer.OrdinalIgnoreCase);
39-
40-
private readonly string metadataPath;
26+
private readonly MetadataFile metadataFile;
4127

4228
private readonly Platform? platform;
4329

@@ -72,14 +58,14 @@ internal class MetadataIndex
7258
/// <summary>
7359
/// Initializes a new instance of the <see cref="MetadataIndex"/> class.
7460
/// </summary>
75-
/// <param name="metadataPath">The path to the metadata that this index will represent.</param>
61+
/// <param name="metadataFile">The metadata file that this index will represent.</param>
7662
/// <param name="platform">The platform filter to apply when reading the metadata.</param>
77-
private MetadataIndex(string metadataPath, Platform? platform)
63+
internal MetadataIndex(MetadataFile metadataFile, Platform? platform)
7864
{
79-
this.metadataPath = metadataPath;
65+
this.metadataFile = metadataFile;
8066
this.platform = platform;
8167

82-
using Rental<MetadataReader> mrRental = GetMetadataReader(metadataPath);
68+
using MetadataFile.Rental mrRental = metadataFile.GetMetadataReader();
8369
MetadataReader mr = mrRental.Value;
8470
this.MetadataName = Path.GetFileNameWithoutExtension(mr.GetString(mr.GetAssemblyDefinition().Name));
8571

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

247233
internal string CommonNamespaceDot { get; }
248234

249-
private string DebuggerDisplay => $"{this.metadataPath} ({this.platform})";
250-
251-
internal static MetadataIndex Get(string metadataPath, Platform? platform)
252-
{
253-
metadataPath = Path.GetFullPath(metadataPath);
254-
CacheKey key = new(metadataPath, platform);
255-
lock (Cache)
256-
{
257-
if (!Cache.TryGetValue(key, out MetadataIndex index))
258-
{
259-
Cache.Add(key, index = new MetadataIndex(metadataPath, platform));
260-
}
261-
262-
return index;
263-
}
264-
}
265-
266-
internal static Rental<MetadataReader> GetMetadataReader(string metadataPath)
267-
{
268-
if (PooledPEReaders.TryGetValue(metadataPath, out ConcurrentBag<(PEReader, MetadataReader)>? pool) && pool.TryTake(out (PEReader, MetadataReader) readers))
269-
{
270-
return new(readers.Item2, ReaderRecycleDelegate, (readers.Item1, metadataPath));
271-
}
272-
273-
PEReader peReader = new PEReader(CreateFileView(metadataPath));
274-
return new(peReader.GetMetadataReader(), ReaderRecycleDelegate, (peReader, metadataPath));
275-
}
276-
277-
internal static MemoryMappedViewStream CreateFileView(string metadataPath)
278-
{
279-
lock (Cache)
280-
{
281-
// We use a memory mapped file so that many threads can perform random access on it concurrently,
282-
// only mapping the file into memory once.
283-
if (!MetadataFiles.TryGetValue(metadataPath, out MemoryMappedFile? file))
284-
{
285-
var metadataStream = new FileStream(metadataPath, FileMode.Open, FileAccess.Read, FileShare.Read);
286-
file = MemoryMappedFile.CreateFromFile(metadataStream, mapName: null, capacity: 0, MemoryMappedFileAccess.Read, HandleInheritability.None, leaveOpen: false);
287-
MetadataFiles.Add(metadataPath, file);
288-
}
289-
290-
return file.CreateViewStream(offset: 0, size: 0, MemoryMappedFileAccess.Read);
291-
}
292-
}
235+
private string DebuggerDisplay => $"{this.metadataFile.Path} ({this.platform})";
293236

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

426-
private static void Recycle(MetadataReader metadataReader, object? state)
427-
{
428-
(PEReader peReader, string metadataPath) = ((PEReader, string))state!;
429-
ConcurrentBag<(PEReader, MetadataReader)> pool = PooledPEReaders.GetOrAdd(metadataPath, _ => new());
430-
if (pool.Count < MaxPooledObjectCount)
431-
{
432-
pool.Add((peReader, metadataReader));
433-
}
434-
else
435-
{
436-
// The pool is full. Dispose of this rather than recycle it.
437-
peReader.Dispose();
438-
}
439-
}
440-
441369
private static string CommonPrefix(IReadOnlyList<string> ss)
442370
{
443371
if (ss.Count == 0)
@@ -497,33 +425,4 @@ private static string CommonPrefix(IReadOnlyList<string> ss)
497425
// Return null if the value was determined to be missing.
498426
return this.enumTypeReference.HasValue && !this.enumTypeReference.Value.IsNil ? this.enumTypeReference.Value : null;
499427
}
500-
501-
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
502-
private struct CacheKey : IEquatable<CacheKey>
503-
{
504-
internal CacheKey(string metadataPath, Platform? platform)
505-
{
506-
this.MetadataPath = metadataPath;
507-
this.Platform = platform;
508-
}
509-
510-
internal string MetadataPath { get; }
511-
512-
internal Platform? Platform { get; }
513-
514-
private string DebuggerDisplay => $"{this.MetadataPath} ({this.Platform})";
515-
516-
public override bool Equals(object obj) => obj is CacheKey other && this.Equals(other);
517-
518-
public bool Equals(CacheKey other)
519-
{
520-
return this.Platform == other.Platform
521-
&& string.Equals(this.MetadataPath, other.MetadataPath, StringComparison.OrdinalIgnoreCase);
522-
}
523-
524-
public override int GetHashCode()
525-
{
526-
return StringComparer.OrdinalIgnoreCase.GetHashCode(this.MetadataPath) + (this.Platform.HasValue ? (int)this.Platform.Value : 0);
527-
}
528-
}
529428
}

0 commit comments

Comments
 (0)