Skip to content

Commit 4517992

Browse files
authored
HttpResponseHeaders.CopyToFast move directly to next rather than if branching (#32337)
1 parent 78a87ec commit 4517992

22 files changed

+619
-663
lines changed

src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs

Lines changed: 319 additions & 447 deletions
Large diffs are not rendered by default.

src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseHeaders.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Buffers;
6+
using System.Diagnostics.CodeAnalysis;
67
using System.IO.Pipelines;
78
using System.Collections;
89
using System.Collections.Generic;
@@ -67,11 +68,18 @@ private static long ParseContentLength(string value)
6768
return parsed;
6869
}
6970

71+
[DoesNotReturn]
7072
private static void ThrowInvalidContentLengthException(string value)
7173
{
7274
throw new InvalidOperationException(CoreStrings.FormatInvalidContentLength_InvalidNumber(value));
7375
}
7476

77+
[DoesNotReturn]
78+
private static void ThrowInvalidHeaderBits()
79+
{
80+
throw new InvalidOperationException("Invalid Header Bits");
81+
}
82+
7583
[MethodImpl(MethodImplOptions.NoInlining)]
7684
private void SetValueUnknown(string key, StringValues value)
7785
{

src/Servers/Kestrel/perf/Microbenchmarks/InMemoryTransportBenchmark.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ public class InMemoryTransportBenchmark
2424
{
2525
private const string _plaintextExpectedResponse =
2626
"HTTP/1.1 200 OK\r\n" +
27+
"Content-Length: 13\r\n" +
2728
"Date: Fri, 02 Mar 2018 18:37:05 GMT\r\n" +
2829
"Content-Type: text/plain\r\n" +
2930
"Server: Kestrel\r\n" +
30-
"Content-Length: 13\r\n" +
3131
"\r\n" +
3232
"Hello, World!";
3333

src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeadersWritingBenchmark.cs

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks
1717
{
1818
public class ResponseHeadersWritingBenchmark
1919
{
20+
private const int Iterations = 1000;
21+
2022
private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: " + Constants.ServerName);
2123
private static readonly byte[] _helloWorldPayload = Encoding.ASCII.GetBytes("Hello, World!");
2224

@@ -32,11 +34,12 @@ public class ResponseHeadersWritingBenchmark
3234
BenchmarkTypes.PlaintextChunked,
3335
BenchmarkTypes.PlaintextWithCookie,
3436
BenchmarkTypes.PlaintextChunkedWithCookie,
35-
BenchmarkTypes.LiveAspNet
37+
BenchmarkTypes.LiveAspNet,
38+
BenchmarkTypes.Common
3639
)]
3740
public BenchmarkTypes Type { get; set; }
3841

39-
[Benchmark]
42+
[Benchmark(OperationsPerInvoke = Iterations)]
4043
public void Output()
4144
{
4245
switch (Type)
@@ -56,6 +59,9 @@ public void Output()
5659
case BenchmarkTypes.LiveAspNet:
5760
LiveAspNet();
5861
break;
62+
case BenchmarkTypes.Common:
63+
Common();
64+
break;
5965
}
6066
}
6167

@@ -66,7 +72,11 @@ private void TechEmpowerPlaintext()
6672
responseHeaders.ContentLength = _helloWorldPayload.Length;
6773

6874
var writer = new BufferWriter<PipeWriter>(_writer);
69-
_responseHeaders.CopyTo(ref writer);
75+
76+
for (var i = 0; i < Iterations; i++)
77+
{
78+
_responseHeaders.CopyTo(ref writer);
79+
}
7080
}
7181

7282
private void PlaintextChunked()
@@ -75,7 +85,11 @@ private void PlaintextChunked()
7585
responseHeaders.ContentType = "text/plain";
7686

7787
var writer = new BufferWriter<PipeWriter>(_writer);
78-
_responseHeaders.CopyTo(ref writer);
88+
89+
for (var i = 0; i < Iterations; i++)
90+
{
91+
_responseHeaders.CopyTo(ref writer);
92+
}
7993
}
8094

8195
private void LiveAspNet()
@@ -88,7 +102,45 @@ private void LiveAspNet()
88102
_responseHeadersDict["X-Powered-By"] = "ASP.NET";
89103

90104
var writer = new BufferWriter<PipeWriter>(_writer);
91-
_responseHeaders.CopyTo(ref writer);
105+
106+
for (var i = 0; i < Iterations; i++)
107+
{
108+
_responseHeaders.CopyTo(ref writer);
109+
}
110+
}
111+
112+
private void Common()
113+
{
114+
var responseHeaders = _responseHeadersDict;
115+
responseHeaders.ContentType = "text/css";
116+
responseHeaders.ContentLength = 421;
117+
118+
responseHeaders.Connection = "Close";
119+
responseHeaders.CacheControl = "public, max-age=30672000";
120+
responseHeaders.Vary = "Accept-Encoding";
121+
responseHeaders.ContentEncoding = "gzip";
122+
responseHeaders.Expires = "Fri, 12 Jan 2018 22:01:55 GMT";
123+
responseHeaders.LastModified = "Wed, 22 Jun 2016 20:08:29 GMT";
124+
responseHeaders.SetCookie = "prov=20629ccd-8b0f-e8ef-2935-cd26609fc0bc; __qca=P0-1591065732-1479167353442; _ga=GA1.2.1298898376.1479167354; _gat=1; sgt=id=9519gfde_3347_4762_8762_df51458c8ec2; acct=t=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric&s=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric";
125+
responseHeaders.ETag = "\"54ef7954-1078\"";
126+
responseHeaders.TransferEncoding = "chunked";
127+
responseHeaders.ContentLanguage = "en-gb";
128+
responseHeaders.Upgrade = "websocket";
129+
responseHeaders.Via = "1.1 varnish";
130+
responseHeaders.AccessControlAllowOrigin = "*";
131+
responseHeaders.AccessControlAllowCredentials = "true";
132+
responseHeaders.AccessControlExposeHeaders = "Client-Protocol, Content-Length, Content-Type, X-Bandwidth-Est, X-Bandwidth-Est2, X-Bandwidth-Est-Comp, X-Bandwidth-Avg, X-Walltime-Ms, X-Sequence-Num";
133+
134+
var dateHeaderValues = _dateHeaderValueManager.GetDateHeaderValues();
135+
_responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
136+
_responseHeaders.SetRawServer("Kestrel", _bytesServer);
137+
138+
var writer = new BufferWriter<PipeWriter>(_writer);
139+
140+
for (var i = 0; i < Iterations; i++)
141+
{
142+
_responseHeaders.CopyTo(ref writer);
143+
}
92144
}
93145

94146
private void PlaintextWithCookie()
@@ -99,7 +151,11 @@ private void PlaintextWithCookie()
99151
responseHeaders.ContentLength = _helloWorldPayload.Length;
100152

101153
var writer = new BufferWriter<PipeWriter>(_writer);
102-
_responseHeaders.CopyTo(ref writer);
154+
155+
for (var i = 0; i < Iterations; i++)
156+
{
157+
_responseHeaders.CopyTo(ref writer);
158+
}
103159
}
104160

105161
private void PlaintextChunkedWithCookie()
@@ -110,7 +166,11 @@ private void PlaintextChunkedWithCookie()
110166
responseHeaders.TransferEncoding = "chunked";
111167

112168
var writer = new BufferWriter<PipeWriter>(_writer);
113-
_responseHeaders.CopyTo(ref writer);
169+
170+
for (var i = 0; i < Iterations; i++)
171+
{
172+
_responseHeaders.CopyTo(ref writer);
173+
}
114174
}
115175

116176
[GlobalSetup]
@@ -151,7 +211,8 @@ public enum BenchmarkTypes
151211
PlaintextChunked,
152212
PlaintextWithCookie,
153213
PlaintextChunkedWithCookie,
154-
LiveAspNet
214+
LiveAspNet,
215+
Common
155216
}
156217
}
157218
}

src/Servers/Kestrel/shared/KnownHeaders.cs

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,7 @@ public static string GeneratedFile()
769769
770770
using System;
771771
using System.Collections.Generic;
772+
using System.Diagnostics;
772773
using System.Buffers;
773774
using System.IO.Pipelines;
774775
using System.Numerics;
@@ -1150,52 +1151,61 @@ internal void ClearInvalidH2H3Headers()
11501151
}}
11511152
internal unsafe void CopyToFast(ref BufferWriter<PipeWriter> output)
11521153
{{
1153-
var tempBits = (ulong)_bits | (_contentLength.HasValue ? {"0x" + (1L << 63).ToString("x" , CultureInfo.InvariantCulture)}L : 0);
1154-
var next = 0;
1155-
var keyStart = 0;
1156-
var keyLength = 0;
1157-
ref readonly StringValues values = ref Unsafe.AsRef<StringValues>(null);
1154+
var tempBits = (ulong)_bits;
1155+
// Set exact next
1156+
var next = BitOperations.TrailingZeroCount(tempBits);
1157+
1158+
// Output Content-Length now as it isn't contained in the bit flags.
1159+
if (_contentLength.HasValue)
1160+
{{
1161+
output.Write(HeaderBytes.Slice(640, 18));
1162+
output.WriteNumeric((ulong)ContentLength.GetValueOrDefault());
1163+
}}
1164+
if (tempBits == 0)
1165+
{{
1166+
return;
1167+
}}
11581168
1169+
ref readonly StringValues values = ref Unsafe.AsRef<StringValues>(null);
11591170
do
11601171
{{
1172+
int keyStart;
1173+
int keyLength;
11611174
switch (next)
1162-
{{{Each(loop.Headers.OrderBy(h => !h.PrimaryHeader).Select((h, i) => (Header: h, Index: i)), hi => $@"
1163-
case {hi.Index}: // Header: ""{hi.Header.Name}""
1164-
if ({hi.Header.TestTempBit()})
1175+
{{{Each(loop.Headers.OrderBy(h => h.Index).Where(h => h.Identifier != "ContentLength"), header => $@"
1176+
case {header.Index}: // Header: ""{header.Name}""
1177+
Debug.Assert({header.TestTempBit()});{(header.EnhancedSetter == false ? $@"
1178+
values = ref _headers._{header.Identifier};
1179+
keyStart = {header.BytesOffset};
1180+
keyLength = {header.BytesCount};" : $@"
1181+
if (_headers._raw{header.Identifier} != null)
11651182
{{
1166-
tempBits ^= {"0x" + (1L << hi.Header.Index).ToString("x" , CultureInfo.InvariantCulture)}L;{(hi.Header.Identifier != "ContentLength" ? $@"{(hi.Header.EnhancedSetter == false ? $@"
1167-
values = ref _headers._{hi.Header.Identifier};
1168-
keyStart = {hi.Header.BytesOffset};
1169-
keyLength = {hi.Header.BytesCount};
1170-
next = {hi.Index + 1};
1171-
break; // OutputHeader" : $@"
1172-
if (_headers._raw{hi.Header.Identifier} != null)
1173-
{{
1174-
output.Write(_headers._raw{hi.Header.Identifier});
1175-
}}
1176-
else
1177-
{{
1178-
values = ref _headers._{hi.Header.Identifier};
1179-
keyStart = {hi.Header.BytesOffset};
1180-
keyLength = {hi.Header.BytesCount};
1181-
next = {hi.Index + 1};
1182-
break; // OutputHeader
1183-
}}")}" : $@"
1184-
output.Write(HeaderBytes.Slice({hi.Header.BytesOffset}, {hi.Header.BytesCount}));
1185-
output.WriteNumeric((ulong)ContentLength.GetValueOrDefault());
1186-
if (tempBits == 0)
1187-
{{
1188-
return;
1189-
}}")}
1183+
// Clear and set next as not using common output.
1184+
tempBits ^= {"0x" + (1L << header.Index).ToString("x", CultureInfo.InvariantCulture)}L;
1185+
next = BitOperations.TrailingZeroCount(tempBits);
1186+
output.Write(_headers._raw{header.Identifier});
1187+
continue; // Jump to next, already output header
11901188
}}
1191-
{(hi.Index + 1 < loop.Headers.Length ? $"goto case {hi.Index + 1};" : "return;")}")}
1189+
else
1190+
{{
1191+
values = ref _headers._{header.Identifier};
1192+
keyStart = {header.BytesOffset};
1193+
keyLength = {header.BytesCount};
1194+
}}")}
1195+
break; // OutputHeader
1196+
")}
11921197
default:
1198+
ThrowInvalidHeaderBits();
11931199
return;
11941200
}}
11951201

11961202
// OutputHeader
11971203
{{
1204+
// Clear bit
1205+
tempBits ^= (1UL << next);
11981206
var valueCount = values.Count;
1207+
Debug.Assert(valueCount > 0);
1208+
11991209
var headerKey = HeaderBytes.Slice(keyStart, keyLength);
12001210
for (var i = 0; i < valueCount; i++)
12011211
{{
@@ -1206,6 +1216,8 @@ internal unsafe void CopyToFast(ref BufferWriter<PipeWriter> output)
12061216
output.WriteAscii(value);
12071217
}}
12081218
}}
1219+
// Set exact next
1220+
next = BitOperations.TrailingZeroCount(tempBits);
12091221
}}
12101222
}} while (tempBits != 0);
12111223
}}" : "")}{(loop.ClassName == "HttpRequestHeaders" ? $@"

src/Servers/Kestrel/test/FunctionalTests/ListenHandleTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ public async Task CanListenToOpenTcpSocketHandle()
3737

3838
await connection.Receive(
3939
"HTTP/1.1 200 OK",
40-
$"Date: {server.Context.DateHeaderValue}",
4140
"Content-Length: 0",
41+
$"Date: {server.Context.DateHeaderValue}",
4242
"",
4343
"");
4444
}

src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,13 @@ await connection2.Send(
214214
"");
215215

216216
await connection1.Receive($"HTTP/1.1 200 OK",
217-
$"Date: {server.Context.DateHeaderValue}",
218217
"Content-Length: 0",
218+
$"Date: {server.Context.DateHeaderValue}",
219219
"",
220220
"");
221221
await connection2.Receive($"HTTP/1.1 200 OK",
222-
$"Date: {server.Context.DateHeaderValue}",
223222
"Content-Length: 0",
223+
$"Date: {server.Context.DateHeaderValue}",
224224
"",
225225
"");
226226
}
@@ -319,8 +319,8 @@ await connection.Send(
319319
// a more critical log message.
320320
await connection.Receive(
321321
"HTTP/1.1 200 OK",
322-
$"Date: {server.Context.DateHeaderValue}",
323322
"Content-Length: 0",
323+
$"Date: {server.Context.DateHeaderValue}",
324324
"",
325325
"");
326326

@@ -583,9 +583,9 @@ await connection.Send(
583583
await connectionClosedTcs.Task.DefaultTimeout();
584584

585585
await connection.ReceiveEnd($"HTTP/1.1 200 OK",
586+
"Content-Length: 0",
586587
"Connection: close",
587588
$"Date: {server.Context.DateHeaderValue}",
588-
"Content-Length: 0",
589589
"",
590590
"");
591591
}
@@ -698,8 +698,8 @@ await connection.Send(
698698

699699
await connection.Receive(
700700
"HTTP/1.1 200 OK",
701-
$"Date: {testContext.DateHeaderValue}",
702701
"Content-Length: 5",
702+
$"Date: {testContext.DateHeaderValue}",
703703
"",
704704
"World");
705705

src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,7 @@ await connection.Send(
880880

881881
await connection.Receive(
882882
"HTTP/1.1 200 OK",
883+
"Content-Length: 0",
883884
$"Date: {dateHeaderValueManager.GetDateHeaderValues().String}");
884885

885886
var minResponseSize = headerSize * headerCount;

src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,9 @@ private async Task ReceiveBadRequestResponse(InMemoryConnection connection, stri
225225
var lines = new[]
226226
{
227227
$"HTTP/1.1 {expectedResponseStatusCode}",
228+
"Content-Length: 0",
228229
"Connection: close",
229230
$"Date: {expectedDateHeaderValue}",
230-
"Content-Length: 0",
231231
expectedAllowHeader,
232232
"",
233233
""

0 commit comments

Comments
 (0)