Repository navigation
Conversation
store and getAndPut wrote values[i] on the overwrite path and again in appendSlot on the append path. Under G1 on JDK 17 and earlier C2 expands each reference-store site's write barriers inline, roughly 250 bytes apiece, and these paths inline into every setTag. Both now resolve the index first (appendSlot reserves the slot and returns it) and store the value once; the tombstone path uses reserveSlot the same way. On fresh spans this alone trims appendSlot (2560 -> 2240 bytes of compiled code) and the by-name setters by about 64-128 bytes; most of the remaining size comes from grow() running on every span's first tag, which the next commit addresses with sizing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ports #12856's SizingHint / SizingHintTable onto the store split. The tracer looks up a hint by operation name (entry and child lanes, bounded, racy by design) and the span's DenseIdStore allocates its arrays at the hint's size when it is created. Spans record their size back when their trace is written -- after the early post-processors and the root's sampling decision have added their tags -- and when serialized. Without it every fresh span grew its store on its first tag, so C2 inlined grow() and its two allocations into DenseIdStore.store (2816 B of compiled code), over InlineSmallCode, and every setTag called store out of line ("already compiled into a big method"). With the hints converged grow() is cold, store compiles to 768 B and inlines again: on fresh spans, dense on, JDK 17, setTag(long, int) is 1792 B and setTag(long, CharSequence) 1408 B, under the 2500 B limit. Recording at trace write rather than at span finish matters: a hint taken at finish misses the sampling tags, so every later span grew its store from the hint to twice that. Dense on, single-threaded SpanCreationBenchmark: realistic spans allocate 24-196 B less per span (jdbc -196, web -96); throughput is unchanged except bare spans (~4% slower, the per-span hint lookup). With the dense store off the hint is null and the lookup folds away. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the dense store, a prototype's known tags are now copied into a span at once instead of one at a time through isPresent, the interceptor and routePut: - IdStore.seedAbsent appends a frozen, parentless, tombstone-free dense source with two array copies when its occupancy words share no bit with the target's live, tombstoned or inherited words, per level: then none of its tags can already be present, so "absent only" holds without a per-tag check. Otherwise it copies nothing and the caller seeds tag by tag. The hash store always declines. - TagInterceptor.canSeedInBulk: no known tag needs the interceptor except span.kind, whose one effect -- the cached kind ordinal -- the seed sets directly. A split-by-tags span.kind counts as a hazard. - The plan (bulk or not, the kind ordinal, and the interceptor it was checked against) is cached on the SpanPrototype on first use, racy and immutable. Custom prototype tags are always seeded one at a time. SpanPrototypeConstructionDenseForkedTest reruns the prototype tests with the dense store on, plus the fallback for a span that already has one of the prototype's tags. The TagStoresTest cases for seedAbsent also caught a bug from the single-store commit (now fixed there): the tombstone path indexed this.values before reserveSlot could replace it. Measured gain is small today: prototypes carry two or three known tags, so single-threaded prototype span creation is within about +-1%. It grows with prototypes that carry more constants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The span held its SizingHint in a new DDSpanContext field, so with the dense store off -- the default -- every span carried an always-null reference: DDSpanContext grew from 160 to 168 bytes (+8 B per span in the 8-thread SpanCreationBenchmark, hash store). The hint now lives on the DenseIdStore it presized, and recordSize() takes no argument: the span asks its store, the hash store does nothing. DDSpanContext is back to 160 bytes; DenseIdStore grows from 72 to 80, paid only with the dense store on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A span built from a SpanPrototype that uses the prototype's own operation name now takes its sizing hint from the prototype, which resolves it once per lane (entry, child) and caches it, so those spans skip the SizingHintTable lookup. A span given a different operation name still gets that name's hint from the table: hints stay keyed by operation name, so the cache never shares a hint the table wouldn't. SpanPrototypeConstructionDenseForkedTest now turns the dense store on with @WithConfig. Its static System.setProperty was cleared by the test extensions before Config read it, so the class had been running on the hash store (where the prototype tests pass too); theDenseStoreIsOn now guards that. Adds a test for the cached hint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The prototype entry points now mirror the plain ones: startSpan (builder-free), buildSpan (a builder for any number of spans) and singleSpanBuilder, which hands out the thread's reusable builder carrying the prototype, so it doesn't allocate. The builder drops the prototype on reset, so it can't leak into the thread's next span. AgentTracer's default seeds identity only, like buildSpan's. Adds jdbcClientSpanViaPrototypeSingleSpanBuilder to SpanCreationBenchmark, next to the buildSpan and startSpan variants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
applyOverwriting -- what BaseDecorator.afterStart does to every span today -- stamped the prototype's tags one at a time through the interceptor. Overwriting a tag that isn't there is just appending it, so when the masks prove none of the prototype's known tags is on the span (the usual case right after startSpan) they are now copied in at once with IdStore.seedAbsent, and span.kind's ordinal is set directly, like the construction-time seed. A conflict falls back to overwriting tag by tag; custom tags always go through the interceptor. Adds tests for stamping after start and for overwriting a tag the span already has (both stores), and webServerSpanRetrofit / jdbcClientSpanRetrofit to SpanCreationBenchmark: startSpan by name, then applyOverwriting, as decorators do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Flag off, every decorated span took the tag lock twice: applyOverwriting tried the bulk path (the plan said bulk), whose seedAbsent always fails on the hash store, then locked again for its fallback. The seed plan is now bulk only when the store can seed in bulk (IdStore.supportsBulkSeed, true on DenseIdStore), so the default path skips the attempt and its lock. Guarded by a test on both stores. - Published spans record their size when serialized, with every tag present; CoreTracer.write records only the traces it drops, so kept traces no longer walk their spans for it on the application thread. - seedPrototypeTags and overwriteInBulk share seedKnownInBulk; the intercept-or-put step is putIntercepted, also used by setAllTags. - IdStoreFactory.create(parent) and DenseIdStore(parent) delegate to their hinted forms; IdStore.create(TagMap) likewise; grow() delegates to growTo. - The hint lookup is one call: NONE has no operation name, so SpanPrototype.sizingHint already falls through to the table. - Docs: the orphaned needsInterceptCustomTag Javadoc is reattached, apply's stale "will land here" paragraph points to seedPrototypeTags, the hint-less DDSpanContext constructor is marked for tests and benchmarks, and a comment in SizingHintTable is rewrapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HashStore's and IdStore's forEach overloads are public, so the prototype paths in DDSpanContext walk a prototype's custom store directly instead of iterating its whole map and skipping known tags. Adds a test for a prototype carrying a custom tag, seeded at construction and overwritten after start (both stores). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Contributor
🟢 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. |
This was referenced Oct 19, 2026
Closed
Clean merge; the IdStore contract test and DDSpanContextStoresTest pass here too, with this branch's sizing and bulk seed in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # internal-api/src/main/java/datadog/trace/api/DenseIdStore.java
With per-operation sizing hints presizing each span's store, the dense store is the default here, so CI and benchmarks exercise it. Set trace.experimental.dense.tags.enabled=false to use the hash store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Does This Do
Optimizations on top of the store split (#12918), mostly for the dense tag store (
trace.experimental.dense.tags.enabled, off by default). With the flag off, spans allocate exactly what #12918's do.SizingHint/SizingHintTable: a bounded, racy table of hints, keyed by operation name, with separate lanes for entry spans and child spans.Where the hint lives: on the span's
DenseIdStore, not onDDSpanContext, so flag-off spans carry no extra field.When spans record their size:
CoreTracer.write, after the early post-processors and the root's sampling decision have added their tags.Recording at span finish would miss those late tags, and every later span of the operation would grow its store to twice the hint.
storeandgetAndPutresolve the slot index first, then store the value once. On JDK 17 and earlier, C2 expands each reference-store site's G1 write barriers inline, at about 250 B apiece.SpanPrototype's known tags in bulk. The new methodIdStore.seedAbsentappends a frozen, parentless, dense source with two array copies, provided the source's occupancy words share no bit with the target's live, tombstoned or inherited words, checked per level. If they share none, no prototype tag can already be present, so "absent only" holds without a per-tag check. If they share any, it copies nothing and the caller falls back to seeding tag by tag.span.kind, andspan.kind's ordinal, which is set directly.applyOverwriting, the pathBaseDecorator.afterStarttakes today.singleSpanBuilder(SpanPrototype, …), so the prototype entry points mirror the plain ones (startSpan,buildSpan,singleSpanBuilder).forEachonHashStoreandIdStore, so the prototype paths can walk a prototype's custom store directly.Motivation
Without sizing, every fresh span grew its dense store on its first tag. C2 then inlined
grow()and its two allocations intoDenseIdStore.store, which compiled to 2816 B, overInlineSmallCode, so everysetTagcalledstoreout of line. With the hints converged,grow()is cold:DenseIdStore.storeDDSpanContext.setTag(long, int)DDSpanContext.setTag(long, CharSequence)The id-keyed setters now compile under the 2500 B limit with the store inlined, which C2 needs before it can coarsen the locks in a decorator's run of
setTagcalls.Measurements
SpanCreationBenchmarkon an M1 Max with Zulu 17,-Xms2g -Xmx2g, 4 forks, 5 × 5 s iterations,-prof gc. Experiment jars carried aLongAdderfix forSpanMetricsImpl's counters, which isn't part of this PR; without it, counter contention dominates at 8 threads.Bytes per span, dense on: jdbc-shaped spans drop by −130 to −196 B (about 12%) at 1 to 8 threads. Web-shaped spans barely change, because they already fit their first allocation. Flag off, bytes are identical to #12918.
Throughput, dense on, vs. #12918: flat. Single-threaded it's within ±1%, except bare spans, which are about 4% slower from the per-span hint lookup. With the
LongAdderfix, this branch is ahead in 13 of 16 cells at 1 to 8 threads (+0.5% to +4.5%), but only one cell clears the error bars. Scaling to 8 threads is limited by per-trace costs outside the tag store, so the byte saving doesn't show up as throughput yet.Prototype entry points (jdbc, dense on, with the
LongAdderfix, ops/µs):setTagsstartSpan+applyOverwriting(decorators today)buildSpan(prototype)singleSpanBuilder(prototype)startSpan(prototype)singleSpanBuilder(prototype)measures slower thanbuildSpan(prototype), whose builder escape analysis already removes. It's kept for API symmetry.Additional Notes
/techdebtand/perf-reviewran over these commits, and their findings are addressed. One was a real default-path regression: an extra lock acquisition inapplyOverwritingwith the flag off, which now has a test guarding it. One remains open: the hint only ever grows, so one unusually large span presizes every later span of its operation. That needs a benchmark with mixed span sizes before deciding on decay.SizingHintTable{,HashOnly}ForkedTest;seedAbsentcases inTagStoresTest;SpanPrototypeConstructionDenseForkedTestreruns the suite with the dense store on and asserts that it is.SpanMetricsImplcounters toLongAdder: +28% at 8 threads in these runs, a separate small PR off master;🤖 Generated with Claude Code