Skip to content

Implement Client-Side Stats (CSS) 1.2.0#8420

Merged
andrewlock merged 48 commits into
masterfrom
andrew/client-side-stats/client-side-stats-1-2
Apr 20, 2026
Merged

Implement Client-Side Stats (CSS) 1.2.0#8420
andrewlock merged 48 commits into
masterfrom
andrew/client-side-stats/client-side-stats-1-2

Conversation

@andrewlock

Copy link
Copy Markdown
Member

Summary of changes

Updates the existing client-side-stats implementation to match version 1.2.0 as defined in the RFC and as implemented in the agent.

Reason for change

Our implementation is severely lagging the latest implementation in the agent. This hasn't been a big deal, as it's not documented and not enabled by default, but we'd like to fix the implementation to make it usable.

Implementation details

This was driven almost entirely by 🤖, by comparing our existing implementation to the RFC, and also taking the agent/go implementation as the canonical implementation.

Implementing this in .NET highlighted a number of missing aspects in the RFC, which I've raised elsewhere, and aim to get incorporated into the RFC.

At a high level, the PR contains the following changes:

  • Stats Wire Format & Serialization
    • Added new aggregation dimensions: SpanKind, IsTraceRoot (as Trilean), HTTPMethod, HTTPEndpoint, GRPCStatusCode, ServiceSource, PeerTags to both the aggregation key and the msgpack wire format
    • Fixed GRPCStatusCode type: Changed from int to string to match Go agent's wire format (agent was returning 400 Bad Request in system tests)
    • gRPC status code extraction: Checks 4 tag names in priority order (rpc.grpc.status_code, grpc.code, rpc.grpc.status.code, grpc.status.code)
    • Stochastic rounding: Hits, Errors, Duration, TopLevelHits accumulated as double (weighted by sampling rate) then rounded probabilistically to int64 for serialization
    • Duration weighting: Durations are now multiplied by sampling weight (1/rate), matching Go agent behavior
    • Default env: Serializes "unknown-env" when environment is not configured
    • git_commit_sha: Added as optional field in stats payload
    • Service: Added as top-level field in stats payload
    • Empty bucket suppression: HasHits() check prevents sending payloads with zero-hit buckets (stale keys retained for sketch reuse)
  • Bucket Timing
    • 10-second alignment: Bucket Start timestamps aligned to 10-second boundaries (ts - ts % 10_000_000_000) matching Go tracer's alignTs
    • Removed unused StartTime property (only Start as aligned nanoseconds)
  • Agent Discovery (/info Endpoint)
    • peer_tags: Parsed, sorted, deduplicated; used for peer tag extraction on client/producer/consumer spans
    • span_kinds_stats_computed: Parsed to override eligible span kinds
    • obfuscation_version: Parsed for obfuscation negotiation
    • Trace filters: Parsed filter_tags, filter_tags_regex, ignore_resources from /info
  • Trace Filtering
    • TraceFilter implementation: Evaluates agent-configured filters (exact tags, regex tags, resource patterns) against root spans before stats computation
    • Tag-only filters: Handles filter entries that match on tag key presence without a specific value
  • SQL Obfuscation
    • Operator splitters: Added * / = < > ! & ^ % ~ ? @ : # as token splitters (matching Go agent's go-sqllexer isOperator()) so queries like WHERE id='1' are properly obfuscated
    • Whitespace normalization: Post-processing pass adds spaces around comparison operators (=, <, >, !) adjacent to ? placeholders, matching Go agent's normalizer output (e.g., id='1'id = ?)
    • Obfuscation gating: Only runs when obfuscation_version is negotiated with agent; sends Datadog-Obfuscation-Version header
  • Peer Tags
    • IP quantization: Peer tag values run through IpAddressObfuscationUtil.QuantizePeerIpAddresses() replacing non-allowed IPs with "blocked-ip-address"
    • Base service handling: Internal/missing-kind spans with non-default service name use _dd.base_service:{serviceName} as sole peer tag
    • FNV-1a hashing: Peer tags hashed with null-byte separators for aggregation key
  • Other
    • No retries for stats: Stats sends are fire-and-forget (retry limit = 0)
    • Synthetics detection: Uses StartsWith("synthetics") prefix matching (not exact match)
    • Mock agent updates: Test mock agent returns obfuscation_version in /info response

Test coverage

There's a lot going on in this PR, because we were so far behind. I could split this into implementing individual features, but there would be a lot of duplication between PRs, and it didn't seem like it would be that easy to track. At least with this big bang we can compare directly against the system tests etc.

The existing system tests for stats computation were checked, and made to pass (which identified a number of hidden expectations which will be added to the RFC). I'll create a PR to enable these in the system-tests repo

Other details

Part of a stack

There are still some theoretical gaps between the go implementation and the .NET implementation, but I think these are non-issues in most cases:

Area Gap Impact
gRPC status code normalization Go agent normalizes string statuses (e.g., "CANCELLED""1"); .NET passes raw tag value Stats mismatch if gRPC library uses string-form status codes
HTTP status code tags Go agent checks both http.status_code and http.response.status_code (OTel convention); .NET only checks http.status_code OTel spans using newer convention would get 0 in .NET
Duration precision truncation Go agent uses float bit masking; .NET uses integer shifting — both target ~10 bits but may produce slightly different values Minor histogram differences
SQL obfuscation Go agent uses full tokenizer + normalizer; .NET uses character-level splitter with targeted normalization around comparison operators only Complex SQL with unusual formatting may produce different resource strings
HTTP_method / HTTP_endpoint .NET always populates from span tags; Go agent only populates from newer OTel pipeline paths Creates different aggregation keys — Go groups all methods/routes together in default path
_top_level metric Go checks both _top_level and _dd.top_level; .NET only checks _dd.top_level Spans using older metric name would be missed by .NET
span_derived_primary_tags Still in Go agent code but implemented in v1.3.0 RFC, which was reverted; removed from .NET No current impact; may need to re-add if spec reverts back

There is another big elephant in the room, which is perf. The peer tags, in particular, currently requires a bunch of allocation. I'd rather defer trying to fight against that to another PR if possible, unless anyone has some clear ideas 😄

Another aspect I'm not sure about is how this interacts with @zacharycmontoya's recent work to publish OTLP stats. I took a random guess and fought the refactoring, but need to verify it.

@andrewlock andrewlock added the area:tracer The core tracer library (Datadog.Trace, does not include OpenTracing, native code, or integrations) label Apr 7, 2026
@andrewlock
andrewlock requested a review from a team as a code owner April 7, 2026 17:47
@andrewlock andrewlock added the AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos label Apr 7, 2026
@lucaspimentel lucaspimentel changed the title Implement Client Side Stats 1.2.0 Implement Client-Side Stats 1.2.0 Apr 7, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e52cc41a00

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tracer/src/Datadog.Trace/Agent/StatsAggregator.cs Outdated
Comment on lines +463 to +466
{
if (span.Context.ParentId is null or 0)
{
return !filter.ShouldKeepTrace(span);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply trace filters when chunks lack a parentless root span

Trace filtering only runs when a span has ParentId null/0, so chunks where every span has a non-zero parent (common in distributed traces with propagated context, or some partial chunks) bypass filter_tags/ignore_resources entirely. In those cases IsTraceFiltered returns false and filtered traces are kept and counted in stats, which defeats agent-configured filtering for a real production traffic pattern.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Interesting. We should probably do whatever the Go agent is doing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

So. This is where things get very interesting 😅 Will discuss this one offline I think 😄

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Mostly fixed this in #8436, though not strictly the same as the Go agent does. Plus this is going to bork with partial flushing (both sides)

@pr-commenter

pr-commenter Bot commented Apr 7, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-04-16 09:05:29

Comparing candidate commit e9ee48d in PR branch andrew/client-side-stats/client-side-stats-1-2 with baseline commit b176976 in branch master.

Found 0 performance improvements and 1 performance regressions! Performance is the same for 26 metrics, 0 unstable metrics, 87 known flaky benchmarks.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

scenario:Benchmarks.Trace.HttpClientBenchmark.SendAsync net472

  • 🟥 throughput [-5139.024op/s; -4940.670op/s] or [-5.867%; -5.640%]

Known flaky benchmarks

These benchmarks are marked as flaky and will not trigger a failure. Modify FLAKY_BENCHMARKS_REGEX to control which benchmarks are marked as flaky.

scenario:Benchmarks.Trace.ActivityBenchmark.StartStopWithChild net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.008%; +0.005%]
  • ignore execution_time [-1224.873µs; -272.358µs] or [-0.609%; -0.135%]
  • ignore throughput [-1513.353op/s; -901.947op/s] or [-1.794%; -1.069%]

scenario:Benchmarks.Trace.ActivityBenchmark.StartStopWithChild net6.0

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.006%; +0.007%]
  • ignore execution_time [-1825.930µs; +1678.206µs] or [-0.911%; +0.837%]
  • 🟩 throughput [+8136.061op/s; +10402.524op/s] or [+6.839%; +8.744%]

scenario:Benchmarks.Trace.ActivityBenchmark.StartStopWithChild netcoreapp3.1

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.003%; +0.007%]
  • ignore execution_time [+0.838ms; +3.000ms] or [+0.421%; +1.509%]
  • ignore throughput [-518.493op/s; +589.143op/s] or [-0.527%; +0.599%]

scenario:Benchmarks.Trace.AgentWriterBenchmark.WriteAndFlushEnrichedTraces net472

  • ignore allocated_mem [-20 bytes; -19 bytes] or [-0.613%; -0.600%]
  • 🟥 execution_time [+309.845ms; +311.867ms] or [+153.756%; +154.760%]
  • ignore throughput [+4.375op/s; +8.266op/s] or [+0.787%; +1.487%]

scenario:Benchmarks.Trace.AgentWriterBenchmark.WriteAndFlushEnrichedTraces net6.0

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.009%; +0.002%]
  • 🟥 execution_time [+381.729ms; +384.167ms] or [+301.589%; +303.516%]
  • ignore throughput [+12.155op/s; +15.824op/s] or [+1.603%; +2.086%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.009%; +0.002%]
  • 🟥 execution_time [+395.526ms; +398.934ms] or [+350.025%; +353.041%]
  • ignore throughput [+2.414op/s; +5.265op/s] or [+0.341%; +0.744%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.AllCycleMoreComplexBody net472

  • 🟥 allocated_mem [+1.308KB; +1.308KB] or [+27.529%; +27.541%]
  • ignore execution_time [+178.896µs; +805.792µs] or [+0.089%; +0.403%]
  • ignore throughput [-4592.248op/s; -4139.921op/s] or [-3.573%; -3.221%]

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

  • 🟥 allocated_mem [+471 bytes; +472 bytes] or [+9.977%; +9.987%]
  • 🟩 execution_time [-16.349ms; -12.187ms] or [-7.636%; -5.692%]
  • ignore throughput [+5463.907op/s; +8244.610op/s] or [+3.988%; +6.018%]

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

  • 🟥 allocated_mem [+1.272KB; +1.272KB] or [+27.502%; +27.510%]
  • ignore execution_time [-12.300ms; -8.103ms] or [-5.857%; -3.859%]
  • ignore throughput [-1291.524op/s; +1027.675op/s] or [-1.168%; +0.929%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.AllCycleSimpleBody net472

  • 🟥 allocated_mem [+1.307KB; +1.307KB] or [+105.746%; +105.759%]
  • ignore execution_time [-926.194µs; -368.057µs] or [-0.461%; -0.183%]
  • 🟥 throughput [-251248.764op/s; -248218.475op/s] or [-25.654%; -25.344%]

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

  • 🟥 allocated_mem [+471 bytes; +472 bytes] or [+38.558%; +38.566%]
  • 🟩 execution_time [-26.882ms; -21.983ms] or [-11.988%; -9.803%]
  • ignore throughput [-55506.009op/s; -32740.824op/s] or [-5.930%; -3.498%]

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

  • 🟥 allocated_mem [+1.272KB; +1.272KB] or [+105.292%; +105.304%]
  • ignore execution_time [-3.572ms; +0.766ms] or [-1.783%; +0.382%]
  • 🟥 throughput [-135846.227op/s; -119671.669op/s] or [-19.518%; -17.195%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.ObjectExtractorMoreComplexBody net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.007%; +0.003%]
  • ignore execution_time [-1334.578µs; -304.312µs] or [-0.665%; -0.152%]
  • ignore throughput [-164.317op/s; +689.581op/s] or [-0.111%; +0.464%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.004%; +0.003%]
  • ignore execution_time [-0.133ms; +3.340ms] or [-0.067%; +1.685%]
  • 🟩 throughput [+10326.218op/s; +13260.766op/s] or [+6.570%; +8.438%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.007%; +0.003%]
  • ignore execution_time [-9.141ms; +0.727ms] or [-4.660%; +0.370%]
  • 🟩 throughput [+8340.703op/s; +11026.376op/s] or [+6.644%; +8.784%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.ObjectExtractorSimpleBody net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.008%; +0.004%]
  • ignore execution_time [-147.000µs; +337.751µs] or [-0.073%; +0.169%]
  • ignore throughput [+75127.443op/s; +83906.988op/s] or [+2.285%; +2.553%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.006%; +0.008%]
  • ignore execution_time [-2.673ms; -2.002ms] or [-1.321%; -0.990%]
  • 🟩 throughput [+496010.835op/s; +512933.539op/s] or [+16.539%; +17.103%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.004%; +0.004%]
  • 🟩 execution_time [-18.999ms; -14.621ms] or [-8.758%; -6.740%]
  • 🟩 throughput [+210575.538op/s; +264672.637op/s] or [+8.358%; +10.506%]

scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeArgs net472

  • ignore allocated_mem [+0 bytes; +2 bytes] or [-0.001%; +0.007%]
  • 🟥 execution_time [+299.579ms; +300.103ms] or [+149.690%; +149.951%]
  • ignore throughput [+97.778op/s; +124.973op/s] or [+1.080%; +1.380%]

scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeArgs net6.0

  • ignore allocated_mem [-1 bytes; +2 bytes] or [-0.004%; +0.008%]
  • 🟥 execution_time [+299.096ms; +302.406ms] or [+150.835%; +152.504%]
  • ignore throughput [+227.962op/s; +446.088op/s] or [+1.744%; +3.412%]

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

  • ignore allocated_mem [-1 bytes; +2 bytes] or [-0.004%; +0.008%]
  • 🟥 execution_time [+299.753ms; +302.258ms] or [+150.992%; +152.254%]
  • ignore throughput [+177.170op/s; +305.090op/s] or [+1.710%; +2.945%]

scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeLegacyArgs net472

  • ignore allocated_mem [+3 bytes; +4 bytes] or [+0.186%; +0.199%]
  • 🟥 execution_time [+297.175ms; +297.787ms] or [+145.961%; +146.261%]
  • ignore throughput [+8.702op/s; +14.740op/s] or [+0.231%; +0.391%]

scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeLegacyArgs net6.0

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.004%; +0.009%]
  • 🟥 execution_time [+294.038ms; +296.845ms] or [+143.744%; +145.116%]
  • ignore throughput [+156.447op/s; +204.213op/s] or [+2.273%; +2.967%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.004%; +0.009%]
  • 🟥 execution_time [+299.539ms; +300.795ms] or [+149.709%; +150.337%]
  • ignore throughput [+55.466op/s; +78.063op/s] or [+1.101%; +1.549%]

scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmark net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [+nan%; +nan%]
  • ignore execution_time [+2.358µs; +5.970µs] or [+0.484%; +1.226%]
  • ignore throughput [-24.676op/s; -9.714op/s] or [-1.202%; -0.473%]

scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmark net6.0

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.000%; +0.010%]
  • ignore execution_time [+13.510µs; +40.089µs] or [+3.099%; +9.194%]
  • ignore throughput [-199.064op/s; -79.467op/s] or [-8.655%; -3.455%]

scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmark netcoreapp3.1

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.003%; +0.003%]
  • ignore execution_time [+7.717µs; +29.697µs] or [+1.653%; +6.363%]
  • ignore throughput [-146.487op/s; -65.644op/s] or [-6.762%; -3.030%]

scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmarkWithAttack net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [+nan%; +nan%]
  • ignore execution_time [-7.055µs; -3.339µs] or [-1.905%; -0.901%]
  • ignore throughput [+25.183op/s; +52.010op/s] or [+0.933%; +1.926%]

scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmarkWithAttack net6.0

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.003%; +0.007%]
  • 🟥 execution_time [+20.994µs; +44.556µs] or [+6.702%; +14.224%]
  • 🟥 throughput [-416.622op/s; -218.321op/s] or [-12.987%; -6.806%]

scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmarkWithAttack netcoreapp3.1

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.003%; +0.003%]
  • ignore execution_time [-14.799µs; +7.597µs] or [-4.048%; +2.078%]
  • ignore throughput [-90.107op/s; +43.639op/s] or [-3.234%; +1.566%]

scenario:Benchmarks.Trace.AspNetCoreBenchmark.SendRequest net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [+nan%; +nan%]
  • 🟥 execution_time [+299.569ms; +300.224ms] or [+149.516%; +149.842%]
  • ignore throughput [-1476927.735op/s; -901777.785op/s] or [-0.740%; -0.452%]

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

  • ignore allocated_mem [+75 bytes; +77 bytes] or [+0.419%; +0.431%]
  • unstable execution_time [+300.305ms; +346.208ms] or [+326.294%; +376.169%]
  • 🟩 throughput [+1169.646op/s; +1315.147op/s] or [+9.611%; +10.807%]

scenario:Benchmarks.Trace.AspNetCoreBenchmark.SendRequest netcoreapp3.1

  • ignore allocated_mem [+20 bytes; +22 bytes] or [+0.099%; +0.110%]
  • unstable execution_time [+339.059ms; +361.169ms] or [+257.444%; +274.232%]
  • 🟩 throughput [+681.280op/s; +884.688op/s] or [+6.595%; +8.564%]

scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces net472

  • 🟥 allocated_mem [+2.837KB; +2.842KB] or [+5.040%; +5.049%]
  • unstable execution_time [+344.347ms; +411.898ms] or [+158.328%; +189.387%]
  • 🟥 throughput [-542.396op/s; -472.771op/s] or [-49.146%; -42.838%]

scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces net6.0

  • ignore allocated_mem [-1.270KB; -1.268KB] or [-2.995%; -2.990%]
  • unstable execution_time [+199.739ms; +332.938ms] or [+85.121%; +141.884%]
  • 🟥 throughput [-742.512op/s; -659.099op/s] or [-49.526%; -43.962%]

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

  • 🟥 allocated_mem [+2.226KB; +2.229KB] or [+5.255%; +5.262%]
  • 🟥 execution_time [+346.868ms; +355.414ms] or [+207.468%; +212.579%]
  • 🟥 throughput [-415.903op/s; -380.034op/s] or [-28.959%; -26.461%]

scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSlice net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [+nan%; +nan%]
  • ignore execution_time [-67.298µs; -33.799µs] or [-3.387%; -1.701%]
  • ignore throughput [+9.877op/s; +18.223op/s] or [+1.963%; +3.621%]

scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSlice net6.0

  • ignore allocated_mem [+0 bytes; +0 bytes] or [+nan%; +nan%]
  • ignore execution_time [-58.664µs; -45.988µs] or [-4.030%; -3.159%]
  • ignore throughput [+22.760op/s; +29.092op/s] or [+3.313%; +4.235%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [+nan%; +nan%]
  • ignore execution_time [-260.842µs; -143.156µs] or [-9.074%; -4.980%]
  • 🟩 throughput [+20.760op/s; +46.156op/s] or [+5.967%; +13.267%]

scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSliceWithPool net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [+nan%; +nan%]
  • ignore execution_time [-4766.738ns; +3441.404ns] or [-0.412%; +0.297%]
  • ignore throughput [-2.305op/s; +3.761op/s] or [-0.267%; +0.435%]

scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSliceWithPool net6.0

  • ignore allocated_mem [+0 bytes; +0 bytes] or [+nan%; +nan%]
  • ignore execution_time [-18.545µs; -5.202µs] or [-1.720%; -0.482%]
  • ignore throughput [+5.409op/s; +16.847op/s] or [+0.583%; +1.816%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [+nan%; +nan%]
  • ignore execution_time [+14.470µs; +24.443µs] or [+0.775%; +1.310%]
  • ignore throughput [-6.897op/s; -4.071op/s] or [-1.287%; -0.760%]

scenario:Benchmarks.Trace.CharSliceBenchmark.OriginalCharSlice net472

  • ignore allocated_mem [-43 bytes; +21 bytes] or [-0.007%; +0.003%]
  • ignore execution_time [+45.618µs; +55.941µs] or [+1.782%; +2.185%]
  • ignore throughput [-8.317op/s; -6.808op/s] or [-2.129%; -1.743%]

scenario:Benchmarks.Trace.CharSliceBenchmark.OriginalCharSlice net6.0

  • ignore allocated_mem [-38 bytes; +46 bytes] or [-0.006%; +0.007%]
  • 🟩 execution_time [-159.264µs; -125.125µs] or [-8.068%; -6.338%]
  • 🟩 throughput [+35.883op/s; +44.902op/s] or [+7.084%; +8.864%]

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

  • ignore allocated_mem [-42 bytes; +23 bytes] or [-0.007%; +0.004%]
  • ignore execution_time [-128.336µs; -88.792µs] or [-3.254%; -2.252%]
  • ignore throughput [+6.091op/s; +8.596op/s] or [+2.402%; +3.390%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearch net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.001%; +0.008%]
  • 🟥 execution_time [+303.574ms; +305.356ms] or [+152.874%; +153.772%]
  • ignore throughput [-706.655op/s; +708.505op/s] or [-0.227%; +0.228%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.006%; +0.004%]
  • 🟥 execution_time [+300.431ms; +302.510ms] or [+150.547%; +151.589%]
  • ignore throughput [+23222.063op/s; +27212.326op/s] or [+3.661%; +4.290%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.006%; +0.004%]
  • 🟥 execution_time [+302.338ms; +305.655ms] or [+151.882%; +153.548%]
  • ignore throughput [+9211.011op/s; +16929.252op/s] or [+1.940%; +3.566%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearchAsync net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.008%; +0.004%]
  • 🟥 execution_time [+300.724ms; +302.512ms] or [+151.013%; +151.911%]
  • ignore throughput [+10113.287op/s; +11847.433op/s] or [+3.388%; +3.969%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.007%; +0.006%]
  • 🟥 execution_time [+296.264ms; +298.141ms] or [+146.489%; +147.417%]
  • ignore throughput [+16004.256op/s; +22114.776op/s] or [+2.579%; +3.563%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.006%; +0.004%]
  • 🟥 execution_time [+304.254ms; +308.110ms] or [+154.209%; +156.163%]
  • ignore throughput [-7247.874op/s; +1563.413op/s] or [-1.565%; +0.338%]

scenario:Benchmarks.Trace.GraphQLBenchmark.ExecuteAsync net472

  • ignore allocated_mem [+0 bytes; +1 bytes] or [+0.108%; +0.119%]
  • 🟥 execution_time [+299.949ms; +301.492ms] or [+150.548%; +151.322%]
  • ignore throughput [-15339.539op/s; -13711.619op/s] or [-3.979%; -3.557%]

scenario:Benchmarks.Trace.GraphQLBenchmark.ExecuteAsync net6.0

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.006%; +0.006%]
  • 🟥 execution_time [+301.220ms; +303.170ms] or [+150.131%; +151.102%]
  • 🟩 throughput [+54957.216op/s; +59848.885op/s] or [+10.913%; +11.884%]

scenario:Benchmarks.Trace.GraphQLBenchmark.ExecuteAsync netcoreapp3.1

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.006%; +0.006%]
  • 🟥 execution_time [+298.575ms; +301.339ms] or [+148.538%; +149.914%]
  • ignore throughput [-8696.390op/s; -3173.996op/s] or [-2.058%; -0.751%]

scenario:Benchmarks.Trace.ILoggerBenchmark.EnrichedLog net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.003%; +0.006%]
  • ignore execution_time [-1095.587µs; -217.875µs] or [-0.545%; -0.108%]
  • ignore throughput [-8834.711op/s; -7651.443op/s] or [-3.553%; -3.077%]

scenario:Benchmarks.Trace.ILoggerBenchmark.EnrichedLog net6.0

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.005%; +0.003%]
  • 🟩 execution_time [-16.023ms; -12.222ms] or [-7.451%; -5.683%]
  • 🟩 throughput [+23371.572op/s; +30173.947op/s] or [+6.411%; +8.278%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.004%; +0.008%]
  • ignore execution_time [-0.498ms; +3.503ms] or [-0.250%; +1.757%]
  • ignore throughput [+6178.349op/s; +11892.885op/s] or [+2.255%; +4.341%]

scenario:Benchmarks.Trace.Iast.StringAspectsBenchmark.StringConcatAspectBenchmark net472

  • ignore allocated_mem [-4.459KB; -4.431KB] or [-1.623%; -1.613%]
  • ignore execution_time [+6.768µs; +47.100µs] or [+1.672%; +11.634%]
  • ignore throughput [-247.689op/s; -42.918op/s] or [-9.967%; -1.727%]

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

  • 🟩 allocated_mem [-18.996KB; -18.975KB] or [-6.929%; -6.922%]
  • unstable execution_time [-50.931µs; +1.277µs] or [-10.066%; +0.252%]
  • ignore throughput [+7.903op/s; +190.143op/s] or [+0.394%; +9.488%]

scenario:Benchmarks.Trace.Iast.StringAspectsBenchmark.StringConcatAspectBenchmark netcoreapp3.1

  • 🟩 allocated_mem [-16.471KB; -16.455KB] or [-6.005%; -5.998%]
  • ignore execution_time [-49.480µs; +7.625µs] or [-8.575%; +1.321%]
  • ignore throughput [-8.934op/s; +146.252op/s] or [-0.510%; +8.356%]

scenario:Benchmarks.Trace.Iast.StringAspectsBenchmark.StringConcatBenchmark net472

  • ignore allocated_mem [-2 bytes; +2 bytes] or [-0.005%; +0.006%]
  • ignore execution_time [+177.474ns; +1637.192ns] or [+0.307%; +2.836%]
  • ignore throughput [-460.762op/s; -48.260op/s] or [-2.659%; -0.278%]

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

  • ignore allocated_mem [-4 bytes; +0 bytes] or [-0.010%; -0.001%]
  • 🟥 execution_time [+5.687µs; +9.672µs] or [+13.442%; +22.861%]
  • 🟥 throughput [-4452.615op/s; -2702.673op/s] or [-18.744%; -11.377%]

scenario:Benchmarks.Trace.Iast.StringAspectsBenchmark.StringConcatBenchmark netcoreapp3.1

  • ignore allocated_mem [-1 bytes; +1 bytes] or [-0.002%; +0.002%]
  • unstable execution_time [-13.324µs; -5.994µs] or [-20.672%; -9.300%]
  • 🟩 throughput [+1535.782op/s; +3101.702op/s] or [+9.423%; +19.030%]

scenario:Benchmarks.Trace.Log4netBenchmark.EnrichedLog net472

  • ignore allocated_mem [+1 bytes; +2 bytes] or [+0.039%; +0.050%]
  • 🟥 execution_time [+301.975ms; +302.974ms] or [+152.635%; +153.140%]
  • ignore throughput [-120.195op/s; -99.578op/s] or [-2.008%; -1.664%]

scenario:Benchmarks.Trace.Log4netBenchmark.EnrichedLog net6.0

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.005%; +0.005%]
  • 🟥 execution_time [+302.891ms; +305.152ms] or [+154.171%; +155.321%]
  • ignore throughput [-101.478op/s; -24.458op/s] or [-1.259%; -0.303%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.005%; +0.005%]
  • 🟥 execution_time [+300.784ms; +302.928ms] or [+150.579%; +151.653%]
  • ignore throughput [-204.910op/s; -139.933op/s] or [-2.610%; -1.783%]

scenario:Benchmarks.Trace.RedisBenchmark.SendReceive net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.005%; +0.005%]
  • ignore execution_time [-431.040µs; +281.804µs] or [-0.215%; +0.140%]
  • 🟥 throughput [-25216.768op/s; -23803.905op/s] or [-6.981%; -6.590%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.004%; +0.007%]
  • ignore execution_time [-339.877µs; +255.923µs] or [-0.170%; +0.128%]
  • 🟩 throughput [+53663.458op/s; +56223.230op/s] or [+10.157%; +10.642%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.004%; +0.006%]
  • ignore execution_time [+0.956ms; +4.624ms] or [+0.485%; +2.344%]
  • ignore throughput [-6978.015op/s; +1547.574op/s] or [-1.651%; +0.366%]

scenario:Benchmarks.Trace.SerilogBenchmark.EnrichedLog net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.005%; +0.006%]
  • 🟥 execution_time [+300.323ms; +302.014ms] or [+149.684%; +150.527%]
  • ignore throughput [-7924.998op/s; -6948.465op/s] or [-5.233%; -4.588%]

scenario:Benchmarks.Trace.SerilogBenchmark.EnrichedLog net6.0

  • ignore allocated_mem [+0 bytes; +0 bytes] or [+0.000%; +0.009%]
  • 🟥 execution_time [+300.786ms; +301.902ms] or [+151.040%; +151.601%]
  • ignore throughput [+3689.217op/s; +5804.692op/s] or [+1.604%; +2.524%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.004%; +0.003%]
  • 🟥 execution_time [+304.109ms; +306.756ms] or [+154.225%; +155.567%]
  • ignore throughput [-1925.090op/s; -11.770op/s] or [-1.084%; -0.007%]

scenario:Benchmarks.Trace.SingleSpanAspNetCoreBenchmark.SingleSpanAspNetCore net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [+nan%; +nan%]
  • 🟥 execution_time [+300.104ms; +300.823ms] or [+149.694%; +150.052%]
  • 🟩 throughput [+61114833.385op/s; +61379888.185op/s] or [+44.508%; +44.701%]

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

  • ignore allocated_mem [+80 bytes; +82 bytes] or [+0.472%; +0.481%]
  • unstable execution_time [+346.037ms; +391.157ms] or [+430.359%; +486.473%]
  • 🟩 throughput [+947.577op/s; +1131.212op/s] or [+7.325%; +8.745%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [+nan%; +nan%]
  • 🟥 execution_time [+299.177ms; +300.070ms] or [+149.223%; +149.668%]
  • ignore throughput [+1699849.421op/s; +2635155.955op/s] or [+0.753%; +1.167%]

scenario:Benchmarks.Trace.SpanBenchmark.StartFinishScope net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.005%; +0.006%]
  • ignore execution_time [-134.360µs; +260.217µs] or [-0.067%; +0.130%]
  • ignore throughput [+18392.851op/s; +21443.877op/s] or [+2.053%; +2.393%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.005%; +0.007%]
  • ignore execution_time [-4.813ms; -3.700ms] or [-2.357%; -1.812%]
  • 🟩 throughput [+102064.376op/s; +108826.825op/s] or [+9.529%; +10.161%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.003%; +0.006%]
  • ignore execution_time [+2.975ms; +7.199ms] or [+1.506%; +3.643%]
  • 🟩 throughput [+52733.190op/s; +72114.383op/s] or [+6.104%; +8.347%]

scenario:Benchmarks.Trace.SpanBenchmark.StartFinishSpan net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.000%; +0.009%]
  • ignore execution_time [-98.406µs; +420.617µs] or [-0.049%; +0.210%]
  • ignore throughput [-1007.115op/s; +1885.592op/s] or [-0.092%; +0.173%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.004%; +0.004%]
  • ignore execution_time [+5.152ms; +14.362ms] or [+2.684%; +7.482%]
  • ignore throughput [+64354.815op/s; +105074.867op/s] or [+4.981%; +8.133%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.001%; +0.008%]
  • ignore execution_time [-4.663ms; -3.181ms] or [-2.291%; -1.563%]
  • 🟩 throughput [+82822.330op/s; +91335.899op/s] or [+8.226%; +9.071%]

scenario:Benchmarks.Trace.SpanBenchmark.StartFinishTwoScopes net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.008%; +0.002%]
  • ignore execution_time [-2.766ms; -1.240ms] or [-1.377%; -0.617%]
  • ignore throughput [+14572.538op/s; +17702.225op/s] or [+3.247%; +3.944%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.004%; +0.009%]
  • ignore execution_time [-2.088ms; -0.512ms] or [-1.043%; -0.256%]
  • 🟩 throughput [+46839.540op/s; +54973.266op/s] or [+8.505%; +9.982%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.006%; +0.004%]
  • ignore execution_time [-0.933ms; +3.157ms] or [-0.469%; +1.586%]
  • ignore throughput [+18965.750op/s; +28716.847op/s] or [+4.245%; +6.428%]

scenario:Benchmarks.Trace.TraceAnnotationsBenchmark.RunOnMethodBegin net472

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.005%; +0.006%]
  • ignore execution_time [-588.651µs; +565.859µs] or [-0.293%; +0.282%]
  • ignore throughput [-10987.220op/s; -7309.312op/s] or [-1.608%; -1.070%]

scenario:Benchmarks.Trace.TraceAnnotationsBenchmark.RunOnMethodBegin net6.0

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.005%; +0.007%]
  • ignore execution_time [-1133.312µs; +2366.921µs] or [-0.567%; +1.184%]
  • 🟩 throughput [+59284.513op/s; +77724.472op/s] or [+6.624%; +8.684%]

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

  • ignore allocated_mem [+0 bytes; +0 bytes] or [-0.005%; +0.005%]
  • ignore execution_time [+1.372ms; +5.277ms] or [+0.697%; +2.680%]
  • ignore throughput [+28130.356op/s; +43024.488op/s] or [+3.928%; +6.007%]

@dd-trace-dotnet-ci-bot

dd-trace-dotnet-ci-bot Bot commented Apr 7, 2026

Copy link
Copy Markdown

Execution-Time Benchmarks Report ⏱️

Execution-time results for samples comparing This PR (8420) 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
duration72.00 ± (71.93 - 72.25) ms71.96 ± (71.89 - 72.21) ms-0.0%
.NET Framework 4.8 - Bailout
duration75.72 ± (75.64 - 76.02) ms75.38 ± (75.30 - 75.62) ms-0.4%
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1069.15 ± (1071.14 - 1080.01) ms1061.71 ± (1062.61 - 1068.35) ms-0.7%
.NET Core 3.1 - Baseline
process.internal_duration_ms22.29 ± (22.26 - 22.32) ms22.33 ± (22.29 - 22.37) ms+0.2%✅⬆️
process.time_to_main_ms83.82 ± (83.62 - 84.01) ms82.78 ± (82.59 - 82.98) ms-1.2%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.92 ± (10.92 - 10.93) MB10.92 ± (10.91 - 10.92) MB-0.1%
runtime.dotnet.threads.count12 ± (12 - 12)12 ± (12 - 12)+0.0%
.NET Core 3.1 - Bailout
process.internal_duration_ms22.31 ± (22.27 - 22.34) ms22.25 ± (22.22 - 22.28) ms-0.3%
process.time_to_main_ms85.12 ± (84.96 - 85.29) ms84.74 ± (84.55 - 84.93) ms-0.5%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.96 ± (10.96 - 10.97) MB10.94 ± (10.94 - 10.95) MB-0.2%
runtime.dotnet.threads.count13 ± (13 - 13)13 ± (13 - 13)+0.0%
.NET Core 3.1 - CallTarget+Inlining+NGEN
process.internal_duration_ms227.08 ± (225.83 - 228.34) ms226.47 ± (225.16 - 227.77) ms-0.3%
process.time_to_main_ms518.37 ± (517.13 - 519.61) ms520.14 ± (519.08 - 521.19) ms+0.3%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed48.42 ± (48.39 - 48.45) MB48.45 ± (48.41 - 48.48) MB+0.1%✅⬆️
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)+0.0%
.NET 6 - Baseline
process.internal_duration_ms21.04 ± (21.00 - 21.08) ms21.02 ± (20.99 - 21.05) ms-0.1%
process.time_to_main_ms72.40 ± (72.24 - 72.56) ms72.55 ± (72.38 - 72.73) ms+0.2%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.63 ± (10.63 - 10.63) MB10.64 ± (10.64 - 10.64) MB+0.1%✅⬆️
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 6 - Bailout
process.internal_duration_ms21.00 ± (20.97 - 21.04) ms20.81 ± (20.77 - 20.85) ms-0.9%
process.time_to_main_ms73.79 ± (73.63 - 73.95) ms72.78 ± (72.64 - 72.91) ms-1.4%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.68 ± (10.68 - 10.69) MB10.75 ± (10.74 - 10.75) MB+0.6%✅⬆️
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms385.43 ± (383.54 - 387.32) ms386.67 ± (384.07 - 389.26) ms+0.3%✅⬆️
process.time_to_main_ms517.75 ± (516.81 - 518.69) ms517.52 ± (516.61 - 518.43) ms-0.0%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed50.08 ± (50.05 - 50.11) MB50.12 ± (50.09 - 50.15) MB+0.1%✅⬆️
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)-0.0%
.NET 8 - Baseline
process.internal_duration_ms19.18 ± (19.14 - 19.21) ms19.14 ± (19.11 - 19.16) ms-0.2%
process.time_to_main_ms71.25 ± (71.08 - 71.42) ms71.02 ± (70.86 - 71.18) ms-0.3%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.69 ± (7.68 - 7.70) MB7.67 ± (7.66 - 7.67) MB-0.3%
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 8 - Bailout
process.internal_duration_ms19.17 ± (19.13 - 19.20) ms19.19 ± (19.15 - 19.22) ms+0.1%✅⬆️
process.time_to_main_ms72.34 ± (72.21 - 72.48) ms72.21 ± (72.06 - 72.36) ms-0.2%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.73 ± (7.72 - 7.74) MB7.74 ± (7.73 - 7.75) MB+0.1%✅⬆️
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms306.20 ± (303.93 - 308.48) ms304.72 ± (302.42 - 307.01) ms-0.5%
process.time_to_main_ms478.84 ± (477.97 - 479.70) ms478.67 ± (477.92 - 479.42) ms-0.0%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed37.11 ± (37.09 - 37.14) MB37.38 ± (37.32 - 37.45) MB+0.7%✅⬆️
runtime.dotnet.threads.count27 ± (27 - 27)27 ± (27 - 27)-0.5%

HttpMessageHandler

Metric Master (Mean ± 95% CI) Current (Mean ± 95% CI) Change Status
.NET Framework 4.8 - Baseline
duration190.29 ± (190.30 - 190.99) ms190.37 ± (190.24 - 191.07) ms+0.0%✅⬆️
.NET Framework 4.8 - Bailout
duration193.46 ± (193.52 - 193.92) ms193.73 ± (193.60 - 194.06) ms+0.1%✅⬆️
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1139.43 ± (1138.81 - 1144.75) ms1139.97 ± (1140.83 - 1147.26) ms+0.0%✅⬆️
.NET Core 3.1 - Baseline
process.internal_duration_ms185.20 ± (184.92 - 185.49) ms184.74 ± (184.40 - 185.08) ms-0.2%
process.time_to_main_ms79.54 ± (79.37 - 79.70) ms79.81 ± (79.64 - 79.98) ms+0.3%✅⬆️
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed16.18 ± (16.16 - 16.21) MB16.17 ± (16.14 - 16.20) MB-0.1%
runtime.dotnet.threads.count20 ± (20 - 20)20 ± (20 - 20)+0.4%✅⬆️
.NET Core 3.1 - Bailout
process.internal_duration_ms184.57 ± (184.34 - 184.81) ms184.32 ± (184.12 - 184.53) ms-0.1%
process.time_to_main_ms80.97 ± (80.86 - 81.09) ms80.96 ± (80.84 - 81.08) ms-0.0%
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed16.16 ± (16.07 - 16.26) MB16.13 ± (16.05 - 16.22) MB-0.2%
runtime.dotnet.threads.count21 ± (20 - 21)21 ± (20 - 21)-0.1%
.NET Core 3.1 - CallTarget+Inlining+NGEN
process.internal_duration_ms392.10 ± (390.63 - 393.58) ms392.31 ± (390.96 - 393.66) ms+0.1%✅⬆️
process.time_to_main_ms504.22 ± (503.13 - 505.31) ms506.36 ± (505.17 - 507.55) ms+0.4%✅⬆️
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed58.64 ± (58.42 - 58.86) MB58.65 ± (58.45 - 58.84) MB+0.0%✅⬆️
runtime.dotnet.threads.count30 ± (30 - 30)30 ± (30 - 30)+0.1%✅⬆️
.NET 6 - Baseline
process.internal_duration_ms189.67 ± (189.36 - 189.98) ms190.29 ± (189.96 - 190.63) ms+0.3%✅⬆️
process.time_to_main_ms69.29 ± (69.14 - 69.45) ms69.85 ± (69.69 - 70.01) ms+0.8%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed15.86 ± (15.68 - 16.03) MB16.17 ± (16.02 - 16.31) MB+1.9%✅⬆️
runtime.dotnet.threads.count18 ± (18 - 18)18 ± (18 - 18)+1.3%✅⬆️
.NET 6 - Bailout
process.internal_duration_ms188.26 ± (188.11 - 188.41) ms192.17 ± (191.73 - 192.61) ms+2.1%✅⬆️
process.time_to_main_ms70.24 ± (70.19 - 70.30) ms71.57 ± (71.45 - 71.69) ms+1.9%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed16.00 ± (15.83 - 16.17) MB16.21 ± (16.06 - 16.35) MB+1.3%✅⬆️
runtime.dotnet.threads.count19 ± (18 - 19)20 ± (20 - 20)+5.4%✅⬆️
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms594.85 ± (591.84 - 597.87) ms599.09 ± (596.43 - 601.75) ms+0.7%✅⬆️
process.time_to_main_ms508.09 ± (507.18 - 508.99) ms518.16 ± (516.96 - 519.37) ms+2.0%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed61.67 ± (61.58 - 61.77) MB61.69 ± (61.58 - 61.80) MB+0.0%✅⬆️
runtime.dotnet.threads.count30 ± (30 - 30)30 ± (30 - 30)+0.2%✅⬆️
.NET 8 - Baseline
process.internal_duration_ms185.99 ± (185.74 - 186.25) ms198.08 ± (197.66 - 198.51) ms+6.5%✅⬆️
process.time_to_main_ms68.37 ± (68.25 - 68.49) ms73.17 ± (72.93 - 73.40) ms+7.0%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.72 ± (11.65 - 11.79) MB11.73 ± (11.71 - 11.75) MB+0.1%✅⬆️
runtime.dotnet.threads.count18 ± (18 - 18)18 ± (18 - 18)+1.7%✅⬆️
.NET 8 - Bailout
process.internal_duration_ms185.71 ± (185.49 - 185.92) ms196.86 ± (196.43 - 197.29) ms+6.0%✅⬆️
process.time_to_main_ms69.62 ± (69.56 - 69.69) ms73.64 ± (73.44 - 73.85) ms+5.8%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.67 ± (11.58 - 11.77) MB11.75 ± (11.73 - 11.78) MB+0.7%✅⬆️
runtime.dotnet.threads.count19 ± (18 - 19)19 ± (19 - 19)+4.1%✅⬆️
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms521.08 ± (518.52 - 523.65) ms522.39 ± (518.94 - 525.84) ms+0.3%✅⬆️
process.time_to_main_ms466.62 ± (465.98 - 467.26) ms490.98 ± (489.59 - 492.38) ms+5.2%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed50.75 ± (50.72 - 50.78) MB50.84 ± (50.79 - 50.89) MB+0.2%✅⬆️
runtime.dotnet.threads.count30 ± (30 - 30)30 ± (30 - 30)+0.4%✅⬆️
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 (8420) - mean (72ms)  : 70, 74
    master - mean (72ms)  : 70, 74

    section Bailout
    This PR (8420) - mean (75ms)  : 74, 77
    master - mean (76ms)  : 74, 78

    section CallTarget+Inlining+NGEN
    This PR (8420) - mean (1,065ms)  : 1024, 1107
    master - mean (1,076ms)  : 1010, 1141

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 (8420) - mean (112ms)  : 109, 115
    master - mean (114ms)  : 110, 117

    section Bailout
    This PR (8420) - mean (114ms)  : 111, 116
    master - mean (115ms)  : 112, 118

    section CallTarget+Inlining+NGEN
    This PR (8420) - mean (786ms)  : 763, 809
    master - mean (784ms)  : 764, 805

Loading
FakeDbCommand (.NET 6)
gantt
    title Execution time (ms) FakeDbCommand (.NET 6)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8420) - mean (100ms)  : 96, 103
    master - mean (100ms)  : 96, 103

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

    section CallTarget+Inlining+NGEN
    This PR (8420) - mean (931ms)  : 893, 969
    master - mean (932ms)  : 899, 964

Loading
FakeDbCommand (.NET 8)
gantt
    title Execution time (ms) FakeDbCommand (.NET 8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8420) - mean (98ms)  : 95, 100
    master - mean (98ms)  : 94, 103

    section Bailout
    This PR (8420) - mean (99ms)  : 97, 101
    master - mean (99ms)  : 97, 101

    section CallTarget+Inlining+NGEN
    This PR (8420) - mean (813ms)  : 779, 847
    master - mean (814ms)  : 781, 848

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 (8420) - mean (191ms)  : 187, 195
    master - mean (191ms)  : 187, 194

    section Bailout
    This PR (8420) - mean (194ms)  : 192, 196
    master - mean (194ms)  : 192, 196

    section CallTarget+Inlining+NGEN
    This PR (8420) - mean (1,144ms)  : 1096, 1192
    master - mean (1,142ms)  : 1100, 1184

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 (8420) - mean (273ms)  : 268, 277
    master - mean (273ms)  : 269, 277

    section Bailout
    This PR (8420) - mean (273ms)  : 271, 276
    master - mean (273ms)  : 270, 276

    section CallTarget+Inlining+NGEN
    This PR (8420) - mean (926ms)  : 906, 946
    master - mean (924ms)  : 895, 953

Loading
HttpMessageHandler (.NET 6)
gantt
    title Execution time (ms) HttpMessageHandler (.NET 6)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8420) - mean (268ms)  : 262, 275
    master - mean (267ms)  : 262, 272

    section Bailout
    This PR (8420) - mean (272ms)  : 267, 277
    master - mean (266ms)  : 264, 268

    section CallTarget+Inlining+NGEN
    This PR (8420) - mean (1,146ms)  : 1115, 1177
    master - mean (1,133ms)  : 1090, 1176

Loading
HttpMessageHandler (.NET 8)
gantt
    title Execution time (ms) HttpMessageHandler (.NET 8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8420) - mean (281ms)  : 274, 289
    master - mean (264ms)  : 261, 267

    section Bailout
    This PR (8420) - mean (280ms)  : crit, 273, 288
    master - mean (265ms)  : 261, 268

    section CallTarget+Inlining+NGEN
    This PR (8420) - mean (1,045ms)  : 996, 1094
    master - mean (1,019ms)  : 980, 1058

Loading

spanMetaStructs: spanMetaStructs,
spanEvents: spanEvents);
spanEvents: spanEvents,
peerTags: peerTags!,

@lucaspimentel lucaspimentel Apr 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

peerTags is nullable so we shouldn't need the ! nullability suppression here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, except the ! is suppressing the inner non-nullability, because the compiler isn't clever enough to know that it's a List<string> not List<string?>, even though we filter string.IsNullOrEmpty (note that StringUtil also doesn't help here)

            var peerTags = (jObject["peer_tags"] as JArray)?.Values<string>().Where(x => !string.IsNullOrEmpty(x)).Distinct().OrderBy(x => x).ToList();

Removing the dammit gives errors 🙁

image

Comment thread tracer/src/Datadog.Trace/Agent/TraceSamplers/TraceFilter.cs Outdated
{
if (!string.IsNullOrEmpty(pattern))
{
compiled.Add(new Regex(pattern, RegexOptions.Compiled, matchTimeout: TimeSpan.FromSeconds(1)));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Remember these aren't actually compiled until the first time they are used. Not sure if it matters here, but somewhere else we do a dummy Match() to force the compilation early.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, it's a good point, hadn't thought about that. I think this is likely actually the correct behaviour, as it means that compilation is happening at serialization time instead of blocking the discovery service updates, but I could see an argument both ways 🤔 Any preference?

Comment thread tracer/src/Datadog.Trace/Agent/Api.cs Outdated
Comment thread tracer/src/Datadog.Trace/Agent/NullStatsAggregator.cs Outdated
Comment thread tracer/src/Datadog.Trace/Agent/StatsAggregator.cs Outdated
Comment thread tracer/src/Datadog.Trace/Agent/StatsAggregator.cs Outdated
}

// Find the root span (ParentId == null or 0) and apply the filter
foreach (var span in spans)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If this span collection follows our usual ordering, the root span will usually be the last span, so it might be faster to search in reverse (from last span to first span).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Heh, that's actually harder than it seems, because it's a SpanCollection which doesn't expose the root array, but it's a good point. I'll address that when looking into the partial flush issue...

Comment thread tracer/src/Datadog.Trace/Processors/ObfuscatorTraceProcessor.cs
Comment thread tracer/test/Datadog.Trace.Tests/Agent/ApiTests.cs Outdated
@lucaspimentel
lucaspimentel requested a review from a team April 8, 2026 04:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates the tracer’s client-side stats pipeline to better match the “client-side-stats” 1.2.0 wire format/behavior by expanding aggregation dimensions, adjusting serialization semantics, and incorporating agent-driven discovery/filtering signals.

Changes:

  • Extend stats aggregation key + msgpack payload to include additional dimensions (span kind, trace-root flag, HTTP method/endpoint, gRPC status, service source, peer tags) and add stochastic rounding/weighted accumulation.
  • Add agent discovery support for peer tags, span kinds eligibility, obfuscation negotiation, and trace-level filtering (exact/regex/resource-based).
  • Update SQL obfuscation tokenization/normalization and adjust stats sending API surface (obfuscation header, no-retry for agent stats).

Reviewed changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tracer/src/Datadog.Trace/Agent/StatsAggregator.cs Computes stats with new dimensions (peer tags, span kinds), filtering, weighting, and obfuscation negotiation.
tracer/src/Datadog.Trace/Agent/StatsBuffer.cs Serializes updated msgpack wire format and suppresses empty buckets.
tracer/src/Datadog.Trace/Agent/StatsBucket.cs Switch counters to double for weighted accumulation; store peer tags.
tracer/src/Datadog.Trace/Agent/StatsAggregationKey.cs Adds new dimensions to key equality/hash.
tracer/src/Datadog.Trace/Agent/DiscoveryService/DiscoveryService.cs Parses /info additions (peer_tags, span_kinds_stats_computed, obfuscation_version, trace filters).
tracer/src/Datadog.Trace/Agent/DiscoveryService/AgentConfiguration.cs Carries newly discovered config fields through the system.
tracer/src/Datadog.Trace/Agent/TraceSamplers/TraceFilter.cs Implements agent-driven trace-level filtering on root spans.
tracer/src/Datadog.Trace/Processors/ObfuscatorTraceProcessor.cs Expands SQL token splitters and normalizes spaces around operators near placeholders.
tracer/src/Datadog.Trace/Agent/Api.cs Sends stats with obfuscation header and disables retries for agent stats.
tracer/src/Datadog.Trace/Agent/ApiOtlp.cs Updates stats API signature/state for OTLP path.
tracer/src/Datadog.Trace/Agent/TraceSamplers/RareSampler.cs Uses stats aggregator to build keys (signature change).
tracer/test/** Updates/adds tests for new dimensions, filtering, obfuscation behavior, and API signature changes.

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

Comment thread tracer/src/Datadog.Trace/Agent/ApiOtlp.cs
Comment thread tracer/src/Datadog.Trace/Agent/StatsAggregationKey.cs
Comment thread tracer/src/Datadog.Trace/Agent/StatsAggregator.cs Outdated
Comment thread tracer/test/Datadog.Trace.Tests/Agent/ApiTests.cs Outdated
Comment on lines +416 to 421
var key = BuildKey(span, out var peerTags);

var buffer = CurrentBuffer;

if (!buffer.Buckets.TryGetValue(key, out var bucket))
{

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

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

AddToBuffer() calls BuildKey(span, out var peerTags) for every span before TryGetValue. For client/producer/consumer spans this allocates UTF-8 bytes (and intermediate strings via $"{tagKey}:{tagValue}") even when the bucket already exists and the computed peerTags list is discarded. Consider restructuring so peer-tag materialization only happens when inserting a new bucket (e.g., compute hash first, or defer building utf8PeerTags until after TryGetValue indicates the key is new) to avoid per-span allocation overhead.

Suggested change
var key = BuildKey(span, out var peerTags);
var buffer = CurrentBuffer;
if (!buffer.Buckets.TryGetValue(key, out var bucket))
{
var buffer = CurrentBuffer;
var key = BuildKey(span, out _);
if (!buffer.Buckets.TryGetValue(key, out var bucket))
{
key = BuildKey(span, out var peerTags);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, this one is a pain, I don't have a great answer to it currently 🙁 Will address in a follow up, when I work out how 😅

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

To Clarify, the suggested approach is much worse, as it allocates twice 😉

Comment thread tracer/src/Datadog.Trace/Agent/StatsBuffer.cs
@andrewlock
andrewlock force-pushed the andrew/client-side-stats/client-side-stats-1-2 branch from e52cc41 to 562d9e7 Compare April 8, 2026 10:40
@andrewlock
andrewlock requested review from a team as code owners April 8, 2026 10:40
@andrewlock
andrewlock force-pushed the andrew/client-side-stats/ip-obfuscation branch from f00d1b6 to 77f3510 Compare April 8, 2026 10:41
@andrewlock
andrewlock force-pushed the andrew/client-side-stats/client-side-stats-1-2 branch from 562d9e7 to 0b3eff8 Compare April 8, 2026 10:57
@andrewlock
andrewlock force-pushed the andrew/client-side-stats/ip-obfuscation branch from 77f3510 to 536d246 Compare April 8, 2026 10:57
@andrewlock
andrewlock force-pushed the andrew/client-side-stats/client-side-stats-1-2 branch from 5573211 to e9ee48d Compare April 16, 2026 07:59
@andrewlock
andrewlock merged commit 014e5d0 into master Apr 20, 2026
140 checks passed
@andrewlock
andrewlock deleted the andrew/client-side-stats/client-side-stats-1-2 branch April 20, 2026 16:11
@github-actions github-actions Bot added this to the vNext-v3 milestone Apr 20, 2026
andrewlock added a commit that referenced this pull request Apr 20, 2026
## Summary of changes

Extracts `ContainsSpanId` from `TraceChunkModel` to `SpanCollection`

## Reason for change

I need to find a given span in a `SpanCollection` for client-side-stats.
The algorithm used by `TraceChunkModel` does it "properly" (by stating
from the last span when looking for a root span), so it seems to make
sense to move this method to `SpanCollection`. Additionally, this
essentially hides some of the internals about the `SpanCollection` type,
so seems like a better place for it anyway.

## Implementation details

- Move the `IndexOf()` method from `TraceChunkModel` to `SpanCollection`
- Switch it to `Contains()` instead, seeing as the index is never
actually used AFAICT. It's easy to switch it back later if we _do_ need
it.

## Test coverage

Added some unit tests for the method

## Other details


Part of a stack

- #8417
- #8418
- #8420
andrewlock added a commit that referenced this pull request Apr 21, 2026
## Summary of changes

Updates the processing pipeline for client side stats

## Reason for change

The bulk of the CSS 1.2.0 work was done in #8420, but the
span-processing pipeline wasn't the same as the Go agent:

| Step | Go Agent  | .NET Tracer                            |
| ---- | --------- | -------------------------------------- |
| 1    | Normalize | Normalize + Obfuscate (`ProcessTrace`) |
| 2    | Filter    | Filter + Sample (`ShouldKeepTrace`)    |
| 3    | Obfuscate |                                        |
| 4    | Sample    |                                        |
| 5    | Stats     | Stats (`AddRange`)                     |


## Implementation details

The difference was mostly due to the way we had separate
`ProcessTrace()` and `ShouldKeepTrace()` methods. By merging them, we
can encapsulate the pipeline, and ensure that we mirror the way the Go
agent processes traces.

Additionally, there are two different mechanisms for dropping spans now
- p0drops and "trace filtering", so added a new metric tag
`reason:trace_filter` for it.

Additionally, made some small optimizations:
- No need for an array of `ITraceProcessor` when it only contains one
- Updated `TraceFilter` algorithm to correctly look for local trace
root, starting from last span
- Made `_statsAggregator` non-null in `AgentWriter` to eliminate a bunch
of null checks

## Test coverage

Covered by existing tests.

## Other details

Part of a stack:
- #8417
- #8418
- #8420
- #8435
andrewlock added a commit that referenced this pull request Apr 21, 2026
## Summary of changes

Improves the performance of peer-tag hash calculation for "fast path"
cases (where there's an existing bucket)

## Reason for change

We have to calculate the hash of all peer tags as part of
client-side-stats bucketing calculations, which involves encoding them
as utf-8. Additionally, when we _send_ the buckets, we have to send the
tags as utf-8.

In the initial CSS 1.2.0 implementation, we encoded the tags every-time
we ran a calculation, which would allocate for every span that had peer
tags, generally quite expensive. In this PR, we switch to doing the
encoding twice: once with a zero-allocation implementation (amortized 0
on .NET Framework) to calculate the hash, and then, _if_ we need the
"real" encoded tags, then we do that encoding again.

## Implementation details

- Split the peer tags work in two, once to calculate the peer tags hash,
once to get the actual tags as `key:value`
- In the hash-calculation stage, we can use `stackalloc` for .NET Core,
and array pool implementation for .NET Framework etc
- As additional optimizations, we also 
- Pre-encode the peer tag _keys_ to utf-8, so that we only need to do
those once for the hash calculation.
- Pass key details from `BuildKey` to `GetEncodedPeerTags` (i.e. is this
a "base service" only tag, if so, what's the tag value, otherwise how
big does the peer tag list need to be)
- If client-side stats is disabled, don't bother doing all the
pre-calculation (or for trace filters)

There are some possible future optimizations, _not_ implemented in this
PR:
- Track which tags require IP quantization to avoid re-doing it. (Maybe
we should allow-list the quantization anyway, so that it only applies to
specific tags? Or alternatively, block list?)
- Instead of doing the encoding of `tag:value`, allow writing the
pre-computed `byte[]` to `MessagePackBinary` and appending the `value`.
This is doable, but requires updating the `MessagePackBinary`
implementation to support it, so I considered it out of scope for now

## Test coverage

Mostly covered by existing tests, but added some additional unit tests
that directly compare the hashing to values used in Go agent tests.

Additionally did some benchmarking. The key thing is that the
`ClientSpanWithPeerTags` path is zero-allocation (and it's nice that the
slow-path is still lower allocation than before, even if it's slower
over all)

| Method | Runtime | Mean | Error | Allocated |
| -------------------------------------------------- |
------------------ | ----------: | ---------: | --------: |
| BuildKey_SimpleSpan_Before | .NET 10.0 | 44.34 ns | 0.906 ns | - |
| BuildKey_SimpleSpan_After | .NET 10.0 | 39.17 ns | 0.445 ns | - |
| BuildKey_SimpleSpan_Before | .NET 6.0 | 93.13 ns | 1.837 ns | - |
| BuildKey_SimpleSpan_After | .NET 6.0 | 87.56 ns | 0.839 ns | - |
| BuildKey_SimpleSpan_Before | .NET Core 3.1 | 192.53 ns | 2.572 ns | -
|
| BuildKey_SimpleSpan_After | .NET Core 3.1 | 217.32 ns | 4.362 ns | - |
| BuildKey_SimpleSpan_Before | .NET Framework 4.8 | 131.98 ns | 2.621 ns
| - |
| BuildKey_SimpleSpan_After | .NET Framework 4.8 | 175.20 ns | 7.647 ns
| - |
| | | | | |
| BuildKey_ClientSpanNoPeerTags_Before | .NET 10.0 | 160.62 ns | 2.622
ns | - |
| BuildKey_ClientSpanNoPeerTags_After | .NET 10.0 | 151.27 ns | 1.995 ns
| - |
| BuildKey_ClientSpanNoPeerTags_Before | .NET 6.0 | 243.62 ns | 3.199 ns
| - |
| BuildKey_ClientSpanNoPeerTags_After | .NET 6.0 | 232.55 ns | 2.975 ns
| - |
| BuildKey_ClientSpanNoPeerTags_Before | .NET Core 3.1 | 550.47 ns |
9.998 ns | - |
| BuildKey_ClientSpanNoPeerTags_After | .NET Core 3.1 | 680.42 ns |
10.022 ns | - |
| BuildKey_ClientSpanNoPeerTags_Before | .NET Framework 4.8 | 442.88 ns
| 8.854 ns | - |
| BuildKey_ClientSpanNoPeerTags_After | .NET Framework 4.8 | 682.12 ns |
13.468 ns | - |
| | | | | |
| BuildKey_ClientSpanWithPeerTags_Before | .NET 10.0 | 800.82 ns |
15.062 ns | 840 B |
| BuildKey_ClientSpanWithPeerTags_After | .NET 10.0 | 575.04 ns | 5.604
ns | - |
| BuildKey_ClientSpanWithPeerTags_Before | .NET 6.0 | 1,076.07 ns |
13.124 ns | 840 B |
| BuildKey_ClientSpanWithPeerTags_After | .NET 6.0 | 1,440.26 ns |
28.755 ns | - |
| BuildKey_ClientSpanWithPeerTags_Before | .NET Core 3.1 | 1,531.27 ns |
13.509 ns | 840 B |
| BuildKey_ClientSpanWithPeerTags_After | .NET Core 3.1 | 1,774.35 ns |
89.601 ns | - |
| BuildKey_ClientSpanWithPeerTags_Before | .NET Framework 4.8 | 1,682.15
ns | 17.082 ns | 859 B |
| BuildKey_ClientSpanWithPeerTags_After | .NET Framework 4.8 | 2,040.90
ns | 162.895 ns | - |
| | | | | |
| BuildKey_ClientSpanWithPeerTags_GetEncodedPeerTags | .NET 10.0 |
1,141.85 ns | 9.980 ns | 760 B |
| BuildKey_ClientSpanWithPeerTags_GetEncodedPeerTags | .NET 6.0 |
2,720.55 ns | 53.097 ns | 760 B |
| BuildKey_ClientSpanWithPeerTags_GetEncodedPeerTags | .NET Core 3.1 |
3,534.28 ns | 141.234 ns | 760 B |
| BuildKey_ClientSpanWithPeerTags_GetEncodedPeerTags | .NET Framework
4.8 | 3,438.80 ns | 84.352 ns | 778 B |


<details><summary>Details</summary>
<p>

```csharp
// <copyright file="StatsAggregatorBenchmark.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Datadog.Trace;
using Datadog.Trace.Agent;
using Datadog.Trace.Agent.DiscoveryService;
using Datadog.Trace.Configuration;
using Datadog.Trace.Tagging;

namespace Benchmarks.Trace;

/// <summary>
/// StatsAggregator.BuildKey benchmarks
/// </summary>
[MemoryDiagnoser]
[BenchmarkCategory(Constants.TracerCategory)]
public class StatsAggregatorBenchmark
{
    private static readonly List<StatsAggregator.PeerTagKey> PeerTagKeys =
    [
        new("_dd.base_service"),
        new("aws.queue.name"),
        new("aws.queue.url"),
        new("aws.s3.bucket"),
        new("aws.stream.name"),
        new("bucketname"),
        new("db.couchbase.seed.nodes"),
        new("db.hostname"),
        new("db.instance"),
        new("db.system"),
        new("messaging.destination"),
        new("messaging.kafka.bootstrap.servers"),
        new("messaging.rabbitmq.exchange"),
        new("messaging.system"),
        new("network.destination.name"),
        new("peer.hostname"),
        new("peer.service"),
        new("server.address"),
        new("topic"),
    ];

    private StatsAggregator _aggregator;
    private Span _simpleSpan;
    private Span _clientSpanNoPeerTags;
    private Span _clientSpanWithPeerTags;
    private StatsAggregationKey _key;
    private List<byte[]> _encoded;

    [GlobalSetup]
    public void GlobalSetup()
    {
        _aggregator = new StatsAggregator(
            new NoOpApi(),
            new TracerSettings(),
            NullDiscoveryService.Instance,
            isOtlp: false);

        var now = DateTimeOffset.UtcNow;

        // Simple span: no span kind, no peer tags — exercises the "internal" fast path
        _simpleSpan = CreateSpan(now, "web-service", "web.request", "GET /api/users", "web");

        // Client span with SpanKind but no matching peer tags — iterates peer tag keys but finds nothing
        var clientTags = new HttpTags { HttpMethod = "GET", HttpStatusCode = "200" };
        _clientSpanNoPeerTags = CreateSpan(now, "web-service", "http.request", "GET /api/orders", "http", clientTags);

        // Client span with several peer tags set — the most expensive path
        var peerTags = new HttpTags { HttpMethod = "POST", HttpStatusCode = "200" };
        _clientSpanWithPeerTags = CreateSpan(now, "web-service", "http.request", "POST /api/data", "http", peerTags);
        _clientSpanWithPeerTags.Tags.SetTag("peer.service", "remote-service");
        _clientSpanWithPeerTags.Tags.SetTag("db.instance", "i-1234");
        _clientSpanWithPeerTags.Tags.SetTag("db.system", "postgres");
        _clientSpanWithPeerTags.Tags.SetTag("server.address", "db.example.com");
        _clientSpanWithPeerTags.Tags.SetTag("network.destination.name", "db.example.com");

    }

    [GlobalCleanup]
    public void GlobalCleanup()
    {
        _aggregator.DisposeAsync().GetAwaiter().GetResult();
        var value = _key;
        var encoded = _encoded;
    }

    /// <summary>
    /// BuildKey for a simple span with no span kind (internal fast-path, no peer tag iteration).
    /// </summary>
    [Benchmark]
    public void BuildKey_SimpleSpan()
    {
        _key = _aggregator.BuildKey(_simpleSpan, PeerTagKeys, out _);
    }

    /// <summary>
    /// BuildKey for a client span that has no matching peer tags (iterates all peer tag keys, finds none).
    /// </summary>
    [Benchmark]
    public void BuildKey_ClientSpanNoPeerTags()
    {
        _key = _aggregator.BuildKey(_clientSpanNoPeerTags, PeerTagKeys, out _);
    }

    /// <summary>
    /// BuildKey for a client span with several peer tags set (UTF-8 encoding + FNV hashing).
    /// </summary>
    [Benchmark]
    public void BuildKey_ClientSpanWithPeerTags()
    {
        _key = _aggregator.BuildKey(_clientSpanWithPeerTags, PeerTagKeys, out var results);
    }

    /// <summary>
    /// BuildKey for a client span with several peer tags set (UTF-8 encoding + FNV hashing).
    /// </summary>
    [Benchmark]
    public void BuildKey_ClientSpanWithPeerTags_GetEncodedPeerTags()
    {
        _key = _aggregator.BuildKey(_clientSpanWithPeerTags, PeerTagKeys, out var results);
        _encoded = StatsAggregator.GetEncodedPeerTags(_clientSpanWithPeerTags, PeerTagKeys, in results);
    }

    private static Span CreateSpan(DateTimeOffset start, string serviceName, string operationName, string resourceName, string type, ITags tags = null)
    {
        var tracer = Benchmarks.Trace.Asm.EmptyDatadogTracer.Instance;
        var traceContext = new TraceContext(tracer);
        var context = new SpanContext(null, traceContext, serviceName);
        var span = new Span(context, start, tags);
        span.OperationName = operationName;
        span.ResourceName = resourceName;
        span.Type = type;
        return span;
    }

    private sealed class NoOpApi : IApi
    {
        public TracesEncoding TracesEncoding => TracesEncoding.DatadogV0_4;

        public Task<bool> Ping() => Task.FromResult(true);

        public Task<bool> SendTracesAsync(ArraySegment<byte> traces, int numberOfTraces, bool statsComputationEnabled, long numberOfDroppedP0Traces, long numberOfDroppedP0Spans, bool apmTracingEnabled = true)
            => Task.FromResult(true);

        public Task<bool> SendStatsAsync(StatsBuffer stats, long bucketDuration, int tracerObfuscationVersion)
            => Task.FromResult(true);
    }
}
```

</p>
</details> 


## Other details


Part of a stack:
- #8417
- #8418
- #8420
- #8435
- #8436
- #8444
andrewlock added a commit that referenced this pull request Apr 23, 2026
## Summary of changes

Removes the minimum-agent version check from the client-side-stats check

## Reason for change

We didn't have it before, which means people who previously had stats,
suddenly won't have them. Also other agents (like bottlecap etc) don't
have the version. And we haven't figured out how to handle that yet.
While CSS is off by default, this seems like the best option to take.

## Implementation details

Remove the code that is checking for CSS support.

## Test coverage

Covered by existing (and removed a test for the behaviour)

## Other details

Follows on from 
- #8417
- #8418
- #8420
- #8435
- #8436
- #8444
- #8445
andrewlock added a commit that referenced this pull request Jun 25, 2026
## Summary of changes

Removes the incorrect sampling-based weighting from Client-Side stats

## Reason for change

When implementing the updated CSS implementation in #8420, we compared
with the canonicial Go implementation, and [added a
`GetWeight()`](https://github.com/DataDog/datadog-agent/blob/ce22e11ee71e55be717b9d9a3f8f3d7721a9c6d7/pkg/trace/stats/weight.go)
method based on this. However, the sampling rate that this is based on
is based on the deprecated `_sample_rate` tag (which .NET never
implemented), _not_ the normal sampling rate. Therefore this weighting
causes overcounting.

## Implementation details

- Rip out GetWeight() and associated usages
- Revert various doubles back to `long`s
- Remove the unnecessary stochastic rounding

## Test coverage

- Removed one test for `GetWeight()` behavior
- TBC a system test that would catch this - serverless e2e tests (thanks
@apiarian-datadog!) caught this one
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos area:client-side-stats area:tracer The core tracer library (Datadog.Trace, does not include OpenTracing, native code, or integrations)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants