Skip to content

Store known span tags densely, behind an experimental flag (phase 3b - dense tag storage) - #12856

Draft
dougqh wants to merge 32 commits into
dougqh/tag-registry-direction-resolutionfrom
dougqh/dense-store-v3
Draft

dougqh wants to merge 32 commits into
dougqh/tag-registry-direction-resolutionfrom
dougqh/dense-store-v3

Conversation

@dougqh

@dougqh dougqh commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What Does This Do

Adds an experimental dense store for known span tags, behind trace.experimental.dense.tags.enabled (off by default). With the flag on, known tags are no longer bucket Entry objects. Their values sit in insertion-ordered parallel arrays (knownIds / knownValues) with no per-tag Entry. Custom tags stay in the hash buckets.

Stacked on #12731 (direction resolution) and #12882 (slot coloring), and merges both. Retarget to master once they merge; until then their commits also show in this diff. This PR replaces the old dense-store drafts, which were built before the build-logic tag registry: #12045, #12046, #12047, #12056 and #11901.

Reading guide

  1. The split is mechanical. TagMap becomes abstract, with two final implementations:

    • HashTagMap (flag off) is today's map.
    • DenseTagMap (flag on) is the hybrid.

    TagMapFactory picks one from the flag. This is rollout scaffolding, as LegacyTagMap was: the hash-only map stays one flag away until the dense store is proven, and is folded away afterwards.

    • One implementation per JVM: EMPTY and every factory method go through the factory, so a JVM loads only one implementation and class hierarchy analysis devirtualizes every TagMap call. A forked test per mode checks that the other implementation never loads.
  2. DenseTagMap can be read on its own, as one unit of about 600 lines. The base class reaches dense state only through a small set of hooks: knownCount, countDenseVisibleIn, forEachDenseVisibleIn, readDenseIfVisible, and the routing overrides.

  3. Slots come from the registry (Color known tags into co-occurrence slots in their ids (phase 3a - groundwork - dense tag storage) #12882). The tag-registry generator colors tags that occur together on a span type into distinct slots; that's reviewed in Color known tags into co-occurrence slots in their ids (phase 3a - groundwork - dense tag storage) #12882, not here. Every known tag is dense. Tags on no modeled span type share occupancy bit 63. Presence is one long; the scan is authoritative, so tags that share a slot can coexist.

  4. Per-operation sizing. SizingHintTable (on FlatHashtable.D1) keeps a hint per operation, for entry spans and child spans separately, each lane capped at 512 operations. Each hint is a racy best-effort maximum: it presizes a span's dense arrays and tunes up as spans finish.

  5. Direction needs nothing here. peer.port is one tag with one id (Resolve direction-scoped OpenTelemetry names at the OpenTelemetry boundary (otlp - tag registry - phase 3) #12731), so it takes one slot. Only its OpenTelemetry name depends on the span's direction, and that is resolved when the span is read or serialized, not in the store.

Changes on the default path (flag off)

Motivation

Known tags are most of a span's tags, and each one costs an Entry. Storing them densely by id removes that allocation and makes setting a constant id cheap. This is part of the tag-id work, whose goal is that instrumentation sets tags by KnownTags.X_ID.

Measurements

Zulu 17, Apple M1 Max, JMH -prof gc, 3–4 forks, logging off. The details are in the commit messages.

flag off flag on
SetTagBenchmark known tag by id 13.8 ns, 48 B 9.6 ns, 0 B
SpanCreationBenchmark.webServerSpan 1856 B 1648 B (−12% vs base 1883–1909)
SpanCreationBenchmark.jdbcClientSpan 1464 B 1184 B (−20% vs base 1464–1501)
bareStartSpan (4 known tags) 992 B 1088–1096 B (+~110 B: small-span overhead, follow-up)
  • Span throughput is within noise in every configuration.
  • With the flag off, allocation and timing match the base.

Additional Notes

🤖 Generated with Claude Code

dougqh and others added 20 commits October 10, 2026 15:41
The generator now assigns each required/conditional/recommended tag a slot
in id bits [47-32] by graph coloring: tags resolved onto one concrete span
type, or both trace-level, get distinct slots; tags that never co-occur --
such as a tag declared per direction -- may share one. The rest get NO_SLOT
and stay hash-stored. The current conventions need 18 slots, within the one
occupancy long the dense store will use. KnownTagCodec gains slotOf,
hasSlot, slotCount, and the slot layout constants.

Nothing reads the slot yet; the dense store does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Selects TagMap's dense store for slotted known tags. Read once into a static
final so the JIT folds the dense branches away when it is off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A known tag with a dense-store slot is held in insertion-ordered parallel
arrays (knownIds/knownValues) with no per-tag Entry; custom tags and known
tags without a slot stay in the hash buckets. One occupancy long (1L << slot)
lets a definitely-absent tag skip the scan; the scan stays authoritative, so
two tags sharing a slot in one map still work. Whether a tag routes dense
depends only on its id and the flag, so the two regions are disjoint and
read-through shadows within a region (parentDenseVisible mirrors
parentEntryVisible). Iteration and forEach emit dense values through a reused
flyweight; point reads materialize an Entry. Buckets start on a shared empty
array and are copied on the first bucket write.

Ported from #12045 and #12046 onto today's TagMap: name canonicalization,
id-keyed setters, and per-direction shared-name lookups all route through the
same chokepoints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TagMapDenseForkedTest covers dense routing against the generated registry:
slotted vs unslotted vs custom tags, id-keyed and OpenTelemetry-name writes,
two tags sharing a slot in one map, iteration/forEach/entrySet (retain-safe
entries), the shared empty buckets, copy/clear/putAll/ledger/freeze, and
read-through (inheritance, shadowing, tombstones, multi-level chains).
TagMapDenseFuzzForkedTest replays random actions against a HashMap oracle
over slotted, unslotted, and custom keys. Both fork, since the flag is read
once per JVM.

DenseStoreAllocBenchmark: with the store on, a 12-tag map of slotted tags
allocates 232 B/op instead of 856; custom tags are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
putKnownValue built an Entry for the value it replaced, which set(long, ...)
then discarded; the 0 B/op on overwrite held only because C2's escape
analysis removed it. Split the store: storeKnownValue/storeDenseLocal return
the raw prior value (dense values are never null) and build nothing, and only
putDenseLocal -- the getAndSet path, which must return the prior entry --
materializes one. With -XX:-DoEscapeAnalysis, SetTagBenchmark's dense
overwrite drops from 48 to 0 B/op.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Internals.setKnown built an Entry and sent it through putEntry, whose dense
routing then built another for the replaced value: two throwaway allocations
on every span id write once the span moved to internals(). Check for a dense
id first and store straight into the dense arrays, as set(long, ...) does.
SetTagBenchmark with the store on: 0 B/op, and 16 B/op (just the Internals
view) with -XX:-DoEscapeAnalysis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the dense store on, every known tag now lives in the dense arrays --
denseIdOf means 'is a known id' -- so the id-keyed setters (set(long, ...)
and Internals.setKnown) never reach the hash path: with the flag a static
final, putEntry isn't compiled into them at all. The public set(long, ...)
checks isKnown itself, since the dense path skips requireKnownName, and the
dense write now applies the shared-name rule (an id write supersedes a value
held under the shared name).

The generator colors every tag resolved onto a modeled span type, whatever
its requirement level (24 slots today). Only tags on no span type -- the
overlay's routing-only keys and the CI tags of the unmodeled test type --
keep NO_SLOT; a long shift maps NO_SLOT to bit 63, which slots now stay below,
so slotBit needs no branch and those tags share that presence bit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
peer.port set by name before a span has a direction used to be stored as a
custom tag, so every id write of a direction's tag had to remove it from
the hash buckets. That pulled removeLocal into DDSpanContext.setTag(long, int)
and pushed it past C2's InlineSmallCode limit.

The registry now adds an undirected tag per shared Datadog name
(PEER_PORT_UNDIRECTED_ID). It is serialed after every declared tag and colored
into the same slot as its directional tags. Each directional id carries the
undirected serial at bits [30-16], so undirectedIdOf folds for a constant id.
In the dense store, a direction's write supersedes the undirected tag by
relabeling it in place during the scan it already does. The hash path removes
it by id. The span-level supersede calls are gone.

keyOf(String) still resolves a shared name to 0. The span's by-name setters
rely on that to take the directional path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A map holds each known tag at most once, so a full dense store grows
straight to KnownTagCodec.serialLimit() entries instead of doubling. Maps
start on shared empty arrays, so one full-capacity test covers a map's
first dense tag too. The first allocation, growth, tombstone clearing and
the frozen-map exception are now separate methods.

This change did not measurably shrink the compiled DDSpanContext.setTag in
SetTagMixBenchmark. In both the reused-span and fresh-span profiles, the
int overload's growth branch is already an uncommon trap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restore the factory design from before #11678 and #11967. TagMap becomes an
abstract class with two final subclasses: HashTagMap (dense store off) and
DenseTagMap (dense store on). TagMapFactory chooses one from the flag.
EMPTY and every factory method go through the factory, so a JVM loads only
one implementation and class hierarchy analysis devirtualizes TagMap calls.
A forked test per mode checks that the other implementation never loads.

This is rollout scaffolding, as LegacyTagMap was: the hash-only map stays
one flag away until the dense store is proven, and is then folded away.
The commit is structural only. The logic stays in TagMap; the next commit
moves the dense state into DenseTagMap.

The public no-arg constructor is removed; TagMap.create() is the intended
way to make a map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TagMap now holds only the hash-bucket store, read-through and views.
DenseTagMap holds the dense fields and helpers. It overrides the routing
methods (putEntry, getLocalEntry, removeLocal, the set(long, ...) overloads,
forEach and the bulk copies) to handle known ids and calls super for custom
tags. Shared walks over the parent chain reach dense state through
per-level hooks (knownCount, countDenseVisibleIn, forEachDenseVisibleIn,
readDenseIfVisible); in HashTagMap those return zero or do nothing.
DenseStore.ENABLED is now read only by TagMapFactory.

Internals.setKnown goes through setKnownTrusted, which skips the frozen check
because the core's span maps are never frozen. Every other rule
(tombstones, the shared-name supersede, routing) is unchanged. A new
Internals.getKnownValue reads a known tag's value with the same semantics
as getEntry(id).objectValue(), but builds no Entry for a local dense hit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DDSpanContext's primitive setTag(long, ...) overloads boxed the value twice
in their compiled code: once to hand it to the interceptor, through setBox,
and again inside Internals.setKnown when storing it. They now box once, and
pass the same box to the interceptor and to new boxed Internals.setKnown
overloads.

HashTagMap builds the tag's primitive-typed Entry around that box, so an
intercepted tag that is stored keeps its primitive type rather than
becoming ANY. DenseTagMap stores the box as is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entry's full constructor and the Internals and Ledger constructors were
private, so javac (targeting Java 8, without nestmates) gave each one a
synthetic access constructor taking the never-loaded class TagMap$1. C2
refuses to inline a call whose signature names an unloaded class
("unloaded signature classes"). So the Internals view the core creates
for each id write could not be scalar-replaced, and every Entry built
through that constructor went through a real call. Making the
constructors package-private removes the accessors and TagMap$1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entry's *2Prim helpers and requireKnownName, and TagMap.parentEntryVisible,
were private but used across nested classes, so javac generated access$
methods for them. Each one adds a level of inlining depth, and the *2Prim
helpers are on HashTagMap's typed id-write path. They are now
package-private. TagMapSyntheticAccessorTest fails if any class in the
TagMap family gains a synthetic accessor again. It is the prototype for
the @NoSyntheticAccess contract (APMLP-1976).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
putAllIntoEmptyMap copied the source's dense arrays at their full capacity.
A source that had grown to the bound would then hand every copy, including
each span's starting tags, a full-bound array. The copy now allocates the
source's count, with the initial capacity as a floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port the per-operation sizing hint from #12056 onto the split TagMap and
the current FlatHashtable.D1 API. SizingHintTable keeps two fixed
512-operation lanes, one for entry spans and one for child spans, each with
a capped overflow hint for once it is full. CoreTracer resolves the hint
when it builds a span context. DenseTagMap presizes its arrays from it, and
DDSpanContext records the span's known-tag count when the span is
serialized. Each hint is a racy best-effort max: a stale or lost update
only mis-sizes an array.

This changes two things from #12056. A fresh hint is seeded at 4, not 1:
the hint only grows, so a large seed would lock small spans into oversized
arrays. And dense growth now doubles, bounded by serialLimit(), instead of
jumping straight to the bound, so outgrowing a small seed costs one
doubling.

With the dense store off, hintFor folds to null, its tables are never
loaded, and span creation does no lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spans that are never serialized, such as those dropped by sampling, never
fed their known-tag count back. Their operation's hint stayed at the seed,
and each such span grew its dense arrays from 4 slots upward. The span now
records its size at finish too. Recording at serialization still captures
the tags added then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
putAllIntoEmptyMap always allocated new dense arrays. A span map presized
from its SizingHint therefore threw its hinted arrays away when its starting
tags were copied in (CoreTracer -> setAllTags -> putAll), and allocated
12-slot arrays instead: two allocations, the wrong size. The copy now goes
into the map's own arrays when they fit, and allocates only when they don't.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SpanCreationByIdBenchmark repeats SpanCreationBenchmark's tagged span
shapes (web server, JDBC client, and their SpanPrototype variants) but sets
each tag by its KnownTags id instead of its name. Same values, threads and
forks, so each arm compares directly with its namesake. peer.port uses its
directional id.

On Zulu 17, with 3 forks and debug logging off, setting tags by id matched
setting them by name within the error bars, in both throughput and
allocation, with the dense store on and off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Box-once (5e600f7) made every DDSpanContext.setTag(long, <primitive>)
box first and pass the box down. With the dense store off, HashTagMap then
kept that box in the tag's Entry, although the serializers read the
primitive. That was a retained allocation the hash path never had, for any
value outside the box cache (most status codes and ports, nearly all
longs, every float and double). The box escapes into interceptTag, which
is not inlined, so C2 cannot remove it.

TagMap.Internals.STORES_BOXES, a static final that is true only for the
dense store, chooses between the two shapes, and the check folds. The
dense store keeps box-once. The hash store goes back to boxing only on the
interception path and storing the primitive otherwise.
SetTagBenchmark gains a knownIntTag_byId_uncached arm, since the existing
arms only use cached values and could not see the allocation.

Also fixes stale documentation found by the techdebt review:
- an orphaned comment describing the removed setBox;
- comments that still said known tags without a slot are stored by hash
  (every known tag is dense now);
- comments that said sizing hints tune only at serialization (they also
  tune when a span finishes).
SizingHintTable.OVERFLOW_SEED now reads DenseTagMap.KNOWN_INIT_CAP
instead of repeating its value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dougqh dougqh added type: feature Enhancements and improvements comp: core Tracer core tag: performance Performance related changes tag: experimental Experimental changes tag: ai generated Largely based on code generated by an AI or LLM labels Oct 13, 2026
@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

🎯 Code Coverage (details)
• Patch Coverage: 83.48%
• Overall Coverage: 59.79% (+0.12%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 06419b4 | Docs | Give us feedback!

@dd-octo-sts

dd-octo-sts Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 13.94 s 13.89 s [-0.3%; +1.0%] (no difference)
startup:insecure-bank:tracing:Agent 12.89 s 12.92 s [-0.9%; +0.4%] (no difference)
startup:petclinic:appsec:Agent 17.02 s 16.99 s [-0.6%; +1.0%] (no difference)
startup:petclinic:iast:Agent 16.89 s 16.93 s [-1.2%; +0.6%] (no difference)
startup:petclinic:profiling:Agent 16.71 s 16.70 s [-1.0%; +1.1%] (no difference)
startup:petclinic:sca:Agent 17.02 s 16.88 s [+0.0%; +1.6%] (maybe worse)
startup:petclinic:tracing:Agent 16.09 s 16.12 s [-1.0%; +0.7%] (no difference)

Commit: 06419b4a · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

dougqh and others added 5 commits October 14, 2026 00:01
Brings master (#12715 merged, Spotless 8.10.4), #12817, and #12731's
direction-neutral peer.port id. The dense store moves onto that model:

- peer.port is one tag with one id, so the undirected id (e7a8721),
  the UNDIRECTED and SHARED_NAME bits, and the dense store's in-place
  relabel are gone. Bit 0 is #12731's HAS_SCOPED_ALIAS.
- Slot coloring is re-applied on #12731's one-tag-per-name registry:
  a name declared per direction is one vertex.
- Tests and benchmarks use PEER_PORT_ID; the undirected-model tests are
  dropped in favor of #12731's SpanTags and directional span tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings master, including #12814, and the carve-bit reordering
(HAS_SCOPED_ALIAS, INTERCEPTED, LEVEL_TRACE: bits 0, 1, 2 in order).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
resolveSplitByTagsPrecedence checked language through the custom-tag
overload, which only matches names that resolve to no known tag.
language is known, so the check was always false and a prototype's
language was never re-applied over another split-by-tags candidate the
prototype set. Check and read it by LANGUAGE_ID, as for component.

Found by Codex, via a review comment from AlexeyKuznetsov-DD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tag-registry generator now assigns each known tag a slot in id bits
[47-32] by graph coloring: each concrete span type's resolved tags, and
the trace-level tags, form a clique and get distinct slots. So
1L << slotOf(id) is an exact presence bit for a span's tags, and one
long holds them all. Tags on no modeled span type get NO_SLOT, which a
long shift maps onto bit 63; real slots are capped below it.

KnownTagCodec gains SLOT_SHIFT, NO_SLOT, slotOf, hasSlot and slotCount.
Nothing in the tracer reads slots yet: this is split out of the dense
store (#12856) so the store, the span builder's tag ledger and a
map-level routing summary can each build on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dougqh and others added 2 commits October 14, 2026 22:02
The tag-slot coloring moves into its own PR, #12882. Where both had it,
this takes #12882's wording, keeps #12731's HAS_SCOPED_ALIAS and serial
range check in encode, and names DenseTagMap as the slots' reader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ForEachContextBenchmark is a local scratch benchmark that the previous
merge picked up; it isn't part of this PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread internal-api/src/main/java/datadog/trace/api/SizingHintTable.java Outdated
@dougqh dougqh changed the title Store known span tags densely, behind an experimental flag (phase 2 - dense tag storage) Store known span tags densely, behind an experimental flag (phase 3 - dense tag storage) Oct 15, 2026
@dougqh dougqh changed the title Store known span tags densely, behind an experimental flag (phase 3 - dense tag storage) Store known span tags densely, behind an experimental flag (phase 3b - dense tag storage) Oct 15, 2026
Comment thread dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java Outdated
void theTagMapFamilyHasNoSyntheticAccessors() {
List<Class<?>> classes = new ArrayList<>();
for (Class<?> root : Arrays.asList(TagMap.class)) {
for (Class<?> root :

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To Claude - another PR is adding a general mechanism for this -- maybe we should adopt that here, too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. #12862's SyntheticAccessors.assertNone is more thorough: it also finds anonymous and local classes. When it lands, I'll switch in #12817, where TagMapSyntheticAccessorTest started, to @NoSyntheticAccessors on TagMap, HashTagMap, DenseTagMap and the factories. Each top-level class is its own nest, so each gets a one-line test. Leaving this open until then.

dougqh and others added 2 commits October 14, 2026 23:32
CoreTracer was its only caller outside datadog.trace.api. It now looks a
hint up through SizingHint.forOperation, on the type it already holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each primitive setTag(long, ...) repeated the same three branches. They
now box only when the box is needed -- the store keeps boxes
(STORES_BOXES, folded) or the tag may be intercepted -- and pass it to
one shared setKnownBox; otherwise they store the primitive. That keeps a
single boxing site per setter and the hash store's unboxed plain path.

setTag(long, int) drops from 195 to 76 bytecode bytes. SetTagBenchmark's
int and peer.port arms are unchanged in time and allocation with the
dense store off and on, and C2 still inlines the setter at its call
sites (no too-big refusals in either version).

The typed boxed Internals.setKnown and setKnownTrusted overloads, used
only by the old setters, go: an intercepted-then-stored primitive on the
hash store is now an ANY entry, which resolves to the same type on read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Initial capacity of the dense arrays when no {@link SizingHint} presized them; the largest span
* type resolves about this many tags.
*/
static final int KNOWN_INIT_CAP = 12;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To Claude - I don't think that's true anymore -- maybe we can source this value from KnownTagsCodec.

dougqh and others added 3 commits October 15, 2026 00:05
TagMapFactory computed INSTANCE in its own static initializer from a
subclass's INSTANCE. Initializing a factory subclass first initializes
TagMapFactory, which then read that subclass's INSTANCE while it was
still null; TagMap's initializer later failed on it, poisoning TagMap
for the JVM's life. And a factory that built its empty map in its own
initializer started TagMap's initializer mid-way, so TagMap.EMPTY could
read a still-null cached empty map.

- The JVM's factory moves to a holder, TagMapFactory.Default.INSTANCE;
  TagMapFactory has no static state.
- TagMap.EMPTY is built fresh by newEmpty(). The JVM's factory returns
  TagMap.EMPTY from empty(); only the other store (tests) caches its own.

Nothing in production initializes a factory before TagMap today; the
parameterized tests did. TagMapFactoryInitOrderForkedTest initializes
both factories first, in a fresh JVM: it fails before this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dense-store flag picks one TagMap implementation per JVM, so
TagMapTest, TagMapReadThroughTest, TagMapFuzzTest and TagMapSetByIdTest
only ever exercised HashTagMap. A TagMapType test enum (HASH, DENSE)
builds maps through each store's own factory, as TagMapType once did for
the optimized and legacy maps, and those suites now run against both
(TagMapTest 112 -> 215 tests, ReadThrough 51 -> 102).

- copy() now creates through the map's own factory (TagMap.factory()),
  so a copy keeps its implementation instead of taking the JVM's
  default store.
- createFromParent's read-through checks take an explicit factory
  (package-private), which TagMapType uses.
- 22 of TagMapDenseForkedTest's 24 tests didn't need the flag; they move
  to DenseTagMapTest in the normal test JVM. The forked test keeps the
  two that only the flag decides: that HashTagMap never loads, and that
  a ledger builds a dense map.
- TagMapFuzzTest's PutAllTagMap builds its source through the test
  map's own store, so the two never mix stores.

No test fails for one store and not the other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6015cbb routed createFromParent(TagMap, SizingHint) through the new
factory-taking overload, so on the span-creation path the factory was a
parameter instead of a static final constant receiver, behind an extra
call level. With the dense store off, SpanCreationBenchmark allocated
24 more bytes per span (jdbcClientSpan 1,472 -> 1,496 B,
webServerSpan 1,840 -> 1,864 B).

The production path calls TagMapFactory.Default.INSTANCE.create(...)
directly again; both overloads share the read-through checks in
readThroughParent. Both arms are back to 1,472 and 1,840 B (webServerSpan
in 6 of 6 forks; one earlier 2-fork run still had one fork at 1,864).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dougqh added a commit that referenced this pull request Oct 15, 2026
The tag-registry generator now assigns each known tag a slot in id bits
[47-32] by graph coloring: each concrete span type's resolved tags, and
the trace-level tags, form a clique and get distinct slots. So
1L << slotOf(id) is an exact presence bit for a span's tags, and one
long holds them all. Tags on no modeled span type get NO_SLOT, which a
long shift maps onto bit 63; real slots are capped below it.

KnownTagCodec gains SLOT_SHIFT, NO_SLOT, slotOf, hasSlot and slotCount.
Nothing in the tracer reads slots yet: this is split out of the dense
store (#12856) so the store, the span builder's tag ledger and a
map-level routing summary can each build on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dougqh added a commit that referenced this pull request Oct 16, 2026
- TagMap.setResolved now calls set(tagId, ...) for a known id, as
  set(String, ...) already does, so every name-keyed span write takes the
  id path. Same result on today's store; a store that keeps known tags
  without an Entry per tag no longer allocates one here.
- TagMap gains a package-private getKnownValue(long), exposed to the core
  as Internals.getKnownValue. DirectionalTags' id reads and
  KnownTagCodec.directionOf use it instead of getEntry(id).objectValue().

Both pulled forward from the dense store work (#12856) so it overrides
them rather than reworking these call sites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dougqh added a commit that referenced this pull request Oct 16, 2026
The tag-registry generator now assigns each known tag a slot in id bits
[47-32] by graph coloring: each concrete span type's resolved tags, and
the trace-level tags, form a clique and get distinct slots. So
1L << slotOf(id) is an exact presence bit for a span's tags, and one
long holds them all. Tags on no modeled span type get NO_SLOT, which a
long shift maps onto bit 63; real slots are capped below it.

KnownTagCodec gains SLOT_SHIFT, NO_SLOT, slotOf, hasSlot and slotCount.
Nothing in the tracer reads slots yet: this is split out of the dense
store (#12856) so the store, the span builder's tag ledger and a
map-level routing summary can each build on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: core Tracer core tag: ai generated Largely based on code generated by an AI or LLM tag: experimental Experimental changes tag: performance Performance related changes type: feature Enhancements and improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant