Skip to content

Reduce allocations in SourceTextStream ctor#80289

Merged
ToddGrun merged 3 commits intodotnet:mainfrom
ToddGrun:dev/toddgrun/SourceTextStreamAllocations
Sep 16, 2025
Merged

Reduce allocations in SourceTextStream ctor#80289
ToddGrun merged 3 commits intodotnet:mainfrom
ToddGrun:dev/toddgrun/SourceTextStreamAllocations

Conversation

@ToddGrun
Copy link
Copy Markdown
Contributor

Simple switch to use a pooled char[] as the class already has a lifetime mechanism we can hook into.

Not a huge win, but it's 0.1% (2.3 MB) of all allocations in the RoslynCodeAnalysisService process in the razor speedometer test,.

image

Simple switch to use a pooled char[] as the class already has a lifetime mechanism we can hook into.

Not a huge win, but it's 0.1% (2.3 MB) of all allocations in the RoslynCodeAnalysisService process in the razor speedometer test,.
@ToddGrun ToddGrun requested a review from a team as a code owner September 15, 2025 21:42

protected override void Dispose(bool disposing)
{
s_charArrayPool.Free(_charBuffer);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we only do this when disposing parameter is true?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think the disposing bool is something we should key off here, but it's a good point that the code should probably be more resilient to double dispose.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As far as I understand, disposing is false in destructor (when the object is being collected by GC) and there we cannot rely on any other objects existing (they can be freed already). I'm not sure null check is enough in that case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's a good way to phrase that, I'll add a disposing check too. Someone should probably take a pass through DIspose(bool disposing) implementations, as some of them also don't use the disposing bool.

var sourceText = SourceText.From(text, encoding);
using (var stream = new SourceTextStream(sourceText, bufferSize: text.Length * 2))
using (var stream = new SourceTextStream(sourceText))
{
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Sep 16, 2025

Choose a reason for hiding this comment

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

How do we know that this change doesn't reduce test coverage? Could it be that the test scenario is not setup properly now? #Closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll take a closer look and make sure the coverage isn't reduced.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This test is a little tricky. I think my confusion lied on whether it was validating whether the buffer it allocated was being filled properly, or whether the buffer SourceTextStream allocated was being refilled properly. I think you're right and that it's the latter.

I've modified the test such that baseText is now one shorter than the array that SourceTextStream uses to match the old behavior.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It looks like this test was added in 6313aa3. Please confirm that the modified test is failing if the fix is undone.

Copy link
Copy Markdown
Contributor Author

@ToddGrun ToddGrun Sep 16, 2025

Choose a reason for hiding this comment

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

Left tab: Local reversion of the changes in the commit you pointed too
Middle tab: New test contents
Right tab: Test explorer window validating test failiure

image

if (_charBuffer != null)
{
s_charArrayPool.Free(_charBuffer);
_charBuffer = null!;
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Sep 16, 2025

Choose a reason for hiding this comment

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

_charBuffer = null!;

Why is this safe to do? #Closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was more concerned about keeping a reference to _charBuffer, as then any usage of it by the class would then be operating on something that had already been returned to the pool, and that's hard to track down. If you don't like the setting of it to null, we could instead use a bool disposed field and check that before all usage of _charBuffer.

Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Sep 16, 2025

Choose a reason for hiding this comment

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

we could instead use a bool disposed field and check that before all usage of _charBuffer.

And throw an exception? How is that different? An existing code might still break.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To be clear, I don't think that adding a bool _disposed field is warranted, and I prefer setting _charBuffer to null.

If _charBuffer is used while null, that would mean that someone is using this stream after it has been disposed. I think that warrants an exception, but if you are wary of throwing an exception in that scenario, then a bool field could be added to avoid that exception at use sites of _charBuffer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It is not clear to me what you are suggesting to do. The change, as it stands right now, affects behavior of the component beyond just changing an allocation strategy. We might discuss what should/could be the safe usage pattern for the component and might even agree on that, but that wouldn't have any effect on the way this component is already used. When I see a PR with a title "Reduce allocations in SourceTextStream ctor". I generally do not expect to see any observable behavior changes.

Copy link
Copy Markdown
Contributor Author

@ToddGrun ToddGrun Sep 16, 2025

Choose a reason for hiding this comment

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

If we can agree on that the code could generally throw if used after disposal, and we can validate that no code currently does that and so doing that doesn't break any existing assumptions, would that be sufficient?

There is only a single usage of this class in product code, and that code disposes the stream and makes no access to it after that point.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we can agree on that the code could generally throw if used after disposal, and we can validate that no code currently does that and so doing that doesn't break any existing assumptions, would that be sufficient?

I think it would.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There is only a single usage of this class in product code, and that code disposes the stream and makes no access to it after that point.

This addresses my concerns

@AlekseyTs
Copy link
Copy Markdown
Contributor

AlekseyTs commented Sep 16, 2025

Done with review pass (commit 2). At a glance, the change looks risky to me. #Closed

2) Modify test back to original intent
@AlekseyTs
Copy link
Copy Markdown
Contributor

AlekseyTs commented Sep 16, 2025

Done with review pass (commit 3). Waiting on a confirmation regarding the modified test. #Closed

Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 3)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants