From a14ba4412a69b59c15e08f9b3c0d57d7572ac9cd Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Fri, 10 Oct 2025 07:07:25 -0700 Subject: [PATCH] Modify TagHelperCollector to use it's per assembly cache more often The caching done in TagHelperCollector.Collect(TagHelperDescriptorProviderContext, CancellationToken) was only being done on the reference assemblies. This PR also adds that caching for the target or compilation assembly. Test insertion: https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/678059 Results from the speedometer tests in the above insertion looked good. The table below shows the average CPU/Allocs in the CA process for the CompletionInCohosting test under the TagHelperCollector.Collect method. | Version| CPU (ms) | CPU (%) | Allocs MB | Allocs (%) | |---|---|---|---|---| | Original| 4600 | 7.7 | 291 | 12.3 | | New | 2965 | 5.4 | 110 | 5.9 | --- .../CSharp/BindTagHelperDescriptorProvider.cs | 14 +++-- .../src/Language/TagHelperCollector.cs | 59 +++++++++++-------- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/BindTagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/BindTagHelperDescriptorProvider.cs index dc14b840fbe..caca023e27d 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/BindTagHelperDescriptorProvider.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/BindTagHelperDescriptorProvider.cs @@ -225,17 +225,21 @@ protected override bool IsCandidateType(INamedTypeSymbol types) => types.DeclaredAccessibility == Accessibility.Public && types.Name == "BindAttributes"; - protected override void Collect(IAssemblySymbol assembly, ICollection results, CancellationToken cancellationToken) + public override void Collect(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken) { + // Overrides the outermost version of Collect as TagHelperCollector.Collect(context, token) + // caches results per assembly. This provider needs to operate on the full set of calculated + // descriptors to handle case #4. + // First, collect the initial set of tag helpers from this assembly. This calls // the Collect(INamedTypeSymbol, ...) overload below for cases #2 & #3. - base.Collect(assembly, results, cancellationToken); + base.Collect(context, cancellationToken); // Then, for case #4 we look at the tag helpers that were already created corresponding to components // and pattern match on properties. - using var componentBindTagHelpers = new PooledArrayBuilder(capacity: results.Count); + using var componentBindTagHelpers = new PooledArrayBuilder(capacity: context.Results.Count); - foreach (var tagHelper in results) + foreach (var tagHelper in context.Results) { cancellationToken.ThrowIfCancellationRequested(); @@ -244,7 +248,7 @@ protected override void Collect(IAssemblySymbol assembly, ICollection results, CancellationToken cancellationToken); - public void Collect(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken) + public virtual void Collect(TagHelperDescriptorProviderContext context, CancellationToken cancellationToken) { if (_targetAssembly is not null) { - Collect(_targetAssembly, context.Results, cancellationToken); + CollectTagHelpersForAssembly(_targetAssembly, context, cancellationToken); } else { - Collect(_compilation.Assembly, context.Results, cancellationToken); + CollectTagHelpersForAssembly(_compilation.Assembly, context, cancellationToken); foreach (var reference in _compilation.References) { @@ -45,34 +45,41 @@ public void Collect(TagHelperDescriptorProviderContext context, CancellationToke if (_compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly) { - // Check to see if we already have tag helpers cached for this assembly - // and use the cached versions if we do. Roslyn shares PE assembly symbols - // across compilations, so this ensures that we don't produce new tag helpers - // for the same assemblies over and over again. + CollectTagHelpersForAssembly(assembly, context, cancellationToken); + } + } + } - var assemblySymbolData = SymbolCache.GetAssemblySymbolData(assembly); - if (!assemblySymbolData.MightContainTagHelpers) - { - continue; - } + return; - var includeDocumentation = context.IncludeDocumentation; - var excludeHidden = context.ExcludeHidden; + void CollectTagHelpersForAssembly(IAssemblySymbol assembly, TagHelperDescriptorProviderContext context, CancellationToken cancellationToken) + { + // Check to see if we already have tag helpers cached for this assembly + // and use the cached versions if we do. Roslyn shares PE assembly symbols + // across compilations, so this ensures that we don't produce new tag helpers + // for the same assemblies over and over again. - var cache = s_perAssemblyCaches.GetValue(assembly, static assembly => new Cache()); - if (!cache.TryGet(includeDocumentation, excludeHidden, out var tagHelpers)) - { - using var _ = ListPool.GetPooledObject(out var referenceTagHelpers); - Collect(assembly, referenceTagHelpers, cancellationToken); + var assemblySymbolData = SymbolCache.GetAssemblySymbolData(assembly); + if (!assemblySymbolData.MightContainTagHelpers) + { + return; + } - tagHelpers = cache.Add(referenceTagHelpers.ToArrayOrEmpty(), includeDocumentation, excludeHidden); - } + var includeDocumentation = context.IncludeDocumentation; + var excludeHidden = context.ExcludeHidden; - foreach (var tagHelper in tagHelpers) - { - context.Results.Add(tagHelper); - } - } + var cache = s_perAssemblyCaches.GetValue(assembly, static assembly => new Cache()); + if (!cache.TryGet(includeDocumentation, excludeHidden, out var tagHelpers)) + { + using var _ = ListPool.GetPooledObject(out var referenceTagHelpers); + Collect(assembly, referenceTagHelpers, cancellationToken); + + tagHelpers = cache.Add(referenceTagHelpers.ToArrayOrEmpty(), includeDocumentation, excludeHidden); + } + + foreach (var tagHelper in tagHelpers) + { + context.Results.Add(tagHelper); } } }