Skip to content

Remove allocations for operation name/service name versioning code#8196

Merged
andrewlock merged 10 commits intomasterfrom
andrew/client-schema-allocations
Feb 18, 2026
Merged

Remove allocations for operation name/service name versioning code#8196
andrewlock merged 10 commits intomasterfrom
andrew/client-schema-allocations

Conversation

@andrewlock
Copy link
Member

@andrewlock andrewlock commented Feb 12, 2026

Summary of changes

Remove allocations from paths that are called for every span creation by hard-coding the required values up-front

Reason for change

When doing some profiling work, I noticed a bunch of string allocations. Digging into it, we can pre-compute the required values, allocated them up front, and avoid allocating every time we create a span.

Implementation details

In summary, the implementations change from doing string interpolation with every call on v0:

public string GetOperationNameForProtocol(string protocol) =>
    _version switch
    {
        SchemaVersion.V0 => $"{protocol}.request",
        _ => V1Values.ProtocolOperationNames,
    };

to creating an enum of "allowed" values with an array of the "updated" values:

public enum Protocol
{
    Http,
    Grpc
}

public static readonly string[] ProtocolOperationNames =
[
    "http.request",
    "grpc.request",
];

and then the runtime call becomes a simple index into the array:

public string GetOperationNameForProtocol(Protocol protocol) => _protocols[(int)protocol];

This removes the allocation, and the switch, so it's nice and quick. There are similar patterns and updates for all the NamingSchema paths. I focused on the ones that were doing string concatenation, but tried to keep consistent patterns where possible.

Note that in places where we need to choose between a "v1" and "v0" array, I put the array in nested types, to avoid the static initializers from running where the type isn't going to be accessed. I haven't confirmed this works as I'm hoping, but I think it does, and tbh, it's not a big deal if not 😅

All in all, these changes get us some nice benchmarking improvements (the following is testing the ClientSchema methods, but I expect similar improvements across the board)

Method Runtime Mean Allocated Alloc Ratio
V0_GetOperationName_Original .NET 10.0 9.3603 ns 48 B 0.86
V0_GetOperationName_Updated .NET 10.0 0.3735 ns - 0.00
V0_GetOperationName_Original .NET 6.0 10.8276 ns 48 B 0.86
V0_GetOperationName_Updated .NET 6.0 0.1621 ns - 0.00
V0_GetOperationName_Original .NET Core 2.1 14.7789 ns 56 B 1.00
V0_GetOperationName_Updated .NET Core 2.1 0.0003 ns - 0.00
V0_GetOperationName_Original .NET Core 3.1 14.9634 ns 48 B 0.86
V0_GetOperationName_Updated .NET Core 3.1 0.0888 ns - 0.00
V0_GetOperationName_Original .NET Framework 4.8 12.9418 ns 56 B 1.00
V0_GetOperationName_Updated .NET Framework 4.8 0.0101 ns - 0.00
V1_GetOperationName_Original .NET 10.0 11.1258 ns 64 B 1.00
V1_GetOperationName_Updated .NET 10.0 0.2705 ns - 0.00
V1_GetOperationName_Original .NET 6.0 10.7945 ns 64 B 1.00
V1_GetOperationName_Updated .NET 6.0 0.0325 ns - 0.00
V1_GetOperationName_Original .NET Core 2.1 16.0458 ns 64 B 1.00
V1_GetOperationName_Updated .NET Core 2.1 0.0339 ns - 0.00
V1_GetOperationName_Original .NET Core 3.1 16.9130 ns 64 B 1.00
V1_GetOperationName_Updated .NET Core 3.1 0.0085 ns - 0.00
V1_GetOperationName_Original .NET Framework 4.8 13.5547 ns 64 B 1.00
V1_GetOperationName_Updated .NET Framework 4.8 0.0630 ns - 0.00
V0_GetServiceName_Original .NET 10.0 13.9854 ns 64 B 0.89
V0_GetServiceName_Updated .NET 10.0 1.0276 ns - 0.00
V0_GetServiceName_Original .NET 6.0 16.9312 ns 64 B 0.89
V0_GetServiceName_Updated .NET 6.0 0.0834 ns - 0.00
V0_GetServiceName_Original .NET Core 2.1 21.3370 ns 72 B 1.00
V0_GetServiceName_Updated .NET Core 2.1 0.0128 ns - 0.00
V0_GetServiceName_Original .NET Core 3.1 22.9226 ns 64 B 0.89
V0_GetServiceName_Updated .NET Core 3.1 0.0154 ns - 0.00
V0_GetServiceName_Original .NET Framework 4.8 19.1088 ns 72 B 1.00
V0_GetServiceName_Updated .NET Framework 4.8 0.0018 ns - 0.00
V1_GetServiceName_Original .NET 10.0 0.3646 ns - NA
V1_GetServiceName_Updated .NET 10.0 0.2475 ns - NA
V1_GetServiceName_Original .NET 6.0 0.9440 ns - NA
V1_GetServiceName_Updated .NET 6.0 0.0601 ns - NA
V1_GetServiceName_Original .NET Core 2.1 1.5292 ns - NA
V1_GetServiceName_Updated .NET Core 2.1 0.0165 ns - NA
V1_GetServiceName_Original .NET Core 3.1 1.4162 ns - NA
V1_GetServiceName_Updated .NET Core 3.1 0.0176 ns - NA
V1_GetServiceName_Original .NET Framework 4.8 1.3256 ns - NA
V1_GetServiceName_Updated .NET Framework 4.8 0.0593 ns - NA

Test coverage

To avoid any potential regressions, I refactored the tests to specify the explicit expected output, instead of essentially duplicating the method inside the test. It's a bit more verbose, but gave me more confidence.

Also, the code as written requires that we keep the enums in sync with the arrays. There's a potential failure case where we update one and don't update the other. We could use code-gen to avoid that, but it seems overkill. Instead, I just added some unit tests to ensure that every value of each enum returns something

Other details

I did the benchmarking and the ClientSchema changes manually, used 🤖 Claude to do the same thing for the other schema types, and then changed the stuff I didn't like (half of it 😅)

Spotted while working on https://datadoghq.atlassian.net/browse/LANGPLAT-842

@andrewlock andrewlock requested a review from a team as a code owner February 12, 2026 09:17
@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 12, 2026
@pr-commenter
Copy link

pr-commenter bot commented Feb 12, 2026

Benchmarks

Benchmark execution time: 2026-02-18 10:49:17

Comparing candidate commit 09bcc8b in PR branch andrew/client-schema-allocations with baseline commit 421a5fb in branch master.

Found 17 performance improvements and 8 performance regressions! Performance is the same for 155 metrics, 12 unstable metrics.

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.AllCycleMoreComplexBody net6.0

  • 🟥 execution_time [+14.255ms; +20.239ms] or [+7.211%; +10.238%]

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

  • 🟩 execution_time [-19.817ms; -19.107ms] or [-9.895%; -9.540%]

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

  • 🟩 execution_time [-104.694ms; -102.698ms] or [-52.520%; -51.518%]

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

  • 🟩 execution_time [-24.386ms; -20.986ms] or [-16.325%; -14.049%]
  • 🟩 throughput [+248.394op/s; +283.860op/s] or [+16.538%; +18.899%]

scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSliceWithPool netcoreapp3.1

  • 🟥 execution_time [+115.639µs; +122.481µs] or [+6.107%; +6.468%]
  • 🟥 throughput [-32.138op/s; -30.347op/s] or [-6.085%; -5.746%]

scenario:Benchmarks.Trace.DbCommandBenchmark.ExecuteNonQuery net6.0

  • 🟩 execution_time [-16.661ms; -13.000ms] or [-7.925%; -6.184%]

scenario:Benchmarks.Trace.DbCommandBenchmark.ExecuteNonQuery netcoreapp3.1

  • 🟩 throughput [+18581.543op/s; +26988.102op/s] or [+5.040%; +7.320%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearch net472

  • 🟩 throughput [+20482.029op/s; +21683.353op/s] or [+6.831%; +7.232%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearch net6.0

  • 🟩 throughput [+47056.250op/s; +61291.133op/s] or [+7.928%; +10.327%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearchAsync net472

  • 🟩 throughput [+22109.638op/s; +23998.153op/s] or [+7.680%; +8.336%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearchAsync net6.0

  • 🟩 throughput [+69654.680op/s; +85902.899op/s] or [+12.441%; +15.343%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearchAsync netcoreapp3.1

  • 🟩 throughput [+23320.153op/s; +34795.994op/s] or [+5.336%; +7.962%]

scenario:Benchmarks.Trace.HttpClientBenchmark.SendAsync netcoreapp3.1

  • 🟩 throughput [+8766.176op/s; +10854.491op/s] or [+7.506%; +9.294%]

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

  • 🟥 execution_time [+13.397ms; +18.780ms] or [+6.872%; +9.633%]

scenario:Benchmarks.Trace.Iast.StringAspectsBenchmark.StringConcatBenchmark net6.0

  • 🟩 throughput [+1581.610op/s; +3474.982op/s] or [+7.406%; +16.273%]

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

  • 🟩 execution_time [-30.630ms; -27.974ms] or [-15.071%; -13.764%]

scenario:Benchmarks.Trace.RedisBenchmark.SendReceive net472

  • 🟩 throughput [+22554.824op/s; +26314.092op/s] or [+6.681%; +7.794%]

scenario:Benchmarks.Trace.RedisBenchmark.SendReceive net6.0

  • 🟩 throughput [+36372.771op/s; +44959.610op/s] or [+7.273%; +8.990%]

scenario:Benchmarks.Trace.RedisBenchmark.SendReceive netcoreapp3.1

  • 🟩 throughput [+29292.413op/s; +40105.582op/s] or [+7.745%; +10.604%]

scenario:Benchmarks.Trace.SerilogBenchmark.EnrichedLog net472

  • 🟥 throughput [-8709.531op/s; -7746.191op/s] or [-5.668%; -5.041%]

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

  • 🟥 throughput [-16486452.143op/s; -15356093.092op/s] or [-6.832%; -6.363%]

scenario:Benchmarks.Trace.SpanBenchmark.StartFinishScope net6.0

  • 🟥 execution_time [+13.817ms; +17.897ms] or [+6.908%; +8.948%]

scenario:Benchmarks.Trace.SpanBenchmark.StartFinishSpan netcoreapp3.1

  • 🟥 execution_time [+13.807ms; +17.719ms] or [+6.949%; +8.918%]

@dd-trace-dotnet-ci-bot
Copy link

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

Execution-Time Benchmarks Report ⏱️

Execution-time results for samples comparing This PR (8196) 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
duration73.03 ± (73.01 - 73.30) ms73.88 ± (73.87 - 74.22) ms+1.2%✅⬆️
.NET Framework 4.8 - Bailout
duration77.33 ± (77.34 - 77.74) ms78.49 ± (78.20 - 78.57) ms+1.5%✅⬆️
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1061.55 ± (1062.59 - 1069.58) ms1069.85 ± (1071.19 - 1078.50) ms+0.8%✅⬆️
.NET Core 3.1 - Baseline
process.internal_duration_ms22.21 ± (22.17 - 22.25) ms22.59 ± (22.54 - 22.63) ms+1.7%✅⬆️
process.time_to_main_ms83.54 ± (83.36 - 83.72) ms85.56 ± (85.39 - 85.72) ms+2.4%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.91 ± (10.91 - 10.91) MB10.92 ± (10.92 - 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.17 ± (22.13 - 22.22) ms22.50 ± (22.45 - 22.54) ms+1.5%✅⬆️
process.time_to_main_ms85.15 ± (84.96 - 85.34) ms87.22 ± (87.01 - 87.43) ms+2.4%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.95 ± (10.94 - 10.95) MB10.95 ± (10.94 - 10.95) MB+0.0%✅⬆️
runtime.dotnet.threads.count13 ± (13 - 13)13 ± (13 - 13)+0.0%
.NET Core 3.1 - CallTarget+Inlining+NGEN
process.internal_duration_ms240.13 ± (236.21 - 244.04) ms242.67 ± (238.63 - 246.70) ms+1.1%✅⬆️
process.time_to_main_ms482.18 ± (481.56 - 482.81) ms489.98 ± (489.39 - 490.58) ms+1.6%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed47.53 ± (47.51 - 47.55) MB47.61 ± (47.59 - 47.63) MB+0.2%✅⬆️
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)+0.7%✅⬆️
.NET 6 - Baseline
process.internal_duration_ms21.33 ± (21.28 - 21.38) ms21.36 ± (21.32 - 21.41) ms+0.2%✅⬆️
process.time_to_main_ms73.88 ± (73.71 - 74.05) ms74.45 ± (74.27 - 74.63) ms+0.8%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.63 ± (10.63 - 10.64) MB10.64 ± (10.63 - 10.64) MB+0.0%✅⬆️
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 6 - Bailout
process.internal_duration_ms21.13 ± (21.09 - 21.17) ms21.38 ± (21.34 - 21.42) ms+1.2%✅⬆️
process.time_to_main_ms74.72 ± (74.57 - 74.86) ms75.96 ± (75.77 - 76.16) ms+1.7%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.72 ± (10.71 - 10.73) MB10.74 ± (10.73 - 10.74) MB+0.2%✅⬆️
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms248.46 ± (245.06 - 251.86) ms252.17 ± (248.68 - 255.67) ms+1.5%✅⬆️
process.time_to_main_ms462.36 ± (461.75 - 462.97) ms472.39 ± (471.78 - 472.99) ms+2.2%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed48.35 ± (48.33 - 48.38) MB48.32 ± (48.30 - 48.34) MB-0.1%
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)+0.2%✅⬆️
.NET 8 - Baseline
process.internal_duration_ms19.37 ± (19.34 - 19.41) ms19.66 ± (19.62 - 19.70) ms+1.5%✅⬆️
process.time_to_main_ms72.33 ± (72.17 - 72.48) ms73.91 ± (73.75 - 74.07) ms+2.2%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.66 ± (7.66 - 7.67) MB7.66 ± (7.65 - 7.66) MB-0.1%
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 8 - Bailout
process.internal_duration_ms19.45 ± (19.41 - 19.50) ms19.81 ± (19.76 - 19.85) ms+1.8%✅⬆️
process.time_to_main_ms73.79 ± (73.64 - 73.94) ms75.65 ± (75.46 - 75.84) ms+2.5%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.71 ± (7.70 - 7.71) MB7.72 ± (7.71 - 7.73) MB+0.2%✅⬆️
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms186.46 ± (185.71 - 187.22) ms190.51 ± (189.72 - 191.30) ms+2.2%✅⬆️
process.time_to_main_ms446.83 ± (446.04 - 447.61) ms453.30 ± (452.63 - 453.96) ms+1.4%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed35.99 ± (35.96 - 36.01) MB36.13 ± (36.09 - 36.16) MB+0.4%✅⬆️
runtime.dotnet.threads.count27 ± (27 - 27)27 ± (27 - 27)-0.1%

HttpMessageHandler

Metric Master (Mean ± 95% CI) Current (Mean ± 95% CI) Change Status
.NET Framework 4.8 - Baseline
duration192.88 ± (193.20 - 194.06) ms193.03 ± (192.87 - 193.66) ms+0.1%✅⬆️
.NET Framework 4.8 - Bailout
duration196.16 ± (196.04 - 196.61) ms196.51 ± (196.54 - 197.27) ms+0.2%✅⬆️
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1133.09 ± (1134.95 - 1142.63) ms1140.19 ± (1141.58 - 1150.52) ms+0.6%✅⬆️
.NET Core 3.1 - Baseline
process.internal_duration_ms187.03 ± (186.69 - 187.37) ms187.94 ± (187.50 - 188.38) ms+0.5%✅⬆️
process.time_to_main_ms81.26 ± (81.05 - 81.47) ms81.53 ± (81.27 - 81.80) ms+0.3%✅⬆️
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed16.19 ± (16.16 - 16.22) MB16.04 ± (16.01 - 16.07) MB-0.9%
runtime.dotnet.threads.count20 ± (20 - 20)20 ± (20 - 20)-0.2%
.NET Core 3.1 - Bailout
process.internal_duration_ms185.82 ± (185.53 - 186.12) ms186.32 ± (186.05 - 186.59) ms+0.3%✅⬆️
process.time_to_main_ms82.50 ± (82.30 - 82.69) ms82.49 ± (82.33 - 82.65) ms-0.0%
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed16.24 ± (16.19 - 16.28) MB16.12 ± (16.09 - 16.15) MB-0.7%
runtime.dotnet.threads.count21 ± (20 - 21)21 ± (20 - 21)-0.3%
.NET Core 3.1 - CallTarget+Inlining+NGEN
process.internal_duration_ms429.29 ± (426.13 - 432.45) ms437.66 ± (435.20 - 440.11) ms+1.9%✅⬆️
process.time_to_main_ms471.29 ± (470.77 - 471.81) ms472.77 ± (472.26 - 473.28) ms+0.3%✅⬆️
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed58.09 ± (57.98 - 58.21) MB58.02 ± (57.90 - 58.14) MB-0.1%
runtime.dotnet.threads.count29 ± (29 - 29)29 ± (29 - 30)+0.1%✅⬆️
.NET 6 - Baseline
process.internal_duration_ms191.26 ± (190.88 - 191.64) ms191.06 ± (190.63 - 191.50) ms-0.1%
process.time_to_main_ms70.35 ± (70.18 - 70.53) ms70.64 ± (70.41 - 70.87) ms+0.4%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed16.14 ± (16.01 - 16.27) MB16.37 ± (16.30 - 16.45) MB+1.4%✅⬆️
runtime.dotnet.threads.count18 ± (18 - 19)19 ± (19 - 19)+3.5%✅⬆️
.NET 6 - Bailout
process.internal_duration_ms190.22 ± (189.94 - 190.51) ms190.34 ± (190.02 - 190.66) ms+0.1%✅⬆️
process.time_to_main_ms71.04 ± (70.93 - 71.15) ms71.43 ± (71.31 - 71.55) ms+0.5%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed15.99 ± (15.84 - 16.14) MB16.05 ± (15.89 - 16.21) MB+0.4%✅⬆️
runtime.dotnet.threads.count20 ± (19 - 20)19 ± (19 - 19)-2.5%
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms444.13 ± (441.83 - 446.43) ms449.67 ± (448.13 - 451.21) ms+1.2%✅⬆️
process.time_to_main_ms446.60 ± (446.13 - 447.07) ms448.57 ± (448.18 - 448.96) ms+0.4%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed58.18 ± (58.07 - 58.29) MB58.20 ± (58.12 - 58.28) MB+0.0%✅⬆️
runtime.dotnet.threads.count29 ± (29 - 29)29 ± (29 - 29)+0.0%✅⬆️
.NET 8 - Baseline
process.internal_duration_ms189.12 ± (188.75 - 189.50) ms188.61 ± (188.30 - 188.93) ms-0.3%
process.time_to_main_ms70.06 ± (69.80 - 70.33) ms69.37 ± (69.19 - 69.54) ms-1.0%
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.75 ± (11.72 - 11.78) MB11.78 ± (11.74 - 11.82) MB+0.3%✅⬆️
runtime.dotnet.threads.count18 ± (18 - 18)18 ± (18 - 18)-0.7%
.NET 8 - Bailout
process.internal_duration_ms188.45 ± (188.05 - 188.84) ms188.63 ± (188.32 - 188.95) ms+0.1%✅⬆️
process.time_to_main_ms70.71 ± (70.56 - 70.86) ms70.80 ± (70.67 - 70.92) ms+0.1%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.79 ± (11.76 - 11.82) MB11.82 ± (11.77 - 11.87) MB+0.2%✅⬆️
runtime.dotnet.threads.count19 ± (19 - 19)19 ± (19 - 19)-0.2%
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms364.40 ± (363.06 - 365.73) ms362.19 ± (360.77 - 363.62) ms-0.6%
process.time_to_main_ms432.20 ± (431.53 - 432.87) ms433.06 ± (432.54 - 433.58) ms+0.2%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed47.69 ± (47.66 - 47.73) MB47.67 ± (47.63 - 47.71) 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 (8196) - mean (74ms)  : 71, 77
    master - mean (73ms)  : 71, 75

    section Bailout
    This PR (8196) - mean (78ms)  : 76, 80
    master - mean (78ms)  : 75, 80

    section CallTarget+Inlining+NGEN
    This PR (8196) - mean (1,075ms)  : 1020, 1129
    master - mean (1,066ms)  : 1014, 1118

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 (8196) - mean (115ms)  : 112, 119
    master - mean (113ms)  : 110, 116

    section Bailout
    This PR (8196) - mean (117ms)  : 113, 120
    master - mean (114ms)  : 112, 117

    section CallTarget+Inlining+NGEN
    This PR (8196) - mean (770ms)  : 713, 827
    master - mean (751ms)  : 690, 811

Loading
FakeDbCommand (.NET 6)
gantt
    title Execution time (ms) FakeDbCommand (.NET 6)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8196) - mean (102ms)  : 99, 105
    master - mean (102ms)  : 99, 104

    section Bailout
    This PR (8196) - mean (104ms)  : 101, 107
    master - mean (102ms)  : 100, 104

    section CallTarget+Inlining+NGEN
    This PR (8196) - mean (757ms)  : 696, 818
    master - mean (743ms)  : 680, 806

Loading
FakeDbCommand (.NET 8)
gantt
    title Execution time (ms) FakeDbCommand (.NET 8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8196) - mean (102ms)  : 99, 104
    master - mean (99ms)  : 96, 102

    section Bailout
    This PR (8196) - mean (103ms)  : 100, 106
    master - mean (101ms)  : 99, 103

    section CallTarget+Inlining+NGEN
    This PR (8196) - mean (676ms)  : 654, 697
    master - mean (661ms)  : 641, 681

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 (8196) - mean (193ms)  : 190, 197
    master - mean (194ms)  : 188, 199

    section Bailout
    This PR (8196) - mean (197ms)  : 193, 200
    master - mean (196ms)  : 193, 199

    section CallTarget+Inlining+NGEN
    This PR (8196) - mean (1,146ms)  : 1082, 1211
    master - mean (1,139ms)  : 1083, 1194

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 (8196) - mean (278ms)  : 271, 285
    master - mean (277ms)  : 271, 283

    section Bailout
    This PR (8196) - mean (277ms)  : 274, 280
    master - mean (276ms)  : 272, 280

    section CallTarget+Inlining+NGEN
    This PR (8196) - mean (939ms)  : 897, 981
    master - mean (930ms)  : 870, 990

Loading
HttpMessageHandler (.NET 6)
gantt
    title Execution time (ms) HttpMessageHandler (.NET 6)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8196) - mean (270ms)  : 265, 276
    master - mean (270ms)  : 264, 276

    section Bailout
    This PR (8196) - mean (270ms)  : 266, 274
    master - mean (269ms)  : 265, 274

    section CallTarget+Inlining+NGEN
    This PR (8196) - mean (929ms)  : 891, 967
    master - mean (920ms)  : 887, 953

Loading
HttpMessageHandler (.NET 8)
gantt
    title Execution time (ms) HttpMessageHandler (.NET 8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8196) - mean (268ms)  : 262, 273
    master - mean (269ms)  : 261, 277

    section Bailout
    This PR (8196) - mean (269ms)  : 265, 273
    master - mean (269ms)  : 263, 275

    section CallTarget+Inlining+NGEN
    This PR (8196) - mean (827ms)  : 812, 842
    master - mean (827ms)  : 811, 844

Loading

Comment on lines +63 to +64
Http,
Grpc
Copy link
Member

Choose a reason for hiding this comment

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

If the backing int values are important (like here), I like to make them explicit:

Http = 0,
Grpc = 1

This avoids issue is someone inserts a value not at the end. But in this case they also have to update the array so 🤷🏽‍♂️

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, and also this risks a classic copy pasta error 😅

Http = 0,
Grpc = 1
NewProtocol = 1

If the backing int values are important (like here), I like to make them explicit

In some ways, the Values aren't important (because they're always the "correct" values from the compiler), but the order is important. My hope is that the unit tests around this are enough to handle and detect where you forget to update the arrays, or you add the item in the wrong place in the array 🤞

_useV0Tags = version == SchemaVersion.V0 && !peerServiceTagsEnabled;

public string GetOperationName(string databaseType) => $"{databaseType}.query";
// Calculate service names once, to avoid allocations with every call
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to calculate each one on its first call to avoid calculating ones we never use?

Copy link
Member Author

Choose a reason for hiding this comment

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

IMO, no, it's probably not worth it 🤔 Using an index into an array makes this super fast, in addition to removing the amortized allocations. If we want to avoid allocating the strings we don't need, then we need to add a branch for every access, and code to conditionally assign it etc, which makes things more complicated and slower at the expense of potentially 5 long-lived strings?

I could be wrong, but unless this has a measurable impact on startup time, I'd be more inclined to leave it as-it?

Copy link
Member

Choose a reason for hiding this comment

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

Make sense, thanks.

/// WARNING: when adding new values, you _must_ update the corresponding array in <see cref="OperationNames"/>
/// and update the service name initialization in the constructor.
/// </summary>
public enum ServiceType
Copy link
Member

Choose a reason for hiding this comment

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

Should we use static class with const int fields instead of all these enum to avoid all the (int) casting everywhere?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not passionate about it either way tbh, but enum is nice because you don't have to care about the numbers, just the order, and it's all the same at runtime.

Happy to go with the majority consensus here

Copy link
Member

Choose a reason for hiding this comment

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

I see now that the enum types are used as parameters in some places like GetOperationName(OperationType) below. For this use it's nice to an enum (so people still rely on IntelliSense in this era of AI agents?). So, "meh."

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 hot-path span creation by removing repeated string allocations in naming/schema code, replacing per-call string interpolation/switches with enum-indexed, precomputed string arrays and cached suffixes. It also updates instrumentation call sites and unit tests to use the new enum-based APIs and to assert explicit expected outputs.

Changes:

  • Refactor ClientSchema, ServerSchema, DatabaseSchema, and MessagingSchema to return operation/service names via enum-indexed precomputed arrays (and cached “.request” suffixes).
  • Update multiple instrumentations (HTTP, gRPC, messaging, DB, Service Fabric remoting, etc.) to call the new enum-based schema APIs.
  • Refactor schema unit tests to use explicit expected strings and add “supports all enum values” coverage.

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tracer/test/Datadog.Trace.Tests/Configuration/Schema/ServerSchemaTests.cs Updates tests to use enum-based server schema APIs and adds enum coverage checks.
tracer/test/Datadog.Trace.Tests/Configuration/Schema/NamingSchemaTests.cs Simplifies peer-service remap assertions with explicit expected outputs.
tracer/test/Datadog.Trace.Tests/Configuration/Schema/MessagingSchemaTests.cs Refactors messaging schema tests for enum-based APIs and adds enum coverage checks.
tracer/test/Datadog.Trace.Tests/Configuration/Schema/DatabaseSchemaTests.cs Refactors DB schema tests for enum-based APIs and adds enum coverage checks.
tracer/test/Datadog.Trace.Tests/Configuration/Schema/ClientSchemaTests.cs Refactors client schema tests for enum-based APIs, adds suffix + enum coverage checks.
tracer/src/Datadog.Trace/ServiceFabric/ServiceRemotingHelpers.cs Avoids per-call request-type allocation logic by appending cached schema suffix directly.
tracer/src/Datadog.Trace/Configuration/Schema/ServerSchema.cs Replaces per-call string construction with enum-indexed precomputed arrays + cached suffix.
tracer/src/Datadog.Trace/Configuration/Schema/MessagingSchema.cs Precomputes inbound/outbound op names + service names; adds enums for operation/service types.
tracer/src/Datadog.Trace/Configuration/Schema/DatabaseSchema.cs Introduces enum-based operation/service name lookup with precomputed arrays and cached service names.
tracer/src/Datadog.Trace/Configuration/Schema/ClientSchema.cs Introduces enum-based protocol/component lookups, cached suffix, and precomputed service names.
tracer/src/Datadog.Trace/ClrProfiler/ScopeFactory.cs Updates HTTP client span naming/service naming to use ClientSchema enums.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Wcf/WcfCommon.cs Updates WCF server op naming to use ServerSchema.Component enum.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Redis/RedisHelper.cs Updates Redis DB service naming to use DatabaseSchema.ServiceType enum.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/RabbitMQ/RabbitMQIntegration.cs Updates messaging op/service naming to use MessagingSchema enums (AMQP ops, RabbitMQ service).
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Msmq/MsmqCommon.cs Updates MSMQ messaging op/service naming to use MessagingSchema enums.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/MongoDb/MongoDbIntegration.cs Updates MongoDB operation/service naming to use DatabaseSchema enums.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Kafka/KafkaHelper.cs Updates Kafka messaging op/service naming to use MessagingSchema enums.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/IbmMq/IbmMqHelper.cs Updates IBM MQ messaging op/service naming to use MessagingSchema enums.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Grpc/GrpcLegacy/Server/GrpcLegacyServerCommon.cs Updates gRPC server op naming to use ServerSchema.Protocol enum.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Grpc/GrpcLegacy/Client/GrpcLegacyClientCommon.cs Updates gRPC client op/service naming to use ClientSchema enums.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Grpc/GrpcDotNet/GrpcNetClient/GrpcDotNetClientCommon.cs Updates gRPC client op/service naming to use ClientSchema enums.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Grpc/GrpcDotNet/GrpcAspNetCoreServer/GrpcDotNetServerCommon.cs Updates gRPC server op naming to use ServerSchema.Protocol enum.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Elasticsearch/ElasticsearchNetCommon.cs Updates Elasticsearch op/service naming to use DatabaseSchema enums and uses const span type.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Couchbase/CouchbaseCommon.cs Updates Couchbase op/service naming to use DatabaseSchema enums.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/CosmosDb/RequestInvokerHandlerSendAsyncIntegration.cs Updates CosmosDB op/service naming to use DatabaseSchema enums.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/CosmosDb/CosmosCommon.cs Updates CosmosDB op/service naming to use DatabaseSchema enums.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/ServiceBus/ServiceBusReceiverReceiveMessagesAsyncIntegration.cs Updates Service Bus service naming to use MessagingSchema.ServiceType enum.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/ServiceBus/AzureServiceBusCommon.cs Updates Service Bus service naming to use MessagingSchema.ServiceType enum.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/EventHubs/EventHubsCommon.cs Updates Event Hubs service naming to use MessagingSchema.ServiceType enum.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/EventHubs/AmqpConsumerReceiveAsyncIntegration.cs Updates Event Hubs service naming to use MessagingSchema.ServiceType enum.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Aerospike/AerospikeCommon.cs Updates Aerospike service naming to use DatabaseSchema.ServiceType enum.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AWS/StepFunctions/AwsStepFunctionsCommon.cs Updates Step Functions messaging op naming to use MessagingSchema.OperationType enum.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AWS/SQS/AwsSqsCommon.cs Updates SQS messaging op naming to use MessagingSchema.OperationType enum.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AWS/SNS/AwsSnsCommon.cs Updates SNS messaging op naming to use MessagingSchema.OperationType enum.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AWS/Kinesis/AwsKinesisCommon.cs Updates Kinesis messaging op naming to use MessagingSchema.OperationType enum.
tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AWS/EventBridge/AwsEventBridgeCommon.cs Updates EventBridge outbound messaging op naming to use MessagingSchema.OperationType enum.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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/client-schema-allocations branch from 12c87bb to 09bcc8b Compare February 18, 2026 10:07
@andrewlock andrewlock requested a review from a team as a code owner February 18, 2026 10:07
@andrewlock andrewlock merged commit e0e1029 into master Feb 18, 2026
142 checks passed
@andrewlock andrewlock deleted the andrew/client-schema-allocations branch February 18, 2026 15:40
@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.

3 participants