Share parent's PropagationTags for local child spans#11701
Conversation
This comment has been minimized.
This comment has been minimized.
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
|
Hi! 👋 Thanks for your pull request! 🎉 To help us review it, please make sure to:
If you need help, please check our contributing guidelines. |
amarziali
left a comment
There was a problem hiding this comment.
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. |
|
Synced with |
I see that parametric system tests fails and seem indeed related to this change: |
@amarziali Yeah, usually, the system test failures have been flakes, but I agree that looks like a legitimate failure. |
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>
57bed99 to
76851be
Compare
What Does This Do
Local child spans now share the parent's (trace-level)
PropagationTagsinstead of each allocating a freshempty(). Collapses N+1 → 1PropagationTagsper N-span trace.Motivation
CoreTracerspan-build allocatedpropagationTagsFactory.empty()for every local child span (the common in-process case); only the distributedExtractedContextpath reused the parent's. A non-root child's ownPropagationTags, however, is never read —DDSpanContext.getPropagationTags()routes to the root (getRootSpanContextOrThis().propagationTags). So the per-spanempty()was pure allocation waste.Additional Notes
50s petclinic run under JMeter multi-endpoint load (Zulu 17, alloc JFR):
DDSpanContextallocations, 311PTagsallocations — 1 PTags per ~17.6 spans (vs 1:1 before)PTagsFactory$PTagsno 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 vsempty()-per-child, all else identical),-prof gc, tracer logging forced to WARN:empty()/childA 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 oneempty()PTagsat 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-f1noise floor.)Correctness — gated by a characterization test
PropagationTagsChildSpanTestpins the load-bearing behavior on both sides of the shared instance:_dd.p.*, becausegetPropagationTags()routes reads to the root. Safe with or without sharing.DDSpanContext.processTagsAndBaggageemits each span'sPropagationTagsinto its own metadata; pre-PR that was gated only implicitly by children holding anempty()instance. With the shared (non-empty) root instance,_dd.p.dmand other trace-level_dd.p.*leaked onto every child span — caught by thetest_sampling_span_tagsparametric test. Fixed by gating the emit on span position (local root only), which exactly restores pre-PR behavior;localChildDoesNotEmitTraceLevelDdpTagsguards it (fails without the gate, passes with it).All three trace mappers (v0.4 / v0.5 / v1) read per-span
_dd.p.*fromMetadata.getBaggage(), so the one gate covers all of them; v1's chunk-levelsamplingMechanismreads the first span only, so it was never a per-span leak.Safety
updateTraceIdHighOrderBitsstamp is guard-idempotent (if (traceIdHighOrderBits != highOrderBits)) and a no-op on the already-stamped shared root instance (same trace ⇒ same high-order bits).dd-trace-corepropagation + 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 viagetPropagationTags()— 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