-
Notifications
You must be signed in to change notification settings - Fork 10.1k
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
Amortize HttpRequestStream & DuplexPipeStream ReadAsync calls #11942
Conversation
Hide whitespace shows the change in the main methods better https://github.com/aspnet/AspNetCore/pull/11942/files?w=1 |
src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestStream.cs
Outdated
Show resolved
Hide resolved
6eb9771
to
3b40f05
Compare
What's the single-threaded CPU overhead of this approach? |
28e2e14
to
86032fb
Compare
src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestStream.cs
Outdated
Show resolved
Hide resolved
src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestStream.cs
Outdated
Show resolved
Hide resolved
86032fb
to
3b5f879
Compare
src/Servers/Kestrel/Core/src/Internal/Infrastructure/AsyncEnumerableReader.cs
Outdated
Show resolved
Hide resolved
Looks good; it only engages the AsynEnumerable when there is no data and goes back to sync when there is data: InMemoryTransportBenchmark Method | Mean | Error | StdDev | Median | Op/s | Allocated |
------------------- |---------:|----------:|-----------:|---------:|---------:|----------:|
- Plaintext | 84.22 us | 3.2848 us | 9.1020 us | 86.95 us | 11,874.2 | 352 B |
+ Plaintext | 79.11 us | 3.8055 us | 10.8573 us | 83.59 us | 12,641.1 | 348 B |
- PlaintextPipelined | 11.52 us | 0.2011 us | 0.1783 us | 11.46 us | 86,778.7 | 368 B |
+ PlaintextPipelined | 10.50 us | 0.2045 us | 0.1913 us | 10.46 us | 95,220.7 | 355 B | |
|
||
return actual; | ||
// Finally blocks in enumerators aren't excuted prior to the yield return, | ||
// so we advance here |
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.
Hm... that seems like it will be a bug farm in the future.
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 don't think this is that bad. The finally still runs given an uncaught exception or the completion of the method. I think it would be more weird if the finally did run for every yield return.
4dd5bde
to
38690bc
Compare
Changed to use |
4af0241
to
2835575
Compare
Rebased |
src/Servers/Kestrel/Core/src/Internal/Http2/Http2MessageBody.cs
Outdated
Show resolved
Hide resolved
@davidfowl @jkotalik Can you also take a look at this PR? It LGTM. |
33b7347
to
ca00257
Compare
Rebased |
@halter73 #12639 fixed the counting, but still needs the timing change
|
ca00257
to
9137a7f
Compare
var result = _context.RequestBodyPipe.Reader.TryRead(out readResult); | ||
_readResult = readResult; | ||
CountBytesRead(readResult.Buffer.Length); | ||
TryStart(); |
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.
@halter73 note: had to include this Http2MessageBody
change for TryStart
/TryStop
to prevent test failures.
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 definitely feels better to call TryStart here. It does important stuff that isn't even timing related like checking that the request body size isn't larger than what's allowed.
What test failures were you seeing specifically.
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.
One example was the following though there were a couple different types
Error message
Moq.MockException :
Expected invocation on the mock should never have been performed, but was 32 times:
h => h.OnTimeout(It.IsAny<TimeoutReason>())
Configured setups:
ITimeoutHandler h => h.OnTimeout(It.IsAny<TimeoutReason>())
Performed invocations:
ITimeoutHandler.OnTimeout(TimeoutReason.ReadDataRate)
ITimeoutHandler.OnTimeout(TimeoutReason.ReadDataRate)
ITimeoutHandler.OnTimeout(TimeoutReason.ReadDataRate)
ITimeoutHandler.OnTimeout(TimeoutReason.ReadDataRate)
ITimeoutHandler.OnTimeout(TimeoutReason.ReadDataRate)
...
Stack trace
at Moq.Mock.VerifyCalls(Mock targetMock, InvocationShape expectation, LambdaExpression expression, Times times, String failMessage)
at Moq.Mock.Verify[T](Mock`1 mock, Expression`1 expression, Times times, String failMessage)
at Moq.Mock`1.Verify(Expression`1 expression, Times times)
at Moq.Mock`1.Verify(Expression`1 expression, Func`1 times)
at Microsoft.AspNetCore.Server.Kestrel.Core.Tests.Http2TimeoutTests.DATA_Received_TooSlowlyOnSecondStream_AbortsConnectionAfterNonAdditiveRateTimeout() in /_/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs:line 775
--- End of stack trace from previous location where exception was thrown ---
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.
Ah. That makes sense. If you start reading a request body with TryRead, and then call ReadAsync, Kestrel will reset _readTimingBytesRead to 0 on the first ReadAsync call ignoring the bytes counted by TryRead.
I wonder how common using both TryRead and ReadAsync on the same RequestBody will be before we do it implicitly ReadAsyncInternal as part of this PR. We might want to backport this part of the change.
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.
Does Http1ContentLengthMessageBody
need this?
Http1ChunkedEncodingMessageBody
has it; but ContentLength doesn't have the same pattern (TryRead
earlier and TryStop
)
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.
Ah; is correct TryStop
is in CreateReadResultFromConnectionReadResult
Added isolated change for Http2 #13305
Probably want to hold on this for now until dotnet/coreclr#26310 is evaluated |
I'll welcome your help evaluating it :) |
I asked @halter73 what the deal is here and he said "oooh, this is a spicy one, but now that we're in 5.0, we can look at it". 😉 @davidfowl @halter73 Let's re-open this now that 5.0 is open |
dotnet/coreclr#26310 did get merged, so adding |
Agreed; but it still needs to be evaluated as my be backed out https://github.com/dotnet/coreclr/issues/27403 /cc @sebastienros |
My hope is that the functionality from #27403 remains and ends up being enabled by default in .NET 5. But I need help from folks in vetting that and coming back with perf numbers from workloads to justify it. Even if that ships on-by-default, though, there's potentially value in optimizing critical cases with hand-rolled implementations, if doing so can, for example, do a more efficient job of caching than the built-in caching. And as any such hand-rolled implementation involves a ton more code, it would need to be so much better that it was worth the maintenance / bug risk that is likely in such complicated code. I don't know if this particular case / implementation meets that bar. |
(btw, I've implemented an analyzer to help flag cases of ValueTask misuse that can cause problems as more and more such caching is applied: dotnet/roslyn-analyzers#3001) |
@benaadams can you rebase to resolve conflicts? @halter73 @jkotalik are we good to merge this once conflicts resolved (and failing builds either kicked into submission or fixed ;)) |
We should make the pipelines change @benaadams |
What changes exactly are you referring to? |
@benaadams @davidfowl @halter73 dotnet/coreclr#26310 has been merged now. Do we still want this change? I'd like to clean it up if not. |
No it isn't needed currently. |
Sweet! Thanks! |
if
_pipeReader.TryRead
fails to get the data synchronously defer to anIAsyncEnumerable<int>
that is allocated once on the first failure. (Use case WebSockets/SignalR over TLS)Before
After
I used an IAsyncEnumerable as then the code very closely mimics the original read loop; other than adding
yield
before thereturn
s and a slightly modified behaviour for thefinally
clause.Resolves: #11940
/cc @stephentoub @davidfowl @halter73