HTTP/2: Improve incoming header performance (#62614)
authorgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sat, 11 Dec 2021 05:27:24 +0000 (18:27 +1300)
committerGitHub <noreply@github.com>
Sat, 11 Dec 2021 05:27:24 +0000 (18:27 +1300)
Co-authored-by: JamesNK <JamesNK@users.noreply.github.com>
src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/DynamicTable.cs
src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/H2StaticTable.Http2.cs
src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/HPackDecoder.cs
src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/HeaderField.cs
src/libraries/Common/src/System/Net/Http/aspnetcore/IHttpHeadersHandler.cs
src/libraries/Common/tests/Tests/System/Net/aspnetcore/Http2/DynamicTableTest.cs

index 7b496ba..d57242b 100644 (file)
@@ -47,6 +47,11 @@ namespace System.Net.Http.HPack
 
         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);
 
@@ -59,7 +64,7 @@ namespace System.Net.Http.HPack
                 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;
index a4db64a..29ee89f 100644 (file)
@@ -30,74 +30,75 @@ namespace System.Net.Http.HPack
 
         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>());
     }
index 526defa..c287478 100644 (file)
@@ -508,7 +508,7 @@ namespace System.Net.Http.HPack
 
                 if (_index)
                 {
-                    _dynamicTable.Insert(H2StaticTable.Get(_headerStaticIndex - 1).Name, headerValueSpan);
+                    _dynamicTable.Insert(_headerStaticIndex, H2StaticTable.Get(_headerStaticIndex - 1).Name, headerValueSpan);
                 }
             }
             else
@@ -548,7 +548,7 @@ namespace System.Net.Http.HPack
             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;
index aff4365..957574c 100644 (file)
@@ -11,8 +11,12 @@ namespace System.Net.Http.HPack
         // 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.
@@ -24,6 +28,8 @@ namespace System.Net.Http.HPack
             Value = value.ToArray();
         }
 
+        public int? StaticTableIndex { get; }
+
         public byte[] Name { get; }
 
         public byte[] Value { get; }
index 824889c..96dfae3 100644 (file)
@@ -21,5 +21,11 @@ namespace System.Net.Http
         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);
+        }
     }
 }
index eaa9c6c..9576a9e 100644 (file)
@@ -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()