diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/HttpUtilities.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/HttpUtilities.cs index c78ddd17ff..f36e1424ef 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/HttpUtilities.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/HttpUtilities.cs @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Internal.System; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure { - public static class HttpUtilities + public static partial class HttpUtilities { public const string Http10Version = "HTTP/1.0"; public const string Http11Version = "HTTP/1.1"; @@ -20,27 +20,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure public const string HttpsUriScheme = "https://"; // readonly primitive statics can be Jit'd to consts https://github.com/dotnet/coreclr/issues/1079 - private readonly static ulong _httpSchemeLong = GetAsciiStringAsLong(HttpUriScheme + "\0"); private readonly static ulong _httpsSchemeLong = GetAsciiStringAsLong(HttpsUriScheme); - private readonly static ulong _httpConnectMethodLong = GetAsciiStringAsLong("CONNECT "); - private readonly static ulong _httpDeleteMethodLong = GetAsciiStringAsLong("DELETE \0"); + private const uint _httpGetMethodInt = 542393671; // retun of GetAsciiStringAsInt("GET "); const results in better codegen - private readonly static ulong _httpHeadMethodLong = GetAsciiStringAsLong("HEAD \0\0\0"); - private readonly static ulong _httpPatchMethodLong = GetAsciiStringAsLong("PATCH \0\0"); - private readonly static ulong _httpPostMethodLong = GetAsciiStringAsLong("POST \0\0\0"); - private readonly static ulong _httpPutMethodLong = GetAsciiStringAsLong("PUT \0\0\0\0"); - private readonly static ulong _httpOptionsMethodLong = GetAsciiStringAsLong("OPTIONS "); - private readonly static ulong _httpTraceMethodLong = GetAsciiStringAsLong("TRACE \0\0"); private const ulong _http10VersionLong = 3471766442030158920; // GetAsciiStringAsLong("HTTP/1.0"); const results in better codegen private const ulong _http11VersionLong = 3543824036068086856; // GetAsciiStringAsLong("HTTP/1.1"); const results in better codegen - private readonly static ulong _mask8Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }); - private readonly static ulong _mask7Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00 }); - private readonly static ulong _mask6Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00 }); - private readonly static ulong _mask5Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00 }); - private readonly static ulong _mask4Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00 }); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetKnownMethod(ulong mask, ulong knownMethodUlong, HttpMethod knownMethod, int length) + { + _knownMethods[GetKnownMethodIndex(knownMethodUlong)] = new Tuple(mask, knownMethodUlong, knownMethod, length); + } private readonly static Tuple[] _knownMethods = { @@ -54,21 +46,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure Tuple.Create(_mask8Chars, _httpOptionsMethodLong, HttpMethod.Options, 7), }; - private readonly static string[] _methodNames = CreateMethodNames(); - - private static string[] CreateMethodNames() + private static void FillKnownMethodsGaps() { - var methodNames = new string[9]; - methodNames[(byte)HttpMethod.Get] = HttpMethods.Get; - methodNames[(byte)HttpMethod.Put] = HttpMethods.Put; - methodNames[(byte)HttpMethod.Delete] = HttpMethods.Delete; - methodNames[(byte)HttpMethod.Post] = HttpMethods.Post; - methodNames[(byte)HttpMethod.Head] = HttpMethods.Head; - methodNames[(byte)HttpMethod.Trace] = HttpMethods.Trace; - methodNames[(byte)HttpMethod.Patch] = HttpMethods.Patch; - methodNames[(byte)HttpMethod.Connect] = HttpMethods.Connect; - methodNames[(byte)HttpMethod.Options] = HttpMethods.Options; - return methodNames; + var knownMethods = _knownMethods; + var length = knownMethods.Length; + var invalidHttpMethod = new Tuple(_mask8Chars, 0ul, HttpMethod.Custom, 0); + for (int i = 0; i < length; i++) + { + if (knownMethods[i] == null) + { + knownMethods[i] = invalidHttpMethod; + } + } } private unsafe static ulong GetAsciiStringAsLong(string str) @@ -187,13 +176,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure else { var value = *(ulong*)data; - foreach (var x in _knownMethods) + var key = GetKnownMethodIndex(value); + var x = _knownMethods[key]; + + if (x != null && (value & x.Item1) == x.Item2) { - if ((value & x.Item1) == x.Item2) - { - methodLength = x.Item4; - return x.Item3; - } + methodLength = x.Item4; + return x.Item3; } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/HttpUtilities.Generated.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/HttpUtilities.Generated.cs new file mode 100644 index 0000000000..bc997c8f6f --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/HttpUtilities.Generated.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; + +namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure +{ + public static partial class HttpUtilities + { + // readonly primitive statics can be Jit'd to consts https://github.com/dotnet/coreclr/issues/1079 + private readonly static ulong _httpConnectMethodLong = GetAsciiStringAsLong("CONNECT "); + private readonly static ulong _httpDeleteMethodLong = GetAsciiStringAsLong("DELETE \0"); + private readonly static ulong _httpHeadMethodLong = GetAsciiStringAsLong("HEAD \0\0\0"); + private readonly static ulong _httpPatchMethodLong = GetAsciiStringAsLong("PATCH \0\0"); + private readonly static ulong _httpPostMethodLong = GetAsciiStringAsLong("POST \0\0\0"); + private readonly static ulong _httpPutMethodLong = GetAsciiStringAsLong("PUT \0\0\0\0"); + private readonly static ulong _httpOptionsMethodLong = GetAsciiStringAsLong("OPTIONS "); + private readonly static ulong _httpTraceMethodLong = GetAsciiStringAsLong("TRACE \0\0"); + + private readonly static ulong _mask8Chars = GetMaskAsLong(new byte[] + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}); + + private readonly static ulong _mask7Chars = GetMaskAsLong(new byte[] + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00}); + + private readonly static ulong _mask6Chars = GetMaskAsLong(new byte[] + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00}); + + private readonly static ulong _mask5Chars = GetMaskAsLong(new byte[] + {0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00}); + + private readonly static ulong _mask4Chars = GetMaskAsLong(new byte[] + {0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00}); + + private readonly static Tuple[] _knownMethods = + new Tuple[17]; + + private readonly static string[] _methodNames = new string[9]; + + static HttpUtilities() + { + SetKnownMethod(_mask4Chars, _httpPutMethodLong, HttpMethod.Put, 3); + SetKnownMethod(_mask5Chars, _httpHeadMethodLong, HttpMethod.Head, 4); + SetKnownMethod(_mask5Chars, _httpPostMethodLong, HttpMethod.Post, 4); + SetKnownMethod(_mask6Chars, _httpPatchMethodLong, HttpMethod.Patch, 5); + SetKnownMethod(_mask6Chars, _httpTraceMethodLong, HttpMethod.Trace, 5); + SetKnownMethod(_mask7Chars, _httpDeleteMethodLong, HttpMethod.Delete, 6); + SetKnownMethod(_mask8Chars, _httpConnectMethodLong, HttpMethod.Connect, 7); + SetKnownMethod(_mask8Chars, _httpOptionsMethodLong, HttpMethod.Options, 7); + FillKnownMethodsGaps(); + _methodNames[(byte)HttpMethod.Connect] = HttpMethods.Connect; + _methodNames[(byte)HttpMethod.Delete] = HttpMethods.Delete; + _methodNames[(byte)HttpMethod.Get] = HttpMethods.Get; + _methodNames[(byte)HttpMethod.Head] = HttpMethods.Head; + _methodNames[(byte)HttpMethod.Options] = HttpMethods.Options; + _methodNames[(byte)HttpMethod.Patch] = HttpMethods.Patch; + _methodNames[(byte)HttpMethod.Post] = HttpMethods.Post; + _methodNames[(byte)HttpMethod.Put] = HttpMethods.Put; + _methodNames[(byte)HttpMethod.Trace] = HttpMethods.Trace; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetKnownMethodIndex(ulong value) + { + const int magicNumer = 0x600000C; + var tmp = (int)value & magicNumer; + return ((tmp >> 2) | (tmp >> 23)) & 0xF; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Microsoft.AspNetCore.Server.Kestrel.csproj b/src/Microsoft.AspNetCore.Server.Kestrel/Microsoft.AspNetCore.Server.Kestrel.csproj index 8adf6b216c..23de1c5087 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Microsoft.AspNetCore.Server.Kestrel.csproj +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Microsoft.AspNetCore.Server.Kestrel.csproj @@ -14,6 +14,9 @@ + + + diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/GeneratedCodeTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/GeneratedCodeTests.cs index 386296d716..36db1894e1 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/GeneratedCodeTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/GeneratedCodeTests.cs @@ -15,27 +15,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { const string frameHeadersGeneratedPath = "../../../../../src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameHeaders.Generated.cs"; const string frameGeneratedPath = "../../../../../src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs"; - + const string httpUtilitiesGeneratedPath = "../../../../../src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/HttpUtilities.Generated.cs"; var testFrameHeadersGeneratedPath = Path.GetTempFileName(); var testFrameGeneratedPath = Path.GetTempFileName(); - + var testHttpUtilitiesGeneratedPath = Path.GetTempFileName(); try { var currentFrameHeadersGenerated = File.ReadAllText(frameHeadersGeneratedPath); var currentFrameGenerated = File.ReadAllText(frameGeneratedPath); + var currentHttpUtilitiesGenerated = File.ReadAllText(httpUtilitiesGeneratedPath); - CodeGenerator.Program.Run(testFrameHeadersGeneratedPath, testFrameGeneratedPath); + CodeGenerator.Program.Run(testFrameHeadersGeneratedPath, testFrameGeneratedPath, testHttpUtilitiesGeneratedPath); var testFrameHeadersGenerated = File.ReadAllText(testFrameHeadersGeneratedPath); var testFrameGenerated = File.ReadAllText(testFrameGeneratedPath); + var testHttpUtilitiesGenerated = File.ReadAllText(testHttpUtilitiesGeneratedPath); Assert.Equal(currentFrameHeadersGenerated, testFrameHeadersGenerated, ignoreLineEndingDifferences: true); Assert.Equal(currentFrameGenerated, testFrameGenerated, ignoreLineEndingDifferences: true); + Assert.Equal(currentHttpUtilitiesGenerated, testHttpUtilitiesGenerated, ignoreLineEndingDifferences: true); + } finally { File.Delete(testFrameHeadersGeneratedPath); File.Delete(testFrameGeneratedPath); + File.Delete(testHttpUtilitiesGeneratedPath); } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/KnownStringsBenchmark.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/KnownStringsBenchmark.cs index d4745bba4f..9aa3ffdf53 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/KnownStringsBenchmark.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/KnownStringsBenchmark.cs @@ -12,17 +12,95 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance { public class KnownStringsBenchmark { - static byte[] _method = Encoding.UTF8.GetBytes("GET "); + static byte[] _methodConnect = Encoding.ASCII.GetBytes("CONNECT "); + static byte[] _methodDelete = Encoding.ASCII.GetBytes("DELETE \0"); + static byte[] _methodGet = Encoding.ASCII.GetBytes("GET "); + static byte[] _methodHead = Encoding.ASCII.GetBytes("HEAD \0\0\0"); + static byte[] _methodPatch = Encoding.ASCII.GetBytes("PATCH \0\0"); + static byte[] _methodPost = Encoding.ASCII.GetBytes("POST \0\0\0"); + static byte[] _methodPut = Encoding.ASCII.GetBytes("PUT \0\0\0\0"); + static byte[] _methodOptions = Encoding.ASCII.GetBytes("OPTIONS "); + static byte[] _methodTrace = Encoding.ASCII.GetBytes("TRACE \0\0"); + static byte[] _version = Encoding.UTF8.GetBytes("HTTP/1.1\r\n"); const int loops = 1000; [Benchmark(OperationsPerInvoke = loops * 10)] public int GetKnownMethod_GET() + { + Span data = _methodGet; + + return GetKnownMethod(data); + } + + [Benchmark(OperationsPerInvoke = loops * 10)] + public int GetKnownMethod_CONNECT() + { + Span data = _methodConnect; + + return GetKnownMethod(data); + } + + [Benchmark(OperationsPerInvoke = loops * 10)] + public int GetKnownMethod_DELETE() + { + Span data = _methodDelete; + + return GetKnownMethod(data); + } + [Benchmark(OperationsPerInvoke = loops * 10)] + public int GetKnownMethod_HEAD() + { + Span data = _methodHead; + + return GetKnownMethod(data); + } + + [Benchmark(OperationsPerInvoke = loops * 10)] + public int GetKnownMethod_PATCH() + { + Span data = _methodPatch; + + return GetKnownMethod(data); + } + [Benchmark(OperationsPerInvoke = loops * 10)] + public int GetKnownMethod_POST() + { + Span data = _methodPost; + + return GetKnownMethod(data); + } + [Benchmark(OperationsPerInvoke = loops * 10)] + public int GetKnownMethod_PUT() + { + Span data = _methodPut; + + return GetKnownMethod(data); + } + + [Benchmark(OperationsPerInvoke = loops * 10)] + public int GetKnownMethod_OPTIONS() + { + Span data = _methodOptions; + + return GetKnownMethod(data); + } + + [Benchmark(OperationsPerInvoke = loops * 10)] + public int GetKnownMethod_TRACE() + { + Span data = _methodTrace; + + return GetKnownMethod(data); + } + + private int GetKnownMethod(Span data) { int len = 0; HttpMethod method; - Span data = _method; - for (int i = 0; i < loops; i++) { + + for (int i = 0; i < loops; i++) + { data.GetKnownMethod(out method, out var length); len += length; data.GetKnownMethod(out method, out length); @@ -53,7 +131,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance int len = 0; HttpVersion version; Span data = _version; - for (int i = 0; i < loops; i++) { + for (int i = 0; i < loops; i++) + { data.GetKnownVersion(out version, out var length); len += length; data.GetKnownVersion(out version, out length); diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/KnownStringsTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/KnownStringsTests.cs new file mode 100644 index 0000000000..1926eaa377 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/KnownStringsTests.cs @@ -0,0 +1,86 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; +using Xunit; + +namespace Microsoft.AspNetCore.Server.KestrelTests +{ + public class KnownStringsTests + { + static byte[] _methodConnect = Encoding.ASCII.GetBytes("CONNECT "); + static byte[] _methodDelete = Encoding.ASCII.GetBytes("DELETE \0"); + static byte[] _methodGet = Encoding.ASCII.GetBytes("GET "); + static byte[] _methodHead = Encoding.ASCII.GetBytes("HEAD \0\0\0"); + static byte[] _methodPatch = Encoding.ASCII.GetBytes("PATCH \0\0"); + static byte[] _methodPost = Encoding.ASCII.GetBytes("POST \0\0\0"); + static byte[] _methodPut = Encoding.ASCII.GetBytes("PUT \0\0\0\0"); + static byte[] _methodOptions = Encoding.ASCII.GetBytes("OPTIONS "); + static byte[] _methodTrace = Encoding.ASCII.GetBytes("TRACE \0\0"); + + const int MagicNumer = 0x0600000C; + static byte[] _invalidMethod1 = BitConverter.GetBytes((ulong)MagicNumer); + static byte[] _invalidMethod2 = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + static byte[] _invalidMethod3 = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + static byte[] _invalidMethod4 = Encoding.ASCII.GetBytes("CONNECT_"); + static byte[] _invalidMethod5 = Encoding.ASCII.GetBytes("DELETE_\0"); + static byte[] _invalidMethod6 = Encoding.ASCII.GetBytes("GET_"); + static byte[] _invalidMethod7 = Encoding.ASCII.GetBytes("HEAD_\0\0\0"); + static byte[] _invalidMethod8 = Encoding.ASCII.GetBytes("PATCH_\0\0"); + static byte[] _invalidMethod9 = Encoding.ASCII.GetBytes("POST_\0\0\0"); + static byte[] _invalidMethod10 = Encoding.ASCII.GetBytes("PUT_\0\0\0\0"); + static byte[] _invalidMethod11 = Encoding.ASCII.GetBytes("OPTIONS_"); + static byte[] _invalidMethod12 = Encoding.ASCII.GetBytes("TRACE_\0\0"); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object[] CreateTestDataEntry(byte[] methodData, HttpMethod expectedMethod, int expectedLength, bool expectedResult) + { + return new object[] { methodData, expectedMethod, expectedLength, expectedResult }; + } + + private static readonly object[][] _testData = new object[][] + { + CreateTestDataEntry(_methodGet, HttpMethod.Get, 3, true), + CreateTestDataEntry(_methodPut, HttpMethod.Put, 3, true), + CreateTestDataEntry(_methodPost, HttpMethod.Post, 4, true), + CreateTestDataEntry(_methodHead, HttpMethod.Head, 4, true), + CreateTestDataEntry(_methodTrace, HttpMethod.Trace, 5, true), + CreateTestDataEntry(_methodPatch, HttpMethod.Patch, 5, true), + CreateTestDataEntry(_methodDelete, HttpMethod.Delete, 6, true), + CreateTestDataEntry(_methodConnect, HttpMethod.Connect, 7, true), + CreateTestDataEntry(_methodOptions, HttpMethod.Options, 7, true), + CreateTestDataEntry(_invalidMethod1, HttpMethod.Custom, 0, false), + CreateTestDataEntry(_invalidMethod2, HttpMethod.Custom, 0, false), + CreateTestDataEntry(_invalidMethod3, HttpMethod.Custom, 0, false), + CreateTestDataEntry(_invalidMethod4, HttpMethod.Custom, 0, false), + CreateTestDataEntry(_invalidMethod5, HttpMethod.Custom, 0, false), + CreateTestDataEntry(_invalidMethod6, HttpMethod.Custom, 0, false), + CreateTestDataEntry(_invalidMethod7, HttpMethod.Custom, 0, false), + CreateTestDataEntry(_invalidMethod8, HttpMethod.Custom, 0, false), + CreateTestDataEntry(_invalidMethod9, HttpMethod.Custom, 0, false), + CreateTestDataEntry(_invalidMethod10, HttpMethod.Custom, 0, false), + CreateTestDataEntry(_invalidMethod11, HttpMethod.Custom, 0, false), + CreateTestDataEntry(_invalidMethod12, HttpMethod.Custom, 0, false), + }; + + public static IEnumerable TestData => _testData; + + [Theory] + [MemberData(nameof(TestData), MemberType = typeof(KnownStringsTests))] + public void GetsKnownMethod(byte[] methodData, HttpMethod expectedMethod, int expectedLength, bool expectedResult) + { + var data = new Span(methodData); + + var result = data.GetKnownMethod(out var method, out var length); + + Assert.Equal(expectedResult, result); + Assert.Equal(expectedMethod, method); + Assert.Equal(expectedLength, length); + } + } +} diff --git a/tools/CodeGenerator/CodeGenerator.csproj b/tools/CodeGenerator/CodeGenerator.csproj index 5f3f1c7203..4205f6894b 100644 --- a/tools/CodeGenerator/CodeGenerator.csproj +++ b/tools/CodeGenerator/CodeGenerator.csproj @@ -13,9 +13,21 @@ + + + + $(MSBuildThisFileDirectory)..\..\src\Microsoft.AspNetCore.Server.Kestrel.Core\Internal\Http FrameHeaders.Generated.cs Frame.Generated.cs + + True + + + + True + + diff --git a/tools/CodeGenerator/HttpUtilities/CombinationsWithoutRepetition.cs b/tools/CodeGenerator/HttpUtilities/CombinationsWithoutRepetition.cs new file mode 100644 index 0000000000..b7de0f4c4e --- /dev/null +++ b/tools/CodeGenerator/HttpUtilities/CombinationsWithoutRepetition.cs @@ -0,0 +1,103 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace CodeGenerator.HttpUtilities +{ + // C code for Algorithm L (Lexicographic combinations) in Section 7.2.1.3 of The Art of Computer Programming, Volume 4A: Combinatorial Algorithms, Part 1 : + internal class CombinationsWithoutRepetition : IEnumerator + { + private int[] _pointers; + private T[] _nElements; + private readonly int _p; + public T[] Current { get; private set; } + object IEnumerator.Current => Current; + + public CombinationsWithoutRepetition(T[] nElements, int p) + { + if (nElements.Length < p) throw new ArgumentOutOfRangeException(nameof(p)); + + _nElements = nElements; + _p = p; + Current = new T[p]; + ResetCurrent(); + } + + private bool _firstElement; + + public bool MoveNext() + { + if (_firstElement) + { + _firstElement = false; + return true; + } + + var p = _p; + var pointers = _pointers; + var current = Current; + var nElements = _nElements; + var index = 1; + + while (pointers[index] + 1 == pointers[index + 1]) + { + var j1 = index - 1; + + pointers[index] = j1; + current[j1] = nElements[j1]; + ++index; + } + + if (index > p) + { + return false; + } + + current[index - 1] = nElements[++pointers[index]]; + + return true; + } + + private void ResetCurrent() + { + var p = _p; + if (_pointers == null) + _pointers = new int[p + 3]; + + var pointers = _pointers; + var current = Current; + var nElements = _nElements; + + pointers[0] = 0; + for (int j = 1; j <= _p; j++) + { + pointers[j] = j - 1; + } + pointers[_p + 1] = nElements.Length; + pointers[_p + 2] = 0; + + for (int j = _p; j > 0; j--) + { + current[j - 1] = nElements[pointers[j]]; + } + _firstElement = true; + } + + public void Reset() + { + Array.Clear(Current, 0, Current.Length); + Current = null; + ResetCurrent(); + } + + public void Dispose() + { + _nElements = null; + Current = null; + _pointers = null; + } + } +} diff --git a/tools/CodeGenerator/HttpUtilities/HttpUtilities.cs b/tools/CodeGenerator/HttpUtilities/HttpUtilities.cs new file mode 100644 index 0000000000..4c15dc75d6 --- /dev/null +++ b/tools/CodeGenerator/HttpUtilities/HttpUtilities.cs @@ -0,0 +1,321 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; + +namespace CodeGenerator.HttpUtilities +{ + public class HttpUtilities + { + public static string GeneratedFile() + { + var httpMethods = new [] + { + new Tuple("CONNECT ", HttpMethod.Connect), + new Tuple("DELETE ", HttpMethod.Delete), + new Tuple("HEAD ", HttpMethod.Head), + new Tuple("PATCH ", HttpMethod.Patch), + new Tuple("POST ", HttpMethod.Post), + new Tuple("PUT ", HttpMethod.Put), + new Tuple("OPTIONS ", HttpMethod.Options), + new Tuple("TRACE ", HttpMethod.Trace), + new Tuple("GET ", HttpMethod.Get) + }; + + return GenerateFile(httpMethods); + } + + private static string GenerateFile(Tuple[] httpMethods) + { + var maskLength = (byte)Math.Ceiling(Math.Log(httpMethods.Length, 2)); + + var methodsInfo = httpMethods.Select(GetMethodStringAndUlongAndMaskLength).ToList(); + + var methodsInfoWithoutGet = methodsInfo.Where(m => m.HttpMethod != HttpMethod.Get.ToString()).ToList(); + + var methodsAsciiStringAsLong = methodsInfo.Select(m => m.AsciiStringAsLong).ToArray(); + + var mask = HttpUtilitiesGeneratorHelpers.SearchKeyByLookThroughMaskCombinations(methodsAsciiStringAsLong, 0, sizeof(ulong) * 8, maskLength); + + if (mask.HasValue == false) + { + throw new InvalidOperationException(string.Format("Generated {0} not found.", nameof(mask))); + } + + var functionGetKnownMethodIndex = GetFunctionBodyGetKnownMethodIndex(mask.Value); + + var methodsSection = GetMethodsSection(methodsInfoWithoutGet); + + var masksSection = GetMasksSection(methodsInfoWithoutGet); + + var setKnownMethodSection = GetSetKnownMethodSection(methodsInfoWithoutGet); + var methodNamesSection = GetMethodNamesSection(methodsInfo); + + int knownMethodsArrayLength = (int)(Math.Pow(2, maskLength) + 1); + int methodNamesArrayLength = httpMethods.Length; + + return string.Format(@"// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; + +namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure +{{ + public static partial class HttpUtilities + {{ + // readonly primitive statics can be Jit'd to consts https://github.com/dotnet/coreclr/issues/1079 +{0} + +{1} + private readonly static Tuple[] _knownMethods = + new Tuple[{2}]; + + private readonly static string[] _methodNames = new string[{3}]; + + static HttpUtilities() + {{ +{4} + FillKnownMethodsGaps(); +{5} + }} + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetKnownMethodIndex(ulong value) + {{ +{6} + }} + }} +}}", methodsSection, masksSection, knownMethodsArrayLength, methodNamesArrayLength, setKnownMethodSection, methodNamesSection, functionGetKnownMethodIndex); + } + + private static string GetMethodsSection(List methodsInfo) + { + var result = new StringBuilder(); + + for (var index = 0; index < methodsInfo.Count; index++) + { + var methodInfo = methodsInfo[index]; + + var httpMethodFieldName = GetHttpMethodFieldName(methodInfo); + result.AppendFormat(" private readonly static ulong {0} = GetAsciiStringAsLong(\"{1}\");", httpMethodFieldName, methodInfo.MethodAsciiString.Replace("\0", "\\0")); + + if (index < methodsInfo.Count - 1) + { + result.AppendLine(); + } + } + + return result.ToString(); + } + + private static string GetMasksSection(List methodsInfo) + { + var distinctLengths = methodsInfo.Select(m => m.MaskLength).Distinct().ToList(); + + distinctLengths.Sort((t1, t2) => -t1.CompareTo(t2)); + + var result = new StringBuilder(); + + for (var index = 0; index < distinctLengths.Count; index++) + { + var maskBytesLength = distinctLengths[index]; + var maskArray = GetMaskArray(maskBytesLength); + + var hexMaskString = HttpUtilitiesGeneratorHelpers.GeHexString(maskArray, "0x", ", "); + var maskFieldName = GetMaskFieldName(maskBytesLength); + + result.AppendFormat(" private readonly static ulong {0} = GetMaskAsLong(new byte[]\r\n {{{1}}});", maskFieldName, hexMaskString); + result.AppendLine(); + if (index < distinctLengths.Count - 1) + { + result.AppendLine(); + } + } + + return result.ToString(); + } + + private static string GetSetKnownMethodSection(List methodsInfo) + { + methodsInfo = methodsInfo.ToList(); + + methodsInfo.Sort((t1, t2) => t1.MaskLength.CompareTo(t2.MaskLength)); + + var result = new StringBuilder(); + + for (var index = 0; index < methodsInfo.Count; index++) + { + var methodInfo = methodsInfo[index]; + var maskFieldName = GetMaskFieldName(methodInfo.MaskLength); + var httpMethodFieldName = GetHttpMethodFieldName(methodInfo); + + result.AppendFormat(" SetKnownMethod({0}, {1}, {2}.{3}, {4});", maskFieldName, httpMethodFieldName, typeof(HttpMethod).Name, methodInfo.HttpMethod, methodInfo.MaskLength - 1); + + if (index < methodsInfo.Count - 1) + { + result.AppendLine(); + } + } + + return result.ToString(); + } + + private static string GetMethodNamesSection(List methodsInfo) + { + methodsInfo = methodsInfo.ToList(); + + methodsInfo.Sort((t1, t2) => t1.HttpMethod.CompareTo(t2.HttpMethod)); + + var result = new StringBuilder(); + + for (var index = 0; index < methodsInfo.Count; index++) + { + var methodInfo = methodsInfo[index]; + + result.AppendFormat(" _methodNames[(byte){0}.{1}] = {2}.{3};", typeof(HttpMethod).Name, methodInfo.HttpMethod, typeof(HttpMethods).Name, methodInfo.HttpMethod); + + if (index < methodsInfo.Count - 1) + { + result.AppendLine(); + } + } + + return result.ToString(); + } + + private static string GetFunctionBodyGetKnownMethodIndex(ulong mask) + { + var shifts = HttpUtilitiesGeneratorHelpers.GetShifts(mask); + + var maskHexString = HttpUtilitiesGeneratorHelpers.MaskToHexString(mask); + + string bodyString; + + if (shifts.Length > 0) + { + var bitsCount = HttpUtilitiesGeneratorHelpers.CountBits(mask); + + var tmpReturn = string.Empty; + foreach (var item in shifts) + { + if (tmpReturn.Length > 0) + { + tmpReturn += " | "; + } + + tmpReturn += string.Format("(tmp >> {1})", HttpUtilitiesGeneratorHelpers.MaskToHexString(item.Mask), item.Shift); + } + + var mask2 = (ulong)(Math.Pow(2, bitsCount) - 1); + + string returnString = string.Format("return ({0}) & {1};", tmpReturn, HttpUtilitiesGeneratorHelpers.MaskToHexString(mask2)); + + bodyString = string.Format(" const int magicNumer = {0};\r\n var tmp = (int)value & magicNumer;\r\n {1}", HttpUtilitiesGeneratorHelpers.MaskToHexString(mask), returnString); + + } + else + { + bodyString = string.Format("return (int)(value & {0});", maskHexString); + } + + return bodyString; + } + + private static string GetHttpMethodFieldName(MethodInfo methodsInfo) + { + return string.Format("_http{0}MethodLong", methodsInfo.HttpMethod.ToString()); + } + + private static string GetMaskFieldName(int nBytes) + { + return string.Format("_mask{0}Chars", nBytes); + } + + private static string GetMethodString(string method) + { + if (method == null) + { + throw new ArgumentNullException(nameof(method)); + } + + const int length = sizeof(ulong); + + if (method.Length > length) + { + throw new ArgumentException(string.Format("MethodAsciiString {0} length is greather than {1}", method, length)); + } + string result = method; + + if (result.Length == length) + { + return result; + } + + if (result.Length < length) + { + var count = length - result.Length; + + for (int i = 0; i < count; i++) + { + result += "\0"; + } + } + + return result; + } + + private class MethodInfo + { + public string MethodAsciiString; + public ulong AsciiStringAsLong; + public string HttpMethod; + public int MaskLength; + } + + private static MethodInfo GetMethodStringAndUlongAndMaskLength(Tuple method) + { + var methodString = GetMethodString(method.Item1); + + var asciiAsLong = GetAsciiStringAsLong(methodString); + + return new MethodInfo + { + MethodAsciiString = methodString, + AsciiStringAsLong = asciiAsLong, + HttpMethod = method.Item2.ToString(), + MaskLength = method.Item1.Length + }; + } + + private static byte[] GetMaskArray(int n, int length = sizeof(ulong)) + { + var maskArray = new byte[length]; + for (int i = 0; i < n; i++) + { + maskArray[i] = 0xff; + } + return maskArray; + } + + private unsafe static ulong GetAsciiStringAsLong(string str) + { + Debug.Assert(str.Length == sizeof(ulong), string.Format("String must be exactly {0} (ASCII) characters long.", sizeof(ulong))); + + var bytes = Encoding.ASCII.GetBytes(str); + + fixed (byte* ptr = &bytes[0]) + { + return *(ulong*)ptr; + } + } + } +} \ No newline at end of file diff --git a/tools/CodeGenerator/HttpUtilities/HttpUtilitiesGeneratorHelpers.cs b/tools/CodeGenerator/HttpUtilities/HttpUtilitiesGeneratorHelpers.cs new file mode 100644 index 0000000000..0cadd80aa0 --- /dev/null +++ b/tools/CodeGenerator/HttpUtilities/HttpUtilitiesGeneratorHelpers.cs @@ -0,0 +1,217 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; + +namespace CodeGenerator.HttpUtilities +{ + internal class HttpUtilitiesGeneratorHelpers + { + public class ShiftInfo + { + public TMask Mask; + public byte Shift; + } + + public static ShiftInfo[] GetShifts(ulong mask) + { + var shifts = new List>(); + + const ulong one = 0x01; + + ulong currentMask = 0; + + int currentBitsCount = 0; + int lastShift = 0; + for (int i = 0; i < sizeof(ulong) * 8; i++) + { + var currentBitMask = one << i; + bool isCurrentBit0 = (currentBitMask & mask) == 0; + + if (isCurrentBit0 == false) + { + currentMask |= currentBitMask; + currentBitsCount++; + } + else if (currentBitsCount > 0) + { + var currentShift = (byte)(i - currentBitsCount - lastShift); + shifts.Add(new ShiftInfo + { + Mask = currentMask, + Shift = currentShift + }); + lastShift = currentShift; + currentMask = 0; + currentBitsCount = 0; + } + } + + return shifts.ToArray(); + } + + public static ulong? SearchKeyByLookThroughMaskCombinations(ulong[] values, byte bitsIndexStart, byte bitsLength, byte bitsCount) + { + if (bitsIndexStart + bitsLength > sizeof(ulong) * 8) + { + throw new ArgumentOutOfRangeException(nameof(bitsIndexStart)); + } + + if (bitsLength < bitsCount || bitsCount == 0) + { + throw new ArgumentOutOfRangeException(nameof(bitsCount)); + } + + var bits = new byte[bitsLength]; + + for (byte i = bitsIndexStart; i < bitsIndexStart + bitsLength; i++) + { + bits[i - bitsIndexStart] = i; + } + + var combinations = new CombinationsWithoutRepetition(bits, bitsCount); + + ulong? maskFound = null; + int bit1ChunksFoundMask = 0; + + int arrayLength = values.Length; + + var mashHash = new HashSet(); + + while (combinations.MoveNext()) + { + var bitsCombination = combinations.Current; + + ulong currentMask = 0; + + for (int i = 0; i < bitsCombination.Length; i++) + { + var index = bitsCombination[i]; + + const ulong oneBit = 0x01; + + currentMask |= oneBit << index; + } + + mashHash.Clear(); + bool invalidMask = false; + for (int j = 0; j < arrayLength; j++) + { + var tmp = values[j] & currentMask; + + bool alreadyExists = mashHash.Add(tmp) == false; + if (alreadyExists) + { + invalidMask = true; + break; + } + } + + if (invalidMask == false) + { + var bit1Chunks = CountBit1Chunks(currentMask); + + if (maskFound.HasValue) + { + if (bit1ChunksFoundMask > bit1Chunks) + { + maskFound = currentMask; + bit1ChunksFoundMask = bit1Chunks; + if (bit1ChunksFoundMask == 0) + { + return maskFound; + } + } + } + else + { + maskFound = currentMask; + bit1ChunksFoundMask = bit1Chunks; + + if (bit1ChunksFoundMask == 0) + { + return maskFound; + } + } + } + } + + return maskFound; + } + + public static int CountBit1Chunks(ulong mask) + { + int currentBitsCount = 0; + + int chunks = 0; + + for (int i = 0; i < sizeof(ulong) * 8; i++) + { + const ulong oneBit = 0x01; + + var currentBitMask = oneBit << i; + bool isCurrentBit0 = (currentBitMask & mask) == 0; + + if (isCurrentBit0 == false) + { + currentBitsCount++; + } + else if (currentBitsCount > 0) + { + chunks++; + currentBitsCount = 0; + } + } + + return chunks; + } + + public static string GeHexString(byte[] array, string prefix, string separator) + { + var result = new StringBuilder(); + int i = 0; + for (; i < array.Length - 1; i++) + { + result.AppendFormat("{0}{1:x2}", prefix, array[i]); + result.Append(separator); + } + + if (array.Length > 0) + { + result.AppendFormat("{0}{1:x2}", prefix, array[i]); + } + + return result.ToString(); + } + + public static string MaskToString(ulong mask) + { + var maskSizeInBIts = Math.Log(mask, 2); + var hexMaskSize = Math.Ceiling(maskSizeInBIts / 4.0); + return string.Format("0x{0:X" + hexMaskSize + "}", mask); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CountBits(ulong v) + { + const ulong Mask01010101 = 0x5555555555555555UL; + const ulong Mask00110011 = 0x3333333333333333UL; + const ulong Mask00001111 = 0x0F0F0F0F0F0F0F0FUL; + const ulong Mask00000001 = 0x0101010101010101UL; + v = v - ((v >> 1) & Mask01010101); + v = (v & Mask00110011) + ((v >> 2) & Mask00110011); + return (int)(unchecked(((v + (v >> 4)) & Mask00001111) * Mask00000001) >> 56); + } + + public static string MaskToHexString(ulong mask) + { + var maskSizeInBIts = Math.Log(mask, 2); + var hexMaskSize = (byte)Math.Ceiling(maskSizeInBIts / 4); + + return string.Format("0x{0:X" + (hexMaskSize == 0 ? 1 : hexMaskSize) + "}", mask); + } + } +} diff --git a/tools/CodeGenerator/Program.cs b/tools/CodeGenerator/Program.cs index a057b078c6..24be8360b9 100644 --- a/tools/CodeGenerator/Program.cs +++ b/tools/CodeGenerator/Program.cs @@ -20,16 +20,22 @@ namespace CodeGenerator Console.Error.WriteLine("Missing path to Frame.Generated.cs"); return 1; } + else if (args.Length < 3) + { + Console.Error.WriteLine("Missing path to HttpUtilities.Generated.cs"); + return 1; + } - Run(args[0], args[1]); + Run(args[0], args[1], args[2]); return 0; } - public static void Run(string knownHeadersPath, string frameFeaturesCollectionPath) + public static void Run(string knownHeadersPath, string frameFeaturesCollectionPath, string httpUtilitiesPath) { var knownHeadersContent = KnownHeaders.GeneratedFile(); var frameFeatureCollectionContent = FrameFeatureCollection.GeneratedFile(); + var httpUtilitiesContent = HttpUtilities.HttpUtilities.GeneratedFile(); var existingKnownHeaders = File.Exists(knownHeadersPath) ? File.ReadAllText(knownHeadersPath) : ""; if (!string.Equals(knownHeadersContent, existingKnownHeaders)) @@ -42,6 +48,12 @@ namespace CodeGenerator { File.WriteAllText(frameFeaturesCollectionPath, frameFeatureCollectionContent); } + + var existingHttpUtilities = File.Exists(httpUtilitiesPath) ? File.ReadAllText(httpUtilitiesPath) : ""; + if (!string.Equals(httpUtilitiesContent, existingHttpUtilities)) + { + File.WriteAllText(httpUtilitiesPath, httpUtilitiesContent); + } } } }