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

Conversation

ToddGrun
Copy link
Contributor

Noticed in customer trace, ~35% of allocations over a nearly two minute period are in razor's sourcegenerator's call to the TagHelperDirectiveVisitor.

As the DirectiveVisitor construction and usage are scoped to the calling method (DefaultRazorTagHelperContextDiscoveryPhase.ExecuteCore), it's easy enough to make the DirectiveVisitor disposable and to make the DirectiveVisitor use a pooled array for it's TagHelperDescriptor array.

*** from the customer trace ***
image

Noticed in customer trace, ~35% of allocations over a nearly two minute period are in razor's sourcegenerator's call to the TagHelperDirectiveVisitor.

As the DirectiveVisitor construction and usage are scoped to the calling method (DefaultRazorTagHelperContextDiscoveryPhase.ExecuteCore), it's easy enough to make the DirectiveVisitor disposable and to make the DirectiveVisitor use a pooled array for it's TagHelperDescriptor array.
@ToddGrun ToddGrun requested a review from a team as a code owner June 25, 2024 20:46
@ryzngard
Copy link
Contributor

@ToddGrun since this is a compiler change it will need 2 sign offs from that team prior to merging. Mine does not count towards that

Copy link
Contributor

@davidwengier davidwengier left a comment

Choose a reason for hiding this comment

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

Feel free to ignore my comments, as a non compiler dev :)

@@ -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)

}

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!

@ToddGrun ToddGrun merged commit 928fe61 into main Jun 27, 2024
12 checks passed
@ToddGrun ToddGrun deleted the dev/toddgrun/DirectiveVisitorAllocations branch June 27, 2024 00:05
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Jun 27, 2024
DustinCampbell added a commit that referenced this pull request Jul 16, 2024
…10602)

This is some follow-up work after a discussion with @ToddGrun.
Previously, @ToddGrun made [a
change](#10537) to use pooled lists
within the `DirectiveVisistors` used by
`DefaultRazorTagHelperContextDiscoveryPhase`. However, looking at
further traces, there is still a ton of CPU-bound work. Notably, there
are loads of string comparisons that happen over and over to compare
strings for assembly names. To address this, I've made the following
changes:

1. Instead of using pooled lists, the `DirectiveVisitors` themselves are
pooled and they own their data structures.
2. `TagHelperDirectiveVisitor` has been updated to store a
`Dictionary<string, List<TagHelperDescriptor>>` rather than a flat
`List<TagHelperDescriptor>`. This is used to index tag helpers by
assembly name, which greatly reduces the number of string comparisons.
The `List<TagHelperDescriptor>` held stored in the dictionary are pooled
to avoid extra allocations of tag helpers per assembly.
@RikkiGibson RikkiGibson modified the milestones: Next, 17.12 Preview 1 Jul 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants