diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs index 01ae9a5c81..40d1cf0c6e 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs @@ -73,6 +73,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private string _requestId; private int _remainingRequestHeadersBytesAllowed; private int _requestHeadersParsed; + private uint _requestCount; protected readonly long _keepAliveTicks; private readonly long _requestHeadersTimeoutTicks; @@ -128,7 +129,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // don't generate an ID until it is requested if (_requestId == null) { - _requestId = CorrelationIdGenerator.GetNextId(); + _requestId = StringUtilities.ConcatAsHexSuffix(ConnectionId, ':', _requestCount); } return _requestId; } @@ -388,6 +389,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _requestHeadersParsed = 0; _responseBytesWritten = 0; + _requestCount++; } /// diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameRequestHeaders.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameRequestHeaders.cs index c11b654a8e..fb543805cb 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameRequestHeaders.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameRequestHeaders.cs @@ -44,7 +44,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http string key = new string('\0', keyLength); fixed (char* keyBuffer = key) { - if (!AsciiUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, keyLength)) + if (!StringUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, keyLength)) { throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidCharactersInHeaderName); } 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 7262abcca4..c78ddd17ff 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/HttpUtilities.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/HttpUtilities.cs @@ -119,7 +119,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure { // This version if AsciiUtilities returns null if there are any null (0 byte) characters // in the string - if (!AsciiUtilities.TryGetAsciiString(buffer, output, span.Length)) + if (!StringUtilities.TryGetAsciiString(buffer, output, span.Length)) { throw new InvalidOperationException(); } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/AsciiUtilities.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/StringUtilities.cs similarity index 64% rename from src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/AsciiUtilities.cs rename to src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/StringUtilities.cs index 5a95493a28..ab9ce0683c 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/AsciiUtilities.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/StringUtilities.cs @@ -3,7 +3,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure { - internal class AsciiUtilities + internal class StringUtilities { public static unsafe bool TryGetAsciiString(byte* input, char* output, int count) { @@ -75,5 +75,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure return isValid; } + + private static readonly string _encode16Chars = "0123456789ABCDEF"; + + /// + /// A faster version of String.Concat(, , .ToString("X8")) + /// + /// + /// + /// + /// + public static unsafe string ConcatAsHexSuffix(string str, char separator, uint number) + { + var length = 1 + 8; + if (str != null) + { + length += str.Length; + } + + // stackalloc to allocate array on stack rather than heap + char* charBuffer = stackalloc char[length]; + + var i = 0; + if (str != null) + { + for (i = 0; i < str.Length; i++) + { + charBuffer[i] = str[i]; + } + } + + charBuffer[i] = separator; + + charBuffer[i + 1] = _encode16Chars[(int)(number >> 28) & 0xF]; + charBuffer[i + 2] = _encode16Chars[(int)(number >> 24) & 0xF]; + charBuffer[i + 3] = _encode16Chars[(int)(number >> 20) & 0xF]; + charBuffer[i + 4] = _encode16Chars[(int)(number >> 16) & 0xF]; + charBuffer[i + 5] = _encode16Chars[(int)(number >> 12) & 0xF]; + charBuffer[i + 6] = _encode16Chars[(int)(number >> 8) & 0xF]; + charBuffer[i + 7] = _encode16Chars[(int)(number >> 4) & 0xF]; + charBuffer[i + 8] = _encode16Chars[(int)number & 0xF]; + + // string ctor overload that takes char* + return new string(charBuffer, 0, length); + } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs index 0f0a2d4ffa..6191ab0747 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs @@ -141,6 +141,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.NotEqual(nextId, secondId); } + [Fact] + public void TraceIdentifierCountsRequestsPerFrame() + { + var connectionId = _frameContext.ConnectionId; + var feature = ((IFeatureCollection)_frame).Get(); + // Reset() is called once in the test ctor + var count = 1; + void Reset() + { + _frame.Reset(); + count++; + } + + var nextId = feature.TraceIdentifier; + Assert.Equal($"{connectionId}:00000001", nextId); + + Reset(); + var secondId = feature.TraceIdentifier; + Assert.Equal($"{connectionId}:00000002", secondId); + + var big = 1_000_000; + while (big-- > 0) Reset(); + Assert.Equal($"{connectionId}:{count:X8}", feature.TraceIdentifier); + } + [Fact] public void TraceIdentifierGeneratesWhenNull() { @@ -149,7 +174,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.NotNull(id); Assert.Equal(id, _frame.TraceIdentifier); - _frame.TraceIdentifier = null; + _frame.Reset(); Assert.NotEqual(id, _frame.TraceIdentifier); } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/StringUtilitiesTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/StringUtilitiesTests.cs new file mode 100644 index 0000000000..5ef5f30cc1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/StringUtilitiesTests.cs @@ -0,0 +1,31 @@ +// 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 Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +{ + public class StringUtilitiesTests + { + [Theory] + [InlineData(uint.MinValue)] + [InlineData(0xF)] + [InlineData(0xA)] + [InlineData(0xFF)] + [InlineData(0xFFC59)] + [InlineData(uint.MaxValue)] + public void ConvertsToHex(uint value) + { + var str = CorrelationIdGenerator.GetNextId(); + Assert.Equal($"{str}:{value:X8}", StringUtilities.ConcatAsHexSuffix(str, ':', value)); + } + + [Fact] + public void HandlesNull() + { + uint value = 0x23BC0234; + Assert.Equal(":23BC0234", StringUtilities.ConcatAsHexSuffix(null, ':', value)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs index 517b6a0cca..6c2713a511 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs @@ -611,13 +611,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [Fact] public async Task TraceIdentifierIsUnique() { - const int IdentifierLength = 13; + const int identifierLength = 22; const int iterations = 10; using (var server = new TestServer(async context => { - Assert.Equal(IdentifierLength, Encoding.ASCII.GetByteCount(context.TraceIdentifier)); - context.Response.ContentLength = IdentifierLength; + Assert.Equal(identifierLength, Encoding.ASCII.GetByteCount(context.TraceIdentifier)); + context.Response.ContentLength = identifierLength; await context.Response.WriteAsync(context.TraceIdentifier); })) { @@ -635,7 +635,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests // requests on same connection using (var connection = server.CreateConnection()) { - var buffer = new char[IdentifierLength]; + var buffer = new char[identifierLength]; for (var i = 0; i < iterations; i++) { await connection.Send("GET / HTTP/1.1", @@ -645,12 +645,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await connection.Receive($"HTTP/1.1 200 OK", $"Date: {server.Context.DateHeaderValue}", - $"Content-Length: {IdentifierLength}", + $"Content-Length: {identifierLength}", "", "").TimeoutAfter(TimeSpan.FromSeconds(10)); - var read = await connection.Reader.ReadAsync(buffer, 0, IdentifierLength); - Assert.Equal(IdentifierLength, read); + var read = await connection.Reader.ReadAsync(buffer, 0, identifierLength); + Assert.Equal(identifierLength, read); var id = new string(buffer, 0, read); Assert.DoesNotContain(id, usedIds.ToArray()); usedIds.Add(id); diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/StringUtilitiesBenchmark.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/StringUtilitiesBenchmark.cs new file mode 100644 index 0000000000..8097a8b744 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/StringUtilitiesBenchmark.cs @@ -0,0 +1,34 @@ +// 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 BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + [Config(typeof(CoreConfig))] + public class StringUtilitiesBenchmark + { + private const int Iterations = 500_000; + + [Benchmark(Baseline = true, OperationsPerInvoke = Iterations)] + public void UintToString() + { + var connectionId = CorrelationIdGenerator.GetNextId(); + for (uint i = 0; i < Iterations; i++) + { + var id = connectionId + ':' + i.ToString("X8"); + } + } + + [Benchmark(OperationsPerInvoke = Iterations)] + public void ConcatAsHexSuffix() + { + var connectionId = CorrelationIdGenerator.GetNextId(); + for (uint i = 0; i < Iterations; i++) + { + var id = StringUtilities.ConcatAsHexSuffix(connectionId, ':', i); + } + } + } +}