Reduce simple HTTP/2 post app allocation by ~40% (#32557)
authorStephen Toub <stoub@microsoft.com>
Thu, 20 Feb 2020 14:28:01 +0000 (09:28 -0500)
committerGitHub <noreply@github.com>
Thu, 20 Feb 2020 14:28:01 +0000 (09:28 -0500)
commite9853d4baa4c9510dc62ed5852f8381141f3c87e
tree2cfc57ede0655e0e82f2dcf6c75319fb2d5677b8
parent7970f722166d16aa890cb9f2afaedb49280c139a
Reduce simple HTTP/2 post app allocation by ~40% (#32557)

* Remove cancellation-related allocations in Http2Stream

We don't need to allocate a linked token source in SendRequestBodyAsync if the caller's token is the default

* Reduce size of SendDataAsync state machine

We're carrying around an extra 24-bytes for a `ReadOnlyMemory<byte>`, when we could instead just use the argument.

* Tweak HeaderField's ctor to use ROS.ToArray

If `value` happens to be empty, this will avoid an allocation.  But what actually led me to do this was just tightening up the code.

* Make HPackDecoder.State enum 1 instead of 4 bytes

* Remove spilled CancellationTokenSource field from SendDataAsync

* Add known-header values for access-control-* headers

* Reduce allocation in SslStream.ReadAsync

The current structure is that ReadAsync makes two calls to FillBufferAsync, one to ensure the frame header is read and another to ensure any additional payload is read.  This has two issues:
1. It ensures that in addition to allocating a state machine for FillBufferAsync (or, rather, a helper it uses) when it needs to yield, it'll also end up allocating for ReadAsync.
2. It complicates error handling, which needs to differentiate whether the first read can't get any bytes or whether a subsequent read can't, which necessitates storing state like how many bytes we initially had buffered so we can compare to that to see if we need to throw.

We can instead:
- Make FillBufferAsync into a simple "read until we get the requested number of bytes" loop and throw if it fails to do so.
- Do the initial read in ReadAsync, thereby allowing us to special-case the first read for both error handling and to minimize the chances that the helper call needs to yield.

This eliminates a bunch of FillBufferAsync state machines and also decreases the size of the state machines when they are needed.

* Replace CreditManager's waiter queue with a circular singly-linked list

This has a variety of benefits:
- We no longer need to allocate a `Queue<Waiter>` and its underlying `Waiter[]`.
- We no longer need to allocate a `TaskCompletionSource<int>` and its `Task<int>`, instead creating a single `IValueTaskSource<T>` implementation.
- For non-cancelable waiters, we can specialize to not need to carry around a meaningless CancellationToken field.
- For cancelable waiters (the common case), we can avoid an entire async method and its state machine by just storing the relevant state onto the waiter itself.

* Fix comment from previous change

* Manually inline and specialize EnsureIncomingBytesAsync

It's not that much more code to just manually inline EnsureIncomingBytesAsync into the three places it's used, and doing so has multiple benefits, both for size and for error messages.

* Update src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs

Co-Authored-By: Cory Nelson <phrosty@gmail.com>
* Fix typo in online feedback

Co-authored-by: Cory Nelson <phrosty@gmail.com>
src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/HPackDecoder.cs
src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/HeaderField.cs
src/libraries/System.Net.Http/src/Resources/Strings.resx
src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeaders.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Implementation.cs