diff --git a/benchmarks/Kestrel.Performance/FrameWritingBenchmark.cs b/benchmarks/Kestrel.Performance/FrameWritingBenchmark.cs index 940b5d2f8c..42af59555d 100644 --- a/benchmarks/Kestrel.Performance/FrameWritingBenchmark.cs +++ b/benchmarks/Kestrel.Performance/FrameWritingBenchmark.cs @@ -52,13 +52,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance [Benchmark] public async Task WriteAsyncAwaited() { - await _frame.WriteAsyncAwaited(new ArraySegment(_writeData), default(CancellationToken)); + await _frame.WriteAsyncAwaited(Task.CompletedTask, new ArraySegment(_writeData), default(CancellationToken)); } [Benchmark] public async Task WriteAsyncAwaitedChunked() { - await _frameChunked.WriteAsyncAwaited(new ArraySegment(_writeData), default(CancellationToken)); + await _frameChunked.WriteAsyncAwaited(Task.CompletedTask, new ArraySegment(_writeData), default(CancellationToken)); } [Benchmark] diff --git a/src/Kestrel.Core/Internal/Http/Frame.cs b/src/Kestrel.Core/Internal/Http/Frame.cs index 251e256d41..46d38c6d6e 100644 --- a/src/Kestrel.Core/Internal/Http/Frame.cs +++ b/src/Kestrel.Core/Internal/Http/Frame.cs @@ -463,22 +463,94 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } - protected async Task FireOnStarting() + protected Task FireOnStarting() { - Stack, object>> onStarting = null; + Stack, object>> onStarting; lock (_onStartingSync) { onStarting = _onStarting; _onStarting = null; } - if (onStarting != null) + + if (onStarting == null) + { + return Task.CompletedTask; + } + else + { + return FireOnStartingMayAwait(onStarting); + } + + } + + private Task FireOnStartingMayAwait(Stack, object>> onStarting) + { + try + { + var count = onStarting.Count; + for(var i = 0; i < count; i++) + { + var entry = onStarting.Pop(); + var task = entry.Key.Invoke(entry.Value); + if (!ReferenceEquals(task, Task.CompletedTask)) + { + return FireOnStartingAwaited(task, onStarting); + } + } + } + catch (Exception ex) + { + ReportApplicationError(ex); + } + + return Task.CompletedTask; + } + + private async Task FireOnStartingAwaited(Task currentTask, Stack, object>> onStarting) + { + try + { + await currentTask; + + var count = onStarting.Count; + for (var i = 0; i < count; i++) + { + var entry = onStarting.Pop(); + await entry.Key.Invoke(entry.Value); + } + } + catch (Exception ex) + { + ReportApplicationError(ex); + } + } + + protected Task FireOnCompleted() + { + Stack, object>> onCompleted; + lock (_onCompletedSync) + { + onCompleted = _onCompleted; + _onCompleted = null; + } + + if (onCompleted == null) + { + return Task.CompletedTask; + } + else + { + return FireOnCompletedAwaited(onCompleted); + } + } + + private async Task FireOnCompletedAwaited(Stack, object>> onCompleted) + { + foreach (var entry in onCompleted) { try { - foreach (var entry in onStarting) - { - await entry.Key.Invoke(entry.Value); - } + await entry.Key.Invoke(entry.Value); } catch (Exception ex) { @@ -487,44 +559,46 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } - protected async Task FireOnCompleted() + public Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) { - Stack, object>> onCompleted = null; - lock (_onCompletedSync) + if (!HasResponseStarted) { - onCompleted = _onCompleted; - _onCompleted = null; - } - if (onCompleted != null) - { - foreach (var entry in onCompleted) + var initializeTask = InitializeResponseAsync(0); + // If return is Task.CompletedTask no awaiting is required + if (!ReferenceEquals(initializeTask, Task.CompletedTask)) { - try - { - await entry.Key.Invoke(entry.Value); - } - catch (Exception ex) - { - ReportApplicationError(ex); - } + return FlushAsyncAwaited(initializeTask, cancellationToken); } } + + return Output.FlushAsync(cancellationToken); } - public async Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) + [MethodImpl(MethodImplOptions.NoInlining)] + private async Task FlushAsyncAwaited(Task initializeTask, CancellationToken cancellationToken) { - await InitializeResponse(0); + await initializeTask; await Output.FlushAsync(cancellationToken); } public Task WriteAsync(ArraySegment data, CancellationToken cancellationToken = default(CancellationToken)) { - if (!HasResponseStarted) - { - return WriteAsyncAwaited(data, cancellationToken); - } + // For the first write, ensure headers are flushed if Write(Chunked)Async isn't called. + var firstWrite = !HasResponseStarted; - VerifyAndUpdateWrite(data.Count); + if (firstWrite) + { + var initializeTask = InitializeResponseAsync(data.Count); + // If return is Task.CompletedTask no awaiting is required + if (!ReferenceEquals(initializeTask, Task.CompletedTask)) + { + return WriteAsyncAwaited(initializeTask, data, cancellationToken); + } + } + else + { + VerifyAndUpdateWrite(data.Count); + } if (_canHaveBody) { @@ -532,7 +606,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (data.Count == 0) { - return Task.CompletedTask; + return !firstWrite ? Task.CompletedTask : FlushAsync(cancellationToken); } return WriteChunkedAsync(data, cancellationToken); } @@ -545,13 +619,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http else { HandleNonBodyResponseWrite(); - return Task.CompletedTask; + return !firstWrite ? Task.CompletedTask : FlushAsync(cancellationToken); } } - public async Task WriteAsyncAwaited(ArraySegment data, CancellationToken cancellationToken) + public async Task WriteAsyncAwaited(Task initializeTask, ArraySegment data, CancellationToken cancellationToken) { - await InitializeResponseAwaited(data.Count); + await initializeTask; // WriteAsyncAwaited is only called for the first write to the body. // Ensure headers are flushed if Write(Chunked)Async isn't called. @@ -667,16 +741,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } - public Task InitializeResponse(int firstWriteByteCount) + public Task InitializeResponseAsync(int firstWriteByteCount) { - if (HasResponseStarted) + var startingTask = FireOnStarting(); + // If return is Task.CompletedTask no awaiting is required + if (!ReferenceEquals(startingTask, Task.CompletedTask)) { - return Task.CompletedTask; - } - - if (_onStarting != null) - { - return InitializeResponseAwaited(firstWriteByteCount); + return InitializeResponseAwaited(startingTask, firstWriteByteCount); } if (_applicationException != null) @@ -690,9 +761,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return Task.CompletedTask; } - private async Task InitializeResponseAwaited(int firstWriteByteCount) + [MethodImpl(MethodImplOptions.NoInlining)] + public async Task InitializeResponseAwaited(Task startingTask, int firstWriteByteCount) { - await FireOnStarting(); + await startingTask; if (_applicationException != null) { @@ -757,6 +829,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return WriteSuffix(); } + [MethodImpl(MethodImplOptions.NoInlining)] private async Task ProduceEndAwaited() { ProduceStart(appCompleted: true); diff --git a/src/Kestrel.Core/Internal/Http/OutputProducer.cs b/src/Kestrel.Core/Internal/Http/OutputProducer.cs index f84571053d..b44af9f1b7 100644 --- a/src/Kestrel.Core/Internal/Http/OutputProducer.cs +++ b/src/Kestrel.Core/Internal/Http/OutputProducer.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics; using System.IO.Pipelines; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; @@ -143,8 +145,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return FlushAsync(writableBuffer, cancellationToken); } - private Task FlushAsync(WritableBuffer writableBuffer, - CancellationToken cancellationToken) + // Single caller, at end of method - so inline + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Task FlushAsync(WritableBuffer writableBuffer, CancellationToken cancellationToken) { var awaitable = writableBuffer.FlushAsync(cancellationToken); if (awaitable.IsCompleted) diff --git a/test/Kestrel.Core.Tests/FrameTests.cs b/test/Kestrel.Core.Tests/FrameTests.cs index d6a1c76637..d5f14333bf 100644 --- a/test/Kestrel.Core.Tests/FrameTests.cs +++ b/test/Kestrel.Core.Tests/FrameTests.cs @@ -641,13 +641,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // Need to compare WaitHandle ref since CancellationToken is struct var original = _frame.RequestAborted.WaitHandle; - foreach (var ch in "hello, worl") + // Only first write can be WriteAsyncAwaited + var startingTask = _frame.InitializeResponseAwaited(Task.CompletedTask, 1); + await _frame.WriteAsyncAwaited(startingTask, new ArraySegment(new[] { (byte)'h' }), default(CancellationToken)); + Assert.Same(original, _frame.RequestAborted.WaitHandle); + + foreach (var ch in "ello, worl") { - await _frame.WriteAsyncAwaited(new ArraySegment(new[] { (byte)ch }), default(CancellationToken)); + await _frame.WriteAsync(new ArraySegment(new[] { (byte)ch }), default(CancellationToken)); Assert.Same(original, _frame.RequestAborted.WaitHandle); } - await _frame.WriteAsyncAwaited(new ArraySegment(new[] { (byte)'d' }), default(CancellationToken)); + await _frame.WriteAsync(new ArraySegment(new[] { (byte)'d' }), default(CancellationToken)); Assert.NotSame(original, _frame.RequestAborted.WaitHandle); }