Skip to content

Commit 77ea404

Browse files
authored
Reduce allocations obtaining classified spans in ClassifierHelper (#79856)
ClassifiedSpan[] shows up as 0.7% of total allocations (in LOH) in the completion scenarios in the razor cohosting speedometer test. Surrounding code already uses a custom pool for ClassifiedSpan[] via Classifier.GetPooledList. This PR changes ClassifierHelper.GetClassifiedSpansAsync to take in a SegmentedList<ClassifiedSpan> to populate rather than returning an ImmutableArray<ClassifiedSpan>.
1 parent 9d89ea9 commit 77ea404

File tree

2 files changed

+19
-18
lines changed

2 files changed

+19
-18
lines changed

src/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public static async Task<int[]> ComputeSemanticTokensDataAsync(
9191
using var _2 = Classifier.GetPooledList(out var updatedClassifiedSpans);
9292

9393
var textSpans = spans.SelectAsArray(static (span, text) => text.Lines.GetTextSpan(span), text);
94-
await GetClassifiedSpansForDocumentAsync(
94+
await AddClassifiedSpansForDocumentAsync(
9595
classifiedSpans, document, textSpans, options, cancellationToken).ConfigureAwait(false);
9696

9797
// Multi-line tokens are not supported by VS (tracked by https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1265495).
@@ -108,7 +108,7 @@ await GetClassifiedSpansForDocumentAsync(
108108
return ComputeTokens(text.Lines, updatedClassifiedSpans, supportsVisualStudioExtensions, tokenTypesToIndex);
109109
}
110110

111-
private static async Task GetClassifiedSpansForDocumentAsync(
111+
private static async Task AddClassifiedSpansForDocumentAsync(
112112
SegmentedList<ClassifiedSpan> classifiedSpans,
113113
Document document,
114114
ImmutableArray<TextSpan> textSpans,
@@ -121,13 +121,11 @@ private static async Task GetClassifiedSpansForDocumentAsync(
121121
// then the semantic token classifications will override them.
122122

123123
// `includeAdditiveSpans` will add token modifiers such as 'static', which we want to include in LSP.
124-
var spans = await ClassifierHelper.GetClassifiedSpansAsync(
125-
document, textSpans, options, includeAdditiveSpans: true, cancellationToken).ConfigureAwait(false);
124+
await ClassifierHelper.AddClassifiedSpansAsync(
125+
classifiedSpans, document, textSpans, options, includeAdditiveSpans: true, cancellationToken).ConfigureAwait(false);
126126

127127
// Some classified spans may not be relevant and should be filtered out before we convert to tokens.
128-
var filteredSpans = spans.Where(s => !ShouldFilterClassification(s));
129-
130-
classifiedSpans.AddRange(filteredSpans);
128+
classifiedSpans.RemoveAll(static s => ShouldFilterClassification(s));
131129
}
132130

133131
private static bool ShouldFilterClassification(ClassifiedSpan s)

src/Workspaces/Core/Portable/Classification/ClassifierHelper.cs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,12 @@ public static async Task<ImmutableArray<ClassifiedSpan>> GetClassifiedSpansAsync
3333
bool includeAdditiveSpans,
3434
CancellationToken cancellationToken)
3535
{
36-
return await GetClassifiedSpansAsync(document, [span], options, includeAdditiveSpans, cancellationToken)
36+
using var _ = Classifier.GetPooledList(out var classifiedSpans);
37+
38+
await AddClassifiedSpansAsync(classifiedSpans, document, [span], options, includeAdditiveSpans, cancellationToken)
3739
.ConfigureAwait(false);
40+
41+
return [.. classifiedSpans];
3842
}
3943

4044
/// <summary>
@@ -49,7 +53,8 @@ public static async Task<ImmutableArray<ClassifiedSpan>> GetClassifiedSpansAsync
4953
/// results or not. 'Additive' spans are things like 'this variable is static' or 'this variable is
5054
/// overwritten'. i.e. they add additional information to a previous classification.</param>
5155
/// <param name="cancellationToken">A cancellation token.</param>
52-
public static async Task<ImmutableArray<ClassifiedSpan>> GetClassifiedSpansAsync(
56+
public static async Task AddClassifiedSpansAsync(
57+
SegmentedList<ClassifiedSpan> classifiedSpans,
5358
Document document,
5459
ImmutableArray<TextSpan> spans,
5560
ClassificationOptions options,
@@ -58,7 +63,7 @@ public static async Task<ImmutableArray<ClassifiedSpan>> GetClassifiedSpansAsync
5863
{
5964
var classificationService = document.GetLanguageService<IClassificationService>();
6065
if (classificationService == null)
61-
return default;
66+
return;
6267

6368
// Call out to the individual language to classify the chunk of text around the
6469
// reference. We'll get both the syntactic and semantic spans for this region.
@@ -90,8 +95,7 @@ public static async Task<ImmutableArray<ClassifiedSpan>> GetClassifiedSpansAsync
9095
}
9196

9297
var widenedSpan = new TextSpan(spans[0].Start, spans[^1].End);
93-
var classifiedSpans = MergeClassifiedSpans(syntaxSpans, semanticSpans, widenedSpan);
94-
return classifiedSpans;
98+
MergeClassifiedSpans(syntaxSpans, semanticSpans, widenedSpan, classifiedSpans);
9599
}
96100

97101
private static void RemoveAdditiveSpans(SegmentedList<ClassifiedSpan> spans)
@@ -104,10 +108,11 @@ private static void RemoveAdditiveSpans(SegmentedList<ClassifiedSpan> spans)
104108
}
105109
}
106110

107-
private static ImmutableArray<ClassifiedSpan> MergeClassifiedSpans(
111+
private static void MergeClassifiedSpans(
108112
SegmentedList<ClassifiedSpan> syntaxSpans,
109113
SegmentedList<ClassifiedSpan> semanticSpans,
110-
TextSpan widenedSpan)
114+
TextSpan widenedSpan,
115+
SegmentedList<ClassifiedSpan> classifiedSpans)
111116
{
112117
// The spans produced by the language services may not be ordered
113118
// (indeed, this happens with semantic classification as different
@@ -130,16 +135,14 @@ private static ImmutableArray<ClassifiedSpan> MergeClassifiedSpans(
130135
AdjustSpans(syntaxSpans, widenedSpan);
131136
AdjustSpans(semanticSpans, widenedSpan);
132137

133-
using var _1 = Classifier.GetPooledList(out var mergedSpans);
138+
using var _ = Classifier.GetPooledList(out var mergedSpans);
134139

135140
MergeParts(syntaxSpans, semanticSpans, mergedSpans);
136141
Order(mergedSpans);
137142

138143
// The classification service will only produce classifications for things it knows about. i.e. there will
139144
// be gaps in what it produces. Fill in those gaps so we have *all* parts of the span classified properly.
140-
using var _2 = Classifier.GetPooledList(out var filledInSpans);
141-
FillInClassifiedSpanGaps(widenedSpan.Start, mergedSpans, filledInSpans);
142-
return [.. filledInSpans];
145+
FillInClassifiedSpanGaps(widenedSpan.Start, mergedSpans, classifiedSpans);
143146
}
144147

145148
private static readonly Comparison<ClassifiedSpan> s_spanComparison = static (s1, s2) => s1.TextSpan.Start - s2.TextSpan.Start;

0 commit comments

Comments
 (0)