Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce allocations in Razor's DirectiveVisitor #10537

Merged
merged 2 commits into from
Jun 27, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ protected override void ExecuteCore(RazorCodeDocument codeDocument)
var context = TagHelperDocumentContext.Create(tagHelperPrefix, matches.ToImmutableArray());
codeDocument.SetTagHelperContext(context);
codeDocument.SetPreTagHelperSyntaxTree(syntaxTree);

visitor.Dispose();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using (visitor) { on line 55-ish, and ending it here, so its clearer to everyone that its being disposed, and so that there is a try..finally in place

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can change to that, I didn't go that route initially as it didn't fall directly into the using pattern and it's really not a big deal if the disposal doesn't end up happening.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and it's really not a big deal if the disposal doesn't end up happening

Is it not? I would have thought that would mean the pool allocates lots of new builders, defeating the purpose of the PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me rephrase, it's not a big deal if it's an exceptional case where this occurs, not if it's the normal code path (which would probably indicate bigger problems than avoiding these allocations)

}

private static bool MatchesDirective(TagHelperDescriptor descriptor, string typePattern, string assemblyName)
Expand Down Expand Up @@ -106,32 +108,34 @@ private static ReadOnlySpan<char> RemoveGlobalPrefix(in ReadOnlySpan<char> span)
return span;
}

internal abstract class DirectiveVisitor(HashSet<TagHelperDescriptor> matches) : SyntaxWalker
internal abstract class DirectiveVisitor(HashSet<TagHelperDescriptor> matches) : SyntaxWalker, IDisposable
{
protected readonly HashSet<TagHelperDescriptor> Matches = matches;

public abstract string TagHelperPrefix { get; }

public abstract void Visit(RazorSyntaxTree tree);

public abstract void Dispose();
}

internal sealed class TagHelperDirectiveVisitor : DirectiveVisitor
{
private readonly List<TagHelperDescriptor> _tagHelpers;
private readonly PooledObject<ImmutableArray<TagHelperDescriptor>.Builder> _tagHelpers;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random thought: Is ImmutableArray the best choice here? Since this builder is never realized, a pooled list might be just as suitable, but a bit simpler, especially if an initial capacity can be set?

Looks like ImmutableArray<>.Builder just resizes an array anyway, so I can't imagine List would be any worse, but I don't really have any knowledge here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or even just a PooledArrayBuilder<T>?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried PooledArrayBuilder initially, and it didn't seem to work because I was trying to store that into a field, and that type is defined as a NonCopyable struct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't go with PooledList as I talked to Andrew and we weren't sure of it's benefit. It's a struct, but not marked as non-copyable, so it could be used, but it doesn't seem to be used a lot throughout the codebase. I'm flexible, and can go with that (or whatever else works) if you'd prefer.

Copy link
Contributor

@davidwengier davidwengier Jun 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, PooledList can't be used, but we also have ListPool which can. Isn't coding fun?!

(sorry, my bad when i said "pooled list" and wasn't specific. I knew we had one that was a ref struct, but couldn't remember which)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this feels cleaner. Thanks for the suggestions!

private string _tagHelperPrefix;

public TagHelperDirectiveVisitor(IReadOnlyList<TagHelperDescriptor> tagHelpers, HashSet<TagHelperDescriptor> matches)
: base(matches)
{
// We don't want to consider components in a view document.
_tagHelpers = new();
_tagHelpers = ArrayBuilderPool<TagHelperDescriptor>.GetPooledObject();

for (var i = 0; i < tagHelpers.Count; i++)
{
var tagHelper = tagHelpers[i];
if (!tagHelper.IsAnyComponentDocumentTagHelper())
{
_tagHelpers.Add(tagHelper);
_tagHelpers.Object.Add(tagHelper);
}
}
}
Expand Down Expand Up @@ -165,13 +169,13 @@ public override void VisitRazorDirective(RazorDirectiveSyntax node)
continue;
}

if (!AssemblyContainsTagHelpers(addTagHelper.AssemblyName, _tagHelpers))
if (!AssemblyContainsTagHelpers(addTagHelper.AssemblyName, _tagHelpers.Object))
{
// No tag helpers in the assembly.
continue;
}

foreach (var tagHelper in _tagHelpers)
foreach (var tagHelper in _tagHelpers.Object)
{
if (MatchesDirective(tagHelper, addTagHelper.TypePattern, addTagHelper.AssemblyName))
{
Expand All @@ -189,13 +193,13 @@ public override void VisitRazorDirective(RazorDirectiveSyntax node)
}


if (!AssemblyContainsTagHelpers(removeTagHelper.AssemblyName, _tagHelpers))
if (!AssemblyContainsTagHelpers(removeTagHelper.AssemblyName, _tagHelpers.Object))
{
// No tag helpers in the assembly.
continue;
}

foreach (var tagHelper in _tagHelpers)
foreach (var tagHelper in _tagHelpers.Object)
{
if (MatchesDirective(tagHelper, removeTagHelper.TypePattern, removeTagHelper.AssemblyName))
{
Expand All @@ -217,7 +221,7 @@ public override void VisitRazorDirective(RazorDirectiveSyntax node)
}
}

private static bool AssemblyContainsTagHelpers(string assemblyName, List<TagHelperDescriptor> tagHelpers)
private static bool AssemblyContainsTagHelpers(string assemblyName, ImmutableArray<TagHelperDescriptor>.Builder tagHelpers)
{
foreach (var tagHelper in tagHelpers)
{
Expand All @@ -229,11 +233,16 @@ private static bool AssemblyContainsTagHelpers(string assemblyName, List<TagHelp

return false;
}

public override void Dispose()
{
_tagHelpers.Dispose();
}
}

internal sealed class ComponentDirectiveVisitor : DirectiveVisitor
{
private readonly ImmutableArray<TagHelperDescriptor> _notFullyQualifiedComponents;
private readonly PooledObject<ImmutableArray<TagHelperDescriptor>.Builder> _notFullyQualifiedComponents;
private readonly string _filePath;
private RazorSourceDocument _source;

Expand All @@ -242,7 +251,7 @@ public ComponentDirectiveVisitor(string filePath, IReadOnlyList<TagHelperDescrip
{
_filePath = filePath;

using var builder = new PooledArrayBuilder<TagHelperDescriptor>(capacity: tagHelpers.Count);
_notFullyQualifiedComponents = ArrayBuilderPool<TagHelperDescriptor>.GetPooledObject();

for (var i = 0; i < tagHelpers.Count; i++)
{
Expand All @@ -260,7 +269,7 @@ public ComponentDirectiveVisitor(string filePath, IReadOnlyList<TagHelperDescrip
continue;
}

builder.Add(tagHelper);
_notFullyQualifiedComponents.Object.Add(tagHelper);

if (currentNamespace is null)
{
Expand All @@ -282,8 +291,6 @@ public ComponentDirectiveVisitor(string filePath, IReadOnlyList<TagHelperDescrip
Matches.Add(tagHelper);
}
}

_notFullyQualifiedComponents = builder.DrainToImmutable();
}

// There is no support for tag helper prefix in component documents.
Expand Down Expand Up @@ -343,7 +350,7 @@ public override void VisitRazorDirective(RazorDirectiveSyntax node)
continue;
}

foreach (var tagHelper in _notFullyQualifiedComponents)
foreach (var tagHelper in _notFullyQualifiedComponents.Object)
{
Debug.Assert(!tagHelper.IsComponentFullyQualifiedNameMatch, "We've already processed these.");

Expand Down Expand Up @@ -427,5 +434,10 @@ internal static bool IsTagHelperFromMangledClass(TagHelperDescriptor tagHelper)
{
return ComponentMetadata.IsMangledClass(tagHelper.GetTypeNameIdentifier());
}

public override void Dispose()
{
_notFullyQualifiedComponents.Dispose();
}
}
}
Loading