Improve performance of AspNetCoreResourceNameHelper.SimplifyRoutePattern#8180
Improve performance of AspNetCoreResourceNameHelper.SimplifyRoutePattern#8180andrewlock merged 2 commits intomasterfrom
AspNetCoreResourceNameHelper.SimplifyRoutePattern#8180Conversation
Execution-Time Benchmarks Report ⏱️Execution-time results for samples comparing This PR (8180) and master. ✅ No regressions detected - check the details below Full Metrics ComparisonFakeDbCommand
HttpMessageHandler
Comparison explanationExecution-time benchmarks measure the whole time it takes to execute a program, and are intended to measure the one-off costs. Cases where the execution time results for the PR are worse than latest master results are highlighted in **red**. The following thresholds were used for comparing the execution times:
Note that these results are based on a single point-in-time result for each branch. For full results, see the dashboard. Graphs show the p99 interval based on the mean and StdDev of the test run, as well as the mean value of the run (shown as a diamond below the graph). Duration chartsFakeDbCommand (.NET Framework 4.8)gantt
title Execution time (ms) FakeDbCommand (.NET Framework 4.8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8180) - mean (74ms) : 72, 76
master - mean (75ms) : 72, 78
section Bailout
This PR (8180) - mean (78ms) : 76, 80
master - mean (79ms) : 78, 81
section CallTarget+Inlining+NGEN
This PR (8180) - mean (1,069ms) : 1032, 1105
master - mean (1,079ms) : 1036, 1121
FakeDbCommand (.NET Core 3.1)gantt
title Execution time (ms) FakeDbCommand (.NET Core 3.1)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8180) - mean (113ms) : 110, 117
master - mean (116ms) : 113, 119
section Bailout
This PR (8180) - mean (115ms) : 112, 117
master - mean (117ms) : 114, 120
section CallTarget+Inlining+NGEN
This PR (8180) - mean (759ms) : 699, 819
master - mean (760ms) : 703, 817
FakeDbCommand (.NET 6)gantt
title Execution time (ms) FakeDbCommand (.NET 6)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8180) - mean (101ms) : 97, 105
master - mean (104ms) : 101, 107
section Bailout
This PR (8180) - mean (102ms) : 100, 105
master - mean (106ms) : 103, 108
section CallTarget+Inlining+NGEN
This PR (8180) - mean (744ms) : 675, 813
master - mean (748ms) : 670, 826
FakeDbCommand (.NET 8)gantt
title Execution time (ms) FakeDbCommand (.NET 8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8180) - mean (101ms) : 96, 105
master - mean (102ms) : 99, 106
section Bailout
This PR (8180) - mean (102ms) : 98, 105
master - mean (104ms) : 100, 109
section CallTarget+Inlining+NGEN
This PR (8180) - mean (663ms) : 646, 679
master - mean (677ms) : 659, 695
HttpMessageHandler (.NET Framework 4.8)gantt
title Execution time (ms) HttpMessageHandler (.NET Framework 4.8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8180) - mean (194ms) : 190, 198
master - mean (194ms) : 190, 197
section Bailout
This PR (8180) - mean (198ms) : 194, 202
master - mean (197ms) : 195, 199
section CallTarget+Inlining+NGEN
This PR (8180) - mean (1,146ms) : 1086, 1207
master - mean (1,139ms) : 1088, 1191
HttpMessageHandler (.NET Core 3.1)gantt
title Execution time (ms) HttpMessageHandler (.NET Core 3.1)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8180) - mean (277ms) : 271, 283
master - mean (278ms) : 272, 285
section Bailout
This PR (8180) - mean (277ms) : 274, 280
master - mean (279ms) : 274, 283
section CallTarget+Inlining+NGEN
This PR (8180) - mean (937ms) : 890, 984
master - mean (931ms) : 877, 985
HttpMessageHandler (.NET 6)gantt
title Execution time (ms) HttpMessageHandler (.NET 6)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8180) - mean (276ms) : 269, 283
master - mean (270ms) : 267, 274
section Bailout
This PR (8180) - mean (275ms) : 268, 283
master - mean (271ms) : 266, 275
section CallTarget+Inlining+NGEN
This PR (8180) - mean (921ms) : 877, 964
master - mean (924ms) : 886, 962
HttpMessageHandler (.NET 8)gantt
title Execution time (ms) HttpMessageHandler (.NET 8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8180) - mean (270ms) : 263, 276
master - mean (271ms) : 266, 276
section Bailout
This PR (8180) - mean (271ms) : 265, 277
master - mean (270ms) : 267, 274
section CallTarget+Inlining+NGEN
This PR (8180) - mean (831ms) : 811, 850
master - mean (827ms) : 809, 844
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
tracer/src/Datadog.Trace/DiagnosticListeners/AspNetCoreResourceNameHelper.cs
Outdated
Show resolved
Hide resolved
f245d13 to
4172b5b
Compare
4172b5b to
6cc2185
Compare
64c6b16 to
db9e7f0
Compare
db9e7f0 to
aa7c23f
Compare
BenchmarksBenchmark execution time: 2026-02-18 12:34:33 Comparing candidate commit 08cc252 in PR branch Found 4 performance improvements and 8 performance regressions! Performance is the same for 166 metrics, 14 unstable metrics. scenario:Benchmarks.Trace.AgentWriterBenchmark.WriteAndFlushEnrichedTraces net6.0
scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.AllCycleSimpleBody netcoreapp3.1
scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.ObjectExtractorSimpleBody netcoreapp3.1
scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeLegacyArgs netcoreapp3.1
scenario:Benchmarks.Trace.AspNetCoreBenchmark.SendRequest net6.0
scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces net472
scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces netcoreapp3.1
scenario:Benchmarks.Trace.ILoggerBenchmark.EnrichedLog netcoreapp3.1
scenario:Benchmarks.Trace.SingleSpanAspNetCoreBenchmark.SingleSpanAspNetCore net6.0
scenario:Benchmarks.Trace.SingleSpanAspNetCoreBenchmark.SingleSpanAspNetCore netcoreapp3.1
scenario:Benchmarks.Trace.TraceAnnotationsBenchmark.RunOnMethodBegin netcoreapp3.1
|
There was a problem hiding this comment.
Pull request overview
This PR optimizes the performance of AspNetCoreResourceNameHelper.SimplifyRoutePattern by switching to ValueStringBuilder for .NET Core 3.1+ and implementing other minor performance improvements. The changes reduce memory allocations by approximately 26% across most .NET versions, with some trade-offs in execution time.
Changes:
- Switched from
StringBuildertoValueStringBuilderfor .NET Core 3.1+ to reduce allocations - Refactored parameter handling logic to consolidate area/controller/action handling with a
mustExpandflag - Changed from
partscounter toaddedPartboolean flag for tracking segment state
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return string.IsNullOrEmpty(simplifiedRoute) ? "/" : simplifiedRoute.ToLowerInvariant(); | ||
| return sb.ToString(); | ||
| #else | ||
| return StringBuilderCache.GetStringAndRelease(sb).ToLowerInvariant(); |
There was a problem hiding this comment.
The non-NETCOREAPP path is missing the empty string check that the original code had. The original code returned "/" when the simplified route was null or empty, but this version will return an empty string (after lowercasing). This breaks the existing behavior for .NET Core 2.1 and 3.0.
The NETCOREAPP path correctly handles this at lines 231-235, but the non-NETCOREAPP path needs a similar check.
| return StringBuilderCache.GetStringAndRelease(sb).ToLowerInvariant(); | |
| var simplifiedRoute = StringBuilderCache.GetStringAndRelease(sb); | |
| return string.IsNullOrEmpty(simplifiedRoute) ? "/" : simplifiedRoute.ToLowerInvariant(); |
## Summary of changes A few minor improvements to the standard `AspNetCoreDiagnosticObserver` ## Reason for change Looking into obvious perf improvements for ASP.NET Core, but only a few minor things stood out (apart from related PRs like #8199 and #8203). ## Implementation details - Reduce size of MVC tags object by not deriving from `WebTags` (we never set those tags anyway) - Delay creating spanlinks collection if we don't need it - HttpRoute always matches AspNetCoreRoute, so can make it readonly ## Test coverage Covered by existing tests, benchmarks show (tiny) allocation gains ## Other details Relates to https://datadoghq.atlassian.net/browse/LANGPLAT-842 Related PRs: - #8167 - #8170 - #8180 - #8196 - #8199 - #8203
aa7c23f to
87b3d91
Compare
6cc2185 to
b02265c
Compare
87b3d91 to
b640b9b
Compare
…plate` (#8170) ## Summary of changes Reduces the allocations produced in `AspNetCoreResourceNameHelper.SimplifyRouteTemplate()` ## Reason for change I'm working on perf for aspnetcore in general, and this was an easy target point, as we know it allocates a bunch of intermediate `string` currently. Note that I believe this is generally only used in the pre-endpoint routing flows, so it will not be commonly used. It doesn't hurt though, and it was easier to work with, hence it's first up here 😄 ## Implementation details This is mostly inspired/based on the work I did in the single-span optimized path, but accounts for the fact that 1. We have to explicitly extract `controller`, `action`, and `area` for tags, so those params are already known 2. Need to not have any breaking changes in this code (that's a subtle difference compared to the single-span version) The high-level improvements are: - Use `ValueStringBuilder` for building up the span (.NET Core 3.1+). The main benefit that gives is the `AppendAsLowerInvariant` method - Remove boxing of the enumerator by directly casting to `List<T>` (it's always that type in all implementations, so this is an easy improvement) - Use the provided `controller`, `action`, and `area` values - This makes the code more complicated, so I toyed with ignoring them and just always doing the route value dictionary lookup, but this approach came out faster. Not massive though, so if there's objections, we could simplify it. - Add a check to `IsIdentifierSegment()` to check for common identifier types that are always going to be ommited. Removes the need for calling `ToString()` on `int` values (for example) .NET Core 2.1/3.0 (where we can't use `ValueStringBuilder`) has the following differences - Use the `StringBuilderCache` instead of `ValueStringBuilder` - Add a "dummy" `AppendAsLowerInvariant()` and call `ToString().ToLowerInvariant()` at the end (as we do today) ## Test coverage We already have unit tests for the behaviour, so I focused on the benchmarks For the first test, I tested against all the templates we test with here: https://github.com/DataDog/dd-trace-dotnet/blob/c0dad8658f35f35a1202036ff1afb247fd9b7d7d/tracer/test/Datadog.Trace.Tests/DiagnosticListeners/AspNetCoreResourceNameHelperTests.cs#L104-L122 and created "aggregated" results, testing against .NET Core 2.1, 3.1, 6, and 10: | Method | Runtime | Expand Param | Mean | Allocated | Alloc Ratio | | ---------------------- | ------------- | ------------ | -------: | --------: | ----------: | | RouteTemplate_Original | .NET 10.0 | False | 3.002 us | 4.3 KB | 1.00 | | RouteTemplate_Updated | .NET 10.0 | False | 2.452 us | 1.7 KB | 0.40 | | RouteTemplate_Original | .NET 6.0 | False | 3.781 us | 4.3 KB | 1.00 | | RouteTemplate_Updated | .NET 6.0 | False | 3.425 us | 1.7 KB | 0.40 | | RouteTemplate_Original | .NET Core 3.1 | False | 5.117 us | 4.3 KB | 1.00 | | RouteTemplate_Updated | .NET Core 3.1 | False | 5.111 us | 1.7 KB | 0.40 | | RouteTemplate_Original | .NET Core 2.1 | False | 8.571 us | 4.48 KB | 1.00 | | RouteTemplate_Updated | .NET Core 2.1 | False | 7.712 us | 3.58 KB | 0.80 | | | | | | | | | RouteTemplate_Original | .NET 10.0 | True | 2.957 us | 3.99 KB | 0.99 | | RouteTemplate_Updated | .NET 10.0 | True | 2.728 us | 1.55 KB | 0.38 | | RouteTemplate_Original | .NET 6.0 | True | 4.135 us | 4.02 KB | 1.00 | | RouteTemplate_Updated | .NET 6.0 | True | 3.663 us | 1.55 KB | 0.38 | | RouteTemplate_Original | .NET Core 3.1 | True | 5.455 us | 4.02 KB | 1.00 | | RouteTemplate_Updated | .NET Core 3.1 | True | 5.593 us | 1.55 KB | 0.38 | | RouteTemplate_Original | .NET Core 2.1 | True | 9.361 us | 4.29 KB | 1.00 | | RouteTemplate_Updated | .NET Core 2.1 | True | 8.878 us | 3.36 KB | 0.78 | In all cases, we allocate less now, and for all .NET Core 3.1+, a _lot_ less. There is one minor regression in .NET Core 3.1 in speed when we expand params, but I think the allocation savings are still worth it. To ensure there were no single degenerate cases, I also ran .NET 6 only against each template independently. <details><summary>Full single-template benchmark results</summary> <p> | Version | Expand | Template | Mean | Allocated | Alloc Ratio | | -------- | ------ | ---------------------------------------------------------------------------------- | -------: | --------: | ----------: | | Original | False | {controller}/{action}/{id} | 152.24ns | 152B | 1.00 | | Updated | False | {controller}/{action}/{id} | 329.65ns | 168B | 1.11 | | Original | False | {controller}/{action} | 183.02ns | 184B | 1.21 | | Updated | False | {controller}/{action} | 257.93ns | 96B | 0.63 | | Original | False | {area:exists}/{controller}/{action} | 154.99ns | 152B | 1.00 | | Updated | False | {area:exists}/{controller}/{action} | 298.77ns | 120B | 0.79 | | Original | False | prefix/{controller}/{action} | 178.64ns | 184B | 1.21 | | Updated | False | prefix/{controller}/{action} | 131.74ns | 56B | 0.37 | | Original | False | prefix-{controller}/{action}-suffix | 153.31ns | 136B | 0.89 | | Updated | False | prefix-{controller}/{action}-suffix | 117.38ns | 72B | 0.47 | | Original | False | prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix | 175.44ns | 184B | 1.21 | | Updated | False | prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix | 107.70ns | 64B | 0.42 | | Original | False | prefix-{controller}-{action}-{Area}-{id}-{FormValue}-suffix | 238.54ns | 184B | 1.21 | | Updated | False | prefix-{controller}-{action}-{Area}-{id}-{FormValue}-suffix | 119.70ns | 64B | 0.42 | | Original | False | prefix-{controller}-{action}-{id}-{FormValue}/{Area?} | 172.45ns | 184B | 1.21 | | Updated | False | prefix-{controller}-{action}-{id}-{FormValue}/{Area?} | 133.16ns | 64B | 0.42 | | Original | False | standalone/prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix/standalone | 172.03ns | 184B | 1.21 | | Updated | False | standalone/prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix/standalone | 248.15ns | 128B | 0.84 | | Original | False | {controller}/{action}/{nonid} | 273.13ns | 184B | 1.21 | | Updated | False | {controller}/{action}/{nonid} | 135.59ns | 64B | 0.42 | | Original | False | {controller}/{action}/{nonid?} | 168.27ns | 168B | 1.11 | | Updated | False | {controller}/{action}/{nonid?} | 130.99ns | 72B | 0.47 | | Original | False | {controller}/{action}/{nonid=2} | 174.51ns | 168B | 1.11 | | Updated | False | {controller}/{action}/{nonid=2} | 125.19ns | 64B | 0.42 | | Original | False | {controller}/{action}/{nonid:int} | 161.26ns | 168B | 1.11 | | Updated | False | {controller}/{action}/{nonid:int} | 136.26ns | 72B | 0.47 | | Original | False | {controller}/{action}/{FormValue} | 160.54ns | 168B | 1.11 | | Updated | False | {controller}/{action}/{FormValue} | 136.97ns | 72B | 0.47 | | Original | False | {controller}/{action}/{FormValue?} | 300.78ns | 376B | 2.47 | | Updated | False | {controller}/{action}/{FormValue?} | 137.28ns | 72B | 0.47 | | Original | False | {controller}/{action}/{FormValue=Edit} | 219.27ns | 232B | 1.53 | | Updated | False | {controller}/{action}/{FormValue=Edit} | 135.32ns | 72B | 0.47 | | Original | False | {controller}/{action}/{FormValue:int} | 221.59ns | 280B | 1.84 | | Updated | False | {controller}/{action}/{FormValue:int} | 121.39ns | 48B | 0.32 | | Original | False | {controller}/{action}/{nonidentity} | 226.88ns | 296B | 1.95 | | Updated | False | {controller}/{action}/{nonidentity} | 137.23ns | 72B | 0.47 | | Original | False | {controller}/{action}/{nonidentity?} | 131.69ns | 184B | 1.21 | | Updated | False | {controller}/{action}/{nonidentity?} | 130.84ns | 72B | 0.47 | | Original | False | {controller}/{action}/{nonidentity=2} | 127.93ns | 168B | 1.11 | | Updated | False | {controller}/{action}/{nonidentity=2} | 94.41ns | 48B | 0.32 | | Original | False | {controller}/{action}/{nonidentity:int} | 132.35ns | 168B | 1.11 | | Updated | False | {controller}/{action}/{nonidentity:int} | 127.66ns | 64B | 0.42 | | Original | False | {controller}/{action}/{idlike} | 119.40ns | 136B | 0.89 | | Updated | False | {controller}/{action}/{idlike} | 128.02ns | 64B | 0.42 | | Original | False | {controller}/{action}/{id:int} | 155.92ns | 168B | 1.11 | | Updated | False | {controller}/{action}/{id:int} | 119.59ns | 56B | 0.37 | | | | | | | | | Original | True | {controller}/{action}/{id} | 146.29ns | 152B | 1.00 | | Updated | True | {controller}/{action}/{id} | 138.64ns | 56B | 0.37 | | Original | True | {controller}/{action} | 144.60ns | 152B | 1.00 | | Updated | True | {controller}/{action} | 133.27ns | 56B | 0.37 | | Original | True | {area:exists}/{controller}/{action} | 108.50ns | 136B | 0.89 | | Updated | True | {area:exists}/{controller}/{action} | 143.76ns | 56B | 0.37 | | Original | True | prefix/{controller}/{action} | 129.71ns | 168B | 1.11 | | Updated | True | prefix/{controller}/{action} | 126.89ns | 72B | 0.47 | | Original | True | prefix-{controller}/{action}-suffix | 126.21ns | 168B | 1.11 | | Updated | True | prefix-{controller}/{action}-suffix | 142.42ns | 56B | 0.37 | | Original | True | prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix | 127.19ns | 184B | 1.21 | | Updated | True | prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix | 124.55ns | 72B | 0.47 | | Original | True | prefix-{controller}-{action}-{Area}-{id}-{FormValue}-suffix | 232.61ns | 248B | 1.63 | | Updated | True | prefix-{controller}-{action}-{Area}-{id}-{FormValue}-suffix | 109.81ns | 48B | 0.32 | | Original | True | prefix-{controller}-{action}-{id}-{FormValue}/{Area?} | 218.34ns | 264B | 1.74 | | Updated | True | prefix-{controller}-{action}-{id}-{FormValue}/{Area?} | 136.71ns | 56B | 0.37 | | Original | True | standalone/prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix/standalone | 206.66ns | 200B | 1.32 | | Updated | True | standalone/prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix/standalone | 141.68ns | 56B | 0.37 | | Original | True | {controller}/{action}/{nonid} | 268.18ns | 344B | 2.26 | | Updated | True | {controller}/{action}/{nonid} | 138.87ns | 56B | 0.37 | | Original | True | {controller}/{action}/{nonid?} | 150.72ns | 152B | 1.00 | | Updated | True | {controller}/{action}/{nonid?} | 112.45ns | 72B | 0.47 | | Original | True | {controller}/{action}/{nonid=2} | 149.30ns | 152B | 1.00 | | Updated | True | {controller}/{action}/{nonid=2} | 283.82ns | 152B | 1.00 | | Original | True | {controller}/{action}/{nonid:int} | 178.65ns | 200B | 1.32 | | Updated | True | {controller}/{action}/{nonid:int} | 204.47ns | 80B | 0.53 | | Original | True | {controller}/{action}/{FormValue} | 153.26ns | 152B | 1.00 | | Updated | True | {controller}/{action}/{FormValue} | 233.21ns | 112B | 0.74 | | Original | True | {controller}/{action}/{FormValue?} | 163.13ns | 152B | 1.00 | | Updated | True | {controller}/{action}/{FormValue?} | 252.30ns | 104B | 0.68 | | Original | True | {controller}/{action}/{FormValue=Edit} | 155.70ns | 152B | 1.00 | | Updated | True | {controller}/{action}/{FormValue=Edit} | 102.94ns | 64B | 0.42 | | Original | True | {controller}/{action}/{FormValue:int} | 163.94ns | 152B | 1.00 | | Updated | True | {controller}/{action}/{FormValue:int} | 102.48ns | 64B | 0.42 | | Original | True | {controller}/{action}/{nonidentity} | 158.25ns | 152B | 1.00 | | Updated | True | {controller}/{action}/{nonidentity} | 84.14ns | 48B | 0.32 | | Original | True | {controller}/{action}/{nonidentity?} | 150.04ns | 184B | 1.21 | | Updated | True | {controller}/{action}/{nonidentity?} | 123.77ns | 72B | 0.47 | | Original | True | {controller}/{action}/{nonidentity=2} | 135.92ns | 136B | 0.89 | | Updated | True | {controller}/{action}/{nonidentity=2} | 120.00ns | 56B | 0.37 | | Original | True | {controller}/{action}/{nonidentity:int} | 153.92ns | 184B | 1.21 | | Updated | True | {controller}/{action}/{nonidentity:int} | 130.82ns | 64B | 0.42 | | Original | True | {controller}/{action}/{idlike} | 152.99ns | 184B | 1.21 | | Updated | True | {controller}/{action}/{idlike} | 128.73ns | 56B | 0.37 | | Original | True | {controller}/{action}/{id:int} | 143.68ns | 152B | 1.00 | | Updated | True | {controller}/{action}/{id:int} | 124.07ns | 56B | 0.37 | </p> </details> In almost all cases there were improvements in both memory and cpu (see details collapsed above), but there were a few cases that stood out as getting worse. The problematic templates (and their results) were: | Route Template | Expand Templates | template | Mean | Allocated | | -------------- | ---------------- | ---------------------------------------------------------------------------------- | --------: | --------: | | Original | False | {controller}/{action}/{id} | 152.24ns | 152B | 1.00 | | Updated | False | {controller}/{action}/{id} | 329.65ns | 168B | 1.11 | | Original | False | standalone/prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix/standalone | 172.03ns | 184B | 1.21 | | Updated | False | standalone/prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix/standalone | 248.15ns | 128B | 0.84 | | Original | True | {controller}/{action}/{nonid=2} | 149.30ns | 152B | 1.00 | | Original | True | {controller}/{action}/{nonid=2} | 149.30ns | 152B | 1.00 | The first case is the most odd. I can't for the life of me see where the _extra_ allocation is coming from 😕 The second case is similarly strange that there's no reduction in allocation. The latter one shows a degradation in time only, so is less concerning, but still strange! 😄 I am a bit concerned about the first case, but even putting dotmemmory on it, the only allocations I see are the final `ToString()` so 🤷♂️ <details><summary>Benchmark code</summary> <p> ```csharp #if !NETFRAMEWORK using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; using Datadog.Trace; using Datadog.Trace.Agent.DiscoveryService; using Datadog.Trace.AppSec; using Datadog.Trace.ClrProfiler.AutoInstrumentation.Http.HttpClient.HttpClientHandler; using Datadog.Trace.Configuration; using Datadog.Trace.Configuration.Telemetry; using Datadog.Trace.Debugger; using Datadog.Trace.Debugger.SpanCodeOrigin; using Datadog.Trace.DiagnosticListeners; using Datadog.Trace.DuckTyping; using Datadog.Trace.Iast.Settings; using Datadog.Trace.RemoteConfigurationManagement; using Datadog.Trace.Security.Unit.Tests.Iast; using Datadog.Trace.Util; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; #if NETCOREAPP3_0_OR_GREATER using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing.Patterns; #endif namespace Benchmarks.Trace { [MemoryDiagnoser, GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory), CategoriesColumn] public class AspNetCoreResourceNameBenchmark { private Datadog.Trace.DiagnosticListeners.RoutePattern[] _routePatterns; private Datadog.Trace.DiagnosticListeners.RoutePattern[] _routePatternsWithDefaults; private RouteValueDictionary _values; private RouteValueDictionary _defaults; private RouteValueDictionary _parameterPolicies; private string[] _results; private string[] _results2; private RouteTemplate[] _routeTemplates = [ TemplateParser.Parse("{controller}/{action}/{id}"), TemplateParser.Parse("{controller}/{action}"), TemplateParser.Parse("{area:exists}/{controller}/{action}"), TemplateParser.Parse("prefix/{controller}/{action}"), TemplateParser.Parse("prefix-{controller}/{action}-suffix"), TemplateParser.Parse("prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix"), TemplateParser.Parse("prefix-{controller}-{action}-{Area}-{id}-{FormValue}-suffix"), TemplateParser.Parse("prefix-{controller}-{action}-{id}-{FormValue}/{Area?}"), TemplateParser.Parse("standalone/prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix/standalone"), TemplateParser.Parse("{controller}/{action}/{nonid}"), TemplateParser.Parse("{controller}/{action}/{nonid?}"), TemplateParser.Parse("{controller}/{action}/{nonid=2}"), TemplateParser.Parse("{controller}/{action}/{nonid:int}"), TemplateParser.Parse("{controller}/{action}/{FormValue}"), TemplateParser.Parse("{controller}/{action}/{FormValue?}"), TemplateParser.Parse("{controller}/{action}/{FormValue=Edit}"), TemplateParser.Parse("{controller}/{action}/{FormValue:int}"), TemplateParser.Parse("{controller}/{action}/{nonidentity}"), TemplateParser.Parse("{controller}/{action}/{nonidentity?}"), TemplateParser.Parse("{controller}/{action}/{nonidentity=2}"), TemplateParser.Parse("{controller}/{action}/{nonidentity:int}"), TemplateParser.Parse("{controller}/{action}/{idlike}"), TemplateParser.Parse("{controller}/{action}/{id:int}"), ]; [GlobalSetup] public void GlobalSetup() { #if NETCOREAPP3_0_OR_GREATER _routePatterns = [ RoutePatternFactory.Parse("{controller}/{action}/{id}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{area:exists}/{controller}/{action}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("prefix/{controller}/{action}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("prefix-{controller}/{action}-suffix").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("prefix-{controller}-{action}-{Area}-{id}-{FormValue}-suffix").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("prefix-{controller}-{action}-{id}-{FormValue}/{Area?}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("standalone/prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix/standalone").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonid}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonid?}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonid=2}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonid:int}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{FormValue}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{FormValue?}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{FormValue=Edit}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{FormValue:int}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonidentity}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonidentity?}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonidentity=2}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonidentity:int}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{idlike}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{id:int}").DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), ]; _routePatternsWithDefaults = [ RoutePatternFactory.Parse("{controller}/{action}/{id}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{area:exists}/{controller}/{action}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("prefix/{controller}/{action}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("prefix-{controller}/{action}-suffix", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("prefix-{controller}-{action}-{Area}-{id}-{FormValue}-suffix", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("prefix-{controller}-{action}-{id}-{FormValue}/{Area?}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("standalone/prefix-{controller}-{action}-{nonid}-{id}-{FormValue}-suffix/standalone", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonid}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonid?}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonid=2}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonid:int}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{FormValue}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{FormValue?}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{FormValue=Edit}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{FormValue:int}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonidentity}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonidentity?}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonidentity=2}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{nonidentity:int}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{idlike}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), RoutePatternFactory.Parse("{controller}/{action}/{id:int}", _defaults, parameterPolicies: _parameterPolicies).DuckCast<Datadog.Trace.DiagnosticListeners.RoutePattern>(), ]; _results = new string[_routePatterns.Length]; #endif _results2 = new string[_routeTemplates.Length]; _values = new() { { "controller", "Home" }, { "action", "Index" }, { "nonid", "oops" }, { "idlike", 123 }, { "FormValue", "View" }, }; _defaults = new() { { "controller", "Home" }, { "action", "Index" }, { "FormValue", "View" } }; _parameterPolicies = new() { { "id", new OptionalRouteConstraint(new IntRouteConstraint()) } }; } [Params(true, false)] public bool ExpandTemplates { get; set; } public IEnumerable<object> RouteTemplates => _routeTemplates.Select(x => (object)x); [GlobalCleanup] public void GlobalCleanup() { } [Benchmark(Baseline = true)] [BenchmarkCategory("RoutePattern")] public string[] RoutePattern_Original() { for (var i = 0; i < _routePatterns.Length; i++) { _results[i] = OriginalAspNetCoreResourceNameHelper.SimplifyRoutePattern( routePattern: _routePatterns[i], routeValueDictionary: _values, areaName: null, controllerName: _values["controller"] as string, actionName: _values["action"] as string, ExpandTemplates); } return _results; } [Benchmark(Baseline = true)] [BenchmarkCategory("RoutePatternWithDefaults")] public string[] RoutePatternWithDefaults_Original() { for (var i = 0; i < _routePatternsWithDefaults.Length; i++) { _results[i] = OriginalAspNetCoreResourceNameHelper.SimplifyRoutePattern( routePattern: _routePatternsWithDefaults[i], routeValueDictionary: _values, areaName: null, controllerName: _values["controller"] as string, actionName: _values["action"] as string, ExpandTemplates); } return _results; } [Benchmark(Baseline = true)] [BenchmarkCategory("RouteTemplate")] public string[] RouteTemplate_Original() { for (var i = 0; i < _routeTemplates.Length; i++) { _results2[i] = OriginalAspNetCoreResourceNameHelper.SimplifyRouteTemplate( routePattern: _routeTemplates[i], routeValueDictionary: _values, areaName: null, controllerName: _values["controller"] as string, actionName: _values["action"] as string, ExpandTemplates); } return _results; } [Benchmark(Baseline = true)] [BenchmarkCategory("RouteTemplateSingle")] [ArgumentsSource(nameof(RouteTemplates))] public string RouteTemplateSingle_Original(RouteTemplate template) { return OriginalAspNetCoreResourceNameHelper.SimplifyRouteTemplate( routePattern: template, routeValueDictionary: _values, areaName: null, controllerName: _values["controller"] as string, actionName: _values["action"] as string, ExpandTemplates); } [Benchmark] [BenchmarkCategory("RoutePattern")] public string[] RoutePattern_Updated() { for (var i = 0; i < _routePatterns.Length; i++) { _results[i] = AspNetCoreResourceNameHelper.SimplifyRoutePattern( routePattern: _routePatterns[i], routeValueDictionary: _values, areaName: null, controllerName: _values["controller"] as string, actionName: _values["action"] as string, ExpandTemplates); } return _results; } [Benchmark] [BenchmarkCategory("RoutePatternWithDefaults")] public string[] RoutePatternWithDefaults_Updated() { for (var i = 0; i < _routePatternsWithDefaults.Length; i++) { _results[i] = AspNetCoreResourceNameHelper.SimplifyRoutePattern( routePattern: _routePatternsWithDefaults[i], routeValueDictionary: _values, areaName: null, controllerName: _values["controller"] as string, actionName: _values["action"] as string, ExpandTemplates); } return _results; } [Benchmark] [BenchmarkCategory("RouteTemplate")] public string[] RouteTemplate_Updated() { for (var i = 0; i < _routeTemplates.Length; i++) { _results2[i] = AspNetCoreResourceNameHelper.SimplifyRouteTemplate( routePattern: _routeTemplates[i], routeValueDictionary: _values, areaName: null, controllerName: _values["controller"] as string, actionName: _values["action"] as string, ExpandTemplates); } return _results; } [Benchmark] [BenchmarkCategory("RouteTemplateSingle")] [ArgumentsSource(nameof(RouteTemplates))] public string RouteTemplateSingle_Updated(RouteTemplate template) { return AspNetCoreResourceNameHelper.SimplifyRouteTemplate( routePattern: template, routeValueDictionary: _values, areaName: null, controllerName: _values["controller"] as string, actionName: _values["action"] as string, ExpandTemplates); } private class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } public void Configure(IApplicationBuilder builder) { #if NETCOREAPP2_1 builder.UseMvcWithDefaultRoute(); #else builder.UseRouting(); builder.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); #endif } } } } #else using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Web.Routing; using BenchmarkDotNet.Attributes; using Datadog.Trace.DuckTyping; using Datadog.Trace.Util; namespace Benchmarks.Trace { [MemoryDiagnoser] public class AspNetCoreResourceNameBenchmark { [GlobalSetup] public void GlobalSetup() { } [GlobalCleanup] public void GlobalCleanup() { } [Benchmark] public string SendRequest() { return null; } } } #endif #if !NETFRAMEWORK internal static class OriginalAspNetCoreResourceNameHelper { internal static string SimplifyRoutePattern( Datadog.Trace.DiagnosticListeners.RoutePattern routePattern, IReadOnlyDictionary<string, object> routeValueDictionary, string? areaName, string? controllerName, string? actionName, bool expandRouteParameters) { var maxSize = (routePattern.RawText?.Length ?? 0) + (string.IsNullOrEmpty(areaName) ? 0 : Math.Max(areaName!.Length - 4, 0)) // "area".Length + (string.IsNullOrEmpty(controllerName) ? 0 : Math.Max(controllerName!.Length - 10, 0)) // "controller".Length + (string.IsNullOrEmpty(actionName) ? 0 : Math.Max(actionName!.Length - 6, 0)) // "action".Length + 1; // '/' prefix var sb = StringBuilderCache.Acquire(maxSize); foreach (var pathSegment in routePattern.PathSegments) { var parts = 0; foreach (var part in pathSegment.DuckCast<AspNetCoreDiagnosticObserver.RoutePatternPathSegmentStruct>().Parts) { parts++; if (part.TryDuckCast(out AspNetCoreDiagnosticObserver.RoutePatternContentPartStruct contentPart)) { if (parts == 1) { sb.Append('/'); } sb.Append(contentPart.Content); } else if (part.TryDuckCast(out AspNetCoreDiagnosticObserver.RoutePatternParameterPartStruct parameter)) { var parameterName = parameter.Name; if (parameterName.Equals("area", StringComparison.OrdinalIgnoreCase)) { if (areaName is null && parameter.IsOptional) { // don't append optional suffixes when no value is provided continue; } if (parts == 1) { sb.Append('/'); } sb.Append(areaName ?? "{area}"); } else if (parameterName.Equals("controller", StringComparison.OrdinalIgnoreCase)) { if (controllerName is null && parameter.IsOptional) { // don't append optional suffixes when no value is provided continue; } if (parts == 1) { sb.Append('/'); } sb.Append(controllerName ?? "{controller}"); } else if (parameterName.Equals("action", StringComparison.OrdinalIgnoreCase)) { if (actionName is null && parameter.IsOptional) { // don't append optional suffixes when no value is provided continue; } if (parts == 1) { sb.Append('/'); } sb.Append(actionName ?? "{action}"); } else { var haveParameter = routeValueDictionary.TryGetValue(parameterName, out var value); if (!parameter.IsOptional || haveParameter) { if (parts == 1) { sb.Append('/'); } if (expandRouteParameters && haveParameter && !IsIdentifierSegment(value, out var valueAsString)) { // write the expanded parameter value sb.Append(valueAsString); } else { // write the route template value sb.Append('{'); if (parameter.IsCatchAll) { if (parameter.EncodeSlashes) { sb.Append("**"); } else { sb.Append('*'); } } sb.Append(parameterName); if (parameter.IsOptional) { sb.Append('?'); } sb.Append('}'); } } } } } } var simplifiedRoute = StringBuilderCache.GetStringAndRelease(sb); return string.IsNullOrEmpty(simplifiedRoute) ? "/" : simplifiedRoute.ToLowerInvariant(); } internal static string SimplifyRouteTemplate( RouteTemplate routePattern, RouteValueDictionary routeValueDictionary, string? areaName, string? controllerName, string? actionName, bool expandRouteParameters) { var maxSize = (routePattern.TemplateText?.Length ?? 0) + (string.IsNullOrEmpty(areaName) ? 0 : Math.Max(areaName!.Length - 4, 0)) // "area".Length + (string.IsNullOrEmpty(controllerName) ? 0 : Math.Max(controllerName!.Length - 10, 0)) // "controller".Length + (string.IsNullOrEmpty(actionName) ? 0 : Math.Max(actionName!.Length - 6, 0)) // "action".Length + 1; // '/' prefix var sb = StringBuilderCache.Acquire(maxSize); foreach (var pathSegment in routePattern.Segments) { var parts = 0; foreach (var part in pathSegment.Parts) { parts++; var partName = part.Name; if (!part.IsParameter) { if (parts == 1) { sb.Append('/'); } sb.Append(part.Text); } else if (partName.Equals("area", StringComparison.OrdinalIgnoreCase)) { if (areaName is null && part.IsOptional) { // don't append optional suffixes when no value is provided continue; } if (parts == 1) { sb.Append('/'); } sb.Append(areaName ?? "{area}"); } else if (partName.Equals("controller", StringComparison.OrdinalIgnoreCase)) { if (controllerName is null && part.IsOptional) { // don't append optional suffixes when no value is provided continue; } if (parts == 1) { sb.Append('/'); } sb.Append(controllerName ?? "{controller}"); } else if (partName.Equals("action", StringComparison.OrdinalIgnoreCase)) { if (actionName is null && part.IsOptional) { // don't append optional suffixes when no value is provided continue; } if (parts == 1) { sb.Append('/'); } sb.Append(actionName ?? "{action}"); } else { var haveParameter = routeValueDictionary.TryGetValue(partName, out var value); if (!part.IsOptional || haveParameter) { if (parts == 1) { sb.Append('/'); } if (expandRouteParameters && haveParameter && !IsIdentifierSegment(value, out var valueAsString)) { // write the expanded parameter value sb.Append(valueAsString); } else { // write the route template value sb.Append('{'); if (part.IsCatchAll) { sb.Append('*'); } sb.Append(partName); if (part.IsOptional) { sb.Append('?'); } sb.Append('}'); } } } } } var simplifiedRoute = StringBuilderCache.GetStringAndRelease(sb); return string.IsNullOrEmpty(simplifiedRoute) ? "/" : simplifiedRoute.ToLowerInvariant(); } private static bool IsIdentifierSegment(object value, out string valueAsString) { valueAsString = value as string ?? value?.ToString(); if (valueAsString is null) { return false; } return UriHelpers.IsIdentifierSegment(valueAsString, 0, valueAsString.Length); } } #endif ``` </p> </details> ## Other details Part of a stack https://datadoghq.atlassian.net/browse/LANGPLAT-842 - #8167 - #8170 👈 - #8180
b640b9b to
08cc252
Compare
Summary of changes
Switches to using
ValueStringBuilderand other minor perf improvementsReason for change
Same as #8170, we want to improve performance where we can. Unfortunately, we can't easily avoid the enumeration allocation like we did in that PR, so most of the benefits here are simply from using
ValueStringBuilderand other minor changes.Implementation details
Incorporated various changes based on the
SimplifyRoutePatternthat we use in the single-span aspnetcore observer and the changes made in #8170. The gains aren't as high here, because we can't reduce enumeration allocation.Test coverage
Covered by existing tests.
Ran benchmarks using the values. In general, there's a slight regression in duration for an improvement in Alloc Ratio. It's not very consistent though, particularly in <.NET Core 3.1. I think it's probably still worth the change, but open to thoughts
Other details
Part of a stack
https://datadoghq.atlassian.net/browse/LANGPLAT-842
ValueStringBuilderavailable in.NET Core 3.1#8167AspNetCoreResourceNameHelper.SimplifyRouteTemplate#8170AspNetCoreResourceNameHelper.SimplifyRoutePattern#8180 👈