From 091d2b8672b7703740db0618e7396d013b8830e0 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Mon, 17 Nov 2025 17:23:46 -0800 Subject: [PATCH 1/2] Reduce intermediate array allocations in inline hint implementation AbstractInlineHintService retrieves two intermediate arraysfrom the type and parameter services and then joins them together and returns the resultant array. Instead, pass an ArrayBuilder to the type and parameter services and have them populate it directly, and then only allocate a single IA from the ArrayBuilder. --- .../InlineHints/AbstractInlineHintsTests.vb | 15 ++++++++----- .../InlineHints/AbstractInlineHintsService.cs | 21 ++++++++++++------- ...AbstractInlineParameterNameHintsService.cs | 11 +++++----- .../AbstractInlineTypeHintsService.cs | 11 ++++------ .../IInlineParameterNameHintsService.cs | 5 +++-- .../InlineHints/IInlineTypeHintsService.cs | 5 +++-- 6 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/EditorFeatures/Test2/InlineHints/AbstractInlineHintsTests.vb b/src/EditorFeatures/Test2/InlineHints/AbstractInlineHintsTests.vb index 65b0fff2670fb..66bc299f1985d 100644 --- a/src/EditorFeatures/Test2/InlineHints/AbstractInlineHintsTests.vb +++ b/src/EditorFeatures/Test2/InlineHints/AbstractInlineHintsTests.vb @@ -6,6 +6,7 @@ Imports System.Collections.Immutable Imports System.Threading Imports Microsoft.CodeAnalysis.InlineHints Imports Microsoft.CodeAnalysis.LanguageService +Imports Microsoft.CodeAnalysis.PooledObjects Imports Microsoft.CodeAnalysis.Text Namespace Microsoft.CodeAnalysis.Editor.UnitTests.InlineHints @@ -28,8 +29,10 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.InlineHints Dim tagService = document.GetRequiredLanguageService(Of IInlineParameterNameHintsService) Dim span = If(hostDocument.SelectedSpans.Any(), hostDocument.SelectedSpans.Single(), New TextSpan(0, snapshot.Length)) - Dim inlineHints = Await tagService.GetInlineHintsAsync( - document, span, options, displayOptions, displayAllOverride:=False, CancellationToken.None) + Dim inlineHints = ArrayBuilder(Of InlineHint).GetInstance() + + Await tagService.AddInlineHintsAsync( + document, span, options, displayOptions, displayAllOverride:=False, inlineHints, CancellationToken.None) Dim producedTags = From hint In inlineHints Select hint.DisplayParts.GetFullText().TrimEnd() + hint.Span.ToString @@ -56,7 +59,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.InlineHints AssertEx.Equal(expectedTags, producedTags) End Sub - Private Shared Async Function ValidateDoubleClick(document As Document, expectedDocument As Document, inlineHints As ImmutableArray(Of InlineHint)) As Task + Private Shared Async Function ValidateDoubleClick(document As Document, expectedDocument As Document, inlineHints As ArrayBuilder(Of InlineHint)) As Task Dim textChanges = New List(Of TextChange) For Each inlineHint In inlineHints If inlineHint.ReplacementTextChange IsNot Nothing Then @@ -88,8 +91,10 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.InlineHints Dim tagService = document.GetRequiredLanguageService(Of IInlineTypeHintsService) Dim span = If(hostDocument.SelectedSpans.Any(), hostDocument.SelectedSpans.Single(), New TextSpan(0, snapshot.Length)) - Dim typeHints = Await tagService.GetInlineHintsAsync( - document, span, options, displayOptions, displayAllOverride:=ephemeral, CancellationToken.None) + Dim typeHints = ArrayBuilder(Of InlineHint).GetInstance() + + Await tagService.AddInlineHintsAsync( + document, span, options, displayOptions, displayAllOverride:=ephemeral, typeHints, CancellationToken.None) Dim producedTags = From hint In typeHints Select hint.DisplayParts.GetFullText() + ":" + hint.Span.ToString() diff --git a/src/Features/Core/Portable/InlineHints/AbstractInlineHintsService.cs b/src/Features/Core/Portable/InlineHints/AbstractInlineHintsService.cs index c42d65e701c76..945605cedb7b8 100644 --- a/src/Features/Core/Portable/InlineHints/AbstractInlineHintsService.cs +++ b/src/Features/Core/Portable/InlineHints/AbstractInlineHintsService.cs @@ -3,10 +3,10 @@ // See the LICENSE file in the project root for more information. using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; @@ -20,14 +20,19 @@ public async Task> GetInlineHintsAsync( var inlineParameterService = document.GetLanguageService(); var inlineTypeService = document.GetLanguageService(); - var parameters = inlineParameterService == null - ? [] - : await inlineParameterService.GetInlineHintsAsync(document, textSpan, options.ParameterOptions, options.DisplayOptions, displayAllOverride, cancellationToken).ConfigureAwait(false); + // Allow large array instances in the pool, as these arrays often exceed the ArrayBuilder reuse size threshold + using var _ = ArrayBuilder.GetInstance(discardLargeInstances: false, out var result); - var types = inlineTypeService == null - ? [] - : await inlineTypeService.GetInlineHintsAsync(document, textSpan, options.TypeOptions, options.DisplayOptions, displayAllOverride, cancellationToken).ConfigureAwait(false); + if (inlineParameterService is not null) + { + await inlineParameterService.AddInlineHintsAsync(document, textSpan, options.ParameterOptions, options.DisplayOptions, displayAllOverride, result, cancellationToken).ConfigureAwait(false); + } - return [.. parameters, .. types]; + if (inlineTypeService is not null) + { + await inlineTypeService.AddInlineHintsAsync(document, textSpan, options.TypeOptions, options.DisplayOptions, displayAllOverride, result, cancellationToken).ConfigureAwait(false); + } + + return result.ToImmutableAndClear(); } } diff --git a/src/Features/Core/Portable/InlineHints/AbstractInlineParameterNameHintsService.cs b/src/Features/Core/Portable/InlineHints/AbstractInlineParameterNameHintsService.cs index f6190e6cba552..79d5ae815048a 100644 --- a/src/Features/Core/Portable/InlineHints/AbstractInlineParameterNameHintsService.cs +++ b/src/Features/Core/Portable/InlineHints/AbstractInlineParameterNameHintsService.cs @@ -32,23 +32,24 @@ protected abstract void AddAllParameterNameHintLocations( protected abstract bool IsIndexer(SyntaxNode node, IParameterSymbol parameter); protected abstract string GetReplacementText(string parameterName); - public async Task> GetInlineHintsAsync( + public async Task AddInlineHintsAsync( Document document, TextSpan textSpan, InlineParameterHintsOptions options, SymbolDescriptionOptions displayOptions, bool displayAllOverride, + ArrayBuilder result, CancellationToken cancellationToken) { var enabledForParameters = displayAllOverride || options.EnabledForParameters; if (!enabledForParameters) - return []; + return; var literalParameters = displayAllOverride || options.ForLiteralParameters; var objectCreationParameters = displayAllOverride || options.ForObjectCreationParameters; var otherParameters = displayAllOverride || options.ForOtherParameters; if (!literalParameters && !objectCreationParameters && !otherParameters) - return []; + return; var indexerParameters = displayAllOverride || options.ForIndexerParameters; var suppressForParametersThatDifferOnlyBySuffix = !displayAllOverride && options.SuppressForParametersThatDifferOnlyBySuffix; @@ -59,8 +60,6 @@ public async Task> GetInlineHintsAsync( var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); var syntaxFacts = document.GetRequiredLanguageService(); - // Allow large array instances in the pool, as these arrays often exceed the ArrayBuilder reuse size threshold - using var _1 = ArrayBuilder.GetInstance(discardLargeInstances: false, out var result); using var _2 = ArrayBuilder<(int position, SyntaxNode argument, IParameterSymbol? parameter, HintKind kind)>.GetInstance(out var buffer); foreach (var node in root.DescendantNodes(textSpan, n => n.Span.IntersectsWith(textSpan))) @@ -75,7 +74,7 @@ public async Task> GetInlineHintsAsync( } } - return result.ToImmutableAndClear(); + return; void AddHintsIfAppropriate(SyntaxNode node) { diff --git a/src/Features/Core/Portable/InlineHints/AbstractInlineTypeHintsService.cs b/src/Features/Core/Portable/InlineHints/AbstractInlineTypeHintsService.cs index 3a3744256ba59..c184b2262612d 100644 --- a/src/Features/Core/Portable/InlineHints/AbstractInlineTypeHintsService.cs +++ b/src/Features/Core/Portable/InlineHints/AbstractInlineTypeHintsService.cs @@ -28,31 +28,30 @@ internal abstract class AbstractInlineTypeHintsService : IInlineTypeHintsService bool forCollectionExpressions, CancellationToken cancellationToken); - public async Task> GetInlineHintsAsync( + public async Task AddInlineHintsAsync( Document document, TextSpan textSpan, InlineTypeHintsOptions options, SymbolDescriptionOptions displayOptions, bool displayAllOverride, + ArrayBuilder result, CancellationToken cancellationToken) { var enabledForTypes = options.EnabledForTypes; if (!enabledForTypes && !displayAllOverride) - return []; + return; var forImplicitVariableTypes = enabledForTypes && options.ForImplicitVariableTypes; var forLambdaParameterTypes = enabledForTypes && options.ForLambdaParameterTypes; var forImplicitObjectCreation = enabledForTypes && options.ForImplicitObjectCreation; var forCollectionExpressions = enabledForTypes && options.ForCollectionExpressions; if (!forImplicitVariableTypes && !forLambdaParameterTypes && !forImplicitObjectCreation && !forCollectionExpressions && !displayAllOverride) - return []; + return; var anonymousTypeService = document.GetRequiredLanguageService(); var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - using var _1 = ArrayBuilder.GetInstance(out var result); - foreach (var node in root.DescendantNodes(n => n.Span.IntersectsWith(textSpan))) { var hint = TryGetTypeHint( @@ -97,8 +96,6 @@ public async Task> GetInlineHintsAsync( span, taggedText, textChange, ranking: InlineHintsConstants.TypeRanking, InlineHintHelpers.GetDescriptionFunction(spanStart, type, displayOptions))); } - - return result.ToImmutableAndClear(); } private static void AddParts( diff --git a/src/Features/Core/Portable/InlineHints/IInlineParameterNameHintsService.cs b/src/Features/Core/Portable/InlineHints/IInlineParameterNameHintsService.cs index 11c6455c1623c..f6c4118e94d26 100644 --- a/src/Features/Core/Portable/InlineHints/IInlineParameterNameHintsService.cs +++ b/src/Features/Core/Portable/InlineHints/IInlineParameterNameHintsService.cs @@ -2,11 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageService; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.InlineHints; @@ -17,11 +17,12 @@ namespace Microsoft.CodeAnalysis.InlineHints; /// internal interface IInlineParameterNameHintsService : ILanguageService { - Task> GetInlineHintsAsync( + Task AddInlineHintsAsync( Document document, TextSpan textSpan, InlineParameterHintsOptions options, SymbolDescriptionOptions displayOptions, bool displayAllOverride, + ArrayBuilder result, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/InlineHints/IInlineTypeHintsService.cs b/src/Features/Core/Portable/InlineHints/IInlineTypeHintsService.cs index 714a61da6feab..53093fa7bdf22 100644 --- a/src/Features/Core/Portable/InlineHints/IInlineTypeHintsService.cs +++ b/src/Features/Core/Portable/InlineHints/IInlineTypeHintsService.cs @@ -2,11 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageService; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.InlineHints; @@ -17,11 +17,12 @@ namespace Microsoft.CodeAnalysis.InlineHints; /// internal interface IInlineTypeHintsService : ILanguageService { - Task> GetInlineHintsAsync( + Task AddInlineHintsAsync( Document document, TextSpan textSpan, InlineTypeHintsOptions options, SymbolDescriptionOptions displayOptions, bool displayAllOverride, + ArrayBuilder result, CancellationToken cancellationToken); } From 04c8d65d338621a1ee103dc27ac0a2ac1bb0ec45 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Tue, 18 Nov 2025 08:44:49 -0800 Subject: [PATCH 2/2] Update src/Features/Core/Portable/InlineHints/AbstractInlineParameterNameHintsService.cs Co-authored-by: Cyrus Najmabadi --- .../InlineHints/AbstractInlineParameterNameHintsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Features/Core/Portable/InlineHints/AbstractInlineParameterNameHintsService.cs b/src/Features/Core/Portable/InlineHints/AbstractInlineParameterNameHintsService.cs index 79d5ae815048a..e1719337135ac 100644 --- a/src/Features/Core/Portable/InlineHints/AbstractInlineParameterNameHintsService.cs +++ b/src/Features/Core/Portable/InlineHints/AbstractInlineParameterNameHintsService.cs @@ -60,7 +60,7 @@ public async Task AddInlineHintsAsync( var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); var syntaxFacts = document.GetRequiredLanguageService(); - using var _2 = ArrayBuilder<(int position, SyntaxNode argument, IParameterSymbol? parameter, HintKind kind)>.GetInstance(out var buffer); + using var _ = ArrayBuilder<(int position, SyntaxNode argument, IParameterSymbol? parameter, HintKind kind)>.GetInstance(out var buffer); foreach (var node in root.DescendantNodes(textSpan, n => n.Span.IntersectsWith(textSpan))) {