Repository navigation
Conversation
Adds id-keyed setters, getters and removal to TagMap, TagMap.Entry and spans, so a writer that knows the tag skips the name lookup. Squashed from the review history of #12715. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
span.setTag(0L, "") or a null value reached TagMap.getAndRemove(long), which rejects unknown ids, so clearing threw IllegalArgumentException while every id-keyed setter ignores an unknown id. removeTag(long) now ignores it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The long-valued setters prechecked with needsIntercept, but when the interceptor declined the tag they discarded the box it was given and stored the primitive. Follow the same precheckIntercept -> setBox shape as the other primitive setters, so TagMap keeps the box. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
peer.port is declared per direction in the registry (#12713), so it has no direction-free id: BaseDecorator, shared by client and server decorators, sets it by name, and the set-by-id tests use an unshared int tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Set-path routing is a per-language concern, so it lives in a Java overlay next to tag-conventions.yaml rather than in the language-agnostic conventions. The overlay declares the keys that exist only to be routed (resource.name, error, sampling directives, ...) and lists every tag TagInterceptor may route. Each listed tag's id carries the INTERCEPTED bit (bit 1), so a setter called with a constant id can fold the interception test away. Serial numbers become public so TagInterceptor can switch on them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TagInterceptor switches on the tag's serial rather than its name, so a known tag is matched under any of its names and the *_OTEL_NAME cases go away. needsIntercept(long) tests the INTERCEPTED bit first, which folds away for a constant id. split-by-tags entries resolve to serials at construction (each direction for a name declared per direction); only custom tags are still matched by name. A test checks that exactly the tags carrying the bit have a case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A name setter resolves a known tag's name to its id once and sets it as the id-keyed setters do; only a custom tag is still handled by name. The id-keyed setters no longer look up the name to feed the interceptor, so with a constant id the interception test folds to the INTERCEPTED bit. Entry paths (builder ledger, prototypes, default tags) route by the entry's id and precheck before boxing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The generated nameOf switched over every tag's serial: 491 bytes of bytecode, too big for C2 to inline, so each id-keyed set paid two out-of-line calls into it (the span's unknown-id guard and the entry's name). Read a NAMES_BY_SERIAL array instead; nameOf is now 23 bytes and inlines at every caller. Also adds SetTagBenchmark (span setTag by constant id, non-constant id, name, and custom name, against a bare and a synchronized TagMap store). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The table is always allocated and never resized, so the split check -- the only run-time check left on a non-intercepted constant-id set -- is a single load, with no null or length test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The span's unknown-id guard called nameOf; KnownTagCodec.isKnown answers the same question from the serial and a generated SERIAL_LIMIT, so it folds away for a constant id. Also note that serials are not stable across releases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DDSpanContext.setTag calls interceptTag only for intercepted tags. Kept out of line (over FreqInlineSize, 325 bytes of bytecode), it never brings the profiled handler bodies into setTag's compiled code, which would push setTag past InlineSmallCode and stop callers inlining it -- the inlining a constant id needs to fold its interception test. A test now fails if the switch shrinks below the limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An overlay tag exists only to be routed, so the generator now marks it intercepted without a second listing; intercepted: lists only the tags declared in tag-conventions.yaml that the tracer also routes. Also restore the doc comment and @Suppress that the overlay helper displaced, read the serial limit one way, inline isUnknownTag, and drop an unused default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tag declared once per direction (peer.port) has two ids but one shared Datadog name, and keyOf resolves that name to neither. An entry keyed by one of those ids hashed by the id while name-based access hashed the name as a custom tag, so get/remove/containsKey by name missed it and a later set by name produced a duplicate key. Encode a SHARED_NAME flag in the reserved id bit 0 so the check folds for a constant id. TagMap's id-keyed create/get/remove reject such ids, and DDSpanContext's id-keyed setters ignore them, as they do unknown ids, until name resolution knows the span's direction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entry's package-private name factories are now custom-only: they skip the registry and assert the name is not a known tag. Callers that start from a name resolve it once -- TagMap.set(String, ...) forwards a known tag to set(long, ...), while getAndSet, put, Ledger.set, putAll and the public Entry.create(String, ...) API go through anyEntryFor and friends -- so a custom tag pays one registry lookup instead of two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builds on the registry directions (#12713) and id-keyed setters (#12715, included here): KnownTagCodec resolves names by direction, TagMap finds a shared name's tag whichever direction it holds, and DDSpanContext resolves direction-dependent names by the span's kind. Squashed from the review history of #12731. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#12817 rejected shared-name ids (peer.port inbound/outbound) on the id paths until name resolution could see a span's direction. With direction resolution in place, accept them again: a read or removal by the shared name already checks each direction's id. To keep one entry per tag, a write by the shared name stores under whichever direction's tag the map holds (otherwise under the name, which the span re-keys once it has a direction), and a write by id drops a value held under the name alone. isKeyableById goes; the SHARED_NAME bit stays and keeps the new check foldable for a constant id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # .github/CODEOWNERS
TagMap.internals() returns a small Internals view whose setKnown(...) skips the id validation the checked set(long, ...) repeats; DDSpanContext's id-keyed setters, which have already checked isKeyableById, now use it. Internals is created per call, so escape analysis removes it. internals() is marked with a new @restricted(allowedIn = ...) annotation, and gradle/forbiddenApiFilters/instrumentation.txt bans it in instrumentation modules, so calling it there fails forbiddenApisMain. Internals is also where the batched setTagsFrom/TagSink writes will go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It exists to be created, used within the expression, and scalar-replaced; storing one would force a real allocation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
peer.port gets one direction-neutral id (PEER_PORT_ID); only its OpenTelemetry name (client.port inbound, server.port outbound) depends on the span's direction. The generator merges per-direction declarations into one tag, and the SHARED_NAME bit and shared-name sentinel go away. Direction-scoped OpenTelemetry names (server.address, client.port, ...) are stored as written. SpanTags resolves them by the rule (name, direction) -> tag when a span reads or removes a tag, with the tag winning over its scoped name. processTagsAndBaggage then folds them into their tags once the kind is final. This removes the unresolvedDirectionalTags deferral and re-keying from DDSpanContext, and with them: - bulk/builder writes bypassing resolution - name-vs-id hash mismatch and parent shadowing in TagMap - Entry equals/hashCode disagreement (equals now compares tagHash) - the race between a deferred write and the first kind - alias write order and kind changes not re-keying Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OTLP JSON and protobuf writers compute the span's direction once from its span.kind (SpanTags.directionOf) and name each tag for it, so peer.hostname exports as server.address on a client span, and network.client.ip as network.peer.address on a server span. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no ordering state (a TagMap has no insertion order): - Once the span has a kind, an alias write (server.address on a client span) is stored as its tag, overwriting it. - Before then, the alias is stored as written. A write of a tag that has an alias removes every pending alias that could denote it, so a pending alias still present is the newer write, and it wins on read and when processTagsAndBaggage folds it into the tag. Every DDSpanContext write path (by name, by id, by entry, and in bulk) goes through SpanTags.prepareWrite. Names that don't depend on direction cost one small map probe; ids that don't, one array load. The accepted corner (a pre-kind write of either possible tag drops a pending alias) is documented in SpanTags and DDSpanContext. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
748a8af added a map probe to every write by name and routed bulk copies entry by entry. Now: - The generator sets a HAS_SCOPED_ALIAS bit (bit 0) on each tag with a direction-scoped OpenTelemetry name (peer.hostname, http.hostname, network.client.ip, peer.port). A mask tests it, and that folds away for a constant id. - A write by name does one lookup, KnownTagCodec.keyOrAliasOf, which returns the id (bit included) or the existing alias sentinel. It then stores through the new TagMap.setResolved without a second lookup. Only ids with the bit, or the alias sentinel, go through SpanTags. - Bulk copies use putAll again. needsIntercept(TagMap), computed once per tracer-level map, is also true for a map with a direction-dependent name. Per-entry paths test the id bit, or a 64-bit hash filter of the aliases for a custom entry. The last-write-wins semantics and their tests are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- isPresent reads the reader's own tagId instead of resolving the name again. - createFromParent delegates to readThroughParent, which now carries the exact-emptiness comment; TagMap(TagMap) uses IdStore.create and HashStore.inheritedFrom; nameStoreForWrite uses HashStore.create. - HashSupport.checkBuckets indexes a bucket group's entries by j, not the bucket index i, so the integrity check really inspects groups (copied as-is from the old TagMap.checkIntegrity). - DDSpanContext's constructors drop the tagsSize parameter, which nothing used once the span owned its stores, and CoreTracer no longer computes it per span (that included a size() walking the read-through parent). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only its own test called it; the span reads values from its stores directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- isPresent moves above setTag(String, Object)'s Javadoc, which it had been inserted under, so setTag is documented again. - A leftover constructor doc stacked on HashStore.inheritedFrom's is removed. - TagMap.EMPTY's comment described the old length-1 bucket array; it now points at IdStoreFactory, which explains why building EMPTY is safe during class initialization. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Span-level and trace-level tags are colored independently, so their slot numbers overlap, and a local root span holds both in one store. With one occupancy word, a trace-level tag (runtime-id) made the slot of an unrelated span-level tag (http.method) look occupied, so a miss on the root span fell through to a scan: GetTagBenchmark's missing known tag read was 6.1 ns dense against 3.2 ns hash. IdStore keeps spanOccupancy and traceOccupancy and picks the word by the tag's level (a select that folds for a constant id). Tombstones keep one shared mask with their own level set. Costs 8 bytes per store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0f1e5acd2a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| while (level != null) { | ||
| for (++this.index; this.index < level.used; ++this.index) { | ||
| if (this.leaf.readIfVisible(level, this.index, this)) { | ||
| return this; |
There was a problem hiding this comment.
Return stable readers from the public iterator
When DD_TRACE_EXPERIMENTAL_DENSE_TAGS_ENABLED is enabled, TagMap.iterator() exposes this cursor directly, and every advance returns the same mutable DenseCursor. Consumers that retain iterator results—such as collecting map.stream() into a list—therefore receive multiple aliases that all report the last visited tag and value, unlike the stable entries returned in hash mode. Materialize a stable reader or Entry at the public iterator boundary.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
👍 Good point, the reuse is problematic. I'll address that.
| if (value == null) { | ||
| return null; |
There was a problem hiding this comment.
Preserve null mappings in the dense store
When dense tags are enabled, writing a known key with a null value through the Map API—for example TagMap.put(name, null) or TagMap.fromMap(...)—stores a slot and increments the map size, but this method then treats that stored value as if the key were absent. The resulting map can report size() == 1 while containsKey(name) is false, and removal returns null despite deleting the slot; hash mode previously retained the null mapping. Use a distinct null-value sentinel or reject null before mutating the store.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Two critical correctness gaps exist in the dense-storage TagMap implementation: concurrent reads on a frozen map share a mutable reader, allowing tag values to bleed across threads; and stored null values are indistinguishable from absent entries, breaking containsKey, getOrDefault, and removal semantics relative to hash mode. A third issue affects sequential traversal: the iterator exposes a single reused cursor, so buffering or collecting the stream loses all but the final entry.
🤖 Bits Code Review · Commit 0f1e5ac
| DenseIdStore level = this.level; | ||
| while (level != null) { | ||
| for (++this.index; this.index < level.used; ++this.index) { | ||
| if (this.leaf.readIfVisible(level, this.index, this)) { |
There was a problem hiding this comment.
Keep public traversal entries stable in dense mode
With dense storage enabled, TagMap.iterator() exposes the same mutable cursor for every known tag. Collecting a two-tag stream into a list therefore retains two references to the final tag, and buffering operations such as sorted() lose earlier entries. Materialize stable entries at public traversal boundaries, including the iterator and related consumer paths, while keeping flyweights internal.
React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it
What each reaction means
- 👍 Useful: correct, and you're fixing it or will.
- 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
- 😕 Unclear: you can't tell what it means or why it matters.
- 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.
🤖 Bits Code Review · Open Bits AI session
|
|
||
| /** The flyweight over {@code value}, or null when there is none. */ | ||
| private EntryReader readerOf(long tagId, Object value) { | ||
| if (value == null) { |
There was a problem hiding this comment.
Distinguish null-valued mappings from absent tags
With dense storage enabled, TagMap.put("http.route", null) stores and counts the mapping, but readerOf() treats its null value as absence. The map consequently reports size 1 and exposes the entry while containsKey("http.route") returns false, unlike the base and hash implementations. Preserve presence separately from the value throughout lookup, replacement, and removal handling.
React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it
What each reaction means
- 👍 Useful: correct, and you're fixing it or will.
- 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
- 😕 Unclear: you can't tell what it means or why it matters.
- 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.
🤖 Bits Code Review · Open Bits AI session
…ution Clean textual merge. Also formats TagRegistryGeneratorTest's holder helper, which build-logic's spotless flagged (the root spotlessApply skips build-logic). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-split-stores #12882 landed on master squashed, so this branch's copy of its earlier coloring commits is replaced by the merged version: - TagRegistry.kt and TagRegistryGeneratorTest.kt: master's (this branch only carried the older coloring there). - KnownTagCodec: keeps this branch's notes (bit 63 kept clear, trace-level tags colored separately) and master's slotOf wording on overflow sharing. - TagStoresTest: the span-level/trace-level slot test no longer assumes runtime-id and http.method share a slot (the merged coloring orders tags differently); it finds a span-level tag in runtime-id's slot at run time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Patch coverage was 75%, mostly DenseIdStore: the dense store is behind a flag that is off by default, so the shared TagMap suites only reached the hash store and the dense store ran in one small forked test. - IdStoreContractTest runs the IdStore contract against HashIdStore and DenseIdStore in the normal test JVM: typed setters and readers, put and getAndPut priors, removing and reviving inherited tags, size and emptiness through parents, every iteration form, copy, putAllLocal (including colliding buckets), the map fills, growth, multi-level reads and id validation. - DDSpanContextStoresTest covers the span's routing: custom tags by name with each value type, primitive sets by id (intercepted, declined and plain), setMetric with known names, map writes, and the custom store borrowed from the tracer-level tags until the span's first custom write. - The by-name setters' custom branches write straight to the custom store: they already know keyOf(tag) is 0, so going through routeSet repeated the lookup. routeSet's boolean overloads lost their last callers and are removed. Local JaCoCo patch coverage (vs #12731): 75% -> 98%. The rest is mostly checkIntegrity's error branches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A reader whose entry() is null (only possible from an EntryReader outside the tracer) made the span's direct store routes throw, where TagMap.set treats it as a no-op. HashIdStore.put/getAndPut and the custom branch of routePut now skip it; DenseIdStore, which never builds an Entry, skips a reader with nothing to store. Tested on both stores and through DDSpanContext.setTag / setMetric. - makeTagId(serial) built only serial << 48, without slot and flag bits: for a slotted tag that id passed isKnown but differed from the KnownTags constant, so a hash store filed it under the partial id. It now returns the real id from the generated serial-to-id table; a serial outside the known range still gets a serial-only id that isn't known. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DenseIdStore answered every point read with one reader cached on the store. Frozen stores are shared between threads (a span prototype's tags, the tracer-level tags), so concurrent TagMap.getEntry calls could overwrite each other's reader before copying it: a new test reproduces it, one thread reading _dd.base_service as _dd.git.commit.sha. - IdStore.getEntry(long) returns an Entry the caller may keep and shares no state: HashIdStore returns its stored Entry (getEntryReader now delegates to it), DenseIdStore builds one from the value. TagMap.getEntry uses it. - DenseIdStore.getEntryReader returns a fresh reader per call. - The store's reused reader now only answers writes' prior values: a store being written has a single writer, while a shared frozen store is only read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- OtelSpanBuilder, OpenTelemetry14DirectionalAttributesTest, and KnownTagCodec.directionalKeyOf: master's versions, carrying #12731's late fixes (held attributes resolved at setSpanKind, null keys ignored). - tag-conventions.yaml: #12893 already declares process_id and _dd.trace_span_attribute_schema as required local-root trace tags, so this branch's duplicate declarations go; _dd.agent_psr joins that group. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
The two most critical issues are: (1) in OtelSpanBuilder, directional attributes are not consistently deferred and re-resolved against the final span kind, so interleaved setAttribute/setSpanKind calls produce stale or wrong attribute mappings at startSpan(); (2) in DenseIdStore, concurrent reads of a frozen TagMap mutate a shared DenseReader flyweight, creating a race where getObject and typed getters can return another tag's value despite no map writes occurring.
🤖 Bits Code Review · Commit ac96cd7
| String kind = toSpanKindTagValue(spanKind); | ||
| this.delegate.withTag(SPAN_KIND, kind); | ||
| this.spanKindSet = true; | ||
| this.direction = KnownTagCodec.directionOfKind(kind); |
There was a problem hiding this comment.
Resolve directional attributes against the final span kind
Calling setSpanKind(CLIENT), setAttribute("server.address", "example.com"), then setSpanKind(SERVER) leaves peer.hostname on the server span instead of http.hostname. Updating direction does not remap attributes already sent to the delegate. Retain enough original attribute state to resolve against the final kind while preserving write order; changing this assignment alone cannot repair existing writes.
React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it
What each reaction means
- 👍 Useful: correct, and you're fixing it or will.
- 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
- 😕 Unclear: you can't tell what it means or why it matters.
- 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.
🤖 Bits Code Review · Open Bits AI session
| DenseReader reader = this.reader; | ||
| if (reader == null) { | ||
| reader = new DenseReader(); | ||
| this.reader = reader; | ||
| } |
There was a problem hiding this comment.
Preserve concurrent reads of frozen tag maps
With dense tags enabled, concurrent reads of the same frozen TagMap mutate this shared reader. Another lookup can replace its ID or value before TagMap.getEntry() materializes the result, causing getObject() to return another key’s value despite no map writes. The child reproduced this race. Each lookup needs an independent result.
| DenseReader reader = this.reader; | |
| if (reader == null) { | |
| reader = new DenseReader(); | |
| this.reader = reader; | |
| } | |
| DenseReader reader = new DenseReader(); |
React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it
What each reaction means
- 👍 Useful: correct, and you're fixing it or will.
- 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
- 😕 Unclear: you can't tell what it means or why it matters.
- 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.
🤖 Bits Code Review · Open Bits AI session
TagMap.getObject and getString went through getEntry, so on the dense store every value read, by the serializer or a post-processor, built an Entry only to unwrap it. A known tag is now read with IdStore.getValue / getString, which build nothing on the dense store. IdStore.getString is new so the hash store can keep answering with its Entry's cached string: a numeric tag the serializer reads repeatedly is still formatted once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What Does This Do
Splits tag storage into strict id and name stores, and has the span own them directly.
TagMapbecomes a thin view for everyone else. No behavior change.The idea: classify a tag name once, at the boundary (known tag → id, custom tag → name, direction-scoped alias → the span's call), then keep the two kinds apart inside.
Layers
KnownTagCodeckeyOfclassifies a name onceIdStoreIdStoreFactorypicks one implementation per JVM fromtrace.experimental.dense.tags.enabled(off by default):HashIdStore(flag off) orDenseIdStore(flag on: insertion-ordered parallel arrays with one occupancy word per level, no per-tagEntry, point reads answer with a reused flyweight)HashStoreDDSpanContextIdStore(also its lock) and itsHashStore. Routes each write to a store (routeSet), and holds all setting logic, withDirectionalTagsandTagInterceptorTagMapknownStore,nameStore). Standalone maps (tracer tags, DD_TAGS, prototypes, baggage) have their own stores; a span's view is direction-awareNotable changes
TagMap.Internalsis gone. The strict store API is the direct path, so core no longer needs a trusted side door aroundTagMap. Its tests move toTagStoresTest.IdStoreimplementation per JVM. The factory chooses once, so only one implementation loads and class hierarchy analysis devirtualizes everyIdStorecall. The dense store replaces Store known span tags densely, behind an experimental flag (phase 3b - dense tag storage) #12856, which is closed; per-operation sizing and bulk prototype seeding follow in Presize dense tag stores and seed span prototypes in bulk #12926.TagMapview over the span's stores.getTags()is still a snapshot copy.Entryobjects and clone onlyBucketGroupchains, as before.Motivation
TagMaphad accreted name canonicalization, known-versus-custom classification and, with #12731, direction awareness. That forced core to reach around it throughInternals, and every write route repeated the known-versus-custom and direction logic. Most of #12731's review findings were one write route missing a rule another route had.This puts policy back in the span layer, keeps storage simple and fast, and gives the dense store a clean place as the known-tag half. That matters for the biggest remaining allocation sites: point reads building an
Entryper read, primitive metrics stored as boxedEntrys, and the dense store's id arrays.Additional Notes
Performance (JDK 17,
-f 2,-prof gc; base is #12731's tip6aa6d4f1e2):SetTag.knownTag_byIdSetTag.knownTag_byNameSetTag.customTag_byNameGetTag.knownTag_unsafeGetTagGetTag.knownTag_getTagSpanCreation.webServerSpanSpanCreation.bareStartSpan(4 forks)Throughput is within noise. These numbers were measured before the store factory replaced the earlier compile-time
HASH_MODEconstant, and they cover only the flag-off store; a re-run on the current tip, flag off and on, is pending.Known cost: +40 B per span. The span's view, handed to the tag post-processor chain, escapes there and isn't scalar-replaced, so the view is cached per span (32 B plus a field). This is a stopgap: the post-processors are due for rework, and once they read the span's stores directly, the view and the field go away. Without the cache it was +32 B on the app thread and another +32 B at serialization.
Semantic note: name-keyed reads through a span's view resolve aliases by the span's direction, where today's raw
unsafeTagsreads don't. No current post-processor reads an alias name.Follow-up: a later commit moves pending aliases into a small lazily created holder: the custom store then holds only custom tags, and the pending-alias check becomes a null check that resets after resolution.
🤖 Generated with Claude Code