Skip to content

Add TagMap read-through support - trace / span tag split mechanism (phase 1a) - #11789

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

Add TagMap read-through support - trace / span tag split mechanism (phase 1a)#11789
gh-worker-dd-mergequeue-cf854d[bot] merged 7 commits into
masterfrom
dougqh/tagmap-read-through

Conversation

@dougqh

@dougqhdougqh commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

What Does This Do

Adds read-through support to TagMap - currently unused

A TagMap can now be constructed via createFromParent(TagMap) to resolve map queries against a frozen parent TagMap. The plan is to use this to separate trace-level tags from span-level tags without any semantic changes.

Motivation

When applied to the tracer core will enable lighter and faster span construction by removing some map-to-map copying.

Additional Notes

From Claude...

Pure mechanism — inert until a consumer attaches a parent. No map has a parent yet (parent == null for every existing map), so there is no behavior change off the read-through path. The consumer wiring lands in a stacked follow-up.

A child TagMap references a frozen parent (createFromParent) and reads through to it on a local miss, while local entries shadow the parent. This is the enabler for level-split phase 1 — a span will stop copying the shared trace-level tags (mergedTracerTags) down into every span; reads route through instead.

Design

  • Single parent by design: the consumer needs one parent (mergedTracerTags). Written so generalizing to multiple flattened parents is additive (the bulk walk is already bucket-aligned, the degenerate single-parent case of the multi-parent merge).
  • Removal via a lazy removedFromParent side-set, not inline tombstones. Inline-in-buckets kept breaking on the bare-Entry-vs-BucketGroup duality (re-type, per-group bitfield + single-Entry gap, two bitfields — all awkward). The side-set is shape-agnostic, keeps Entry/BucketGroupcompletely untouched, and the lazy null field doubles as the gate. Tombstones are rare (only when a parent-exposed key is removed).
  • Bulk reads via a bucket-aligned merge: exploits universal hashing (fixed-size 16-bucket table, no resize) so a parent entry's shadow check is scoped to the same-index local bucket, reusing the entry's cached hash — no re-hash, no global seen-set, no Set/BloomFilter. forEach (×3) stays alloc-free; IteratorBase does a two-phase local-then-parent walk so iterator/entrySet/keySet/values/stream all emit the deduped union.

What's covered

  • Read path: getEntry fall-through (the whole get*/containsKey family inherits it). isDefinitelyEmpty() + estimateSize() added as cheap conservative/upper-bound variants (mirroring Ledger); isEmpty()/size() stay exact (Map contract) and resolve the union.
  • Removal: tombstone a parent-exposed key; re-setting clears it; remove() returns the prior visible value (Map contract holds via read-through).
  • Bulk: forEach/iterators/collection views all emit the deduped, first-occurrence-wins union, skipping shadowed/tombstoned parent entries.
  • copy() preserves read-through (shares the frozen parent + copies tombstones) — was dropping both.
  • Behavior-identical-to-flat-map tests are the safety contract for the consumer flip.

TagMapReadThroughTest covers the read path, removal/tombstoning, the deduped bulk union, and copy() preservation; the full TagMap* suite stays green (inert when parent == null).

Deferred / follow-ups

  • Inlining baseline for get/set across the parent != null branch — the perf-gate before marking this ready.
  • Consumer PR (stacked): the !needsIntercept-gated mergedTracerTags → createFromParent wiring. Includes the removeTag(VERSION) cleanup — config version moves out of the read-through bundle so the existing InternalTagsAdder conditional-add works unchanged and no per-span tombstone is minted (the apply-then-remove dance is vestigial; read-through keeps config-in-parent / manual-in-local). Plus an audit of any other per-span parent-key removals and of read-through maps as a merge/clone source (only copy() handled here).
  • Multi-parent + the cross-parent dedup clause — additive, with the dream.

tag: ai generated · pure mechanism, no behavior change until a consumer attaches a parent.

🤖 Generated with Claude Code

@dougqhdougqh added comp: core Tracer core tag: ai generated Largely based on code generated by an AI or LLM tag: no release notes Changes to exclude from release notes type: feature Enhancements and improvements labels Jun 29, 2026
@dd-octo-sts

dd-octo-stsBot commented Jun 29, 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:Agent14.79 s14.65 s[+0.2%; +1.7%] (maybe worse)
startup:insecure-bank:tracing:Agent13.61 s13.61 s[-0.9%; +0.9%] (no difference)
startup:petclinic:appsec:Agent16.83 s17.36 s[-7.5%; +1.4%] (no difference)
startup:petclinic:iast:Agent17.41 s17.00 s[-2.1%; +7.0%] (no difference)
startup:petclinic:profiling:Agent17.34 s17.32 s[-0.9%; +1.1%] (no difference)
startup:petclinic:sca:Agent17.37 s17.33 s[-0.7%; +1.2%] (no difference)
startup:petclinic:tracing:Agent16.55 s16.65 s[-1.7%; +0.5%] (no difference)

Commit:706437d0 · 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

Copy link
Copy Markdown
ContributorAuthor

Inlining gate — off-path (parent == null) reads: parity confirmed

Ran -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (no-fork, -f0) on UnsynchronizedMapBenchmark.get_tagMap / iterate_tagMap_forEach, master vs this branch, to verify the getEntry refactor + forEach parent loop don't regress hot-read inlining for the common parent == null case (every existing map).

Reads — parity, both fully inline hot:

getEntrydecision
mastermonolith, 90 Binline (hot)
branchgetEntry 51 B → getLocalEntry 24 B → findInBucket 45 Ball inline (hot)

Same fully-inlined read — the branch inlines a 120 B chain of tiny methods where master inlines a 90 B monolith (+30 B for the parent != null check + framing, all far under FreqInlineSize 325). Cold-site "callee is too large" is identical on both (> the 35 B cold cutoff). So the split is inlining-neutral.

forEach: the read-through parent loop was extracted to per-variant forEachParent(...) (called only when parent != null), so off-path forEach is back to ~90 B (it had grown to 242 B inline) and compiles as its own loop unit exactly as before; the parent-loop code is out of line and dead when parent == null.

Writes: descoped — setTag → TagInterceptor → getAndSet is already a non-inlinable big method due to the interceptor cascade, so a write-side inlining measurement is confounded. The one gated removedFromParent != null field-check is noise against that; meaningful write-path inlining waits on interceptor retirement.

Not yet measured: the with-parent hot path (read-through active) — there's no benchmark with a parent attached yet. That belongs on the consumer PR's span-level -prof gc benchmark, which also demonstrates the per-span allocation win.

@dougqh

Copy link
Copy Markdown
ContributorAuthor

Measured read-through win (quiet box, firmed)

TagMapReadThroughBenchmark (on the stacked consumer branch, where the wiring lands) models span-build tag assembly — copyDown (today: putAll the frozen trace-level bundle + span tags) vs readThrough (attach the bundle as a read-through parent, span tags local) — swept by trace-bundle size. 5 forks, @Threads(8), -prof gc, 25 measurements/point.

traceTagCountcopyDown allocreadThrough allocsavedcopyDown thrptreadThrough thrptspeedup
3360 B/op312 B/op−13%170.8M ops/s213.7M1.25×
7 (realistic)456 B/op312 B/op−32%133.0M ops/s215.0M1.62×
15600 B/op312 B/op−48%101.1M ops/s207.3M2.05×

The headline is the shape, not a single number:

  • read-through is invariant to bundle size — 312 B/op and ~210M ops/s flat across 3/7/15 trace tags. A span no longer pays anything for the trace-level bundle; its cost is O(its own tags).
  • copyDown grows with the bundle (more BucketGroup clones + collisions to copy), so the win scales with mergedTracerTags size: −13%/1.25× at 3 tags → −48%/2.05× at 15.

Reliability: alloc deterministic (±0.001 over 25 samples); throughput tight (±2–5M).

Honest attribution / scope: the alloc delta is bucket structure (BucketGroup clones + fewer local collisions), notEntry objects — putAll-into-empty already shares the frozen entries. Bucket structures are ~3% of tracer alloc, so absolute macro impact at roomy heap is modest; the win matters most under GC pressure and on the app/request thread (the throughput column). The structural point is the invariance — read-through decouples per-span cost from trace-bundle size, which is the level-split thesis (trace tags paid once, not per span).

Comment threadinternal-api/src/main/java/datadog/trace/api/TagMap.java Outdated
Comment threadinternal-api/src/main/java/datadog/trace/api/TagMap.java Outdated
Comment threadinternal-api/src/main/java/datadog/trace/api/TagMap.java Outdated
Comment threadinternal-api/src/main/java/datadog/trace/api/TagMap.java Outdated
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through branch from 3a0a318 to e692601CompareJuly 15, 2026 18:20
@dougqh
dougqh changed the base branch from master to dougqh/fold-optimized-tagmapJuly 15, 2026 18:20
dougqh added a commit that referenced this pull request Jul 15, 2026
…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>
@datadog-datadog-prod-us1-2

This comment has been minimized.

dougqh added a commit that referenced this pull request Jul 15, 2026
…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/fold-optimized-tagmap branch from 6abe29d to 0737ca6CompareJuly 16, 2026 16:56
@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through branch from e692601 to b58bcc0CompareJuly 20, 2026 13:59
dougqh added a commit that referenced this pull request Jul 20, 2026
…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>
Base automatically changed from dougqh/fold-optimized-tagmap to masterJuly 20, 2026 17:41
@dougqhdougqh changed the title Add TagMap read-through support (level-split phase 1 mechanism)Add TagMap read-through support (level-split mechanism) (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
dougqh marked this pull request as ready for review July 21, 2026 02:48
@dougqh
dougqh requested a review from a team as a code ownerJuly 21, 2026 02:48
@dougqh
dougqh requested a review from mccullsJuly 21, 2026 02:48
@dd-octo-sts

Copy link
Copy Markdown
Contributor

Hi! 👋 Thanks for your pull request! 🎉

To help us review it, please make sure to:

  • Remove the issue linking keyword

If you need help, please check our contributing guidelines.

@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

This PR adds read-through support to TagMap, enabling span-level maps to read through a frozen parent (trace-level) map on local misses. The mechanism is inert when no parent is attached (parent == null), ensuring no behavioral change for existing code. Comprehensive testing verifies read-through correctness, tombstone handling, iterator deduplication, and equivalence to flat-map semantics. No production bugs detected.

Was this helpful? React 👍 or 👎

Open Bits AI session

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

@dougqh
dougqhforce-pushed the dougqh/tagmap-read-through branch from ae64f2a to 5cd8289CompareJuly 21, 2026 15:19
dougqh added a commit that referenced this pull request Jul 21, 2026
…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 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 added a commit that referenced this pull request Jul 21, 2026
…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 added a commit that referenced this pull request Jul 21, 2026
…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>
Comment threadinternal-api/src/main/java/datadog/trace/api/TagMap.java Outdated
Comment threadinternal-api/src/main/java/datadog/trace/api/TagMap.java Outdated
…mall
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment threadinternal-api/src/main/java/datadog/trace/api/TagMap.java Outdated
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>
@dougqhdougqh added type: refactoring and removed type: feature Enhancements and improvements labels Jul 24, 2026
@dougqh

Copy link
Copy Markdown
ContributorAuthor

Span-creation allocation benchmark — do-no-harm gate

This PR adds the read-through mechanism to TagMap but doesn't wire it into the span-creation path, so the bar here is no regression on the existing (parentless) paths. Ran SpanCreationBenchmark before/after, isolated to just this PR's 3 commits on one lineage (before = parent of the first commit, after = PR head — not the stale merge-base).

gc.alloc.rate.norm (B/op) — primary signal

ArmbeforeafterΔ B/opΔ%
bareStartSpan904.0 ±0.0897.1 ±12.5−6.9−0.8%
bareBuildSpan924.3 ±47.9930.7 ±49.2+6.4+0.7%
jdbcClientSpan1290.7 ±29.21298.7 ±29.2+8.0+0.6%
webServerSpan1586.7 ±41.71618.7 ±41.7+32.0+2.0%
webServerSpanViaBuilder1688.0 ±12.51730.7 ±41.7+42.7+2.5%

Verdict: flat within noise — do-no-harm pass. Every delta sits inside overlapping confidence intervals; none is a statistically defensible regression.

Two reasons it reads as variance, not a real cost:

  • Not systematic. A real cost would come from TagMap gaining ~2 ref fields (~16 B/op) and would show on every TagMap-allocating arm. Instead the bare arms are flat (one dropped), and the tagged deltas don't scale with tag count (jdbc sets more tags than web but moved less: +8 vs +32).
  • Wide error bars = per-fork inlining bimodality — what @Fork(3) exists to expose (bareBuild is ±48 on both sides; viaBuilder's error swung 12→42 between runs). The point estimates ride fork-to-fork JIT variance.

Throughput (directional only, laptop): mostly up on the after side (+1.6%…+6.5%), one arm down — no regression signal.

Methodology

SpanCreationBenchmark trimmed to the drift-stable arms (no SpanPrototype, which doesn't exist at these commits). @Threads(8) @Fork(3) @Warmup(5) @Measurement(5), -prof gc, finished against a no-op DropWriter so only front-half (create/tag/finish) allocation is measured. Read gc.alloc.rate.norm (deterministic) as the signal; throughput is thermal/bimodality-fragile.

@dougqhdougqh changed the title Add TagMap read-through support (level-split mechanism) (phase 1a)Add TagMap read-through support - trace / span tag split mechanism (phase 1a)Jul 28, 2026
@dougqh
dougqh enabled auto-merge August 17, 2026 13:17
@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 19:10:48 UTC ℹ️ Start processing command /merge


2026-08-17 19:10:52 UTC ℹ️ MergeQueue: pull request added to the queue

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


2026-08-17 20:22:14 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 b7c65cb into masterAug 17, 2026
601 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854dBot deleted the dougqh/tagmap-read-through branch August 17, 2026 20:22
@github-actionsgithub-actionsBot added this to the 1.66.0 milestone Aug 17, 2026
gh-worker-dd-mergequeue-cf854dBot pushed a commit that referenced this pull request Aug 17, 2026
…plit) (phase 1a) (#11932)
Add TagMap read-through support (level-split phase 1 mechanism)
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>
Wire mergedTracerTags as a read-through parent at span build (level-split 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>
Mark parent field @VisibleForTesting and size the tombstone HashSet small
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Collapse redundant isDefinitelyEmpty() call in isEmpty()
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>
Merge branch 'dougqh/tagmap-read-through' into dougqh/tagmap-read-through-consumer
Merge branch 'master' into dougqh/tagmap-read-through
Merge branch 'master' into dougqh/tagmap-read-through
Merge branch 'dougqh/tagmap-read-through' into dougqh/tagmap-read-through-consumer
Merge branch 'master' into dougqh/tagmap-read-through
Merge branch 'dougqh/tagmap-read-through' into dougqh/tagmap-read-through-consumer
Make TagMap fillMap/fillStringMap read-through aware
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>
Drop observationally-empty read-through parents (exact isEmpty)
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>
Merge remote-tracking branch 'origin/master' into dougqh/tagmap-read-through-consumer
# Conflicts:
#	internal-api/src/main/java/datadog/trace/api/TagMap.java
#	internal-api/src/test/java/datadog/trace/api/TagMapReadThroughTest.java
Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
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 LLMtag: no release notesChanges to exclude from release notestype: refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@dougqh@mcculls