diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index f1556af04e..a4932de60f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -198,7 +198,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - public ValueTask WriteResponseTrailers(int streamId, HttpResponseTrailers headers) + public ValueTask WriteResponseTrailersAsync(int streamId, HttpResponseTrailers headers) { lock (_writeLock) { @@ -256,6 +256,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public ValueTask WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, in ReadOnlySequence data, bool endStream, bool firstWrite, bool forceFlush) { + // Logic in this method is replicated in WriteDataAndTrailersAsync. + // Changes here may need to be mirrored in WriteDataAndTrailersAsync. + // The Length property of a ReadOnlySequence can be expensive, so we cache the value. var dataLength = data.Length; @@ -286,6 +289,43 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } + public ValueTask WriteDataAndTrailersAsync(int streamId, StreamOutputFlowControl flowControl, in ReadOnlySequence data, bool firstWrite, HttpResponseTrailers headers) + { + // This method combines WriteDataAsync and WriteResponseTrailers. + // Changes here may need to be mirrored in WriteDataAsync. + + // The Length property of a ReadOnlySequence can be expensive, so we cache the value. + var dataLength = data.Length; + + lock (_writeLock) + { + if (_completed || flowControl.IsAborted) + { + return default; + } + + // Zero-length data frames are allowed to be sent immediately even if there is no space available in the flow control window. + // https://httpwg.org/specs/rfc7540.html#rfc.section.6.9.1 + if (dataLength != 0 && dataLength > flowControl.Available) + { + return WriteDataAndTrailersAsyncCore(this, streamId, flowControl, data, dataLength, firstWrite, headers); + } + + // This cast is safe since if dataLength would overflow an int, it's guaranteed to be greater than the available flow control window. + flowControl.Advance((int)dataLength); + WriteDataUnsynchronized(streamId, data, dataLength, endStream: false); + + return WriteResponseTrailersAsync(streamId, headers); + } + + static async ValueTask WriteDataAndTrailersAsyncCore(Http2FrameWriter writer, int streamId, StreamOutputFlowControl flowControl, ReadOnlySequence data, long dataLength, bool firstWrite, HttpResponseTrailers headers) + { + await writer.WriteDataAsync(streamId, flowControl, data, dataLength, endStream: false, firstWrite); + + return await writer.WriteResponseTrailersAsync(streamId, headers); + } + } + /* Padding is not implemented +---------------+ |Pad Length? (8)| diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs index a548ae4fc1..ede4621cce 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs @@ -423,16 +423,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { // Output is ending and there are trailers to write // Write any remaining content then write trailers - if (readResult.Buffer.Length > 0) - { - // Only flush if required (i.e. content length exceeds flow control availability) - // Writing remaining content without flushing allows content and trailers to be sent in the same packet - await _frameWriter.WriteDataAsync(StreamId, _flowControl, readResult.Buffer, endStream: false, firstWrite, forceFlush: false); - } _stream.ResponseTrailers.SetReadOnly(); _stream.DecrementActiveClientStreamCount(); - flushResult = await _frameWriter.WriteResponseTrailers(StreamId, _stream.ResponseTrailers); + + if (readResult.Buffer.Length > 0) + { + // It is faster to write data and trailers together. Locking once reduces lock contention. + flushResult = await _frameWriter.WriteDataAndTrailersAsync(StreamId, _flowControl, readResult.Buffer, firstWrite, _stream.ResponseTrailers); + } + else + { + flushResult = await _frameWriter.WriteResponseTrailersAsync(StreamId, _stream.ResponseTrailers); + } } else if (readResult.IsCompleted && _streamEnded) {