Add TagMap read-through support (level-split mechanism) (phase 1a)#11789
Add TagMap read-through support (level-split mechanism) (phase 1a)#11789dougqh wants to merge 1 commit into
Conversation
🟢 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. |
Inlining gate — off-path (
|
getEntry |
decision | |
|---|---|---|
| master | monolith, 90 B | inline (hot) |
| branch | getEntry 51 B → getLocalEntry 24 B → findInBucket 45 B |
all 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.
Measured read-through win (quiet box, firmed)
The headline is the shape, not a single number:
Reliability: alloc deterministic (±0.001 over 25 samples); throughput tight (±2–5M). Honest attribution / scope: the alloc delta is bucket structure ( |
3a0a318 to
e692601
Compare
…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 <[email protected]>
|
🎯 Code Coverage (details) 🔗 Commit SHA: 64c42e6 | Docs | Datadog PR Page | Give us feedback! |
…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 <[email protected]>
6abe29d to
0737ca6
Compare
e692601 to
b58bcc0
Compare
…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 <[email protected]>
bef20fa to
50cf53d
Compare
|
Hi! 👋 Thanks for your pull request! 🎉 To help us review it, please make sure to:
If you need help, please check our contributing guidelines. |
There was a problem hiding this comment.
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.
🤖 Datadog Autotest · Commit 50cf53d · What is Autotest? · Any feedback? Reach out in #autotest
There was a problem hiding this comment.
💡 Codex Review
dd-trace-java/internal-api/src/main/java/datadog/trace/api/TagMap.java
Lines 1884 to 1885 in 50cf53d
When clear() is called on a TagMap created with a non-empty parent, these lines only wipe the local buckets and leave the read-through parent visible. In that scenario map.clear(); map.isEmpty() is still false and get() continues returning parent tags, which violates the Map.clear() contract and can resurrect inherited tags after callers believe they removed every mapping.
dd-trace-java/internal-api/src/main/java/datadog/trace/api/TagMap.java
Lines 1417 to 1418 in 50cf53d
When the source map is a read-through TagMap, this optimized path copies only that.buckets/that.size, so entries that are visible solely through that.parent are silently skipped; for example TagMap.create().putAll(childFromParent) loses all inherited tags even though Map.putAll should add every visible mapping from the source. The optimized path needs to iterate the read-through union or fall back to the normal entrySet() path when that has a parent/tombstones.
ℹ️ 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".
| if (parent != null && !parent.frozen) { | ||
| throw new IllegalStateException("read-through parent must be frozen"); | ||
| } | ||
| return new TagMap(parent); |
There was a problem hiding this comment.
Reject or flatten read-through parents
createFromParent accepts any frozen TagMap, including one produced by freezing or immutableCopy() on a read-through child, but the new bulk paths only scan this.parent.buckets while getEntry() recurses through ancestors. With a grandparent tag, grandChild.get("a") succeeds while size(), forEach(), keySet(), and iterators omit it, so either nested parents need to be rejected here or the union walkers must traverse the full parent chain.
Useful? React with 👍 / 👎.
| if (this.removedFromParent != null) { | ||
| this.removedFromParent.remove(newEntry.tag); |
There was a problem hiding this comment.
Return the inherited value when overwriting parent tags
With a read-through child, overwriting a key that exists only in the parent reaches this path, inserts a new local entry, and returns null because no local entry was replaced. That makes put("a", ...), getAndSet("a", ...), and default replace calls report that no prior value existed even though get("a") returned the parent value immediately before, violating Map.put semantics for inherited tags.
Useful? React with 👍 / 👎.
…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 <[email protected]>
50cf53d to
ae64f2a
Compare
…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 <[email protected]>
ae64f2a to
5cd8289
Compare
…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 <[email protected]>
5cd8289 to
ab56044
Compare
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 <[email protected]>
ab56044 to
64c42e6
Compare
…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 <[email protected]>
…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 <[email protected]>
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 apply 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 == nullfor 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
TagMapreferences 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
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).removedFromParentside-set, not inline tombstones. Inline-in-buckets kept breaking on the bare-Entry-vs-BucketGroupduality (re-type, per-group bitfield + single-Entry gap, two bitfields — all awkward). The side-set is shape-agnostic, keepsEntry/BucketGroupcompletely untouched, and the lazy null field doubles as the gate. Tombstones are rare (only when a parent-exposed key is removed).Set/BloomFilter.forEach(×3) stays alloc-free;IteratorBasedoes a two-phase local-then-parent walk soiterator/entrySet/keySet/values/streamall emit the deduped union.What's covered
getEntryfall-through (the wholeget*/containsKeyfamily inherits it).isDefinitelyEmpty()+estimateSize()added as cheap conservative/upper-bound variants (mirroringLedger);isEmpty()/size()stay exact (Mapcontract) and resolve the union.remove()returns the prior visible value (Map contract holds via read-through).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.TagMapReadThroughTestcovers the read path, removal/tombstoning, the deduped bulk union, andcopy()preservation; the fullTagMap*suite stays green (inert whenparent == null).Deferred / follow-ups
get/setacross theparent != nullbranch — the perf-gate before marking this ready.!needsIntercept-gatedmergedTracerTags → createFromParentwiring. Includes theremoveTag(VERSION)cleanup — config version moves out of the read-through bundle so the existingInternalTagsAdderconditional-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 (onlycopy()handled here).tag: ai generated· pure mechanism, no behavior change until a consumer attaches a parent.🤖 Generated with Claude Code