Skip to content

Commit f8c1bb3

Browse files
authored
http.sys; add opt-in support for kernel-mode response buffering (#47776)
* http.sys; opt-in support for kernel-mode response buffering * remove obsolete comment fragment * address PR feedback
1 parent 2971950 commit f8c1bb3

File tree

5 files changed

+55
-9
lines changed

5 files changed

+55
-9
lines changed

src/Servers/HttpSys/src/HttpSysOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@ public string? RequestQueueName
110110
/// </summary>
111111
public bool ThrowWriteExceptions { get; set; }
112112

113+
/// <summary>
114+
/// Enable buffering of response data in the Kernel. The default value is <code>false</code>.
115+
/// It should be used by an application doing synchronous I/O or by an application doing asynchronous I/O with
116+
/// no more than one outstanding write at a time, and can significantly improve throughput over high-latency connections.
117+
/// Applications that use asynchronous I/O and that may have more than one send outstanding at a time should not use this flag.
118+
/// Enabling this can results in higher CPU and memory usage by Http.Sys.
119+
/// </summary>
120+
public bool EnableKernelResponseBuffering { get; set; }
121+
113122
/// <summary>
114123
/// Gets or sets the maximum number of concurrent connections to accept. Set `-1` for infinite.
115124
/// Set to `null` to use the registry's machine-wide setting.

src/Servers/HttpSys/src/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.EnableKernelResponseBuffering.get -> bool
3+
Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.EnableKernelResponseBuffering.set -> void
24
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
35
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.ConnectionStart = 0 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
46
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.DataStart = 1 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType

src/Servers/HttpSys/src/RequestProcessing/Response.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,6 @@ actual HTTP header will be generated by the application and sent as entity body.
275275
// This will give us more control of the bytes that hit the wire, including encodings, HTTP 1.0, etc..
276276
// It may also be faster to do this work in managed code and then pass down only one buffer.
277277
// What would we loose by bypassing HttpSendHttpResponse?
278-
//
279-
// TODO: Consider using the HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA flag for most/all responses rather than just Opaque.
280278
internal unsafe uint SendHeaders(ref UnmanagedBufferAllocator allocator,
281279
Span<HttpApiTypes.HTTP_DATA_CHUNK> dataChunks,
282280
ResponseStreamAsyncResult? asyncResult,

src/Servers/HttpSys/src/RequestProcessing/ResponseBody.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ internal RequestContext RequestContext
3939

4040
internal bool ThrowWriteExceptions => RequestContext.Server.Options.ThrowWriteExceptions;
4141

42+
internal bool EnableKernelResponseBuffering => RequestContext.Server.Options.EnableKernelResponseBuffering;
43+
4244
internal bool IsDisposed => _disposed;
4345

4446
public override bool CanSeek
@@ -496,6 +498,13 @@ private HttpApiTypes.HTTP_FLAGS ComputeLeftToWrite(long writeCount, bool endOfRe
496498
{
497499
flags |= HttpApiTypes.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
498500
}
501+
if (EnableKernelResponseBuffering)
502+
{
503+
// "When this flag is set, it should also be used consistently in calls to the HttpSendResponseEntityBody function."
504+
// so: make sure we add it in *all* scenarios where it applies - our "close" could be at the end of a bunch
505+
// of buffered chunks
506+
flags |= HttpApiTypes.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA;
507+
}
499508

500509
// Update _leftToWrite now so we can queue up additional async writes.
501510
if (_leftToWrite > 0)

src/Servers/HttpSys/test/FunctionalTests/ResponseBodyTests.cs

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.IO;
7-
using System.Linq;
84
using System.Net.Http;
95
using System.Text;
10-
using System.Threading;
11-
using System.Threading.Tasks;
126
using Microsoft.AspNetCore.Http;
137
using Microsoft.AspNetCore.Http.Features;
148
using Microsoft.AspNetCore.Testing;
15-
using Xunit;
169

1710
namespace Microsoft.AspNetCore.Server.HttpSys;
1811

@@ -133,6 +126,41 @@ public async Task ResponseBody_WriteNoHeaders_SetsChunked()
133126
}
134127
}
135128

129+
[ConditionalTheory]
130+
[InlineData(true)]
131+
[InlineData(false)]
132+
public async Task ResponseBody_WriteNoHeaders_SetsChunked_LargeBody(bool enableKernelBuffering)
133+
{
134+
const int WriteSize = 1024 * 1024;
135+
const int NumWrites = 32;
136+
137+
string address;
138+
using (Utilities.CreateHttpServer(
139+
baseAddress: out address,
140+
configureOptions: options => { options.EnableKernelResponseBuffering = enableKernelBuffering; },
141+
app: async httpContext =>
142+
{
143+
httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
144+
for (int i = 0; i < NumWrites - 1; i++)
145+
{
146+
httpContext.Response.Body.Write(new byte[WriteSize], 0, WriteSize);
147+
}
148+
await httpContext.Response.Body.WriteAsync(new byte[WriteSize], 0, WriteSize);
149+
}))
150+
{
151+
var response = await SendRequestAsync(address);
152+
Assert.Equal(200, (int)response.StatusCode);
153+
Assert.Equal(new Version(1, 1), response.Version);
154+
IEnumerable<string> ignored;
155+
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
156+
Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
157+
158+
var bytes = await response.Content.ReadAsByteArrayAsync();
159+
Assert.Equal(WriteSize * NumWrites, bytes.Length);
160+
Assert.True(bytes.All(b => b == 0));
161+
}
162+
}
163+
136164
[ConditionalFact]
137165
public async Task ResponseBody_WriteNoHeadersAndFlush_DefaultsToChunked()
138166
{

0 commit comments

Comments
 (0)