From d12b12aee3320d7e1ca90d17f70e8e59073dc9d0 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Mon, 25 Mar 2024 00:07:03 +0100 Subject: [PATCH] Cache more info on HttpMethod (#100177) * Cache more info on HttpMethod * Avoid allocating in more cases --- .../src/System.Net.Http.csproj | 2 +- .../src/System/Net/Http/HttpMethod.Http3.cs | 29 --- .../Net/Http/HttpMethod.SocketsHttpHandler.cs | 118 ++++++++++++ .../src/System/Net/Http/HttpMethod.cs | 173 ++++-------------- .../SocketsHttpHandler/Http2Connection.cs | 32 +--- .../SocketsHttpHandler/Http3RequestStream.cs | 5 +- .../Http/SocketsHttpHandler/HttpConnection.cs | 18 +- 7 files changed, 174 insertions(+), 203 deletions(-) delete mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.Http3.cs create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.SocketsHttpHandler.cs diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index 729f78dd752be6..26e14365f292b5 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -169,7 +169,7 @@ - + diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.Http3.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.Http3.cs deleted file mode 100644 index c8580d1394569a..00000000000000 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.Http3.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net.Http.QPack; -using System.Threading; - -namespace System.Net.Http -{ - public partial class HttpMethod - { - private byte[]? _http3EncodedBytes; - - internal byte[] Http3EncodedBytes - { - get - { - byte[]? http3EncodedBytes = Volatile.Read(ref _http3EncodedBytes); - if (http3EncodedBytes is null) - { - Volatile.Write(ref _http3EncodedBytes, http3EncodedBytes = _http3Index is int index && index >= 0 ? - QPackEncoder.EncodeStaticIndexedHeaderFieldToArray(index) : - QPackEncoder.EncodeLiteralHeaderFieldWithStaticNameReferenceToArray(H3StaticTable.MethodGet, _method)); - } - - return http3EncodedBytes; - } - } - } -} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.SocketsHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.SocketsHttpHandler.cs new file mode 100644 index 00000000000000..c833d1e4da7148 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.SocketsHttpHandler.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Net.Http.HPack; +using System.Net.Http.QPack; +using System.Text; + +namespace System.Net.Http +{ + public partial class HttpMethod + { + private byte[]? _http1EncodedBytes; + private byte[]? _http2EncodedBytes; + private byte[]? _http3EncodedBytes; + private int _http3Index; + + internal bool MustHaveRequestBody { get; private set; } + internal bool IsConnect { get; private set; } + internal bool IsHead { get; private set; } + + partial void Initialize(string method) + { + Initialize(GetKnownMethod(method)?._http3Index ?? 0); + } + + partial void Initialize(int http3Index) + { + _http3Index = http3Index; + + if (http3Index == H3StaticTable.MethodConnect) + { + IsConnect = true; + } + else if (http3Index == H3StaticTable.MethodHead) + { + IsHead = true; + } + else + { + MustHaveRequestBody = http3Index is not (H3StaticTable.MethodGet or H3StaticTable.MethodOptions or H3StaticTable.MethodDelete); + } + } + + internal byte[] Http1EncodedBytes => _http1EncodedBytes ?? CreateHttp1EncodedBytes(); + internal byte[] Http2EncodedBytes => _http2EncodedBytes ?? CreateHttp2EncodedBytes(); + internal byte[] Http3EncodedBytes => _http3EncodedBytes ?? CreateHttp3EncodedBytes(); + + private byte[] CreateHttp1EncodedBytes() + { + HttpMethod? knownMethod = GetKnownMethod(Method); + byte[]? bytes = knownMethod?._http1EncodedBytes; + + if (bytes is null) + { + Debug.Assert(Ascii.IsValid(Method)); + + string method = knownMethod?.Method ?? Method; + bytes = new byte[method.Length + 1]; + Ascii.FromUtf16(method, bytes, out _); + bytes[^1] = (byte)' '; + + if (knownMethod is not null) + { + knownMethod._http1EncodedBytes = bytes; + } + } + + _http1EncodedBytes = bytes; + return bytes; + } + + private byte[] CreateHttp2EncodedBytes() + { + HttpMethod? knownMethod = GetKnownMethod(Method); + byte[]? bytes = knownMethod?._http2EncodedBytes; + + if (bytes is null) + { + bytes = _http3Index switch + { + H3StaticTable.MethodGet => [0x80 | H2StaticTable.MethodGet], + H3StaticTable.MethodPost => [0x80 | H2StaticTable.MethodPost], + _ => HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(H2StaticTable.MethodGet, knownMethod?.Method ?? Method) + }; + + if (knownMethod is not null) + { + knownMethod._http2EncodedBytes = bytes; + } + } + + _http2EncodedBytes = bytes; + return bytes; + } + + private byte[] CreateHttp3EncodedBytes() + { + HttpMethod? knownMethod = GetKnownMethod(Method); + byte[]? bytes = knownMethod?._http3EncodedBytes; + + if (bytes is null) + { + bytes = _http3Index > 0 + ? QPackEncoder.EncodeStaticIndexedHeaderFieldToArray(_http3Index) + : QPackEncoder.EncodeLiteralHeaderFieldWithStaticNameReferenceToArray(H3StaticTable.MethodGet, knownMethod?.Method ?? Method); + + if (knownMethod is not null) + { + knownMethod._http3EncodedBytes = bytes; + } + } + + _http3EncodedBytes = bytes; + return bytes; + } + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs index e8ed9315016758..fb3c7fe5ea43f4 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net.Http.QPack; @@ -10,71 +9,22 @@ namespace System.Net.Http public partial class HttpMethod : IEquatable { private readonly string _method; - private readonly int? _http3Index; - private int _hashcode; - private static readonly HttpMethod s_getMethod = new HttpMethod("GET", http3StaticTableIndex: H3StaticTable.MethodGet); - private static readonly HttpMethod s_putMethod = new HttpMethod("PUT", http3StaticTableIndex: H3StaticTable.MethodPut); - private static readonly HttpMethod s_postMethod = new HttpMethod("POST", http3StaticTableIndex: H3StaticTable.MethodPost); - private static readonly HttpMethod s_deleteMethod = new HttpMethod("DELETE", http3StaticTableIndex: H3StaticTable.MethodDelete); - private static readonly HttpMethod s_headMethod = new HttpMethod("HEAD", http3StaticTableIndex: H3StaticTable.MethodHead); - private static readonly HttpMethod s_optionsMethod = new HttpMethod("OPTIONS", http3StaticTableIndex: H3StaticTable.MethodOptions); - private static readonly HttpMethod s_traceMethod = new HttpMethod("TRACE", -1); - private static readonly HttpMethod s_patchMethod = new HttpMethod("PATCH", -1); - private static readonly HttpMethod s_connectMethod = new HttpMethod("CONNECT", http3StaticTableIndex: H3StaticTable.MethodConnect); - - public static HttpMethod Get - { - get { return s_getMethod; } - } - - public static HttpMethod Put - { - get { return s_putMethod; } - } - - public static HttpMethod Post - { - get { return s_postMethod; } - } - - public static HttpMethod Delete - { - get { return s_deleteMethod; } - } - - public static HttpMethod Head - { - get { return s_headMethod; } - } - - public static HttpMethod Options - { - get { return s_optionsMethod; } - } - - public static HttpMethod Trace - { - get { return s_traceMethod; } - } - - public static HttpMethod Patch - { - get { return s_patchMethod; } - } + public static HttpMethod Get { get; } = new("GET", H3StaticTable.MethodGet); + public static HttpMethod Put { get; } = new("PUT", H3StaticTable.MethodPut); + public static HttpMethod Post { get; } = new("POST", H3StaticTable.MethodPost); + public static HttpMethod Delete { get; } = new("DELETE", H3StaticTable.MethodDelete); + public static HttpMethod Head { get; } = new("HEAD", H3StaticTable.MethodHead); + public static HttpMethod Options { get; } = new("OPTIONS", H3StaticTable.MethodOptions); + public static HttpMethod Trace { get; } = new("TRACE", http3StaticTableIndex: -1); + public static HttpMethod Patch { get; } = new("PATCH", http3StaticTableIndex: -1); /// Gets the HTTP CONNECT protocol method. /// The HTTP CONNECT method. - public static HttpMethod Connect - { - get { return s_connectMethod; } - } + public static HttpMethod Connect { get; } = new("CONNECT", H3StaticTable.MethodConnect); - public string Method - { - get { return _method; } - } + public string Method => _method; public HttpMethod(string method) { @@ -85,39 +35,26 @@ public HttpMethod(string method) } _method = method; + Initialize(method); } private HttpMethod(string method, int http3StaticTableIndex) { _method = method; - _http3Index = http3StaticTableIndex; + Initialize(http3StaticTableIndex); } - #region IEquatable Members - - public bool Equals([NotNullWhen(true)] HttpMethod? other) - { - if (other is null) - { - return false; - } - - if (object.ReferenceEquals(_method, other._method)) - { - // Strings are static, so there is a good chance that two equal methods use the same reference - // (unless they differ in case). - return true; - } - - return string.Equals(_method, other._method, StringComparison.OrdinalIgnoreCase); - } + // SocketsHttpHandler-specific implementation has extra init logic. + partial void Initialize(int http3Index); + partial void Initialize(string method); - #endregion + public bool Equals([NotNullWhen(true)] HttpMethod? other) => + other is not null && + string.Equals(_method, other._method, StringComparison.OrdinalIgnoreCase); - public override bool Equals([NotNullWhen(true)] object? obj) - { - return Equals(obj as HttpMethod); - } + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is HttpMethod method && + Equals(method); public override int GetHashCode() { @@ -129,22 +66,15 @@ public override int GetHashCode() return _hashcode; } - public override string ToString() - { - return _method; - } + public override string ToString() => _method; - public static bool operator ==(HttpMethod? left, HttpMethod? right) - { - return left is null || right is null ? - ReferenceEquals(left, right) : - left.Equals(right); - } + public static bool operator ==(HttpMethod? left, HttpMethod? right) => + left is null || right is null + ? ReferenceEquals(left, right) + : left.Equals(right); - public static bool operator !=(HttpMethod? left, HttpMethod? right) - { - return !(left == right); - } + public static bool operator !=(HttpMethod? left, HttpMethod? right) => + !(left == right); /// Parses the provided into an instance. /// The method to parse. @@ -159,41 +89,24 @@ public static HttpMethod Parse(ReadOnlySpan method) => GetKnownMethod(method) ?? new HttpMethod(method.ToString()); - /// - /// Returns a singleton method instance with a capitalized method name for the supplied method - /// if it's known; otherwise, returns the original. - /// - internal static HttpMethod Normalize(HttpMethod method) - { - Debug.Assert(method != null); - Debug.Assert(!string.IsNullOrEmpty(method._method)); - - // _http3Index is only set for the singleton instances, so if it's not null, - // we can avoid the lookup. Otherwise, look up the method instance and return the - // normalized instance if it's found. - return method._http3Index is null && GetKnownMethod(method._method) is HttpMethod match ? - match : - method; - } - internal static HttpMethod? GetKnownMethod(ReadOnlySpan method) { if (method.Length >= 3) // 3 == smallest known method { HttpMethod? match = (method[0] | 0x20) switch { - 'c' => s_connectMethod, - 'd' => s_deleteMethod, - 'g' => s_getMethod, - 'h' => s_headMethod, - 'o' => s_optionsMethod, + 'c' => Connect, + 'd' => Delete, + 'g' => Get, + 'h' => Head, + 'o' => Options, 'p' => method.Length switch { - 3 => s_putMethod, - 4 => s_postMethod, - _ => s_patchMethod, + 3 => Put, + 4 => Post, + _ => Patch, }, - 't' => s_traceMethod, + 't' => Trace, _ => null, }; @@ -206,17 +119,5 @@ internal static HttpMethod Normalize(HttpMethod method) return null; } - - internal bool MustHaveRequestBody - { - get - { - // Normalize before calling this - Debug.Assert(ReferenceEquals(this, Normalize(this))); - - return !ReferenceEquals(this, HttpMethod.Get) && !ReferenceEquals(this, HttpMethod.Head) && !ReferenceEquals(this, HttpMethod.Connect) && - !ReferenceEquals(this, HttpMethod.Options) && !ReferenceEquals(this, HttpMethod.Delete); - } - } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs index 67e6bb5305910b..70b68f38a0b262 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs @@ -1495,27 +1495,7 @@ private void WriteHeaders(HttpRequestMessage request, ref ArrayBuffer headerBuff { if (NetEventSource.Log.IsEnabled()) Trace(""); - // HTTP2 does not support Transfer-Encoding: chunked, so disable this on the request. - if (request.HasHeaders && request.Headers.TransferEncodingChunked == true) - { - request.Headers.TransferEncodingChunked = false; - } - - HttpMethod normalizedMethod = HttpMethod.Normalize(request.Method); - - // Method is normalized so we can do reference equality here. - if (ReferenceEquals(normalizedMethod, HttpMethod.Get)) - { - WriteIndexedHeader(H2StaticTable.MethodGet, ref headerBuffer); - } - else if (ReferenceEquals(normalizedMethod, HttpMethod.Post)) - { - WriteIndexedHeader(H2StaticTable.MethodPost, ref headerBuffer); - } - else - { - WriteIndexedHeader(H2StaticTable.MethodGet, normalizedMethod.Method, ref headerBuffer); - } + WriteBytes(request.Method.Http2EncodedBytes, ref headerBuffer); WriteIndexedHeader(_pool.IsSecure ? H2StaticTable.SchemeHttps : H2StaticTable.SchemeHttp, ref headerBuffer); @@ -1543,6 +1523,12 @@ private void WriteHeaders(HttpRequestMessage request, ref ArrayBuffer headerBuff if (request.HasHeaders) { + // HTTP2 does not support Transfer-Encoding: chunked, so disable this on the request. + if (request.Headers.TransferEncodingChunked == true) + { + request.Headers.TransferEncodingChunked = false; + } + if (request.Headers.Protocol is string protocol) { WriteBytes(ProtocolLiteralHeaderBytes, ref headerBuffer); @@ -1571,7 +1557,7 @@ private void WriteHeaders(HttpRequestMessage request, ref ArrayBuffer headerBuff { // Write out Content-Length: 0 header to indicate no body, // unless this is a method that never has a body. - if (normalizedMethod.MustHaveRequestBody) + if (request.Method.MustHaveRequestBody) { WriteBytes(KnownHeaders.ContentLength.Http2EncodedName, ref headerBuffer); WriteLiteralHeaderValue("0", valueEncoding: null, ref headerBuffer); @@ -1586,7 +1572,7 @@ private void WriteHeaders(HttpRequestMessage request, ref ArrayBuffer headerBuff // The headerListSize is an approximation of the total header length. // This is acceptable as long as the value is always >= the actual length. // We must avoid ever sending more than the server allowed. - // This approach must be revisted if we ever support the dynamic table or compression when sending requests. + // This approach must be revisited if we ever support the dynamic table or compression when sending requests. headerListSize += headerBuffer.ActiveLength; uint maxHeaderListSize = _maxHeaderListSize; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs index 8ff03d84ca67c1..459abc39e5c882 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs @@ -573,8 +573,7 @@ private void BufferHeaders(HttpRequestMessage request) _sendBuffer.AvailableSpan[1] = 0x00; // s + delta base. _sendBuffer.Commit(2); - HttpMethod normalizedMethod = HttpMethod.Normalize(request.Method); - BufferBytes(normalizedMethod.Http3EncodedBytes); + BufferBytes(request.Method.Http3EncodedBytes); BufferIndexedHeader(H3StaticTable.SchemeHttps); if (request.HasHeaders && request.Headers.Host is string host) @@ -626,7 +625,7 @@ private void BufferHeaders(HttpRequestMessage request) if (request.Content == null) { - if (normalizedMethod.MustHaveRequestBody) + if (request.Method.MustHaveRequestBody) { BufferIndexedHeader(H3StaticTable.ContentLength0); headerListSize += HttpKnownHeaderNames.ContentLength.Length + HeaderField.RfcOverhead; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs index 8a72805bc43fd8..3127b69bcaaba8 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs @@ -270,17 +270,14 @@ private void ConsumeFromRemainingBuffer(int bytesToConsume) _readBuffer.Discard(bytesToConsume); } - private void WriteHeaders(HttpRequestMessage request, HttpMethod normalizedMethod) + private void WriteHeaders(HttpRequestMessage request) { Debug.Assert(request.RequestUri is not null); // Write the request line - WriteAsciiString(normalizedMethod.Method); - _writeBuffer.EnsureAvailableSpace(1); - _writeBuffer.AvailableSpan[0] = (byte)' '; - _writeBuffer.Commit(1); + WriteBytes(request.Method.Http1EncodedBytes); - if (ReferenceEquals(normalizedMethod, HttpMethod.Connect)) + if (request.Method.IsConnect) { // RFC 7231 #section-4.3.6. // Write only CONNECT foo.com:345 HTTP/1.1 @@ -353,7 +350,7 @@ private void WriteHeaders(HttpRequestMessage request, HttpMethod normalizedMetho { // Write out Content-Length: 0 header to indicate no body, // unless this is a method that never has a body. - if (normalizedMethod.MustHaveRequestBody) + if (request.Method.MustHaveRequestBody) { WriteBytes("Content-Length: 0\r\n"u8); } @@ -513,7 +510,6 @@ public async Task SendAsync(HttpRequestMessage request, boo Task? sendRequestContentTask = null; _currentRequest = request; - HttpMethod normalizedMethod = HttpMethod.Normalize(request.Method); _canRetry = false; @@ -524,7 +520,7 @@ public async Task SendAsync(HttpRequestMessage request, boo { if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestHeadersStart(Id); - WriteHeaders(request, normalizedMethod); + WriteHeaders(request); if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestHeadersStop(); @@ -748,12 +744,12 @@ public async Task SendAsync(HttpRequestMessage request, boo // Create the response stream. Stream responseStream; - if (ReferenceEquals(normalizedMethod, HttpMethod.Head) || response.StatusCode == HttpStatusCode.NoContent || response.StatusCode == HttpStatusCode.NotModified) + if (request.Method.IsHead || response.StatusCode is HttpStatusCode.NoContent or HttpStatusCode.NotModified) { responseStream = EmptyReadStream.Instance; CompleteResponse(); } - else if (ReferenceEquals(normalizedMethod, HttpMethod.Connect) && response.StatusCode == HttpStatusCode.OK) + else if (request.Method.IsConnect && response.StatusCode == HttpStatusCode.OK) { // Successful response to CONNECT does not have body. // What ever comes next should be opaque.