Skip to content

Share parent's PropagationTags for local child spans#11701

Draft
dougqh wants to merge 6 commits into
masterfrom
dougqh/ptags-per-segment
Draft

Share parent's PropagationTags for local child spans#11701
dougqh wants to merge 6 commits into
masterfrom
dougqh/ptags-per-segment

Conversation

@dougqh

@dougqh dougqh commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What Does This Do

Local child spans now share the parent's (trace-level) PropagationTags instead of each allocating a fresh empty(). Collapses N+1 → 1 PropagationTags per N-span trace.

Motivation

CoreTracer span-build allocated propagationTagsFactory.empty() for every local child span (the common in-process case); only the distributed ExtractedContext path reused the parent's. A non-root child's own PropagationTags, however, is never readDDSpanContext.getPropagationTags() routes to the root (getRootSpanContextOrThis().propagationTags). So the per-span empty() was pure allocation waste.

Additional Notes

50s petclinic run under JMeter multi-endpoint load (Zulu 17, alloc JFR):

  • 5,481 DDSpanContext allocations, 311 PTags allocations — 1 PTags per ~17.6 spans (vs 1:1 before)
  • The 311 are all root spans (expected — no parent to share from); child spans share the root's instance
  • PTagsFactory$PTags no longer appears in the top 30 allocating classes (below 0.5% of sampled allocation)

Allocation measured (controlled toggle, via #11915)

Using #11915's TraceAssemblyBenchmark (web-shape root + N children), isolating just this one-line change (share vs empty()-per-child, all else identical), -prof gc, tracer logging forced to WARN:

arm empty()/child shared Δ B/op Δ/child
bareStartSpan (control) 899.7 898.3 ~0
trace[1] 2202.3 2177.5 24.8 (noise floor)
trace[5] 4705.5 4263.8 441.7 88
trace[20] 14032.1 12000.5 2031.6 102

A slope reduction, not an intercept shift: the saving scales with fan-out — ~2 KB/op (≈14.5%) at 20 children — while the single-span control stays flat (roots always allocate their own PropagationTags, so they can't benefit). JOL puts one empty() PTags at 96 B shallow (compressed oops), matching the ~88–106 B/child slope on the resolvable arms. (trace[1] is one 96 B object in ~2.2 KB/op — at the -f1 noise floor.)

Correctness — gated by a characterization test

PropagationTagsChildSpanTest pins the load-bearing behavior on both sides of the shared instance:

  • Injection (read side) — a non-root local child still injects the inbound distributed _dd.p.*, because getPropagationTags() routes reads to the root. Safe with or without sharing.
  • Serialization (emit side) — sharing surfaced a real regression. DDSpanContext.processTagsAndBaggage emits each span's PropagationTags into its own metadata; pre-PR that was gated only implicitly by children holding an empty() instance. With the shared (non-empty) root instance, _dd.p.dm and other trace-level _dd.p.* leaked onto every child span — caught by the test_sampling_span_tags parametric test. Fixed by gating the emit on span position (local root only), which exactly restores pre-PR behavior; localChildDoesNotEmitTraceLevelDdpTags guards it (fails without the gate, passes with it).

All three trace mappers (v0.4 / v0.5 / v1) read per-span _dd.p.* from Metadata.getBaggage(), so the one gate covers all of them; v1's chunk-level samplingMechanism reads the first span only, so it was never a per-span leak.

Safety

  • The ctor's updateTraceIdHighOrderBits stamp is guard-idempotent (if (traceIdHighOrderBits != highOrderBits)) and a no-op on the already-stamped shared root instance (same trace ⇒ same high-order bits).
  • The full dd-trace-core propagation + span-build suite passes (W3C/Datadog extractors+injectors, W3CPropagationTags, OPM round-trip, OrgGuard, CoreSpanBuilderTest, DDSpanContextTest) — 0 failures.

The lastParentId/OPM inject-time mutation race (a transient per-injection value parked in the shared object) is pre-existing — injection already routes to the root's shared instance via getPropagationTags() — so it is a separate concurrency concern, not introduced or worsened by this change. Tracked in #11702.

tag: no release note
tag: ai generated

🤖 Generated with Claude Code

@dougqh dougqh changed the title Characterize non-root span _dd.p.* propagation (gate for PTags per-span alloc fix) Share parent's PropagationTags for local child spans Jun 22, 2026
@datadog-prod-us1-4

This comment has been minimized.

@dd-octo-sts

dd-octo-sts Bot commented Jun 22, 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.97 s 14.00 s [-1.1%; +0.6%] (no difference)
startup:insecure-bank:tracing:Agent 12.99 s 13.00 s [-0.8%; +0.6%] (no difference)
startup:petclinic:appsec:Agent 16.80 s 15.98 s [+0.7%; +9.5%] (maybe worse)
startup:petclinic:iast:Agent 16.36 s 16.85 s [-7.1%; +1.3%] (no difference)
startup:petclinic:profiling:Agent 16.58 s 16.66 s [-1.6%; +0.5%] (no difference)
startup:petclinic:sca:Agent 16.80 s 16.81 s [-0.9%; +0.8%] (no difference)
startup:petclinic:tracing:Agent 16.07 s 16.36 s [-2.8%; -0.8%] (maybe better)

Commit: 76851be3 · 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.

Comment thread dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java Outdated
@dougqh dougqh marked this pull request as ready for review June 24, 2026 15:30
@dougqh dougqh requested a review from a team as a code owner June 24, 2026 15:30
@dougqh dougqh requested a review from amarziali June 24, 2026 15:30
@dd-octo-sts

dd-octo-sts Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Hi! 👋 Thanks for your pull request! 🎉

To help us review it, please make sure to:

  • Add at least one type, and one component or instrumentation label to the pull request

If you need help, please check our contributing guidelines.

@dd-octo-sts dd-octo-sts Bot added the tag: ai generated Largely based on code generated by an AI or LLM label Jun 24, 2026

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

Doug the intent is ok to me. It looks some tests are still failing

@dougqh dougqh added tag: performance Performance related changes type: feature Enhancements and improvements comp: context propagation Trace context propagation labels Jun 29, 2026
@dougqh

dougqh commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

Doug the intent is ok to me. It looks some tests are still failing

@amarziali Thanks. It looks like a rename got merged in last week that broke the tests that I ported from Groovy. It should be better now.

@dougqh dougqh added this to the 1.64.0 milestone Jun 30, 2026
@dougqh dougqh requested a review from amarziali June 30, 2026 17:44
@dougqh

dougqh commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Synced with master (the branch was ~57 commits behind) and confirmed :dd-trace-core compiles + PropagationTags* tests pass locally on the merge. The reds you saw were the flaky parametric/system-tests jobs, not this change — a fresh run is underway. Targeted at the 1.64.0 milestone. 🙏

@amarziali

Copy link
Copy Markdown
Contributor

Synced with master (the branch was ~57 commits behind) and confirmed :dd-trace-core compiles + PropagationTags* tests pass locally on the merge. The reds you saw were the flaky parametric/system-tests jobs, not this change — a fresh run is underway. Targeted at the 1.64.0 milestone. 🙏

I see that parametric system tests fails and seem indeed related to this change:

 AssertionError: non-root spans that are not first in a chunk should never includ the _dd.p.dm tag

@dougqh

dougqh commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Synced with master (the branch was ~57 commits behind) and confirmed :dd-trace-core compiles + PropagationTags* tests pass locally on the merge. The reds you saw were the flaky parametric/system-tests jobs, not this change — a fresh run is underway. Targeted at the 1.64.0 milestone. 🙏

I see that parametric system tests fails and seem indeed related to this change:

 AssertionError: non-root spans that are not first in a chunk should never includ the _dd.p.dm tag

@amarziali Yeah, usually, the system test failures have been flakes, but I agree that looks like a legitimate failure.
Looks like like I'll have to figure out a different approach to solving this.

@dougqh dougqh marked this pull request as draft July 2, 2026 11:36
@mhlidd mhlidd removed this from the 1.64.0 milestone Jul 7, 2026
dougqh and others added 6 commits July 14, 2026 14:29
CoreTracer span-build allocates a fresh propagationTagsFactory.empty()
per local child span; only the distributed ExtractedContext path reuses
the parent's. Before optimizing that per-span allocation away, this test
pins the load-bearing behavior: that a non-root (local child) span still
carries the inbound distributed _dd.p.* tags when it injects.

Verdict (both tests pass): it does. DDSpanContext.getPropagationTags()
routes to the root (getRootSpanContextOrThis), so a non-root child's own
propagationTags field is never read for injection. The per-span empty()
is pure allocation waste, not a latent correctness bug.

This test is the gate + safety net for the planned "share the parent's
PropagationTags for local children" allocation fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CoreTracer span-build allocated a fresh propagationTagsFactory.empty()
for every local child span, reused from the parent only on the
distributed ExtractedContext path — N+1 PropagationTags per N-span
trace, N of them needless empties.

A non-root child's own PropagationTags is never read for injection:
DDSpanContext.getPropagationTags() routes to the root
(getRootSpanContextOrThis), confirmed by PropagationTagsChildSpanTest
(inbound _dd.p.* survive child injection). So local children can share
the parent's (trace-level) instance instead of allocating their own,
collapsing N+1 -> 1 per trace.

Safe: the ctor's updateTraceIdHighOrderBits stamp is guard-idempotent
and a no-op on the already-stamped shared root instance (same trace =>
same high-order bits). Full dd-trace-core propagation + span-build
suite passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The change landed, so drop the pre-implementation 'open question / planned fix'
framing, the scratch-doc reference (ptags-allocation-findings.md), and the
brittle CoreTracer line numbers. State what it guards: local children share the
root's PropagationTags, and reads route to the root, so a non-root span still
injects the inbound _dd.p.* tags.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The master merge renamed AgentSpan.context() to spanContext(); update the
two call sites so :dd-trace-core:compileTestJava passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local children now share the root's PropagationTags, so
DDSpanContext.processTagsAndBaggage emitted _dd.p.* (including the
_dd.p.dm decision-maker) into every child span's metadata -- the emit
was previously gated only implicitly by children holding an empty()
instance. Gate it on span position (local root only), which exactly
restores pre-PR behavior. All three trace mappers read per-span
_dd.p.* from Metadata.getBaggage(), so the one gate covers them.

Caught by the test_sampling_span_tags parametric test; add a
serialization-side case to PropagationTagsChildSpanTest that fails
without the gate and passes with it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqh dougqh force-pushed the dougqh/ptags-per-segment branch from 57bed99 to 76851be Compare July 14, 2026 18:31
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: performance Performance related changes type: feature Enhancements and improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants