Make ValueStringBuilder available in .NET Core 3.1#8167
Conversation
BenchmarksBenchmark execution time: 2026-02-06 14:28:34 Comparing candidate commit 5c729ed in PR branch Found 16 performance improvements and 6 performance regressions! Performance is the same for 151 metrics, 19 unstable metrics. scenario:Benchmarks.Trace.ActivityBenchmark.StartStopWithChild netcoreapp3.1
scenario:Benchmarks.Trace.AgentWriterBenchmark.WriteAndFlushEnrichedTraces net6.0
scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.AllCycleSimpleBody netcoreapp3.1
scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.ObjectExtractorMoreComplexBody net6.0
scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.ObjectExtractorSimpleBody netcoreapp3.1
scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces net472
scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces net6.0
scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces netcoreapp3.1
scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSlice net472
scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSlice netcoreapp3.1
scenario:Benchmarks.Trace.CharSliceBenchmark.OriginalCharSlice net472
scenario:Benchmarks.Trace.CharSliceBenchmark.OriginalCharSlice net6.0
scenario:Benchmarks.Trace.GraphQLBenchmark.ExecuteAsync net6.0
scenario:Benchmarks.Trace.ILoggerBenchmark.EnrichedLog net6.0
scenario:Benchmarks.Trace.RedisBenchmark.SendReceive netcoreapp3.1
scenario:Benchmarks.Trace.SingleSpanAspNetCoreBenchmark.SingleSpanAspNetCore netcoreapp3.1
scenario:Benchmarks.Trace.SpanBenchmark.StartFinishTwoScopes net6.0
|
Execution-Time Benchmarks Report ⏱️Execution-time results for samples comparing This PR (8167) and master.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Metric | Master (Mean ± 95% CI) | Current (Mean ± 95% CI) | Change | Status |
|---|---|---|---|---|
| .NET Framework 4.8 - Baseline | ||||
| duration | 68.30 ± (68.33 - 68.58) ms | 77.35 ± (77.38 - 77.78) ms | +13.2% | ❌⬆️ |
| .NET Framework 4.8 - Bailout | ||||
| duration | 72.05 ± (71.98 - 72.20) ms | 81.92 ± (81.71 - 82.17) ms | +13.7% | ❌⬆️ |
| .NET Framework 4.8 - CallTarget+Inlining+NGEN | ||||
| duration | 1025.15 ± (1027.07 - 1033.25) ms | 1112.88 ± (1112.39 - 1120.18) ms | +8.6% | ❌⬆️ |
HttpMessageHandler
| Metric | Master (Mean ± 95% CI) | Current (Mean ± 95% CI) | Change | Status |
|---|---|---|---|---|
| .NET Framework 4.8 - Baseline | ||||
| duration | 193.79 ± (193.64 - 194.38) ms | 205.47 ± (205.57 - 206.81) ms | +6.0% | ❌⬆️ |
| .NET Framework 4.8 - Bailout | ||||
| duration | 197.11 ± (197.02 - 197.59) ms | 210.64 ± (210.27 - 211.71) ms | +6.9% | ❌⬆️ |
Full Metrics Comparison
FakeDbCommand
| Metric | Master (Mean ± 95% CI) | Current (Mean ± 95% CI) | Change | Status |
|---|---|---|---|---|
| .NET Framework 4.8 - Baseline | ||||
| duration | 68.30 ± (68.33 - 68.58) ms | 77.35 ± (77.38 - 77.78) ms | +13.2% | ❌⬆️ |
| .NET Framework 4.8 - Bailout | ||||
| duration | 72.05 ± (71.98 - 72.20) ms | 81.92 ± (81.71 - 82.17) ms | +13.7% | ❌⬆️ |
| .NET Framework 4.8 - CallTarget+Inlining+NGEN | ||||
| duration | 1025.15 ± (1027.07 - 1033.25) ms | 1112.88 ± (1112.39 - 1120.18) ms | +8.6% | ❌⬆️ |
| .NET Core 3.1 - Baseline | ||||
| process.internal_duration_ms | 22.46 ± (22.44 - 22.48) ms | 24.67 ± (24.61 - 24.73) ms | +9.8% | ✅⬆️ |
| process.time_to_main_ms | 86.82 ± (86.67 - 86.96) ms | 102.04 ± (101.79 - 102.29) ms | +17.5% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 15.51 ± (15.51 - 15.52) MB | 15.51 ± (15.50 - 15.51) MB | -0.0% | ✅ |
| runtime.dotnet.threads.count | 12 ± (12 - 12) | 12 ± (12 - 12) | +0.0% | ✅ |
| .NET Core 3.1 - Bailout | ||||
| process.internal_duration_ms | 22.40 ± (22.37 - 22.43) ms | 24.49 ± (24.43 - 24.54) ms | +9.3% | ✅⬆️ |
| process.time_to_main_ms | 87.84 ± (87.73 - 87.95) ms | 102.53 ± (102.28 - 102.78) ms | +16.7% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 15.55 ± (15.55 - 15.56) MB | 15.55 ± (15.55 - 15.55) MB | -0.0% | ✅ |
| runtime.dotnet.threads.count | 13 ± (13 - 13) | 13 ± (13 - 13) | +0.0% | ✅ |
| .NET Core 3.1 - CallTarget+Inlining+NGEN | ||||
| process.internal_duration_ms | 255.49 ± (251.86 - 259.12) ms | 284.69 ± (281.38 - 287.99) ms | +11.4% | ✅⬆️ |
| process.time_to_main_ms | 503.66 ± (503.12 - 504.20) ms | 558.12 ± (557.29 - 558.95) ms | +10.8% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 53.23 ± (53.20 - 53.25) MB | 53.16 ± (53.14 - 53.18) MB | -0.1% | ✅ |
| runtime.dotnet.threads.count | 28 ± (28 - 28) | 28 ± (28 - 28) | +0.1% | ✅⬆️ |
| .NET 6 - Baseline | ||||
| process.internal_duration_ms | 21.04 ± (21.01 - 21.06) ms | 22.94 ± (22.88 - 23.00) ms | +9.0% | ✅⬆️ |
| process.time_to_main_ms | 75.06 ± (74.92 - 75.21) ms | 87.23 ± (87.00 - 87.47) ms | +16.2% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 15.23 ± (15.23 - 15.23) MB | 15.24 ± (15.24 - 15.24) MB | +0.1% | ✅⬆️ |
| runtime.dotnet.threads.count | 10 ± (10 - 10) | 10 ± (10 - 10) | +0.0% | ✅ |
| .NET 6 - Bailout | ||||
| process.internal_duration_ms | 20.87 ± (20.84 - 20.90) ms | 23.15 ± (23.09 - 23.22) ms | +10.9% | ✅⬆️ |
| process.time_to_main_ms | 75.77 ± (75.68 - 75.86) ms | 90.31 ± (90.09 - 90.54) ms | +19.2% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 15.36 ± (15.36 - 15.37) MB | 15.35 ± (15.34 - 15.35) MB | -0.1% | ✅ |
| runtime.dotnet.threads.count | 11 ± (11 - 11) | 11 ± (11 - 11) | +0.0% | ✅ |
| .NET 6 - CallTarget+Inlining+NGEN | ||||
| process.internal_duration_ms | 253.80 ± (252.89 - 254.71) ms | 278.15 ± (276.89 - 279.41) ms | +9.6% | ✅⬆️ |
| process.time_to_main_ms | 481.12 ± (480.50 - 481.75) ms | 530.38 ± (529.63 - 531.13) ms | +10.2% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 53.98 ± (53.95 - 54.01) MB | 53.89 ± (53.87 - 53.92) MB | -0.2% | ✅ |
| runtime.dotnet.threads.count | 28 ± (28 - 28) | 28 ± (28 - 28) | +0.0% | ✅ |
| .NET 8 - Baseline | ||||
| process.internal_duration_ms | 19.24 ± (19.21 - 19.26) ms | 21.09 ± (21.03 - 21.14) ms | +9.6% | ✅⬆️ |
| process.time_to_main_ms | 73.87 ± (73.76 - 73.98) ms | 86.11 ± (85.89 - 86.33) ms | +16.6% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 12.27 ± (12.27 - 12.28) MB | 12.27 ± (12.26 - 12.28) MB | -0.0% | ✅ |
| runtime.dotnet.threads.count | 10 ± (10 - 10) | 10 ± (10 - 10) | +0.0% | ✅ |
| .NET 8 - Bailout | ||||
| process.internal_duration_ms | 19.20 ± (19.17 - 19.23) ms | 20.94 ± (20.88 - 21.01) ms | +9.1% | ✅⬆️ |
| process.time_to_main_ms | 75.08 ± (75.00 - 75.16) ms | 86.77 ± (86.56 - 86.97) ms | +15.6% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 12.34 ± (12.33 - 12.35) MB | 12.32 ± (12.31 - 12.33) MB | -0.2% | ✅ |
| runtime.dotnet.threads.count | 11 ± (11 - 11) | 11 ± (11 - 11) | +0.0% | ✅ |
| .NET 8 - CallTarget+Inlining+NGEN | ||||
| process.internal_duration_ms | 181.20 ± (180.26 - 182.14) ms | 202.54 ± (201.81 - 203.27) ms | +11.8% | ✅⬆️ |
| process.time_to_main_ms | 461.65 ± (461.02 - 462.29) ms | 510.52 ± (509.56 - 511.48) ms | +10.6% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 41.23 ± (41.20 - 41.25) MB | 41.71 ± (41.67 - 41.75) MB | +1.2% | ✅⬆️ |
| runtime.dotnet.threads.count | 27 ± (27 - 27) | 27 ± (27 - 27) | +0.3% | ✅⬆️ |
HttpMessageHandler
| Metric | Master (Mean ± 95% CI) | Current (Mean ± 95% CI) | Change | Status |
|---|---|---|---|---|
| .NET Framework 4.8 - Baseline | ||||
| duration | 193.79 ± (193.64 - 194.38) ms | 205.47 ± (205.57 - 206.81) ms | +6.0% | ❌⬆️ |
| .NET Framework 4.8 - Bailout | ||||
| duration | 197.11 ± (197.02 - 197.59) ms | 210.64 ± (210.27 - 211.71) ms | +6.9% | ❌⬆️ |
| .NET Framework 4.8 - CallTarget+Inlining+NGEN | ||||
| duration | 1145.42 ± (1145.20 - 1150.97) ms | 1203.02 ± (1202.23 - 1209.30) ms | +5.0% | ✅⬆️ |
| .NET Core 3.1 - Baseline | ||||
| process.internal_duration_ms | 193.39 ± (192.93 - 193.85) ms | 204.63 ± (203.86 - 205.41) ms | +5.8% | ✅⬆️ |
| process.time_to_main_ms | 89.41 ± (89.12 - 89.70) ms | 94.58 ± (94.18 - 94.98) ms | +5.8% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 3 ± (3 - 3) | 3 ± (3 - 3) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 20.85 ± (20.83 - 20.88) MB | 20.66 ± (20.65 - 20.68) MB | -0.9% | ✅ |
| runtime.dotnet.threads.count | 20 ± (20 - 20) | 20 ± (20 - 20) | +1.1% | ✅⬆️ |
| .NET Core 3.1 - Bailout | ||||
| process.internal_duration_ms | 191.60 ± (191.32 - 191.88) ms | 208.92 ± (207.94 - 209.90) ms | +9.0% | ✅⬆️ |
| process.time_to_main_ms | 90.28 ± (90.12 - 90.45) ms | 98.97 ± (98.38 - 99.56) ms | +9.6% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 3 ± (3 - 3) | 3 ± (3 - 3) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 20.78 ± (20.76 - 20.81) MB | 20.65 ± (20.64 - 20.67) MB | -0.6% | ✅ |
| runtime.dotnet.threads.count | 21 ± (21 - 21) | 21 ± (21 - 21) | +1.6% | ✅⬆️ |
| .NET Core 3.1 - CallTarget+Inlining+NGEN | ||||
| process.internal_duration_ms | 443.88 ± (441.58 - 446.18) ms | 460.27 ± (457.11 - 463.44) ms | +3.7% | ✅⬆️ |
| process.time_to_main_ms | 509.20 ± (508.55 - 509.84) ms | 542.33 ± (541.37 - 543.29) ms | +6.5% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 3 ± (3 - 3) | 3 ± (3 - 3) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 63.62 ± (63.51 - 63.72) MB | 63.04 ± (62.88 - 63.20) MB | -0.9% | ✅ |
| runtime.dotnet.threads.count | 29 ± (29 - 29) | 29 ± (29 - 30) | +0.2% | ✅⬆️ |
| .NET 6 - Baseline | ||||
| process.internal_duration_ms | 196.37 ± (196.07 - 196.67) ms | 210.84 ± (210.03 - 211.65) ms | +7.4% | ✅⬆️ |
| process.time_to_main_ms | 77.04 ± (76.81 - 77.26) ms | 83.23 ± (82.92 - 83.54) ms | +8.0% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 4 ± (4 - 4) | 4 ± (4 - 4) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 20.96 ± (20.93 - 20.99) MB | 20.76 ± (20.74 - 20.78) MB | -0.9% | ✅ |
| runtime.dotnet.threads.count | 19 ± (19 - 19) | 20 ± (20 - 20) | +0.8% | ✅⬆️ |
| .NET 6 - Bailout | ||||
| process.internal_duration_ms | 195.41 ± (195.11 - 195.70) ms | 210.56 ± (209.85 - 211.27) ms | +7.8% | ✅⬆️ |
| process.time_to_main_ms | 78.00 ± (77.86 - 78.14) ms | 83.69 ± (83.37 - 84.00) ms | +7.3% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 4 ± (4 - 4) | 4 ± (4 - 4) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 21.02 ± (20.99 - 21.05) MB | 20.88 ± (20.86 - 20.90) MB | -0.7% | ✅ |
| runtime.dotnet.threads.count | 20 ± (20 - 21) | 21 ± (21 - 21) | +0.9% | ✅⬆️ |
| .NET 6 - CallTarget+Inlining+NGEN | ||||
| process.internal_duration_ms | 463.92 ± (462.32 - 465.51) ms | 487.85 ± (485.94 - 489.76) ms | +5.2% | ✅⬆️ |
| process.time_to_main_ms | 487.27 ± (486.58 - 487.96) ms | 520.32 ± (519.31 - 521.34) ms | +6.8% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 4 ± (4 - 4) | 4 ± (4 - 4) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 63.13 ± (63.02 - 63.24) MB | 62.27 ± (62.20 - 62.35) MB | -1.4% | ✅ |
| runtime.dotnet.threads.count | 30 ± (30 - 30) | 30 ± (30 - 30) | +0.6% | ✅⬆️ |
| .NET 8 - Baseline | ||||
| process.internal_duration_ms | 194.33 ± (194.00 - 194.66) ms | 210.30 ± (209.47 - 211.13) ms | +8.2% | ✅⬆️ |
| process.time_to_main_ms | 77.18 ± (76.98 - 77.38) ms | 82.78 ± (82.40 - 83.16) ms | +7.3% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 4 ± (4 - 4) | 4 ± (4 - 4) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 16.32 ± (16.30 - 16.34) MB | 16.19 ± (16.17 - 16.21) MB | -0.8% | ✅ |
| runtime.dotnet.threads.count | 19 ± (19 - 19) | 19 ± (19 - 19) | +1.0% | ✅⬆️ |
| .NET 8 - Bailout | ||||
| process.internal_duration_ms | 193.13 ± (192.82 - 193.45) ms | 211.45 ± (210.60 - 212.30) ms | +9.5% | ✅⬆️ |
| process.time_to_main_ms | 77.91 ± (77.76 - 78.06) ms | 85.14 ± (84.79 - 85.49) ms | +9.3% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 4 ± (4 - 4) | 4 ± (4 - 4) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 16.42 ± (16.39 - 16.45) MB | 16.25 ± (16.23 - 16.27) MB | -1.0% | ✅ |
| runtime.dotnet.threads.count | 20 ± (20 - 20) | 20 ± (20 - 20) | +1.2% | ✅⬆️ |
| .NET 8 - CallTarget+Inlining+NGEN | ||||
| process.internal_duration_ms | 371.48 ± (370.24 - 372.72) ms | 441.74 ± (434.10 - 449.39) ms | +18.9% | ✅⬆️ |
| process.time_to_main_ms | 468.84 ± (468.24 - 469.44) ms | 503.72 ± (502.45 - 505.00) ms | +7.4% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 4 ± (4 - 4) | 4 ± (4 - 4) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 52.98 ± (52.94 - 53.01) MB | 55.08 ± (55.05 - 55.12) MB | +4.0% | ✅⬆️ |
| runtime.dotnet.threads.count | 29 ± (29 - 29) | 29 ± (29 - 29) | -1.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 (8167) - mean (78ms) : 75, 80
master - mean (68ms) : 67, 70
section Bailout
This PR (8167) - mean (82ms) : crit, 79, 84
master - mean (72ms) : 71, 73
section CallTarget+Inlining+NGEN
This PR (8167) - mean (1,116ms) : crit, 1058, 1175
master - mean (1,030ms) : 986, 1074
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 (8167) - mean (135ms) : 130, 139
master - mean (115ms) : 113, 118
section Bailout
This PR (8167) - mean (135ms) : crit, 130, 140
master - mean (116ms) : 114, 118
section CallTarget+Inlining+NGEN
This PR (8167) - mean (885ms) : crit, 839, 931
master - mean (797ms) : 742, 852
FakeDbCommand (.NET 6)
gantt
title Execution time (ms) FakeDbCommand (.NET 6)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8167) - mean (118ms) : 113, 123
master - mean (101ms) : 99, 104
section Bailout
This PR (8167) - mean (121ms) : crit, 117, 125
master - mean (102ms) : 101, 103
section CallTarget+Inlining+NGEN
This PR (8167) - mean (846ms) : crit, 821, 871
master - mean (776ms) : 755, 797
FakeDbCommand (.NET 8)
gantt
title Execution time (ms) FakeDbCommand (.NET 8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8167) - mean (116ms) : 112, 121
master - mean (100ms) : 97, 102
section Bailout
This PR (8167) - mean (117ms) : crit, 113, 120
master - mean (101ms) : 99, 102
section CallTarget+Inlining+NGEN
This PR (8167) - mean (757ms) : crit, 737, 778
master - mean (686ms) : 673, 698
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 (8167) - mean (206ms) : 197, 215
master - mean (194ms) : 190, 198
section Bailout
This PR (8167) - mean (211ms) : crit, 201, 221
master - mean (197ms) : 195, 200
section CallTarget+Inlining+NGEN
This PR (8167) - mean (1,206ms) : 1155, 1257
master - mean (1,148ms) : 1107, 1189
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 (8167) - mean (310ms) : 294, 327
master - mean (292ms) : 284, 300
section Bailout
This PR (8167) - mean (319ms) : crit, 293, 345
master - mean (291ms) : 287, 295
section CallTarget+Inlining+NGEN
This PR (8167) - mean (1,044ms) : 998, 1091
master - mean (991ms) : 953, 1029
HttpMessageHandler (.NET 6)
gantt
title Execution time (ms) HttpMessageHandler (.NET 6)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8167) - mean (305ms) : 289, 321
master - mean (282ms) : 278, 287
section Bailout
This PR (8167) - mean (305ms) : crit, 291, 319
master - mean (282ms) : 279, 286
section CallTarget+Inlining+NGEN
This PR (8167) - mean (1,053ms) : crit, 1011, 1096
master - mean (988ms) : 950, 1026
HttpMessageHandler (.NET 8)
gantt
title Execution time (ms) HttpMessageHandler (.NET 8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8167) - mean (306ms) : 287, 326
master - mean (282ms) : 277, 287
section Bailout
This PR (8167) - mean (309ms) : crit, 292, 326
master - mean (281ms) : 277, 286
section CallTarget+Inlining+NGEN
This PR (8167) - mean (993ms) : crit, 884, 1103
master - mean (873ms) : 849, 896
SummaryClean, well-scoped PR. The approach of widening the outer Only minor observation: the source file uses Review by 🤖 Claude Code |
## 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
…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
…tern` (#8180) ## 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 - #8167 - #8170 - #8180 👈
Summary of changes
Makes
ValueStringBuilderusable in .NET Core 3.1+, instead of just .NET 6Reason for change
I want to use it in some aspnetcore improvements, but currently it's only exposed in .NET 6
Implementation details
Update the
#if. Note that I originally made this available in all TFMs, using our vendored spans, but benchmarking showed that this was actively harmful compared to just usingStringBuilderCache, so to avoid incorrect use, just restricting it for now.Test coverage
Updated the tests to cover .NET Core 3.1 too.
Other details
Part of a stack
https://datadoghq.atlassian.net/browse/LANGPLAT-842
ValueStringBuilderavailable in.NET Core 3.1#8167 👈AspNetCoreResourceNameHelper.SimplifyRouteTemplate#8170