Remove allocations for operation name/service name versioning code#8196
Remove allocations for operation name/service name versioning code#8196andrewlock merged 10 commits intomasterfrom
Conversation
BenchmarksBenchmark execution time: 2026-02-18 10:49:17 Comparing candidate commit 09bcc8b in PR branch 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
scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeLegacyArgs netcoreapp3.1
scenario:Benchmarks.Trace.AspNetCoreBenchmark.SendRequest net6.0
scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces netcoreapp3.1
scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSliceWithPool netcoreapp3.1
scenario:Benchmarks.Trace.DbCommandBenchmark.ExecuteNonQuery net6.0
scenario:Benchmarks.Trace.DbCommandBenchmark.ExecuteNonQuery netcoreapp3.1
scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearch net472
scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearch net6.0
scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearchAsync net472
scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearchAsync net6.0
scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearchAsync netcoreapp3.1
scenario:Benchmarks.Trace.HttpClientBenchmark.SendAsync netcoreapp3.1
scenario:Benchmarks.Trace.ILoggerBenchmark.EnrichedLog netcoreapp3.1
scenario:Benchmarks.Trace.Iast.StringAspectsBenchmark.StringConcatBenchmark net6.0
scenario:Benchmarks.Trace.Log4netBenchmark.EnrichedLog netcoreapp3.1
scenario:Benchmarks.Trace.RedisBenchmark.SendReceive net472
scenario:Benchmarks.Trace.RedisBenchmark.SendReceive net6.0
scenario:Benchmarks.Trace.RedisBenchmark.SendReceive netcoreapp3.1
scenario:Benchmarks.Trace.SerilogBenchmark.EnrichedLog net472
scenario:Benchmarks.Trace.SingleSpanAspNetCoreBenchmark.SingleSpanAspNetCore netcoreapp3.1
scenario:Benchmarks.Trace.SpanBenchmark.StartFinishScope net6.0
scenario:Benchmarks.Trace.SpanBenchmark.StartFinishSpan netcoreapp3.1
|
Execution-Time Benchmarks Report ⏱️Execution-time results for samples comparing This PR (8196) and master. ✅ No regressions detected - check the details below Full Metrics ComparisonFakeDbCommand
HttpMessageHandler
Comparison explanationExecution-time benchmarks measure the whole time it takes to execute a program, and are intended to measure the one-off costs. Cases where the execution time results for the PR are worse than latest master results are highlighted in **red**. The following thresholds were used for comparing the execution times:
Note that these results are based on a single point-in-time result for each branch. For full results, see the dashboard. Graphs show the p99 interval based on the mean and StdDev of the test run, as well as the mean value of the run (shown as a diamond below the graph). Duration chartsFakeDbCommand (.NET Framework 4.8)gantt
title Execution time (ms) FakeDbCommand (.NET Framework 4.8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (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
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
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
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
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
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
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
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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Http, | ||
| Grpc |
There was a problem hiding this comment.
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 🤷🏽♂️
There was a problem hiding this comment.
Yeah, and also this risks a classic copy pasta error 😅
Http = 0,
Grpc = 1
NewProtocol = 1If 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 |
There was a problem hiding this comment.
Would it make sense to calculate each one on its first call to avoid calculating ones we never use?
There was a problem hiding this comment.
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?
| /// 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 |
There was a problem hiding this comment.
Should we use static class with const int fields instead of all these enum to avoid all the (int) casting everywhere?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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."
There was a problem hiding this comment.
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, andMessagingSchemato 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.
tracer/test/Datadog.Trace.Tests/Configuration/Schema/ClientSchemaTests.cs
Show resolved
Hide resolved
tracer/test/Datadog.Trace.Tests/Configuration/Schema/ClientSchemaTests.cs
Show resolved
Hide resolved
## 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
Could be simpler, but keeping the same pattern for usability
12c87bb to
09bcc8b
Compare
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:to creating an enum of "allowed" values with an array of the "updated" values:
and then the runtime call becomes a simple index into the array:
This removes the allocation, and the switch, so it's nice and quick. There are similar patterns and updates for all the
NamingSchemapaths. 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
ClientSchemamethods, but I expect similar improvements across the board)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