44using System ;
55using System . Collections . Generic ;
66using System . Diagnostics . CodeAnalysis ;
7- using System . Threading ;
8- using System . Threading . Tasks ;
97using Microsoft . CodeAnalysis . Razor ;
108using Microsoft . CodeAnalysis . Razor . ProjectSystem ;
119
@@ -16,17 +14,16 @@ internal class DefaultDocumentVersionCache : DocumentVersionCache
1614 internal const int MaxDocumentTrackingCount = 20 ;
1715
1816 // Internal for testing
19- internal readonly Dictionary < string , List < DocumentEntry > > DocumentLookup ;
20- private readonly ProjectSnapshotManagerDispatcher _dispatcher ;
17+ internal readonly Dictionary < string , List < DocumentEntry > > DocumentLookup_NeedsLock ;
18+ private readonly ReadWriterLocker _lock = new ( ) ;
2119 private ProjectSnapshotManagerBase ? _projectSnapshotManager ;
2220
2321 private ProjectSnapshotManagerBase ProjectSnapshotManager
2422 => _projectSnapshotManager ?? throw new InvalidOperationException ( "ProjectSnapshotManager accessed before Initialized was called." ) ;
2523
26- public DefaultDocumentVersionCache ( ProjectSnapshotManagerDispatcher dispatcher )
24+ public DefaultDocumentVersionCache ( )
2725 {
28- _dispatcher = dispatcher ?? throw new ArgumentNullException ( nameof ( dispatcher ) ) ;
29- DocumentLookup = new Dictionary < string , List < DocumentEntry > > ( FilePathComparer . Instance ) ;
26+ DocumentLookup_NeedsLock = new ( FilePathComparer . Instance ) ;
3027 }
3128
3229 public override void TrackDocumentVersion ( IDocumentSnapshot documentSnapshot , int version )
@@ -36,27 +33,34 @@ public override void TrackDocumentVersion(IDocumentSnapshot documentSnapshot, in
3633 throw new ArgumentNullException ( nameof ( documentSnapshot ) ) ;
3734 }
3835
39- _dispatcher . AssertDispatcherThread ( ) ;
40-
4136 var filePath = documentSnapshot . FilePath . AssumeNotNull ( ) ;
37+ using var upgradeableReadLock = _lock . EnterUpgradeAbleReadLock ( ) ;
38+ TrackDocumentVersion ( documentSnapshot , version , filePath , upgradeableReadLock ) ;
39+ }
4240
43- if ( ! DocumentLookup . TryGetValue ( filePath , out var documentEntries ) )
41+ private void TrackDocumentVersion ( IDocumentSnapshot documentSnapshot , int version , string filePath , ReadWriterLocker . UpgradeableReadLock upgradeableReadLock )
42+ {
43+ // Need to ensure the write lock covers all uses of documentEntries, not just DocumentLookup
44+ using ( upgradeableReadLock . EnterWriteLock ( ) )
4445 {
45- documentEntries = new List < DocumentEntry > ( ) ;
46- DocumentLookup [ filePath ] = documentEntries ;
47- }
46+ if ( ! DocumentLookup_NeedsLock . TryGetValue ( filePath , out var documentEntries ) )
47+ {
48+ documentEntries = new List < DocumentEntry > ( ) ;
49+ DocumentLookup_NeedsLock [ filePath ] = documentEntries ;
50+ }
4851
49- if ( documentEntries . Count == MaxDocumentTrackingCount )
50- {
51- // Clear the oldest document entry
52+ if ( documentEntries . Count == MaxDocumentTrackingCount )
53+ {
54+ // Clear the oldest document entry
5255
53- // With this approach we'll slowly leak memory as new documents are added to the system. We don't clear up
54- // document file paths where where all of the corresponding entries are expired.
55- documentEntries . RemoveAt ( 0 ) ;
56- }
56+ // With this approach we'll slowly leak memory as new documents are added to the system. We don't clear up
57+ // document file paths where where all of the corresponding entries are expired.
58+ documentEntries . RemoveAt ( 0 ) ;
59+ }
5760
58- var entry = new DocumentEntry ( documentSnapshot , version ) ;
59- documentEntries . Add ( entry ) ;
61+ var entry = new DocumentEntry ( documentSnapshot , version ) ;
62+ documentEntries . Add ( entry ) ;
63+ }
6064 }
6165
6266 public override bool TryGetDocumentVersion ( IDocumentSnapshot documentSnapshot , [ NotNullWhen ( true ) ] out int ? version )
@@ -66,11 +70,10 @@ public override bool TryGetDocumentVersion(IDocumentSnapshot documentSnapshot, [
6670 throw new ArgumentNullException ( nameof ( documentSnapshot ) ) ;
6771 }
6872
69- _dispatcher . AssertDispatcherThread ( ) ;
70-
7173 var filePath = documentSnapshot . FilePath . AssumeNotNull ( ) ;
74+ using var _ = _lock . EnterReadLock ( ) ;
7275
73- if ( ! DocumentLookup . TryGetValue ( filePath , out var documentEntries ) )
76+ if ( ! DocumentLookup_NeedsLock . TryGetValue ( filePath , out var documentEntries ) )
7477 {
7578 version = null ;
7679 return false ;
@@ -98,22 +101,6 @@ public override bool TryGetDocumentVersion(IDocumentSnapshot documentSnapshot, [
98101 return true ;
99102 }
100103
101- public override Task < int ? > TryGetDocumentVersionAsync ( IDocumentSnapshot documentSnapshot , CancellationToken cancellationToken )
102- {
103- if ( documentSnapshot is null )
104- {
105- throw new ArgumentNullException ( nameof ( documentSnapshot ) ) ;
106- }
107-
108- return _dispatcher . RunOnDispatcherThreadAsync (
109- ( ) =>
110- {
111- TryGetDocumentVersion ( documentSnapshot , out var version ) ;
112- return version ;
113- } ,
114- cancellationToken ) ;
115- }
116-
117104 public override void Initialize ( ProjectSnapshotManagerBase projectManager )
118105 {
119106 if ( projectManager is null )
@@ -133,18 +120,21 @@ private void ProjectSnapshotManager_Changed(object? sender, ProjectChangeEventAr
133120 return ;
134121 }
135122
136- _dispatcher . AssertDispatcherThread ( ) ;
123+ var upgradeableLock = _lock . EnterUpgradeAbleReadLock ( ) ;
137124
138125 switch ( args . Kind )
139126 {
140127 case ProjectChangeKind . DocumentChanged :
141- var documentFilePath = args . DocumentFilePath ! ;
142- if ( DocumentLookup . ContainsKey ( documentFilePath ) &&
143- ! ProjectSnapshotManager . IsDocumentOpen ( documentFilePath ) )
144- {
145- // Document closed, evict entry.
146- DocumentLookup . Remove ( documentFilePath ) ;
147- }
128+ var documentFilePath = args . DocumentFilePath ! ;
129+ if ( DocumentLookup_NeedsLock . ContainsKey ( documentFilePath ) &&
130+ ! ProjectSnapshotManager . IsDocumentOpen ( documentFilePath ) )
131+ {
132+ using ( upgradeableLock . EnterWriteLock ( ) )
133+ {
134+ // Document closed, evict entry.
135+ DocumentLookup_NeedsLock . Remove ( documentFilePath ) ;
136+ }
137+ }
148138
149139 break ;
150140 }
@@ -158,27 +148,22 @@ private void ProjectSnapshotManager_Changed(object? sender, ProjectChangeEventAr
158148 return ;
159149 }
160150
161- CaptureProjectDocumentsAsLatest ( project ) ;
151+ CaptureProjectDocumentsAsLatest ( project , upgradeableLock ) ;
162152 }
163153
164154 // Internal for testing
165155 internal void MarkAsLatestVersion ( IDocumentSnapshot document )
166156 {
167- var filePath = document . FilePath . AssumeNotNull ( ) ;
168-
169- if ( ! TryGetLatestVersionFromPath ( filePath , out var latestVersion ) )
170- {
171- return ;
172- }
173-
174- // Update our internal tracking state to track the changed document as the latest document.
175- TrackDocumentVersion ( document , latestVersion . Value ) ;
157+ using var upgradeableLock = _lock . EnterUpgradeAbleReadLock ( ) ;
158+ MarkAsLatestVersion ( document , upgradeableLock ) ;
176159 }
177160
178161 // Internal for testing
179162 internal bool TryGetLatestVersionFromPath ( string filePath , [ NotNullWhen ( true ) ] out int ? version )
180163 {
181- if ( ! DocumentLookup . TryGetValue ( filePath , out var documentEntries ) )
164+ using var _ = _lock . EnterReadLock ( ) ;
165+
166+ if ( ! DocumentLookup_NeedsLock . TryGetValue ( filePath , out var documentEntries ) )
182167 {
183168 version = null ;
184169 return false ;
@@ -190,18 +175,33 @@ internal bool TryGetLatestVersionFromPath(string filePath, [NotNullWhen(true)] o
190175 return true ;
191176 }
192177
193- private void CaptureProjectDocumentsAsLatest ( IProjectSnapshot projectSnapshot )
178+ private void CaptureProjectDocumentsAsLatest ( IProjectSnapshot projectSnapshot , ReadWriterLocker . UpgradeableReadLock upgradeableReadLock )
194179 {
195180 foreach ( var documentPath in projectSnapshot . DocumentFilePaths )
196181 {
197- if ( DocumentLookup . ContainsKey ( documentPath ) &&
182+ if ( DocumentLookup_NeedsLock . ContainsKey ( documentPath ) &&
198183 projectSnapshot . GetDocument ( documentPath ) is { } document )
199184 {
200- MarkAsLatestVersion ( document ) ;
185+ MarkAsLatestVersion ( document , upgradeableReadLock ) ;
201186 }
202187 }
203188 }
204189
190+ private void MarkAsLatestVersion ( IDocumentSnapshot document , ReadWriterLocker . UpgradeableReadLock upgradeableReadLock )
191+ {
192+ var filePath = document . FilePath . AssumeNotNull ( ) ;
193+
194+ if ( ! DocumentLookup_NeedsLock . TryGetValue ( filePath , out var documentEntries ) )
195+ {
196+ return ;
197+ }
198+
199+ var latestEntry = documentEntries [ ^ 1 ] ;
200+
201+ // Update our internal tracking state to track the changed document as the latest document.
202+ TrackDocumentVersion ( document , latestEntry . Version , document . FilePath . AssumeNotNull ( ) , upgradeableReadLock ) ;
203+ }
204+
205205 internal class DocumentEntry
206206 {
207207 public DocumentEntry ( IDocumentSnapshot document , int version )
0 commit comments