Repository navigation
Conversation
Contributor
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
dougqh
force-pushed
the
dougqh/prototype-fast-path
branch
from
October 23, 2026 04:24
2df973a to
0a4ba91
Compare
dougqh
changed the base branch from
dougqh/tagmap-split-stores
to
dougqh/tagmap-dense-id-store
October 23, 2026 04:24
bric3
added this pull request to stack #13019
October 23, 2026 16:02
Turns the dense tag store on by default (trace.experimental.dense.tags.enabled=false switches back to the hash store) and makes it cheaper: - Each span's dense store is presized from a per-operation sizing hint (SizingHint / SizingHintTable: a bounded, racy table keyed by operation name, with separate lanes for entry and child spans). The hint lives on the DenseIdStore, so flag-off spans carry no extra field. A span records its size when it's serialized, or, in a dropped trace, in CoreTracer.write after the early post-processors and the root's sampling decision have added their tags. - The dense store's write paths resolve the slot index first and store the value once, so C2 expands one reference-store barrier per path. - A SpanPrototype's known tags are seeded in bulk (IdStore.seedAbsent) when the source's occupancy words share no bit with the target's; otherwise seeding falls back to tag by tag. The decision is cached on the prototype, along with its sizing hint. - singleSpanBuilder(SpanPrototype, ...) mirrors the plain entry points. - HashStore and IdStore get a public forEach, for the prototype paths. TraceShapeBenchmark records each span's size before dropping the trace, as serialization would, so the hints converge in the benchmark too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dougqh
force-pushed
the
dougqh/prototype-fast-path
branch
from
October 23, 2026 16:57
0a4ba91 to
1877eec
Compare
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 dense tag store (#13004), mostly for that store, which this PR also turns on by default (
trace.experimental.dense.tags.enabled=falseswitches back to the hash store). With the flag off, spans allocate exactly what #13004'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
Whole-trace allocation (
TraceShapeBenchmark, from #12992). Each op builds one web-request-shaped trace: a server root, five jdbc prepared-statement children and a@Tracespan, all finished, with ports and status outsideInteger's box cache. Bytes per trace (7 spans), JDK 17,-prof gc, single-threaded, error under ±1 B:legacy: every tag by namenormal: the calls today's decorators makenewApi: prototype at construction, tags by idnewApi−464 B).Earlier measurements. The numbers below are from
SpanCreationBenchmarkbefore the above was found. It also drops traces, so the hints there may not have converged; treat its byte figures as superseded by the whole-trace table. The throughput comparisons between entry points still hold, since every arm ran under the same condition.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;Where this is going
This PR is one step in replacing the span's single
TagMapwith storage that knows each tag's kind. The goal is to cut what every span allocates on the application thread, without changing behavior.The end state
TagMapbecomes a thin view for code outside the span.Entryobject per tag, and each span's store is presized from a per-operation hint. With the flag on, a web-request-shaped trace allocates about 30% less on the application thread than on master.SpanStorage, not aTagMap. That covers post-processors now and serializers later, so noTagMapview gets built for them.SpanStoragehas its own verbs,loadandstore, apart fromTagMap'sget/setand the span's interceptedgetTag/setTag, soDDSpanContextcan offer both. It starts with four methods and grows id, typed-load and primitive-store variants. Later, an intercepted counterpart (thesetTagvocabulary) becomes the typesetTagsFrom,TagContributorandTagExtractortake, with one lock per batch.Later, not in this stack: cheaper read-through to parent spans, trace-level tags in a per-trace holder (
TraceSegment), and primitive tag values without boxing.The stack. This replaces #12918, split into pieces that can each be reviewed, and measured, on their own.
TagMap. This is the stack's only behavior change, kept separate on purpose.TagMap, and addTraceShapeBenchmarkSpanStoragefor the eager post-processorsTagMap's bucket mechanics intoHashSupport(a pure move)TagMapa facade over the two storestrace.experimental.dense.tags.enabled(off by default)Every PR after #12999 is behavior-preserving.
The pieces' allocation figures come from a deterministic per-thread probe (
ThreadMXBeanallocated bytes) that builds the same 7-span trace asTraceShapeBenchmark. The #12992 and #12926 figures come fromTraceShapeBenchmarkitself.🤖 Generated with Claude Code