Skip to content

Commit

Permalink
HTTP/2: Improve incoming header performance (dotnet#62614)
Browse files Browse the repository at this point in the history
Co-authored-by: JamesNK <[email protected]>
  • Loading branch information
github-actions[bot] and JamesNK authored Dec 11, 2021
1 parent f65c469 commit 57a740c
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ public ref readonly HeaderField this[int index]
}

public void Insert(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
Insert(staticTableIndex: null, name, value);
}

public void Insert(int? staticTableIndex, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
int entryLength = HeaderField.GetLength(name.Length, value.Length);
EnsureAvailable(entryLength);
Expand All @@ -59,7 +64,7 @@ public void Insert(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
return;
}

var entry = new HeaderField(name, value);
var entry = new HeaderField(staticTableIndex, name, value);
_buffer[_insertIndex] = entry;
_insertIndex = (_insertIndex + 1) % _buffer.Length;
_size += entry.Length;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,74 +30,75 @@ public static bool TryGetStatusIndex(int status, out int index)

private static readonly HeaderField[] s_staticDecoderTable = new HeaderField[]
{
CreateHeaderField(":authority", ""),
CreateHeaderField(":method", "GET"),
CreateHeaderField(":method", "POST"),
CreateHeaderField(":path", "/"),
CreateHeaderField(":path", "/index.html"),
CreateHeaderField(":scheme", "http"),
CreateHeaderField(":scheme", "https"),
CreateHeaderField(":status", "200"),
CreateHeaderField(":status", "204"),
CreateHeaderField(":status", "206"),
CreateHeaderField(":status", "304"),
CreateHeaderField(":status", "400"),
CreateHeaderField(":status", "404"),
CreateHeaderField(":status", "500"),
CreateHeaderField("accept-charset", ""),
CreateHeaderField("accept-encoding", "gzip, deflate"),
CreateHeaderField("accept-language", ""),
CreateHeaderField("accept-ranges", ""),
CreateHeaderField("accept", ""),
CreateHeaderField("access-control-allow-origin", ""),
CreateHeaderField("age", ""),
CreateHeaderField("allow", ""),
CreateHeaderField("authorization", ""),
CreateHeaderField("cache-control", ""),
CreateHeaderField("content-disposition", ""),
CreateHeaderField("content-encoding", ""),
CreateHeaderField("content-language", ""),
CreateHeaderField("content-length", ""),
CreateHeaderField("content-location", ""),
CreateHeaderField("content-range", ""),
CreateHeaderField("content-type", ""),
CreateHeaderField("cookie", ""),
CreateHeaderField("date", ""),
CreateHeaderField("etag", ""),
CreateHeaderField("expect", ""),
CreateHeaderField("expires", ""),
CreateHeaderField("from", ""),
CreateHeaderField("host", ""),
CreateHeaderField("if-match", ""),
CreateHeaderField("if-modified-since", ""),
CreateHeaderField("if-none-match", ""),
CreateHeaderField("if-range", ""),
CreateHeaderField("if-unmodified-since", ""),
CreateHeaderField("last-modified", ""),
CreateHeaderField("link", ""),
CreateHeaderField("location", ""),
CreateHeaderField("max-forwards", ""),
CreateHeaderField("proxy-authenticate", ""),
CreateHeaderField("proxy-authorization", ""),
CreateHeaderField("range", ""),
CreateHeaderField("referer", ""),
CreateHeaderField("refresh", ""),
CreateHeaderField("retry-after", ""),
CreateHeaderField("server", ""),
CreateHeaderField("set-cookie", ""),
CreateHeaderField("strict-transport-security", ""),
CreateHeaderField("transfer-encoding", ""),
CreateHeaderField("user-agent", ""),
CreateHeaderField("vary", ""),
CreateHeaderField("via", ""),
CreateHeaderField("www-authenticate", "")
CreateHeaderField(1, ":authority", ""),
CreateHeaderField(2, ":method", "GET"),
CreateHeaderField(3, ":method", "POST"),
CreateHeaderField(4, ":path", "/"),
CreateHeaderField(5, ":path", "/index.html"),
CreateHeaderField(6, ":scheme", "http"),
CreateHeaderField(7, ":scheme", "https"),
CreateHeaderField(8, ":status", "200"),
CreateHeaderField(9, ":status", "204"),
CreateHeaderField(10, ":status", "206"),
CreateHeaderField(11, ":status", "304"),
CreateHeaderField(12, ":status", "400"),
CreateHeaderField(13, ":status", "404"),
CreateHeaderField(14, ":status", "500"),
CreateHeaderField(15, "accept-charset", ""),
CreateHeaderField(16, "accept-encoding", "gzip, deflate"),
CreateHeaderField(17, "accept-language", ""),
CreateHeaderField(18, "accept-ranges", ""),
CreateHeaderField(19, "accept", ""),
CreateHeaderField(20, "access-control-allow-origin", ""),
CreateHeaderField(21, "age", ""),
CreateHeaderField(22, "allow", ""),
CreateHeaderField(23, "authorization", ""),
CreateHeaderField(24, "cache-control", ""),
CreateHeaderField(25, "content-disposition", ""),
CreateHeaderField(26, "content-encoding", ""),
CreateHeaderField(27, "content-language", ""),
CreateHeaderField(28, "content-length", ""),
CreateHeaderField(29, "content-location", ""),
CreateHeaderField(30, "content-range", ""),
CreateHeaderField(31, "content-type", ""),
CreateHeaderField(32, "cookie", ""),
CreateHeaderField(33, "date", ""),
CreateHeaderField(34, "etag", ""),
CreateHeaderField(35, "expect", ""),
CreateHeaderField(36, "expires", ""),
CreateHeaderField(37, "from", ""),
CreateHeaderField(38, "host", ""),
CreateHeaderField(39, "if-match", ""),
CreateHeaderField(40, "if-modified-since", ""),
CreateHeaderField(41, "if-none-match", ""),
CreateHeaderField(42, "if-range", ""),
CreateHeaderField(43, "if-unmodified-since", ""),
CreateHeaderField(44, "last-modified", ""),
CreateHeaderField(45, "link", ""),
CreateHeaderField(46, "location", ""),
CreateHeaderField(47, "max-forwards", ""),
CreateHeaderField(48, "proxy-authenticate", ""),
CreateHeaderField(49, "proxy-authorization", ""),
CreateHeaderField(50, "range", ""),
CreateHeaderField(51, "referer", ""),
CreateHeaderField(52, "refresh", ""),
CreateHeaderField(53, "retry-after", ""),
CreateHeaderField(54, "server", ""),
CreateHeaderField(55, "set-cookie", ""),
CreateHeaderField(56, "strict-transport-security", ""),
CreateHeaderField(57, "transfer-encoding", ""),
CreateHeaderField(58, "user-agent", ""),
CreateHeaderField(59, "vary", ""),
CreateHeaderField(60, "via", ""),
CreateHeaderField(61, "www-authenticate", "")
};

// TODO: The HeaderField constructor will allocate and copy again. We should avoid this.
// Tackle as part of header table allocation strategy in general (see note in HeaderField constructor).

private static HeaderField CreateHeaderField(string name, string value) =>
private static HeaderField CreateHeaderField(int staticTableIndex, string name, string value) =>
new HeaderField(
staticTableIndex,
Encoding.ASCII.GetBytes(name),
value.Length != 0 ? Encoding.ASCII.GetBytes(value) : Array.Empty<byte>());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ private void ProcessHeaderValue(ReadOnlySpan<byte> data, IHttpHeadersHandler han

if (_index)
{
_dynamicTable.Insert(H2StaticTable.Get(_headerStaticIndex - 1).Name, headerValueSpan);
_dynamicTable.Insert(_headerStaticIndex, H2StaticTable.Get(_headerStaticIndex - 1).Name, headerValueSpan);
}
}
else
Expand Down Expand Up @@ -548,7 +548,7 @@ private void OnIndexedHeaderField(int index, IHttpHeadersHandler handler)
else
{
ref readonly HeaderField header = ref GetDynamicHeader(index);
handler.OnHeader(header.Name, header.Value);
handler.OnDynamicIndexedHeader(header.StaticTableIndex, header.Name, header.Value);
}

_state = State.Ready;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ internal readonly struct HeaderField
// http://httpwg.org/specs/rfc7541.html#rfc.section.4.1
public const int RfcOverhead = 32;

public HeaderField(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
public HeaderField(int? staticTableIndex, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
// Store the static table index (if there is one) for the header field.
// ASP.NET Core has a fast path that sets a header value using the static table index instead of the name.
StaticTableIndex = staticTableIndex;

Debug.Assert(name.Length > 0);

// TODO: We're allocating here on every new table entry.
Expand All @@ -24,6 +28,8 @@ public HeaderField(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
Value = value.ToArray();
}

public int? StaticTableIndex { get; }

public byte[] Name { get; }

public byte[] Value { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,11 @@ interface IHttpHeadersHandler
void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value);
void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value);
void OnHeadersComplete(bool endStream);

// DIM to avoid breaking change on public interface (for Kestrel).
public void OnDynamicIndexedHeader(int? index, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
OnHeader(name, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ namespace System.Net.Http.Unit.Tests.HPack
{
public class DynamicTableTest
{
private readonly HeaderField _header1 = new HeaderField(Encoding.ASCII.GetBytes("header-1"), Encoding.ASCII.GetBytes("value1"));
private readonly HeaderField _header2 = new HeaderField(Encoding.ASCII.GetBytes("header-02"), Encoding.ASCII.GetBytes("value_2"));
private readonly HeaderField _header1 = new HeaderField(staticTableIndex: null, Encoding.ASCII.GetBytes("header-1"), Encoding.ASCII.GetBytes("value1"));
private readonly HeaderField _header2 = new HeaderField(staticTableIndex: null, Encoding.ASCII.GetBytes("header-02"), Encoding.ASCII.GetBytes("value_2"));

[Fact]
public void DynamicTable_IsInitiallyEmpty()
Expand Down

0 comments on commit 57a740c

Please sign in to comment.