Skip to content

Commit dff00b2

Browse files
Use DocumentKey to avoid holding Razor project/document snapshots when the most recent will be used. (#11644)
The goal of this change is to move `DocumentKey` close to `ProjectKey` and use it various async batching work queues rather than holding onto project or document snapshots. I recommend reviewing commit-by-commit. Each commit describes its changes. CI Build: https://dev.azure.com/dnceng/internal/_build/results?buildId=2668867&view=results Test Insertion: https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/621273
2 parents 5a3144e + f5ee183 commit dff00b2

File tree

35 files changed

+474
-365
lines changed

35 files changed

+474
-365
lines changed

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/GeneratedDocumentPublisher.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ private void ProjectManager_Changed(object? sender, ProjectChangeEventArgs args)
180180
{
181181
if (_publishedCSharpData.Remove(key))
182182
{
183-
_logger.LogDebug($"Removing previous C# publish data for {key.ProjectKey}/{key.DocumentFilePath}");
183+
_logger.LogDebug($"Removing previous C# publish data for {key.ProjectKey}/{key.FilePath}");
184184
}
185185
}
186186

@@ -208,15 +208,15 @@ private void ProjectManager_Changed(object? sender, ProjectChangeEventArgs args)
208208
{
209209
if (_publishedCSharpData.Remove(documentKey))
210210
{
211-
_logger.LogDebug($"Removing previous C# publish data for {documentKey.ProjectKey}/{documentKey.DocumentFilePath}");
211+
_logger.LogDebug($"Removing previous C# publish data for {documentKey.ProjectKey}/{documentKey.FilePath}");
212212
}
213213
}
214214

215215
lock (_publishedHtmlData)
216216
{
217217
if (_publishedHtmlData.Remove(documentFilePath))
218218
{
219-
_logger.LogDebug($"Removing previous Html publish data for {documentKey.ProjectKey}/{documentKey.DocumentFilePath}");
219+
_logger.LogDebug($"Removing previous Html publish data for {documentKey.ProjectKey}/{documentKey.FilePath}");
220220
}
221221
}
222222
}
@@ -249,7 +249,7 @@ private void ProjectManager_Changed(object? sender, ProjectChangeEventArgs args)
249249
{
250250
if (_publishedCSharpData.Remove(key))
251251
{
252-
_logger.LogDebug($"Removing previous C# publish data for {key.ProjectKey}/{key.DocumentFilePath}");
252+
_logger.LogDebug($"Removing previous C# publish data for {key.ProjectKey}/{key.FilePath}");
253253
}
254254
}
255255
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.Comparer.cs

Lines changed: 0 additions & 44 deletions
This file was deleted.

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.cs

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Immutable;
77
using System.Threading;
88
using System.Threading.Tasks;
9+
using Microsoft.AspNetCore.Razor.ProjectSystem;
910
using Microsoft.AspNetCore.Razor.Utilities;
1011
using Microsoft.CodeAnalysis.Razor.Logging;
1112
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@@ -32,21 +33,29 @@ internal partial class OpenDocumentGenerator : IRazorStartupService, IDisposable
3233
private readonly LanguageServerFeatureOptions _options;
3334
private readonly ILogger _logger;
3435

35-
private readonly AsyncBatchingWorkQueue<DocumentSnapshot> _workQueue;
36+
private readonly AsyncBatchingWorkQueue<DocumentKey> _workQueue;
3637
private readonly CancellationTokenSource _disposeTokenSource;
38+
private readonly HashSet<DocumentKey> _workerSet;
39+
40+
// Note: This is likely to always be false. Only the Visual Studio ProjectSnapshotManager
41+
// is notified of the solution opening and closing, so the language server shouldn't
42+
// update this value. However, this may change at some point and keeping the check here means
43+
// that the logic between this class and the Visual Studio BackgroundDocumentGenerator are in sync.
44+
private bool _solutionIsClosing;
3745

3846
public OpenDocumentGenerator(
3947
IEnumerable<IDocumentProcessedListener> listeners,
4048
ProjectSnapshotManager projectManager,
4149
LanguageServerFeatureOptions options,
4250
ILoggerFactory loggerFactory)
4351
{
44-
_listeners = listeners.ToImmutableArray();
52+
_listeners = [.. listeners];
4553
_projectManager = projectManager;
4654
_options = options;
4755

56+
_workerSet = [];
4857
_disposeTokenSource = new();
49-
_workQueue = new AsyncBatchingWorkQueue<DocumentSnapshot>(
58+
_workQueue = new AsyncBatchingWorkQueue<DocumentKey>(
5059
s_delay,
5160
ProcessBatchAsync,
5261
_disposeTokenSource.Token);
@@ -66,15 +75,30 @@ public void Dispose()
6675
_disposeTokenSource.Dispose();
6776
}
6877

69-
private async ValueTask ProcessBatchAsync(ImmutableArray<DocumentSnapshot> items, CancellationToken token)
78+
private async ValueTask ProcessBatchAsync(ImmutableArray<DocumentKey> items, CancellationToken token)
7079
{
71-
foreach (var document in items.GetMostRecentUniqueItems(Comparer.Instance))
80+
_workerSet.Clear();
81+
82+
foreach (var key in items.GetMostRecentUniqueItems(_workerSet))
7283
{
7384
if (token.IsCancellationRequested)
7485
{
7586
return;
7687
}
7788

89+
// If the solution is closing, avoid any in-progress work.
90+
if (_solutionIsClosing)
91+
{
92+
return;
93+
}
94+
95+
if (!_projectManager.TryGetDocument(key, out var document))
96+
{
97+
continue;
98+
}
99+
100+
_logger.LogDebug($"Generating {key} at version {document.Version}");
101+
78102
var codeDocument = await document.GetGeneratedOutputAsync(token).ConfigureAwait(false);
79103

80104
foreach (var listener in _listeners)
@@ -91,26 +115,27 @@ private async ValueTask ProcessBatchAsync(ImmutableArray<DocumentSnapshot> items
91115

92116
private void ProjectManager_Changed(object? sender, ProjectChangeEventArgs args)
93117
{
94-
// Don't do any work if the solution is closing
118+
// We don't want to do any work on solution close.
95119
if (args.IsSolutionClosing)
96120
{
121+
_solutionIsClosing = true;
97122
return;
98123
}
99124

125+
_solutionIsClosing = false;
126+
100127
_logger.LogDebug($"Got a project change of type {args.Kind} for {args.ProjectKey.Id}");
101128

102129
switch (args.Kind)
103130
{
131+
case ProjectChangeKind.ProjectAdded:
104132
case ProjectChangeKind.ProjectChanged:
105133
{
106134
var newProject = args.Newer.AssumeNotNull();
107135

108136
foreach (var documentFilePath in newProject.DocumentFilePaths)
109137
{
110-
if (newProject.TryGetDocument(documentFilePath, out var document))
111-
{
112-
EnqueueIfNecessary(document);
113-
}
138+
EnqueueIfNecessary(new(newProject.Key, documentFilePath));
114139
}
115140

116141
break;
@@ -125,14 +150,11 @@ private void ProjectManager_Changed(object? sender, ProjectChangeEventArgs args)
125150
var newProject = args.Newer.AssumeNotNull();
126151
var documentFilePath = args.DocumentFilePath.AssumeNotNull();
127152

128-
if (newProject.TryGetDocument(documentFilePath, out var document))
129-
{
130-
EnqueueIfNecessary(document);
153+
EnqueueIfNecessary(new(newProject.Key, documentFilePath));
131154

132-
foreach (var relatedDocument in newProject.GetRelatedDocuments(document))
133-
{
134-
EnqueueIfNecessary(relatedDocument);
135-
}
155+
foreach (var relatedDocumentFilePath in newProject.GetRelatedDocumentFilePaths(documentFilePath))
156+
{
157+
EnqueueIfNecessary(new(newProject.Key, relatedDocumentFilePath));
136158
}
137159

138160
break;
@@ -144,16 +166,14 @@ private void ProjectManager_Changed(object? sender, ProjectChangeEventArgs args)
144166
var oldProject = args.Older.AssumeNotNull();
145167
var documentFilePath = args.DocumentFilePath.AssumeNotNull();
146168

147-
if (oldProject.TryGetDocument(documentFilePath, out var document))
169+
// For removals use the old snapshot to find related documents to update if they exist
170+
// in the new snapshot.
171+
172+
foreach (var relatedDocumentFilePath in oldProject.GetRelatedDocumentFilePaths(documentFilePath))
148173
{
149-
foreach (var relatedDocument in oldProject.GetRelatedDocuments(document))
174+
if (newProject.ContainsDocument(relatedDocumentFilePath))
150175
{
151-
var relatedDocumentFilePath = relatedDocument.FilePath;
152-
153-
if (newProject.TryGetDocument(relatedDocumentFilePath, out var newRelatedDocument))
154-
{
155-
EnqueueIfNecessary(newRelatedDocument);
156-
}
176+
EnqueueIfNecessary(new(newProject.Key, relatedDocumentFilePath));
157177
}
158178
}
159179

@@ -162,22 +182,24 @@ private void ProjectManager_Changed(object? sender, ProjectChangeEventArgs args)
162182

163183
case ProjectChangeKind.ProjectRemoved:
164184
{
165-
// No-op. We don't need to enqueue recompilations if the project is being removed
185+
// No-op. We don't need to compile anything if the project is being removed
166186
break;
167187
}
188+
189+
default:
190+
Assumed.Unreachable($"Unknown {nameof(ProjectChangeKind)}: {args.Kind}");
191+
break;
168192
}
169193

170-
void EnqueueIfNecessary(DocumentSnapshot document)
194+
void EnqueueIfNecessary(DocumentKey documentKey)
171195
{
172-
if (!_projectManager.IsDocumentOpen(document.FilePath) &&
173-
!_options.UpdateBuffersForClosedDocuments)
196+
if (!_options.UpdateBuffersForClosedDocuments &&
197+
!_projectManager.IsDocumentOpen(documentKey.FilePath))
174198
{
175199
return;
176200
}
177201

178-
_logger.LogDebug($"Enqueuing generation of {document.FilePath} in {document.Project.Key.Id} at version {document.Version}");
179-
180-
_workQueue.AddWork(document);
202+
_workQueue.AddWork(documentKey);
181203
}
182204
}
183205
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.CodeAnalysis.Razor;
6+
using Microsoft.Extensions.Internal;
7+
8+
namespace Microsoft.AspNetCore.Razor.ProjectSystem;
9+
10+
internal readonly record struct DocumentKey : IComparable<DocumentKey>
11+
{
12+
public ProjectKey ProjectKey { get; }
13+
public string FilePath { get; }
14+
15+
public DocumentKey(ProjectKey projectKey, string filePath)
16+
{
17+
ProjectKey = projectKey;
18+
FilePath = filePath;
19+
}
20+
21+
public bool Equals(DocumentKey other)
22+
=> ProjectKey.Equals(other.ProjectKey) &&
23+
FilePathComparer.Instance.Equals(FilePath, other.FilePath);
24+
25+
public override int GetHashCode()
26+
{
27+
var hash = HashCodeCombiner.Start();
28+
hash.Add(ProjectKey);
29+
hash.Add(FilePath, FilePathComparer.Instance);
30+
31+
return hash;
32+
}
33+
34+
public int CompareTo(DocumentKey other)
35+
{
36+
var comparison = ProjectKey.CompareTo(other.ProjectKey);
37+
if (comparison != 0)
38+
{
39+
return comparison;
40+
}
41+
42+
return FilePathComparer.Instance.Compare(FilePath, other.FilePath);
43+
}
44+
}

src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/ProjectSystem/ProjectKey.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Razor.ProjectSystem;
1414
/// identifier for a project.
1515
/// </summary>
1616
[DebuggerDisplay("id: {Id}")]
17-
internal readonly record struct ProjectKey
17+
internal readonly record struct ProjectKey : IComparable<ProjectKey>
1818
{
1919
public static ProjectKey Unknown { get; } = default;
2020

@@ -38,4 +38,19 @@ public override int GetHashCode()
3838

3939
public override string ToString()
4040
=> IsUnknown ? "<Unknown Project>" : Id;
41+
42+
public int CompareTo(ProjectKey other)
43+
{
44+
// Sort "unknown" project keys after other project keys.
45+
if (IsUnknown)
46+
{
47+
return other.IsUnknown ? 0 : 1;
48+
}
49+
else if (other.IsUnknown)
50+
{
51+
return -1;
52+
}
53+
54+
return FilePathComparer.Instance.Compare(Id, other.Id);
55+
}
4156
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentKey.cs

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/AbstractRazorProjectInfoDriver.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ private sealed record Remove(ProjectKey ProjectKey) : Work(ProjectKey);
2727

2828
private readonly CancellationTokenSource _disposeTokenSource;
2929
private readonly AsyncBatchingWorkQueue<Work> _workQueue;
30+
private readonly HashSet<Work> _workerSet;
3031
private readonly Dictionary<ProjectKey, RazorProjectInfo> _latestProjectInfoMap;
3132
private ImmutableArray<IRazorProjectInfoListener> _listeners;
3233
private readonly TaskCompletionSource<bool> _initializationTaskSource;
@@ -37,6 +38,7 @@ protected AbstractRazorProjectInfoDriver(ILoggerFactory loggerFactory, TimeSpan?
3738
{
3839
Logger = loggerFactory.GetOrCreateLogger(GetType());
3940

41+
_workerSet = new(Comparer.Instance);
4042
_disposeTokenSource = new();
4143
_workQueue = new AsyncBatchingWorkQueue<Work>(delay ?? DefaultDelay, ProcessBatchAsync, _disposeTokenSource.Token);
4244
_latestProjectInfoMap = [];
@@ -89,7 +91,9 @@ protected void StartInitialization()
8991

9092
private async ValueTask ProcessBatchAsync(ImmutableArray<Work> items, CancellationToken token)
9193
{
92-
foreach (var work in items.GetMostRecentUniqueItems(Comparer.Instance))
94+
_workerSet.Clear();
95+
96+
foreach (var work in items.GetMostRecentUniqueItems(_workerSet))
9397
{
9498
if (token.IsCancellationRequested)
9599
{

0 commit comments

Comments
 (0)