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 2 commits into
Conversation
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. The enabler for level-split phase 1 (a span no longer copies the shared trace-level tags down per span). Single-parent by design in phase 1 (anti-false-generalization); written so generalizing to multiple flattened parents is additive. Inert when parent == null (every existing map), so no behavior change off the read-through path. - Read path: getEntry falls through to the parent on a local miss (get*/containsKey inherit it). isDefinitelyEmpty()/estimateSize() added as cheap conservative/upper- bound variants; isEmpty()/size() stay exact (Map contract) and resolve the union. - Removal: a lazily-allocated removedFromParent side-set (also the gate) tombstones a parent-exposed key so it no longer reads through; re-setting clears it. Entry and BucketGroup stay untouched (the side-set is shape-agnostic vs the bare/group duality). - Bulk reads (forEach x3, iterators, collection views): bucket-aligned self-vs-parent merge, first-occurrence-wins, exploiting universal hashing so the shadow check is scoped to the same-index local bucket (no re-hash, no global seen-set). Alloc-free. - checkIntegrity asserts the local emptiness invariant (size==0), not union isEmpty(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
copy() went through putAllIntoEmptyMap, which clones only the local buckets and the local size — so a copy of a read-through map dropped the inherited parent tags and the tombstones. Fix copy() to share the (frozen, immutable) parent and copy the tombstone set, so the copy is observationally identical to the original (same union) and independently mutable. Adds behavior-identical-to-flat-map tests: copy equivalence, independent mutability, tombstone preservation, equality with an equivalent flat map, and immutableCopy of a read-through map. This is the safety contract for flipping mergedTracerTags to a parent in the consumer change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🟢 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 ( |
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