| File: RedactionBenchmark.cs | Web Access |
| Project: src\bench\Libraries\Microsoft.AspNetCore.Diagnostics.Middleware.PerformanceTests\Microsoft.AspNetCore.Diagnostics.Middleware.PerformanceTests.csproj (Microsoft.AspNetCore.Diagnostics.Middleware.PerformanceTests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Text; using BenchmarkDotNet.Attributes; using Microsoft.Extensions.Compliance.Classification; using Microsoft.Extensions.Compliance.Testing; using Microsoft.Extensions.ObjectPool; using Microsoft.Shared.Pools; namespace Microsoft.AspNetCore.Diagnostics.Bench; [GcServer(true)] [MinColumn] [MaxColumn] [MemoryDiagnoser] public class RedactionBenchmark { private readonly string _httpPath; private readonly Dictionary<string, DataClassification> _routeParameterDataClasses = []; private readonly ObjectPool<StringBuilder> _stringBuilderPool; private readonly Dictionary<string, object?> _routeValues = new() { { "userId", "testUserId" }, { "chatId", "testChatId" }, }; public RedactionBenchmark() { _routeParameterDataClasses.Add("userId", FakeTaxonomy.PrivateData); _routeParameterDataClasses.Add("chatId", FakeTaxonomy.PrivateData); _httpPath = "/users/{userId}/chats/{chatId}/test1/test2/{userId}"; _stringBuilderPool = PoolFactory.CreateStringBuilderPool(); } private static RouteSegment[] GetRouteSegments(string httpRoute) { var routeSegments = new List<RouteSegment>(); int startIndex = 0; while (startIndex < httpRoute.Length) { var startIndexOfParam = httpRoute.IndexOf('{', startIndex); if (startIndexOfParam == -1) { // We have reached to the end of the segment, no more parameters routeSegments.Add(new RouteSegment(httpRoute.Substring(startIndex), false)); break; } var endIndexOfParam = httpRoute.IndexOf('}', startIndexOfParam); var routeNonParamameterSegment = httpRoute.Substring(startIndex, startIndexOfParam - startIndex); var routeParameterSegment = httpRoute.Substring(startIndexOfParam + 1, endIndexOfParam - startIndexOfParam - 1); routeSegments.Add(new RouteSegment(routeNonParamameterSegment, false)); routeSegments.Add(new RouteSegment(routeParameterSegment, true)); startIndex = endIndexOfParam + 1; } return routeSegments.ToArray(); } [Benchmark] public void RedactHttpPathStringBuilderNETStd() { Span<char> destinationBuffer = stackalloc char[256]; var startIndex = 0; var span = _httpPath.AsSpan(); var isRouteKeyFound = false; var pathStringBuilder = _stringBuilderPool.Get(); ReadOnlySpan<char> segment; try { for (int i = 0; i <= span.Length; i++) { if (i == span.Length || span[i] == '/') { segment = span.Slice(startIndex, i - startIndex); foreach (var item in _routeValues) { if (((string)item.Value!).AsSpan().SequenceEqual(segment)) { if (_routeParameterDataClasses.TryGetValue(item.Key, out DataClassification classification)) { pathStringBuilder.Append(Redact(segment, destinationBuffer)); isRouteKeyFound = true; } break; } } if (!isRouteKeyFound) { pathStringBuilder.Append(segment); } if (i < span.Length) { _ = pathStringBuilder.Append('/'); } startIndex = i + 1; isRouteKeyFound = true; } } _ = pathStringBuilder.ToString(); } finally { _stringBuilderPool.Return(pathStringBuilder); } } [Benchmark] public void RedactHttpPathStringBuilderOptimizedForSpeedNETStd() { Span<char> destinationBuffer = stackalloc char[256]; var startIndex = 0; var span = _httpPath.AsSpan(); var pathStringBuilder = _stringBuilderPool.Get(); var newDict = new Dictionary<string, string>(_routeValues.Count); foreach (var item in _routeValues) { if (item.Value is not null) { newDict.Add((string)item.Value, item.Key); } } try { for (int i = 0; i <= span.Length; i++) { if (i == span.Length || span[i] == '/') { var segment = span.Slice(startIndex, i - startIndex).ToString(); if (newDict.TryGetValue(segment, out var value) && _routeParameterDataClasses.TryGetValue(value, out DataClassification classification)) { _ = pathStringBuilder.Append(Redact(segment, destinationBuffer)); } else { _ = pathStringBuilder.Append(segment); } if (i < span.Length) { _ = pathStringBuilder.Append('/'); } startIndex = i + 1; } } _ = pathStringBuilder.ToString(); } finally { _stringBuilderPool.Return(pathStringBuilder); } } [Benchmark] public void RedactHttpPathUsingIndexOfNETStd() { Span<char> destinationBuffer = stackalloc char[256]; var span = _httpPath.AsSpan(); var isRouteKeyFound = false; var pathStringBuilder = _stringBuilderPool.Get(); try { var startIndex = 0; var endIndex = 0; var segment = span; while (startIndex < span.Length) { _ = pathStringBuilder.Append('/'); startIndex = span.Slice(startIndex).IndexOf('/') + startIndex; endIndex = span.Slice(startIndex + 1).IndexOf('/') + startIndex; if (endIndex < startIndex) { endIndex = span.Length - 1; } segment = span.Slice(startIndex + 1, endIndex - startIndex); if (segment.Length > 0) { foreach (var item in _routeValues) { if (((string)item.Value!).AsSpan().SequenceEqual(segment)) { if (_routeParameterDataClasses.TryGetValue(item.Key, out DataClassification classification)) { _ = pathStringBuilder.Append(Redact(segment, destinationBuffer)); isRouteKeyFound = true; } break; } } if (!isRouteKeyFound) { _ = pathStringBuilder.Append(segment); } } startIndex = endIndex + 1; isRouteKeyFound = false; } _ = pathStringBuilder.ToString(); } finally { _stringBuilderPool.Return(pathStringBuilder); } } private static ReadOnlySpan<char> Redact(ReadOnlySpan<char> source, Span<char> destination) { return destination.Slice(0, source.Length); } private static int[] GetPathSegments(ReadOnlySpan<char> httpPath) { var numSegments = 0; for (var i = 0; i < httpPath.Length; i++) { if (httpPath[i] == '/') { numSegments++; } } var routeSegments = new int[numSegments + 1]; var j = 0; for (var i = 0; i < httpPath.Length; i++) { if (httpPath[i] == '/') { routeSegments[j] = i + 1; j++; } } routeSegments[numSegments] = httpPath.Length + 1; return routeSegments; } [Benchmark] public void RedactHttpPathWithSegmentsNETStd() { Span<char> destinationBuffer = stackalloc char[256]; var span = _httpPath.AsSpan(); var segments = GetPathSegments(span); var isRouteKeyFound = false; var pathStringBuilder = _stringBuilderPool.Get(); try { for (int i = 1; i < segments.Length; i++) { _ = pathStringBuilder.Append('/'); var segment = span.Slice(segments[i - 1], segments[i] - segments[i - 1] - 1); foreach (var item in _routeValues) { if (((string)item.Value!).AsSpan().SequenceEqual(segment)) { if (_routeParameterDataClasses.TryGetValue(item.Key, out DataClassification classification)) { _ = pathStringBuilder.Append(Redact(segment, destinationBuffer)); isRouteKeyFound = true; } break; } } if (!isRouteKeyFound) { _ = pathStringBuilder.Append(segment); } isRouteKeyFound = false; } _ = pathStringBuilder.ToString(); } finally { _stringBuilderPool.Return(pathStringBuilder); } } [Benchmark] public void RedactHttpRouteNETCore() { Span<char> destinationBuffer = stackalloc char[256]; var routeSegments = GetRouteSegments(_httpPath); var pathStringBuilder = _stringBuilderPool.Get(); try { foreach (var routeSegment in routeSegments) { if (routeSegment.IsParameter) { if (_routeValues.TryGetValue(routeSegment.Segment, out var paramValue)) { if (_routeParameterDataClasses.TryGetValue(routeSegment.Segment, out DataClassification classification)) { _ = pathStringBuilder.Append(Redact((string)paramValue!, destinationBuffer)); } else { _ = pathStringBuilder.Append(paramValue); } } } else { _ = pathStringBuilder.Append(routeSegment.Segment); } } _ = pathStringBuilder.ToString(); } finally { _stringBuilderPool.Return(pathStringBuilder); } } }