Skip to content

Wire mergedTracerTags as a read-through parent at span build (level-split) (phase 1a) - #11932

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 13 commits into
masterfrom
dougqh/tagmap-read-through-consumer
Aug 17, 2026
Merged

Wire mergedTracerTags as a read-through parent at span build (level-split) (phase 1a)#11932
gh-worker-dd-mergequeue-cf854d[bot] merged 13 commits into
masterfrom
dougqh/tagmap-read-through-consumer

Conversation

@dougqh

@dougqhdougqh commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

What Does This Do?

Splits trace-level tags & span-level tags into two separate TagMap-s
To maintain full semantic compatibility, this is done via the read-through parent capability introduced in #11789.

Motivation

Reduces some allocation by avoiding the cloning of the BucketGroup collision chain
Reduces span creation time by replacing O(n) map-walk with O(1) referencing of parent TagMap

Additional Notes

Stacked on #11789 (TagMap read-through mechanism) — review that first.

Level-split phase 1: the wiring. Attaches mergedTracerTags as a read-through parent at span build (gated on !mergedTracerTagsNeedsIntercept) instead of copying it into every span's storage. This is the change that actually activates read-through — #11789 alone is inert (parent == null).

Commits: config-version handling out of the trace-level bundle, the read-through flip (DDSpanContext.parentTags / CoreTracer copy-vs-share gate), and a TagMapReadThroughBenchmark.

🤖 Generated with Claude Code

@dd-octo-sts

dd-octo-stsBot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

SuiteStatus
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
ScenarioCandidatemasterΔ (95% CI of mean)
startup:insecure-bank:iast:Agent13.95 s14.01 s[-1.0%; +0.2%] (no difference)
startup:insecure-bank:tracing:Agent12.96 s12.98 s[-1.0%; +0.7%] (no difference)
startup:petclinic:appsec:Agent16.95 s16.86 s[-0.4%; +1.5%] (no difference)
startup:petclinic:iast:Agent16.99 s17.01 s[-0.9%; +0.6%] (no difference)
startup:petclinic:profiling:Agent16.82 s16.34 s[-1.6%; +7.5%] (no difference)
startup:petclinic:sca:Agent16.97 s16.64 s[+0.9%; +3.1%] (maybe worse)
startup:petclinic:tracing:Agent16.21 s16.12 s[-0.4%; +1.5%] (no difference)

Commit:3236edb3 · 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.

@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through-consumer branch 2 times, most recently from 7bb20f4 to e65e58fCompareJuly 13, 2026 18:24
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through branch from 3a0a318 to e692601CompareJuly 15, 2026 18:20
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through-consumer branch from e65e58f to 3b9156eCompareJuly 15, 2026 18:24
@datadog-datadog-prod-us1-2

This comment has been minimized.

dougqh added a commit that referenced this pull request Jul 15, 2026
StringIndex is a compact open-addressed string→index structure (the keyOf
substrate the dense tag store builds on): parallel hash/name arrays, linear
probing, on par with HashSet on lookup at a smaller footprint. Includes unit
tests, a footprint test (jol), and comparison benchmarks (vs HashSet/switch).
No TagMap changes — standalone util. Rebased onto the level-split stack
(consumer #11932) as the layer dense-store sits on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through-consumer branch from 3b9156e to d2aa48fCompareJuly 15, 2026 21:19
dougqh added a commit that referenced this pull request Jul 15, 2026
StringIndex is a compact open-addressed string→index structure (the keyOf
substrate the dense tag store builds on): parallel hash/name arrays, linear
probing, on par with HashSet on lookup at a smaller footprint. Includes unit
tests, a footprint test (jol), and comparison benchmarks (vs HashSet/switch).
No TagMap changes — standalone util. Rebased onto the level-split stack
(consumer #11932) as the layer dense-store sits on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through branch from e692601 to b58bcc0CompareJuly 20, 2026 13:59
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through-consumer branch from d2aa48f to 1f257aaCompareJuly 20, 2026 15:43
dougqh added a commit that referenced this pull request Jul 20, 2026
StringIndex is a compact open-addressed string→index structure (the keyOf
substrate the dense tag store builds on): parallel hash/name arrays, linear
probing, on par with HashSet on lookup at a smaller footprint. Includes unit
tests, a footprint test (jol), and comparison benchmarks (vs HashSet/switch).
No TagMap changes — standalone util. Rebased onto the level-split stack
(consumer #11932) as the layer dense-store sits on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqhdougqh changed the title Wire mergedTracerTags as a read-through parent at span build (level-split phase 1)Wire mergedTracerTags as a read-through parent at span build (level-split) (phase 1a)Jul 20, 2026
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through branch 2 times, most recently from bef20fa to 50cf53dCompareJuly 21, 2026 02:47
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through-consumer branch from 1f257aa to 3237b90CompareJuly 21, 2026 13:48
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through branch from 50cf53d to ae64f2aCompareJuly 21, 2026 14:50
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through-consumer branch from 3237b90 to 2225a42CompareJuly 21, 2026 14:51
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through branch from ae64f2a to 5cd8289CompareJuly 21, 2026 15:19
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through-consumer branch from 2225a42 to ea094ceCompareJuly 21, 2026 15:29
@dougqh
dougqh marked this pull request as ready for review July 21, 2026 16:03
@dougqh
dougqh requested a review from a team as a code ownerJuly 21, 2026 16:03
@dougqh
dougqh requested a review from mhliddJuly 21, 2026 16:03
@dd-octo-stsdd-octo-stsBot added the tag: ai generated Largely based on code generated by an AI or LLM label Jul 21, 2026

@chatgpt-codex-connectorchatgpt-codex-connectorBot 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:ea094ce0bc

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment threaddd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java Outdated

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

Datadog Autotest: PASS

More details

54 adversarial synthetic scenarios — simulating the full CoreTracer.buildSpan() wiring from the PR across 8 core behavioral equivalences and 5 edge cases — all passed. The read-through TagMap path is observationally identical to the old copy-down path for every tested scenario: trace-tag visibility, coreTags/contextualTags precedence override, builder-ledger tombstoning, multi-span parent isolation across config updates, and the version-exclusion change that prevents tombstone allocation on the shared frozen parent.

Was this helpful? React 👍 or 👎

📊 Validated against 54 scenarios · Open Bits AI session

🤖 Datadog Autotest · Commit ea094ce · What is Autotest? · Any feedback? Reach out in #autotest

@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through branch from 5cd8289 to ab56044CompareJuly 21, 2026 17:04
A fresh, mutable TagMap can read through to a frozen parent on local
misses, so a span can layer its own tags over a shared, immutable set
(e.g. merged tracer tags) without copying them.
- createFromParent(parent): the only way to attach a parent; the parent
must be frozen and is fixed at construction (no re-parenting), so
read-through can treat it as stable. Single-parent by design in phase 1.
- Reads resolve local-first, then the parent; a local entry shadows the
parent's (local-wins). Removing a parent key locally records a lazy
tombstone (removedFromParent) so it stops reading through; the tombstone
set is null until first needed, keeping the hot paths untouched.
- size()/isEmpty() are exact (Map contract) and resolve the parent;
isDefinitelyEmpty()/estimateSize() are the cheap conservative variants
for the hot path. copy() preserves the parent and tombstones; forEach
walks local then parent.
Built on the folded final-class TagMap (#11967); composes cleanly with the
null-tolerant Entry pathway (#11963).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through branch from ab56044 to 64c42e6CompareJuly 21, 2026 17:06
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through-consumer branch from ea094ce to 8c7bf14CompareJuly 21, 2026 17:13
…plit phase 1)
Attach the trace's merged tracer tags to each span's TagMap as a frozen
read-through parent (via TagMap.createFromParent) at span construction,
instead of copying them into every span. The span sees the shared tags on
read and only stores its own local tags, so the common trace-level bundle
is held once per trace rather than duplicated per span.
- CoreTracer builds the frozen merged-tracer-tags parent once; config
version is kept out of that bundle.
- DDSpanContext attaches the parent at construction (fixed, no re-parenting).
- Adds TagMapReadThroughBenchmark (copy-down vs read-through, -prof gc).
Stacked on the read-through mechanism (#11789), which builds on the folded
final-class TagMap (#11967).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through-consumer branch from 8c7bf14 to 170eedbCompareJuly 21, 2026 17:36
dougqh added a commit that referenced this pull request Jul 23, 2026
StringIndex is a compact open-addressed string→index structure (the keyOf
substrate the dense tag store builds on): parallel hash/name arrays, linear
probing, on par with HashSet on lookup at a smaller footprint. Includes unit
tests, a footprint test (jol), and comparison benchmarks (vs HashSet/switch).
No TagMap changes — standalone util. Rebased onto the level-split stack
(consumer #11932) as the layer dense-store sits on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dougqhand others added 2 commits July 22, 2026 20:15
…mall
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A live read-through parent is never definitely-empty: createFromParent
drops an empty parent, copy() only forwards an already-vetted parent, and
the parent is frozen so it can't become empty. So the size==0 /
no-tombstones branch of isEmpty() always returned false via the parent
chain walk — collapse it to a documented `return false`.
Addresses mcculls' review comment on #11789.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqh

Copy link
Copy Markdown
ContributorAuthor

Before / after: allocation drops ~130–200 B/op per span

Ran the front-half span-creation microbenchmark across this PR's commit range (64c42e6170eedb) with a production-shaped tracerdd.service/dd.env/dd.version set plus 6 global dd.tags, so mergedTracerTags is a realistically-sized shared bundle (env + 6 globals + runtime-id/language). That bundle is exactly what the read-through parent avoids cloning into every span, so it's the config under which this change's lever actually bites.

Allocation — gc.alloc.rate.norm (B/op, deterministic; primary signal)

ArmBeforeAfterΔ
bareStartSpan1056.0 ± 0.0896.0 ± 12.5−160.0 (−15.2%)
bareBuildSpan1049.6 ± 11.7930.7 ± 49.2−118.9 (−11.3%)
jdbcClientSpan1498.1 ± 42.31295.4 ± 44.8−202.7 (−13.5%)
webServerSpan1768.0 ± 12.51640.0 ± 0.0−128.0 (−7.2%)
webServerSpanViaBuilder1949.3 ± 41.71752.0 ± 0.0−197.3 (−10.1%)

The saving is ~130–200 B/op roughly flat across arms, not scaling with each span's own tag count — the signature of the level-split mechanism: it avoids cloning the fixed shared trace-level bundle, so every span saves about the same regardless of how many of its own tags it sets (even the "bare" arms drop ~150 B/op, since a tagless span still merges that bundle). All deltas clear their error bars.

Throughput — ops/us (directional corroboration only)

ArmBeforeAfterΔ
bareStartSpan5.30 ± 0.205.74 ± 0.52+8.4%
bareBuildSpan5.94 ± 0.315.77 ± 0.18−2.8%
jdbcClientSpan5.36 ± 0.145.48 ± 0.60+2.1%
webServerSpan4.24 ± 0.185.17 ± 0.24+22.0%
webServerSpanViaBuilder4.41 ± 0.094.98 ± 0.09+12.9%

Throughput agrees directionally (less allocation → less GC pressure). The trustworthy points are the two web arms — tight error bars, clearing their intervals decisively. bareBuildSpan's −2.8% sits inside the combined error, so it's flat, not a regression; the wide after-side bars on bareStartSpan/jdbcClientSpan are per-fork bimodality — don't over-read them.

Methodology
  • JMH: @Threads(8), @Fork(3), @Warmup(5), @Measurement(5), @BenchmarkMode(Throughput), -prof gc.
  • Front-half only: a no-op DropWriter drops finished traces so no serialization/agent I/O leaks into the alloc number.
  • Tracer config via @Fork jvmArgs (the real Config path, not hand-injected tags): -Ddd.service=petclinic -Ddd.env=staging -Ddd.version=1.2.3 -Ddd.tags=team:apm,dc:us1,cluster:prod-1,owner:tracing,tier:backend,region:us-east-1.
  • Commit range: 64c42e6 (read-through mechanism, parent of this PR's commit) → 170eedb (this PR — wires the parent in).
  • Primary signal is gc.alloc.rate.norm (deterministic B/op); throughput is directional-only (thermal/bimodality-fragile).

Note: with a bare (unconfigured) tracer this win is invisible — mergedTracerTags is near-empty, so there's nothing to avoid copying. Same code, the only variable is whether the tracer is configured like production.

@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

View session in Datadog

Bits Code status: ✅ Done

Comment @DataDog to request changes

Base automatically changed from dougqh/tagmap-read-through to masterAugust 17, 2026 20:22

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

Datadog Autotest: FAIL

Parent-backed TagMaps diverge from their flat predecessors in two paths: fillMap/fillStringMap omit inherited tracer tags, and a fully tombstoned ancestor chain reports non-empty. The first can silently strip configured tags from callers that materialize a span’s tag map.

Open Bits AI session

🤖 Datadog Autotest · Commit 0bbefb0 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

Comment threadinternal-api/src/main/java/datadog/trace/api/TagMap.java Outdated
Comment threadinternal-api/src/main/java/datadog/trace/api/TagMap.java
dougqhand others added 3 commits August 17, 2026 17:49
Both bulk-fill helpers walked only this.buckets, so a parent-backed
TagMap materialized through them dropped tags visible only via the
parent chain and ignored shadowing/tombstones -- unlike putAll, forEach,
and iteration, which already honor the read-through union.
Branch to the visible-union walk (forEach) when a parent is attached,
mirroring putAllOptimizedMap; keep the untouched local-only loop for the
common no-parent case so it pays zero overhead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
createFromParent used isDefinitelyEmpty(), which only checks each
level's local size and ignores shadowing/tombstones. With multi-level
read-through, a frozen intermediate can be observationally empty (no
local entries, every inherited key tombstoned) yet report
isDefinitelyEmpty() == false because a farther ancestor still holds
entries. Attaching such a parent then let a child take isEmpty()'s
no-tombstone fast path and return false while size(), lookup, and
iteration all report empty -- a Map-contract violation.
Use exact isEmpty() so semantically empty parents are dropped, which
also restores the invariant isEmpty()'s fast path relies on: an
attached parent always contributes at least one visible entry. The
hot path is unaffected -- isEmpty() short-circuits on size != 0, only
walking tombstones in the rare all-tombstoned case this fix targets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…through-consumer
# Conflicts:
#	internal-api/src/main/java/datadog/trace/api/TagMap.java
#	internal-api/src/test/java/datadog/trace/api/TagMapReadThroughTest.java
@dougqh
dougqh added this pull request to the merge queueAug 17, 2026
@dd-octo-sts

Copy link
Copy Markdown
Contributor

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351Bot commented Aug 17, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-08-17 22:57:25 UTC ℹ️ Start processing command /merge


2026-08-17 22:57:30 UTC ℹ️ MergeQueue: pull request added to the queue

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


2026-08-17 23:54:54 UTC ℹ️ MergeQueue: This merge request was merged

@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 17, 2026
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854dBot merged commit 8180203 into masterAug 17, 2026
601 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854dBot deleted the dougqh/tagmap-read-through-consumer branch August 17, 2026 23:54
@github-actionsgithub-actionsBot added this to the 1.66.0 milestone Aug 17, 2026
dougqh added a commit that referenced this pull request Aug 18, 2026
StringIndex is a compact open-addressed string→index structure (the keyOf
substrate the dense tag store builds on): parallel hash/name arrays, linear
probing, on par with HashSet on lookup at a smaller footprint. Includes unit
tests, a footprint test (jol), and comparison benchmarks (vs HashSet/switch).
No TagMap changes — standalone util. Rebased onto the level-split stack
(consumer #11932) as the layer dense-store sits on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: coreTracer coretag: ai generatedLargely based on code generated by an AI or LLMtype: refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@dougqh@mhlidd