Add TagMap read-through support (level-split phase 1 mechanism)#11789
Add TagMap read-through support (level-split phase 1 mechanism)#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 ( |
| } | ||
|
|
||
| // read-through: emit parent entries not shadowed locally or tombstoned (bucket-aligned merge) | ||
| OptimizedTagMap p = this.parent; |
There was a problem hiding this comment.
I'm not fond of the initial variable names, so can we just use parent instead.
I don't mind the member variable shadowing.
| Object parentBucket = parentBuckets[i]; | ||
| Object localBucket = thisBuckets[i]; | ||
| if (parentBucket instanceof Entry) { | ||
| Entry pe = (Entry) parentBucket; |
There was a problem hiding this comment.
I'd prefer parentEntry rather than pe
| Entry pe = (Entry) parentBucket; | ||
| if (parentEntryVisibleInBucket(localBucket, pe)) consumer.accept(pe); | ||
| } else if (parentBucket instanceof BucketGroup) { | ||
| for (BucketGroup g = (BucketGroup) parentBucket; g != null; g = g.prev) { |
There was a problem hiding this comment.
I'd prefer cuGroup rather than g
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>
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 <noreply@anthropic.com>
|
🎯 Code Coverage (details) 🔗 Commit SHA: e692601 | 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 <noreply@anthropic.com>
Mechanism PR — read-through for
OptimizedTagMap(single parent)Adds optional read-through to
OptimizedTagMap: a child map references a frozen parent (withParent) 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.This PR is 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.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 slices 1–4; the fullTagMap*suite stays green (inert whenparent == null).Deferred / follow-ups
get/setacross theparent != nullbranch — the perf-gate before marking this ready.!needsIntercept-gatedmergedTracerTags → withParentwiring. 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