Skip to content

Precompute decision maker tag values and defer building the OpenTelemetry tracestate - #12981

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 3 commits into
masterfrom
dougqh/ptags-decision-maker-table
Oct 22, 2026
Merged

gh-worker-dd-mergequeue-cf854d[bot] merged 3 commits into
masterfrom
dougqh/ptags-decision-maker-table

Conversation

@dougqh

@dougqh dougqh commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Note: this PR also carries #12982, "Defer building the OpenTelemetry tracestate until it's injected or exported". #12982 was stacked on this branch and was squash-merged into it (1c462878a1) rather than waiting for this PR to land first, so both changes land together here. Both were reviewed and approved by @mcculls. The second half of this description is #12982's.

What Does This Do

PTags.installSamplingState built the _dd.p.dm value with TagValue.from("-" + samplingMechanism) on every keep decision. That allocated a String, then ran validation, hashing and a lookup in TagValue's 128-entry cache. All of this happens inside samplingStateLock.

This PR builds the TagValues for mechanisms 0–15 once, at class init, and indexes them by mechanism. Larger mechanisms still go through TagValue.from. The negative/UNKNOWN guard is unchanged.

Motivation

This is the first of several small follow-ups on per-trace sampling overhead in propagation tags. The eager OtelTraceState from #12397 is the second, below. The per-PTags lock object and a race in the single-entry Knuth sampling-rate cache will come as separate PRs.

The concat itself is older than #12397. #12397 moved it inside the sampling lock.

Benchmark

This PR adds SamplingDecisionBenchmark:

  • span* arms run a bare root span through start → sample → finish.
  • ptags* arms make the decision on fresh PropagationTags with no span around it.

Before = master's PTagsFactory; after = this branch. Both runs used -f 1 -wi 3 -w 2s -i 5 -r 3s -t 8 -prof gc on JDK 17 / M-series laptop. B/op is the primary signal. Throughput comes from a single fork and is directional only.

arm B/op before B/op after Δ B/op ops/µs before ops/µs after
ptagsPriorityKeep 176 152 −24 259 ± 17 319 ± 36
ptagsProbabilityKeep 240 216 −24 185 ± 16 284 ± 47
spanDefaultSamplerKeep 1074 ± 94 1048 ~−26 3.53 ± 0.50 3.51 ± 0.89
spanRuleSamplerKeep 1096 1048 −48 2.87 ± 0.29 2.92 ± 0.12
spanManualKeep 936 840 −96 3.34 ± 1.26 3.31 ± 0.69
  • Each decision-maker update saved 24 B. The span arms save a multiple of that because a trace goes through more than one decision-maker update.
  • The baseline spanDefaultSamplerKeep B/op varied run to run (±94), so treat its Δ as approximate.
  • spanManualKeep also varies between runs: a later run of the same post-change jar measured 888 B/op, not 840. Its Δ could be −48 rather than −96. The ptags* arms gave the same values in every run.
  • The ptags* arms still allocate 150–220 B per trace, for PTags itself, its lock object, SamplingState and the eager OtelTraceState. Those are the targets of the follow-up PRs.

Additional Notes

  • The table holds 16 entries rather than more because each entry also takes a slot in TagValue's shared 128-entry cache.
  • New DecisionMakerTagValueTest covers values inside and beyond the table, and instance reuse.
  • datadog.trace.core.propagation.*, DDSpanContextPropagationTagsTest and CoreSpanBuilderTest pass locally.

Also included: deferred OpenTelemetry tracestate (from #12982)

What Does This Do

#12397 made every root probability decision build an OtelTraceState, inside the sampling lock. Only W3C tracestate injection and OTLP export ever read it.

This PR defers building it for the common case: a fresh probability decision with no inherited ot tracestate member, not rejected by the rate limiter.

  • tryUpdateProbabilitySamplingDecision installs a new ProbabilitySamplingState. It keeps only the trace id low-order bits and the rate, and builds the ot member on the first getOtelTraceState().
  • Racing readers agree on a single instance through a CAS. Duplicates would still be correct, because the state is immutable. One instance keeps the identity-based "unchanged" checks from installing a new state or clearing the W3C header cache for nothing. It also means publication doesn't depend on OtelTraceState keeping its fields final. The CAS runs once per injected or exported trace, uncontended.
  • PropagationTags.SamplingState is no longer final. preservesInheritedOtelPosition now reads through the getter.
  • The decision-maker computation moves out of installSamplingState into nextDecisionMakerTagValue, so the new path can share it.

Every other path is unchanged:

  • states derived from an inherited ot member
  • rate-limiter rejection
  • priority-only decisions
  • forceKeep

Motivation

This is part of the follow-ups to #12397's per-trace sampling overhead.

It builds on the decision-maker table above: moving the decision-maker computation into its own method would otherwise make the "-" + mechanism concat escape, adding 24 B/op to priority-only decisions.

Benchmark

Decision and header paths: SamplingDecisionBenchmark

Before = the decision-maker change alone; after = both changes. -prof gc, B/op.

arm before after Δ
ptagsProbabilityKeep (decision only) 216 168 −48
ptagsProbabilityKeepDatadogHeader 440 392 −48
ptagsProbabilityKeepW3CHeader 752 768 +16
ptagsProbabilityKeepOtlpTracestate 960 1072 +112, see below
ptagsPriorityKeep 152 152 0
spanDefaultSamplerKeep 1048 1000 −48
spanRuleSamplerKeep 1048 1000 −48
spanManualKeep 888 888 0
  • Never read (Datadog-only propagation): the full 48 B saving holds end to end.
  • W3C inject: the OtelTraceState is still built, plus 16 B for the two inputs ProbabilitySamplingState carries.
  • OTLP tracestate: with -XX:-DoEscapeAnalysis the gap is +16 B (1080 → 1096) at equal throughput. The rest is a scalar-replacement win the old code got in this direct-call arm and this code doesn't. It only applies to OTLP export with rule sampling: under OTLP export the default sampler is ParentBasedAlwaysOnSampler, a priority-only decision that never creates a ProbabilitySamplingState. Real export goes through OtlpTraceJson.writeSpan, so whether the JIT effect carries over there isn't measured.

Scenarios: PropagationScenariosBenchmark

A scenario benchmark: each arm is a whole request driven through the tracer's public API, with a default-configured tracer (datadog,tracecontext,baggage):

  • headNoPropagation, headPropagation: a root that decides, with or without an outbound inject
  • leaf, intermediate: extract an upstream decision, with or without relaying it

It's here to put this PR in context of #12397's overall cost. It only uses API unchanged since v1.66.0, so it builds onto both commits.

#12397 alone (79a1710fcd^ vs 79a1710fcd, B/op):

  • head, no propagation: +32 to +104 B
  • head, propagating: +48 to +288 B
  • leaf: +160 to +260 B
  • intermediate: +256 to +495 B

This PR on that benchmark (default sampler, v1.66.0 → master → this branch):

scenario v1.66.0 master this branch
head, no propagation 976 1096 1000
head, propagating 3688 3656 3656
leaf 2109 2342 2339
intermediate 4642 5056 4976

B/op on the larger scenario arms varies by ±100 B between forks, so treat small deltas there as noise.

Additional Notes

  • New ProbabilitySamplingStateTest covers:
    • the deferred value matches the eager conversion (keep, drop, rate 1.0, rate 0.0)
    • repeated reads return a single instance, and so do racing reads from several threads
    • rate-limiter rejection records no otel state
    • a later forceKeep starts from the deferred state
  • The full dd-trace-core test suite passes, apart from:
    • 2 Docker-dependent TracerConnectionReliabilityTest cases (no Docker locally)
    • DDAgentWriterCombinedTest and PendingTraceBufferTest, which failed under full-suite load and pass when run on their own

🤖 Generated with Claude Code

Every keep decision built "-" + samplingMechanism and then validated, hashed
and looked it up in TagValue's cache. The mechanisms are a small fixed set, so
build their TagValues once and index by mechanism, falling back to
TagValue.from for anything beyond the table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dougqh dougqh added tag: no release notes Changes to exclude from release notes type: refactoring comp: context propagation Trace context propagation tag: ai generated Largely based on code generated by an AI or LLM labels Oct 21, 2026
@datadog-prod-us1-4

datadog-prod-us1-4 Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

🎯 Code Coverage (details)
• Patch Coverage: 93.02%
• Overall Coverage: 59.86% (+0.01%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 1c46287 | Docs | Give us feedback!

Measures the per-trace propagation-tag bookkeeping done on a root sampling
decision, both in a bare start/sample/finish span and on fresh
PropagationTags with no span around it, for the default, rule and manual
keep paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dougqh
dougqh marked this pull request as ready for review October 21, 2026 13:27
@dougqh
dougqh requested review from a team as code owners October 21, 2026 13:27
@dougqh
dougqh requested review from khanayan123 and mcculls and removed request for a team October 21, 2026 13:27
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T13:29:50.222445Z 4ddb5d9 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@dougqh
dougqh requested a review from MilanGarnier October 21, 2026 13:28

@datadog-prod-us1-4 datadog-prod-us1-4 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.

Bits Code Review: PASS

More details

The precomputed decision-maker values preserve existing formatting, the negative-mechanism guard, and fallback behavior for mechanisms outside the table.

Was this helpful? React 👍 or 👎

Open Bits AI session

🤖 Bits Code Review · Commit 4ddb5d9

@dd-octo-sts

dd-octo-sts Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 13.98 s 14.01 s [-1.1%; +0.7%] (no difference)
startup:insecure-bank:tracing:Agent 12.90 s 12.98 s [-1.3%; -0.0%] (maybe better)
startup:petclinic:appsec:Agent 17.03 s 16.90 s [-0.1%; +1.7%] (no difference)
startup:petclinic:iast:Agent 17.01 s 16.98 s [-0.7%; +1.1%] (no difference)
startup:petclinic:profiling:Agent 16.09 s 16.70 s [-7.9%; +0.6%] (no difference)
startup:petclinic:sca:Agent 17.00 s 16.85 s [-0.1%; +1.9%] (no difference)
startup:petclinic:tracing:Agent 16.06 s 15.77 s [-2.6%; +6.2%] (no difference)

Commit: 1c462878 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

@mcculls mcculls 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.

Makes sense to me, thanks

@dougqh
dougqh added this pull request to the merge queue Oct 22, 2026
@dd-octo-sts

dd-octo-sts Bot commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-10-22 13:09:28 UTC ℹ️ Start processing command /merge


2026-10-22 13:09:33 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 1h (p90).


2026-10-22 13:39:56 UTC ❌ MergeQueue: The build pipeline contains failing jobs for this merge request

Build pipeline has failing jobs for 701e3e7:

⚠️ Do NOT retry failed jobs directly (why?).

What to do next?

  • Investigate the failures and when ready, re-add your pull request to the queue!
  • If your PR checks are green, try to rebase/merge. It might be because the CI run is a bit old.
  • Any question, go check the FAQ.
Details

Since those jobs are not marked as being allowed to fail, the pipeline will most likely fail.
Therefore, and to allow other builds to be processed, this merge request has been rejected and the pipeline got canceled.

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 22, 2026
…ported (#12982)

* Defer building the OpenTelemetry tracestate for fresh probability decisions

Every root probability decision built an OtelTraceState inside the sampling
lock, though only W3C injection and OTLP export read it. Install a
ProbabilitySamplingState that keeps the trace id bits and rate, and builds
the ot member on first read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Clarify why lazy otel state readers agree on one instance

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Add benchmarks for the lazy otel state's inject, export and service-shape cost

SamplingDecisionBenchmark gains arms that follow a probability decision with
the Datadog header, the W3C tracestate header and the OTLP tracestate, so the
deferred OtelTraceState is measured where it's built or skipped.

ServiceShapePropagationBenchmark measures head, leaf and intermediate
services with a default-configured tracer. It only uses API unchanged since
v1.66.0, so it can be built onto release tags to put a cost on #12397.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Rename ServiceShapePropagationBenchmark to PropagationScenariosBenchmark

Describe it as a scenario benchmark: each arm is a whole request driven
through the tracer's public API, rather than one method in isolation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test that racing readers get one OpenTelemetry state instance

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@dougqh dougqh changed the title Precompute decision maker tag values per sampling mechanism Precompute decision maker tag values and defer building the OpenTelemetry tracestate Oct 22, 2026
@dougqh

dougqh commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Heads-up: #12982 (deferred OpenTelemetry tracestate, also approved by @mcculls) was squash-merged into this branch as 1c462878a1, so this PR now lands both changes. Title and description are updated to cover both.

@dougqh
dougqh enabled auto-merge October 22, 2026 15:46
@dougqh
dougqh added this pull request to the merge queue Oct 22, 2026
@dd-octo-sts

dd-octo-sts Bot commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-10-22 16:14:43 UTC ℹ️ Start processing command /merge


2026-10-22 16:14:47 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 1h (p90).


2026-10-22 17:28:23 UTC ℹ️ MergeQueue: This merge request was merged

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 22, 2026
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 3dc7661 into master Oct 22, 2026
612 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the dougqh/ptags-decision-maker-table branch October 22, 2026 17:28
@github-actions github-actions Bot added this to the 1.68.0 milestone Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: context propagation Trace context propagation tag: ai generated Largely based on code generated by an AI or LLM tag: no release notes Changes to exclude from release notes type: refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants