Skip to content

Improve performance of AspNetCoreResourceNameHelper.SimplifyRoutePattern#8180

Merged
andrewlock merged 2 commits intomasterfrom
andrew/aspnetcoreperf/route-patterns
Feb 18, 2026
Merged

Improve performance of AspNetCoreResourceNameHelper.SimplifyRoutePattern#8180
andrewlock merged 2 commits intomasterfrom
andrew/aspnetcoreperf/route-patterns

Conversation

@andrewlock
Copy link
Member

@andrewlock andrewlock commented Feb 9, 2026

Summary of changes

Switches to using ValueStringBuilder and other minor perf improvements

Reason 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 ValueStringBuilder and other minor changes.

Implementation details

Incorporated various changes based on the SimplifyRoutePattern that 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

Method Runtime Has Defaults Expand Templates Mean Allocated Alloc Ratio
Original .NET 10.0 False False 6.605 us 6.51 KB 1.00
Updated .NET 10.0 False False 8.559 us 4.8 KB 0.74
Original .NET 6.0 False False 11.466 us 6.51 KB 1.00
Updated .NET 6.0 False False 11.654 us 4.8 KB 0.74
Original .NET Core 3.1 False False 13.808 us 6.51 KB 1.00
Updated .NET Core 3.1 False False 15.266 us 4.8 KB 0.74
Original .NET Core 3.0 False False 13.962 us 6.51 KB 1.00
Updated .NET Core 3.0 False False 19.757 us 6.51 KB 1.00
Original .NET 10.0 False True 6.453 us 6.2 KB 0.99
Updated .NET 10.0 False True 8.116 us 4.65 KB 0.75
Original .NET 6.0 False True 11.121 us 6.23 KB 1.00
Updated .NET 6.0 False True 11.375 us 4.65 KB 0.75
Original .NET Core 3.1 False True 14.075 us 6.23 KB 1.00
Updated .NET Core 3.1 False True 15.000 us 4.65 KB 0.75
Original .NET Core 3.0 False True 14.027 us 6.23 KB 1.00
Updated .NET Core 3.0 False True 13.687 us 6.2 KB 0.99
Original .NET 10.0 True False 6.348 us 6.51 KB 1.00
Updated .NET 10.0 True False 8.310 us 4.8 KB 0.74
Original .NET 6.0 True False 11.082 us 6.51 KB 1.00
Updated .NET 6.0 True False 11.406 us 4.8 KB 0.74
Original .NET Core 3.1 True False 9.853 us 6.51 KB 1.00
Updated .NET Core 3.1 True False 10.865 us 4.8 KB 0.74
Original .NET Core 3.0 True False 14.879 us 6.51 KB 1.00
Updated .NET Core 3.0 True False 10.221 us 6.51 KB 1.00
Original .NET 10.0 True True 4.002 us 6.2 KB 0.99
Updated .NET 10.0 True True 5.685 us 4.65 KB 0.75
Original .NET 6.0 True True 7.964 us 6.23 KB 1.00
Updated .NET 6.0 True True 8.385 us 4.65 KB 0.75
Original .NET Core 3.1 True True 13.949 us 6.23 KB 1.00
Updated .NET Core 3.1 True True 15.370 us 4.65 KB 0.75
Original .NET Core 3.0 True True 10.116 us 6.23 KB 1.00
Updated .NET Core 3.0 True True 10.241 us 6.2 KB 0.99

Other details

Part of a stack
https://datadoghq.atlassian.net/browse/LANGPLAT-842

@andrewlock andrewlock requested a review from a team as a code owner February 9, 2026 14:28
@andrewlock andrewlock added area:tracer The core tracer library (Datadog.Trace, does not include OpenTracing, native code, or integrations) type:performance Performance, speed, latency, resource usage (CPU, memory) labels Feb 9, 2026
@dd-trace-dotnet-ci-bot
Copy link

dd-trace-dotnet-ci-bot bot commented Feb 9, 2026

Execution-Time Benchmarks Report ⏱️

Execution-time results for samples comparing This PR (8180) and master.

✅ No regressions detected - check the details below

Full Metrics Comparison

FakeDbCommand

Metric Master (Mean ± 95% CI) Current (Mean ± 95% CI) Change Status
.NET Framework 4.8 - Baseline
duration74.60 ± (74.64 - 74.99) ms73.75 ± (73.76 - 74.03) ms-1.1%
.NET Framework 4.8 - Bailout
duration79.36 ± (79.20 - 79.56) ms77.93 ± (77.83 - 78.21) ms-1.8%
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1074.50 ± (1075.75 - 1081.67) ms1065.26 ± (1066.13 - 1071.28) ms-0.9%
.NET Core 3.1 - Baseline
process.internal_duration_ms22.75 ± (22.71 - 22.79) ms22.43 ± (22.39 - 22.47) ms-1.4%
process.time_to_main_ms85.95 ± (85.78 - 86.13) ms84.17 ± (83.99 - 84.36) ms-2.1%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.91 ± (10.90 - 10.91) MB10.92 ± (10.91 - 10.92) MB+0.1%✅⬆️
runtime.dotnet.threads.count12 ± (12 - 12)12 ± (12 - 12)+0.0%
.NET Core 3.1 - Bailout
process.internal_duration_ms22.74 ± (22.70 - 22.79) ms22.35 ± (22.30 - 22.41) ms-1.7%
process.time_to_main_ms86.96 ± (86.78 - 87.14) ms85.75 ± (85.56 - 85.95) ms-1.4%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.94 ± (10.93 - 10.94) MB10.96 ± (10.96 - 10.97) MB+0.2%✅⬆️
runtime.dotnet.threads.count13 ± (13 - 13)13 ± (13 - 13)+0.0%
.NET Core 3.1 - CallTarget+Inlining+NGEN
process.internal_duration_ms238.17 ± (234.18 - 242.15) ms247.25 ± (243.35 - 251.15) ms+3.8%✅⬆️
process.time_to_main_ms491.44 ± (490.67 - 492.21) ms485.77 ± (484.92 - 486.62) ms-1.2%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed47.58 ± (47.56 - 47.61) MB47.63 ± (47.61 - 47.65) MB+0.1%✅⬆️
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)+0.0%
.NET 6 - Baseline
process.internal_duration_ms21.66 ± (21.61 - 21.71) ms21.24 ± (21.21 - 21.27) ms-1.9%
process.time_to_main_ms75.49 ± (75.29 - 75.68) ms73.12 ± (72.98 - 73.27) ms-3.1%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.62 ± (10.61 - 10.62) MB10.64 ± (10.64 - 10.64) MB+0.2%✅⬆️
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 6 - Bailout
process.internal_duration_ms21.69 ± (21.65 - 21.73) ms21.23 ± (21.19 - 21.28) ms-2.1%
process.time_to_main_ms77.20 ± (77.00 - 77.39) ms74.71 ± (74.51 - 74.90) ms-3.2%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.68 ± (10.67 - 10.68) MB10.75 ± (10.74 - 10.75) MB+0.7%✅⬆️
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms248.02 ± (244.16 - 251.87) ms247.09 ± (243.25 - 250.94) ms-0.4%
process.time_to_main_ms473.31 ± (472.68 - 473.95) ms468.60 ± (467.47 - 469.72) ms-1.0%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed48.37 ± (48.34 - 48.39) MB48.30 ± (48.29 - 48.32) MB-0.1%
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)+0.1%✅⬆️
.NET 8 - Baseline
process.internal_duration_ms19.75 ± (19.71 - 19.80) ms19.52 ± (19.47 - 19.58) ms-1.2%
process.time_to_main_ms74.55 ± (74.36 - 74.74) ms73.17 ± (72.95 - 73.38) ms-1.9%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.67 ± (7.66 - 7.67) MB7.68 ± (7.68 - 7.69) MB+0.2%✅⬆️
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 8 - Bailout
process.internal_duration_ms19.92 ± (19.88 - 19.96) ms19.52 ± (19.48 - 19.56) ms-2.0%
process.time_to_main_ms76.30 ± (76.13 - 76.47) ms74.39 ± (74.21 - 74.57) ms-2.5%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.71 ± (7.71 - 7.72) MB7.73 ± (7.72 - 7.74) MB+0.2%✅⬆️
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms191.04 ± (190.19 - 191.90) ms186.43 ± (185.58 - 187.27) ms-2.4%
process.time_to_main_ms455.50 ± (454.62 - 456.37) ms447.50 ± (446.81 - 448.19) ms-1.8%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed35.96 ± (35.93 - 36.00) MB35.96 ± (35.93 - 35.99) MB-0.0%
runtime.dotnet.threads.count27 ± (27 - 27)27 ± (27 - 27)+0.0%✅⬆️

HttpMessageHandler

Metric Master (Mean ± 95% CI) Current (Mean ± 95% CI) Change Status
.NET Framework 4.8 - Baseline
duration193.00 ± (193.18 - 193.89) ms193.44 ± (193.54 - 194.31) ms+0.2%✅⬆️
.NET Framework 4.8 - Bailout
duration196.61 ± (196.51 - 196.94) ms197.60 ± (197.58 - 198.48) ms+0.5%✅⬆️
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1134.35 ± (1135.79 - 1142.86) ms1139.64 ± (1142.24 - 1150.51) ms+0.5%✅⬆️
.NET Core 3.1 - Baseline
process.internal_duration_ms187.94 ± (187.56 - 188.33) ms187.48 ± (187.05 - 187.90) ms-0.2%
process.time_to_main_ms81.64 ± (81.45 - 81.83) ms81.42 ± (81.17 - 81.66) ms-0.3%
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed16.07 ± (16.05 - 16.10) MB16.11 ± (16.08 - 16.13) MB+0.2%✅⬆️
runtime.dotnet.threads.count20 ± (19 - 20)20 ± (20 - 20)+0.4%✅⬆️
.NET Core 3.1 - Bailout
process.internal_duration_ms187.95 ± (187.54 - 188.36) ms186.48 ± (186.25 - 186.70) ms-0.8%
process.time_to_main_ms82.80 ± (82.64 - 82.96) ms82.39 ± (82.28 - 82.50) ms-0.5%
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed16.08 ± (16.05 - 16.11) MB16.13 ± (16.10 - 16.16) MB+0.3%✅⬆️
runtime.dotnet.threads.count21 ± (21 - 21)20 ± (20 - 21)-0.8%
.NET Core 3.1 - CallTarget+Inlining+NGEN
process.internal_duration_ms428.53 ± (425.05 - 432.01) ms428.24 ± (424.65 - 431.82) ms-0.1%
process.time_to_main_ms472.49 ± (471.92 - 473.06) ms475.37 ± (474.53 - 476.22) ms+0.6%✅⬆️
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed57.95 ± (57.83 - 58.08) MB58.10 ± (57.97 - 58.22) MB+0.3%✅⬆️
runtime.dotnet.threads.count29 ± (29 - 29)29 ± (29 - 29)+0.1%✅⬆️
.NET 6 - Baseline
process.internal_duration_ms191.80 ± (191.50 - 192.09) ms195.90 ± (195.39 - 196.41) ms+2.1%✅⬆️
process.time_to_main_ms70.43 ± (70.25 - 70.60) ms71.68 ± (71.43 - 71.93) ms+1.8%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed16.25 ± (16.13 - 16.36) MB16.42 ± (16.39 - 16.45) MB+1.0%✅⬆️
runtime.dotnet.threads.count18 ± (18 - 19)19 ± (19 - 19)+4.2%✅⬆️
.NET 6 - Bailout
process.internal_duration_ms191.06 ± (190.72 - 191.40) ms194.41 ± (193.95 - 194.88) ms+1.8%✅⬆️
process.time_to_main_ms71.71 ± (71.56 - 71.87) ms72.50 ± (72.33 - 72.67) ms+1.1%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed16.14 ± (15.99 - 16.28) MB16.44 ± (16.42 - 16.46) MB+1.9%✅⬆️
runtime.dotnet.threads.count19 ± (19 - 20)20 ± (20 - 20)+3.8%✅⬆️
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms445.03 ± (442.74 - 447.31) ms443.63 ± (440.44 - 446.81) ms-0.3%
process.time_to_main_ms447.73 ± (447.27 - 448.19) ms447.41 ± (446.89 - 447.94) ms-0.1%
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed58.12 ± (58.02 - 58.23) MB58.06 ± (57.93 - 58.20) MB-0.1%
runtime.dotnet.threads.count29 ± (29 - 29)29 ± (29 - 29)+0.1%✅⬆️
.NET 8 - Baseline
process.internal_duration_ms190.54 ± (190.19 - 190.88) ms189.73 ± (189.41 - 190.06) ms-0.4%
process.time_to_main_ms70.54 ± (70.32 - 70.76) ms69.97 ± (69.79 - 70.16) ms-0.8%
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.74 ± (11.71 - 11.77) MB11.72 ± (11.70 - 11.75) MB-0.2%
runtime.dotnet.threads.count18 ± (18 - 18)18 ± (18 - 18)+0.5%✅⬆️
.NET 8 - Bailout
process.internal_duration_ms189.52 ± (189.25 - 189.79) ms190.00 ± (189.54 - 190.47) ms+0.3%✅⬆️
process.time_to_main_ms71.08 ± (70.98 - 71.17) ms71.30 ± (71.16 - 71.45) ms+0.3%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.79 ± (11.73 - 11.85) MB11.79 ± (11.77 - 11.82) MB+0.1%✅⬆️
runtime.dotnet.threads.count19 ± (19 - 19)19 ± (19 - 19)+1.0%✅⬆️
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms363.96 ± (362.58 - 365.34) ms365.49 ± (364.25 - 366.72) ms+0.4%✅⬆️
process.time_to_main_ms431.57 ± (431.02 - 432.13) ms433.61 ± (433.02 - 434.19) ms+0.5%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed47.72 ± (47.68 - 47.76) MB47.75 ± (47.73 - 47.78) MB+0.1%✅⬆️
runtime.dotnet.threads.count29 ± (29 - 29)29 ± (29 - 29)-0.1%
Comparison explanation

Execution-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:

  • Welch test with statistical test for significance of 5%
  • Only results indicating a difference greater than 5% and 5 ms are considered.

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 charts
FakeDbCommand (.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

Loading
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

Loading
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

Loading
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

Loading
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

Loading
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

Loading
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

Loading
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

Loading

Copy link
Collaborator

@NachoEchevarria NachoEchevarria left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@andrewlock andrewlock force-pushed the andrew/aspnetcore-perf/route-templates branch from f245d13 to 4172b5b Compare February 11, 2026 10:55
@andrewlock andrewlock force-pushed the andrew/aspnetcore-perf/route-templates branch from 4172b5b to 6cc2185 Compare February 12, 2026 08:57
@andrewlock andrewlock force-pushed the andrew/aspnetcoreperf/route-patterns branch from 64c6b16 to db9e7f0 Compare February 16, 2026 17:47
@andrewlock andrewlock requested review from a team as code owners February 16, 2026 17:47
@andrewlock andrewlock requested review from duncanpharvey and removed request for a team February 16, 2026 17:47
@andrewlock andrewlock changed the base branch from andrew/aspnetcore-perf/route-templates to master February 16, 2026 17:48
@andrewlock andrewlock force-pushed the andrew/aspnetcoreperf/route-patterns branch from db9e7f0 to aa7c23f Compare February 17, 2026 09:04
@andrewlock andrewlock changed the base branch from master to andrew/aspnetcore-perf/route-templates February 17, 2026 09:04
@pr-commenter
Copy link

pr-commenter bot commented Feb 17, 2026

Benchmarks

Benchmark execution time: 2026-02-18 12:34:33

Comparing candidate commit 08cc252 in PR branch andrew/aspnetcoreperf/route-patterns with baseline commit 421a5fb in branch master.

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

  • 🟥 execution_time [+18.480ms; +18.772ms] or [+9.221%; +9.367%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.AllCycleSimpleBody netcoreapp3.1

  • 🟩 execution_time [-18.844ms; -12.575ms] or [-8.705%; -5.809%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.ObjectExtractorSimpleBody netcoreapp3.1

  • 🟥 execution_time [+17.437ms; +23.489ms] or [+8.836%; +11.903%]

scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeLegacyArgs netcoreapp3.1

  • 🟩 execution_time [-18.460ms; -16.943ms] or [-9.217%; -8.460%]

scenario:Benchmarks.Trace.AspNetCoreBenchmark.SendRequest net6.0

  • 🟩 execution_time [-101.551ms; -99.733ms] or [-50.943%; -50.031%]

scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces net472

  • 🟩 execution_time [-19.478ms; -11.744ms] or [-8.831%; -5.324%]

scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces netcoreapp3.1

  • 🟥 execution_time [+8.696ms; +12.604ms] or [+5.822%; +8.438%]
  • 🟥 throughput [-121.480op/s; -82.565op/s] or [-8.088%; -5.497%]

scenario:Benchmarks.Trace.ILoggerBenchmark.EnrichedLog netcoreapp3.1

  • 🟥 execution_time [+11.539ms; +15.734ms] or [+5.919%; +8.071%]

scenario:Benchmarks.Trace.SingleSpanAspNetCoreBenchmark.SingleSpanAspNetCore net6.0

  • 🟥 execution_time [+5.179ms; +6.693ms] or [+5.541%; +7.161%]

scenario:Benchmarks.Trace.SingleSpanAspNetCoreBenchmark.SingleSpanAspNetCore netcoreapp3.1

  • 🟥 throughput [-15896616.858op/s; -14604691.749op/s] or [-6.587%; -6.052%]

scenario:Benchmarks.Trace.TraceAnnotationsBenchmark.RunOnMethodBegin netcoreapp3.1

  • 🟥 execution_time [+11.669ms; +17.069ms] or [+5.953%; +8.708%]

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 StringBuilder to ValueStringBuilder for .NET Core 3.1+ to reduce allocations
  • Refactored parameter handling logic to consolidate area/controller/action handling with a mustExpand flag
  • Changed from parts counter to addedPart boolean 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();
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
return StringBuilderCache.GetStringAndRelease(sb).ToLowerInvariant();
var simplifiedRoute = StringBuilderCache.GetStringAndRelease(sb);
return string.IsNullOrEmpty(simplifiedRoute) ? "/" : simplifiedRoute.ToLowerInvariant();

Copilot uses AI. Check for mistakes.
andrewlock added a commit that referenced this pull request Feb 18, 2026
## 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
@andrewlock andrewlock force-pushed the andrew/aspnetcoreperf/route-patterns branch from aa7c23f to 87b3d91 Compare February 18, 2026 09:39
@andrewlock andrewlock requested review from a team as code owners February 18, 2026 09:39
@andrewlock andrewlock requested a review from a team as a code owner February 18, 2026 09:39
@andrewlock andrewlock requested review from leoromanovsky and typotter and removed request for a team February 18, 2026 09:39
@andrewlock andrewlock force-pushed the andrew/aspnetcore-perf/route-templates branch from 6cc2185 to b02265c Compare February 18, 2026 09:39
@andrewlock andrewlock force-pushed the andrew/aspnetcoreperf/route-patterns branch from 87b3d91 to b640b9b Compare February 18, 2026 09:43
andrewlock added a commit that referenced this pull request Feb 18, 2026
…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
Base automatically changed from andrew/aspnetcore-perf/route-templates to master February 18, 2026 11:52
@andrewlock andrewlock force-pushed the andrew/aspnetcoreperf/route-patterns branch from b640b9b to 08cc252 Compare February 18, 2026 11:53
@andrewlock andrewlock merged commit c446bb5 into master Feb 18, 2026
142 checks passed
@andrewlock andrewlock deleted the andrew/aspnetcoreperf/route-patterns branch February 18, 2026 15:08
@github-actions github-actions bot added this to the vNext-v3 milestone Feb 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:tracer The core tracer library (Datadog.Trace, does not include OpenTracing, native code, or integrations) type:performance Performance, speed, latency, resource usage (CPU, memory)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants