-
Notifications
You must be signed in to change notification settings - Fork 10.2k
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
Fix corrupted streaming SSR output #50380
Fix corrupted streaming SSR output #50380
Conversation
@@ -23,7 +23,7 @@ internal HtmlRootComponent(StaticHtmlRenderer renderer, int componentId, Task qu | |||
/// <summary> | |||
/// Gets a <see cref="Task"/> that completes when the component hierarchy has completed asynchronous tasks such as loading. | |||
/// </summary> | |||
public Task QuiescenceTask { get; } = Task.CompletedTask; | |||
public Task QuiescenceTask { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's always assigned during the constructor anyway.
@@ -72,9 +73,7 @@ private async Task RenderComponentCore(HttpContext context) | |||
// Matches MVC's MemoryPoolHttpResponseStreamWriterFactory.DefaultBufferSize | |||
var defaultBufferSize = 16 * 1024; | |||
await using var writer = new HttpResponseStreamWriter(context.Response.Body, Encoding.UTF8, defaultBufferSize, ArrayPool<byte>.Shared, ArrayPool<char>.Shared); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't we buffering twice? Once here, and once in the BufferedTextWriter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's a good point. I retained this usage of HttpResponseStreamWriter
from the earlier implementation but it might be that we could simply omit it and have TextChunkListBuilder
able to WriteAsync
directly to Response.Body
.
I think the benefit of HttpResponseStreamWriter
is that it's making fewer calls to Encoding.UTF8.Encode
, as it only encodes once each time its buffer fills up. But I don't really know what level of perf gain this gives, if any.
I'll benchmark it both ways and go with whatever appears to be more performant. That sound OK?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just saw it and I think we would remove one buffering layer, which is generally good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I looked into this and tried replacing HttpResponseStreamWriter
with:
StreamWriter
. This works but is marginally slower thanHttpResponseStreamWriter
. Perhaps the default buffer size inHttpResponseStreamWriter
is just better tuned to these cases. They are doing mostly the same things.- No string encoding buffer at all and writing directly to the
Stream
. This was much more complicated, since it's necessary to then replicate a lot of whatStreamWriter
orHttpResponseStreamWriter
do to encode the different types of things we might be writing. And doing it in a simplistic way resulted in much lower perf than the above techniques - I suspect the optimal way might turn out to be the same as copying the internals fromStreamWriter
orHttpResponseStreamWriter
.
So I think we'll stick with HttpResponseStreamWriter
as in practice it appears to give the best results, and is the most well-tested code path in this scenario. It was worth looking into though, so thanks for the suggestion!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The solution looks reasonable, but I have a higher-level question.
Instead of dealing with this at the buffer level. Would it make sense to deal with this at the HtmlRenderer somehow?
Is the challenge here that since UpdateDisplayAsync is not awaited, the contents of the RenderTreeFrames
might change while we are waiting to write the response and the rendering continues?
Is that the reason why we need to buffer in this way?
- Would it work if we buffered more like Blazor Server (Take a Snapshot) then continue rendering? And then simply "emit" the sequence of snapshots (which are the updates I believe).
- Is my understanding correct in that this is more or less what's already happening?
It is more or less equivalent. The only difference is that the approach used here ( Strictly speaking this is an internal implementation detail - we could change the buffering format in the future if we wanted - but this PR is the more minimal change vs what we've been testing so far in .NET 8, and I'm not aware we'd get any extra benefit from going through a SerializedRenderBatch representation. |
The current approach is fine I think, I was mostly asking because it's a bit hard to reason about the interaction at this level. I was not suggesting that we serialize the render batch either (exactly like we do on Blazor Server), but wanted to draw the parallel to see if I was getting what was going on correctly. We might benefit in the future from pushing this directly into the HTML renderer, as it might simplify the interactions (it would look more like what happens in the remote renderer, again). |
Fixes #50198
For details about the problem causing corrupted output, see #50198 (comment). As part of investigating, I also discovered that the MVC-style
ViewBufferTextWriter
solution performed very badly, reducing the number of requests per second by over half (compared with unbuffered output).So, the fix here is to replace the buffering layer with a new implementation that is:
FlushAsync
is still in progress (fixing the corruption)IHtmlContent
that we were not actually using (fixing the performance)Benchmarks
Consider two cases:
BlazorUnitedSample
'sIndex
page but with the<EditForm>
commented out, so it's just rendering the layout with nothing on the page.BlazorUnitedSample
'sFetchData
page but with the number of weather forecasts increased to 5000 and theawait Task.Delay
removed.ArrayPool.Shared
with a per-response pool on top of that to reduce contention.Note that after measuring the above, I also added a further optimization to TextChunkListBuilder and now just measured 64233 but didn't re-run the other cases so it's possible my laptop is just being faster right now.
E2E test
It's hard to repro #50198 in an E2E test because it's based on timing of network traffic. I found a way to reproduce the corruption by picking specific numbers that surface the problem when I run locally. It's possible that on another machine the E2E test would pass anyway even without this fix. However with this fix, the E2E test should always pass.