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

Remove two async state machines for typical HTTP/1.1 request path #58092

Merged
merged 5 commits into from
Aug 27, 2021

Conversation

stephentoub
Copy link
Member

@stephentoub stephentoub commented Aug 25, 2021

Rather than having a dedicated async method for SendAndProcessAltSvcAsync, we can just have the sole call site call DetermineVersionAndSendAsync and ProcessAltSvc. And in DetermineVersionAndSendAsync, special-case the default configuration for SocketsHttpHandler that will result in HTTP/1.1 messages being sent, such that we avoid all the tests for HTTP/2 and HTTP/3 and the async state machine for DetermineVersionAndSendAsync when HTTP/1.1 is being requested and used.

This doesn't entirely get us back to the allocation profile of .NET 5, but it's much closer.

Method Toolchain Mean Error StdDev Ratio Allocated
Request \5.0.9\corerun.exe 56.43 us 0.829 us 0.735 us 1.00 1,416 B
Request \main\corerun.exe 56.15 us 0.735 us 0.688 us 1.00 1,886 B
Request \pr\corerun.exe 55.07 us 0.823 us 0.770 us 0.97 1,568 B

I also noticed an unused parameter to the HTTP/2 and HTTP/3 try send async methods, so I removed those as well, removing those parameters from the corresponding state machines.

Best reviewed without whitespace diffing: https://github.com/dotnet/runtime/pull/58092/files?w=1

Fixes #57977

@stephentoub stephentoub added this to the 6.0.0 milestone Aug 25, 2021
@ghost
Copy link

ghost commented Aug 25, 2021

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Issue Details

Rather than having a dedicated async method for SendAndProcessAltSvcAsync, we can just have the sole call site call DetermineVersionAndSendAsync and ProcessAltSvc. And in DetermineVersionAndSendAsync, special-case the default configuration for SocketsHttpHandler that will result in HTTP/1.1 messages being sent, such that we avoid all the tests for HTTP/2 and HTTP/3 and the async state machine for DetermineVersionAndSendAsync when HTTP/1.1 is being requested and used.

This doesn't entirely get us back to the allocation profile of .NET 5, but it's much closer.

Method Toolchain Mean Error StdDev Ratio Allocated
Request \5.0.9\corerun.exe 56.43 us 0.829 us 0.735 us 1.00 1,416 B
Request \main\corerun.exe 56.15 us 0.735 us 0.688 us 1.00 1,886 B
Request \pr\corerun.exe 55.07 us 0.823 us 0.770 us 0.97 1,568 B

I also noticed an unused parameter to the HTTP/2 and HTTP/3 try send async methods, so I removed those as well, removing those parameters from the corresponding state machines.

Fixes #57977

Author: stephentoub
Assignees: -
Labels:

area-System.Net.Http, tenet-performance

Milestone: 6.0.0

@geoffkizer
Copy link
Contributor

Should we keep #57977 open and consider other improvements, or do we think this is good for 6.0?

@stephentoub
Copy link
Member Author

Should we keep #57977 open and consider other improvements, or do we think this is good for 6.0?

The changes in this PR seemed reasonable to me even without a goal of reducing state machines, i.e. I don't feel like we're sacrificing layering or factoring or anything like that. Beyond these two, though, it seems like we would have to make such choices, which makes me uncomfortable. I think we can live with the resulting 10% allocation increase over .NET 5 for this raw, do-as-little-as-possible benchmark. The moment you start doing anything more real, this won't matter. And if you start touching headers, .NET 6 is already way better.

That said, if you had ideas for getting rid of the SendUsingHttp11Async state machine (it's basically what remains now), I'd be interested.

There's also the option of using the pooling async method builder, but I think it's late in the game at this point to be exploring that here.

@geoffkizer
Copy link
Contributor

We could just inline GetHttp11ConnectionAsync into SendUsingHttp11Async.

@stephentoub
Copy link
Member Author

We could just inline GetHttp11ConnectionAsync into SendUsingHttp11Async.

Would you be ok with that? That felt like something you were trying to keep separate. But if you're cool with it, I'll add it to this PR.

@geoffkizer
Copy link
Contributor

Would you be ok with that? That felt like something you were trying to keep separate. But if you're cool with it, I'll add it to this PR.

I think it's fine. SendUsingHttp11Async is not doing much currently aside from calling GetHttp11ConnectionAsync and then actually doing the send on the connection.

@stephentoub
Copy link
Member Author

Oh, wait, that's not going to help. GetHttp11ConnectionAsync typically completes synchronously. It's SendUsingHttp11Async itself we'd need to inline, into SendWithRetryAsync.

@geoffkizer
Copy link
Contributor

It's SendUsingHttp11Async itself we'd need to inline, into SendWithRetryAsync.

That doesn't seem crazy either.

@stephentoub
Copy link
Member Author

It's SendUsingHttp11Async itself we'd need to inline, into SendWithRetryAsync.

That doesn't seem crazy either.

Inlining it would look like:
stephentoub@d93ed57
You're ok with that?

@geoffkizer
Copy link
Contributor

You're ok with that?

I think so, yeah. It doesn't seem that ugly to me. I kind of wish we had a better name than "SendWithRetryAsync" now, since it's also doing the version determination... but I don't have a great suggestion. It's basically the main Send routine now (the only thing that happens before this is proxy auth) so perhaps we could call it SendAsyncCore or similar? I dunno. Thoughts?

Does this give a noticeable win vs previous changes?

I imagine we could do something similar here to get rid of a state machine for each of HTTP2 and HTTP3, right? Not super important at this point, but perhaps something to consider in the future.

That said, re HTTP3, I kind of want to refactor that a bit. Seems like some logic that's currently in TrySendUsingHttp3Async could instead be in GetHttp3ConnectionAsync, and the 421 retry logic could just be incorporated into our general retry logic. But none of that needs to happen for 6.0.

@stephentoub
Copy link
Member Author

Ok, I think this is reasonable. @geoffkizer please take another look.

At this point, the simple scenario previously benchmarked is only 48 bytes more than .NET 5 (I could easily get it to 32 bytes by doing something unnatural I chose not to do).

Method Toolchain Mean Ratio Allocated
Request d:\coreclrtest\5.0.9\corerun.exe 56.31 us 1.00 1,416 B
Request d:\coreclrtest\main\corerun.exe 56.60 us 1.01 1,888 B
Request d:\coreclrtest\pr\corerun.exe 54.84 us 0.97 1,464 B

Copy link
Contributor

@geoffkizer geoffkizer left a comment

Choose a reason for hiding this comment

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

One nit re the assert, otherwise LGTM

@geoffkizer
Copy link
Contributor

Also, let's run outerloop before merging.

@geoffkizer
Copy link
Contributor

Is the plan to backport to 6.0 as is? If so I'm fine with that (assuming clean outerloop run)

@stephentoub
Copy link
Member Author

/azp list

@stephentoub
Copy link
Member Author

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@stephentoub
Copy link
Member Author

Is the plan to backport to 6.0 as is? If so I'm fine with that (assuming clean outerloop run)

I'd like to, yes. I feel a lot better about an only ~3% increase in allocation than an ~33% increase in allocation, even for a minimal benchmark.

@stephentoub stephentoub merged commit 999e412 into dotnet:main Aug 27, 2021
@stephentoub stephentoub deleted the httpasm branch August 27, 2021 10:51
@stephentoub
Copy link
Member Author

/backport to release/6.0

@github-actions
Copy link
Contributor

Started backporting to release/6.0: https://github.com/dotnet/runtime/actions/runs/1173957232

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

SocketsHttpHandler allocating more per HTTP/1.1 request due to additional async state machines
4 participants