2
2
// Licensed under the MIT license. See License.txt in the project root for license information.
3
3
4
4
using System . Collections . Immutable ;
5
+ using Microsoft . AspNetCore . Razor . Utilities ;
5
6
using Microsoft . CodeAnalysis ;
6
7
using Microsoft . Extensions . Logging ;
7
8
@@ -15,11 +16,33 @@ public class RazorWorkspaceListener : IDisposable
15
16
16
17
private string ? _projectInfoFileName ;
17
18
private Workspace ? _workspace ;
18
- private ImmutableDictionary < ProjectId , TaskDelayScheduler > _workQueues = ImmutableDictionary < ProjectId , TaskDelayScheduler > . Empty ;
19
+
20
+ // Use an immutable dictionary for ImmutableInterlocked operations. The value isn't checked, just
21
+ // the existance of the key so work is only done for projects with dynamic files.
22
+ private ImmutableDictionary < ProjectId , bool > _projectsWithDynamicFile = ImmutableDictionary < ProjectId , bool > . Empty ;
23
+ private readonly CancellationTokenSource _disposeTokenSource = new ( ) ;
24
+ private readonly AsyncBatchingWorkQueue < ProjectId > _workQueue ;
19
25
20
26
public RazorWorkspaceListener ( ILoggerFactory loggerFactory )
21
27
{
22
28
_logger = loggerFactory . CreateLogger ( nameof ( RazorWorkspaceListener ) ) ;
29
+ _workQueue = new ( TimeSpan . FromMilliseconds ( 500 ) , UpdateCurrentProjectsAsync , EqualityComparer < ProjectId > . Default , _disposeTokenSource . Token ) ;
30
+ }
31
+
32
+ public void Dispose ( )
33
+ {
34
+ if ( _workspace is not null )
35
+ {
36
+ _workspace . WorkspaceChanged -= Workspace_WorkspaceChanged ;
37
+ }
38
+
39
+ if ( _disposeTokenSource . IsCancellationRequested )
40
+ {
41
+ return ;
42
+ }
43
+
44
+ _disposeTokenSource . Cancel ( ) ;
45
+ _disposeTokenSource . Dispose ( ) ;
23
46
}
24
47
25
48
public void EnsureInitialized ( Workspace workspace , string projectInfoFileName )
@@ -45,14 +68,11 @@ public void NotifyDynamicFile(ProjectId projectId)
45
68
return ;
46
69
}
47
70
48
- // We expect this to be called multiple times per project so a no-op update operation seems like a better choice
49
- // than constructing a new TaskDelayScheduler each time, and using the TryAdd method, which doesn't support a
50
- // valueFactory argument.
51
- var scheduler = ImmutableInterlocked . AddOrUpdate ( ref _workQueues , projectId , static _ => new TaskDelayScheduler ( s_debounceTime , CancellationToken . None ) , static ( _ , val ) => val ) ;
71
+ ImmutableInterlocked . GetOrAdd ( ref _projectsWithDynamicFile , projectId , static ( _ ) => true ) ;
52
72
53
73
// Schedule a task, in case adding a dynamic file is the last thing that happens
54
74
_logger . LogTrace ( "{projectId} scheduling task due to dynamic file" , projectId ) ;
55
- scheduler . ScheduleAsyncTask ( ct => SerializeProjectAsync ( projectId , ct ) , CancellationToken . None ) ;
75
+ _workQueue . AddWork ( projectId ) ;
56
76
}
57
77
58
78
private void Workspace_WorkspaceChanged ( object ? sender , WorkspaceChangeEventArgs e )
@@ -61,11 +81,6 @@ private void Workspace_WorkspaceChanged(object? sender, WorkspaceChangeEventArgs
61
81
{
62
82
case WorkspaceChangeKind . SolutionChanged :
63
83
case WorkspaceChangeKind . SolutionReloaded :
64
- foreach ( var project in e . OldSolution . Projects )
65
- {
66
- RemoveProject ( project ) ;
67
- }
68
-
69
84
foreach ( var project in e . NewSolution . Projects )
70
85
{
71
86
EnqueueUpdate ( project ) ;
@@ -81,15 +96,14 @@ private void Workspace_WorkspaceChanged(object? sender, WorkspaceChangeEventArgs
81
96
82
97
break ;
83
98
84
- case WorkspaceChangeKind . ProjectRemoved :
85
- RemoveProject ( e . OldSolution . GetProject ( e . ProjectId ) ) ;
86
- break ;
87
-
88
99
case WorkspaceChangeKind . ProjectReloaded :
89
- RemoveProject ( e . OldSolution . GetProject ( e . ProjectId ) ) ;
90
100
EnqueueUpdate ( e . NewSolution . GetProject ( e . ProjectId ) ) ;
91
101
break ;
92
102
103
+ case WorkspaceChangeKind . ProjectRemoved :
104
+ RemoveProject ( e . ProjectId . AssumeNotNull ( ) ) ;
105
+ break ;
106
+
93
107
case WorkspaceChangeKind . ProjectAdded :
94
108
case WorkspaceChangeKind . ProjectChanged :
95
109
case WorkspaceChangeKind . DocumentAdded :
@@ -112,30 +126,24 @@ private void Workspace_WorkspaceChanged(object? sender, WorkspaceChangeEventArgs
112
126
}
113
127
114
128
break ;
129
+
115
130
case WorkspaceChangeKind . SolutionCleared :
116
131
case WorkspaceChangeKind . SolutionRemoved :
117
132
foreach ( var project in e . OldSolution . Projects )
118
133
{
119
- RemoveProject ( project ) ;
134
+ RemoveProject ( project . Id ) ;
120
135
}
121
136
122
137
break ;
138
+
123
139
default :
124
140
break ;
125
141
}
126
142
}
127
143
128
- private void RemoveProject ( Project ? project )
144
+ private void RemoveProject ( ProjectId projectId )
129
145
{
130
- if ( project is null )
131
- {
132
- return ;
133
- }
134
-
135
- if ( ImmutableInterlocked . TryRemove ( ref _workQueues , project . Id , out var scheduler ) )
136
- {
137
- scheduler . Dispose ( ) ;
138
- }
146
+ ImmutableInterlocked . TryRemove ( ref _projectsWithDynamicFile , projectId , out var _ ) ;
139
147
}
140
148
141
149
private void EnqueueUpdate ( Project ? project )
@@ -149,44 +157,42 @@ project is not
149
157
return ;
150
158
}
151
159
152
- var projectId = project . Id ;
153
- if ( _workQueues . TryGetValue ( projectId , out var scheduler ) )
160
+ // Don't queue work for projects that don't have a dynamic file
161
+ if ( ! _projectsWithDynamicFile . TryGetValue ( project . Id , out var _ ) )
154
162
{
155
- _logger . LogTrace ( "{projectId} scheduling task due to workspace event" , projectId ) ;
156
-
157
- scheduler . ScheduleAsyncTask ( ct => SerializeProjectAsync ( projectId , ct ) , CancellationToken . None ) ;
163
+ return ;
158
164
}
165
+
166
+ var projectId = project . Id ;
167
+ _workQueue . AddWork ( projectId ) ;
159
168
}
160
169
161
- // Protected for testing
162
- protected virtual Task SerializeProjectAsync ( ProjectId projectId , CancellationToken ct )
170
+ private async ValueTask UpdateCurrentProjectsAsync ( ImmutableArray < ProjectId > projectIds , CancellationToken cancellationToken )
163
171
{
164
- if ( _projectInfoFileName is null || _workspace is null )
165
- {
166
- return Task . CompletedTask ;
167
- }
172
+ var solution = _workspace . AssumeNotNull ( ) . CurrentSolution ;
168
173
169
- var project = _workspace . CurrentSolution . GetProject ( projectId ) ;
170
- if ( project is null )
174
+ foreach ( var projectId in projectIds )
171
175
{
172
- return Task . CompletedTask ;
173
- }
176
+ if ( _disposeTokenSource . IsCancellationRequested )
177
+ {
178
+ return ;
179
+ }
174
180
175
- _logger . LogTrace ( "{projectId} writing json file" , projectId ) ;
176
- return RazorProjectInfoSerializer . SerializeAsync ( project , _projectInfoFileName , ct ) ;
177
- }
181
+ var project = solution . GetProject ( projectId ) ;
182
+ if ( project is null )
183
+ {
184
+ _logger ? . LogTrace ( "Project {projectId} is not in workspace" , projectId ) ;
185
+ continue ;
186
+ }
178
187
179
- public void Dispose ( )
180
- {
181
- if ( _workspace is not null )
182
- {
183
- _workspace . WorkspaceChanged -= Workspace_WorkspaceChanged ;
188
+ await SerializeProjectAsync ( project , solution , cancellationToken ) . ConfigureAwait ( false ) ;
184
189
}
190
+ }
185
191
186
- var queues = Interlocked . Exchange ( ref _workQueues , ImmutableDictionary < ProjectId , TaskDelayScheduler > . Empty ) ;
187
- foreach ( var ( _ , value ) in queues )
188
- {
189
- value . Dispose ( ) ;
190
- }
192
+ // private protected for testing
193
+ private protected virtual Task SerializeProjectAsync ( Project project , Solution solution , CancellationToken cancellationToken )
194
+ {
195
+ _logger ? . LogTrace ( "Serializing information for {projectId}" , project . Id ) ;
196
+ return RazorProjectInfoSerializer . SerializeAsync ( project , _projectInfoFileName . AssumeNotNull ( ) , _logger , cancellationToken ) ;
191
197
}
192
198
}
0 commit comments