Skip to content

Have the span own its tag stores directly (dense store - part 7) - #13003

Draft
dougqh wants to merge 1 commit into
dougqh/tagmap-store-facadefrom
dougqh/tagmap-split-known-custom
Draft

dougqh wants to merge 1 commit into
dougqh/tagmap-store-facadefrom
dougqh/tagmap-split-known-custom

Conversation

@dougqh

@dougqh dougqh commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What Does This Do

DDSpanContext owns its tag stores directly, instead of one TagMap:

  • Known tags go in an IdStore, which is also the span's tag lock.
  • Custom tags go in a HashStore, borrowed read-only from the read-through parent until the span's first custom write.

Each write is routed to the store that holds the tag. A name is resolved once and routed, and a write by id goes straight to the id store.

  • The eager post-processors already read and write the span through SpanContents (Introduce SpanContents for eager tag post-processors (dense store - part 3) #13001), so they need no TagMap.
  • The lazy post-processors and the serializer still take a TagMap. The span builds a view over its stores once, at serialization, and caches it. That costs about 32 B per span until they move to SpanContents.
  • TagMap.Internals is gone, since the stores are now the direct path. The forbidden-API entry for it goes too.
  • The constructors drop the unused tagsSize parameter, which is why many tests change by one line.

Motivation

This is the last of three PRs that replace the span's single TagMap with two stores split by tag kind. The stores and the TagMap facade are in the two PRs below this one. Here, the span stops going through TagMap for its own reads and writes. That gives the dense store (#13004) a place as the known-tag half.

Additional Notes

Allocation, 7-span web-request trace, application thread only:

step base this PR
bare spans 4,448 B 4,504 B
whole trace 7,936 B 8,040 B (+104 B, +1.3%)
  • +56 B is the new DDSpanContext fields (customTags, customTagsBorrowed, tagView), 8 B per span.
  • +48 B appears when the first custom tag is written. That's most likely the custom HashStore being created, but it isn't confirmed yet.
  • Without SpanContents, building the view on the application thread made this +4.1%.

Tests: internal-api and dd-trace-core pass. The only failures are the two TracerConnectionReliabilityTest cases, which need Docker.

Where this is going

This PR is one step in replacing the span's single TagMap with 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

  • A tag is classified once, at the boundary. A known tag becomes an id (from the tag registry, Resolve direction-scoped OpenTelemetry names at the OpenTelemetry boundary (otlp - tag registry - phase 3) #12731), and a custom tag stays a name. Instrumentation sets known tags by id (Route intercepted tags by id instead of by name (otlp - tag registry - phase 3) #12817), so the JIT can constant-fold the routing.
  • The span owns its tag stores directly. Known tags are kept by id and custom tags by name. TagMap becomes a thin view for code outside the span.
  • Known tags live in a dense store by default. Parallel arrays of serials and values replace an Entry object 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.
  • Core reads and writes a span through SpanContents, not a TagMap. That covers post-processors now and serializers later, so no TagMap view gets built for them. SpanContents starts with four methods and grows a name, id and primitive API as more readers move to it.

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.

PR Step Allocation per 7-span trace (flag off unless noted)
#12999 A null tag value removes the tag in TagMap. This is the stack's only behavior change, kept separate on purpose. unchanged
#12992 Bucket known tags by slot in TagMap, and add TraceShapeBenchmark −96 B vs master
#13001 SpanContents for the eager post-processors unchanged
#13002 Extract TagMap's bucket mechanics into HashSupport (a pure move) unchanged
#13006 Add the id and name tag stores, not yet used unchanged
#13007 Make TagMap a facade over the two stores unchanged
#13003 Have the span own its tag stores directly +104 B (+1.3%)
#13004 Dense id store behind trace.experimental.dense.tags.enabled (off by default) flag on: −30.6%
#12926 Turn the dense store on by default, add sizing hints and bulk prototype seeding flag on: about 3% less again

Every PR after #12999 is behavior-preserving.

The pieces' allocation figures come from a deterministic per-thread probe (ThreadMXBean allocated bytes) that builds the same 7-span trace as TraceShapeBenchmark. The #12992 and #12926 figures come from TraceShapeBenchmark itself.

🤖 Generated with Claude Code

@datadog-official

This comment has been minimized.

@dd-octo-sts

dd-octo-sts Bot commented Oct 23, 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.91 s 13.91 s [-0.6%; +0.7%] (no difference)
startup:insecure-bank:tracing:Agent 12.90 s 12.90 s [-0.7%; +0.6%] (no difference)
startup:petclinic:appsec:Agent 17.08 s 17.09 s [-0.9%; +0.8%] (no difference)
startup:petclinic:iast:Agent 16.90 s 16.96 s [-1.2%; +0.5%] (no difference)
startup:petclinic:profiling:Agent 16.72 s 16.16 s [-0.9%; +7.8%] (no difference)
startup:petclinic:sca:Agent 17.07 s 17.04 s [-0.7%; +1.1%] (no difference)
startup:petclinic:tracing:Agent 16.16 s 15.68 s [-1.2%; +7.4%] (no difference)

Commit: 074d3dbe · 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.

DDSpanContext holds its IdStore (also its tag lock) and its HashStore
instead of one TagMap, and routes each write to the store that holds the
tag: a name is resolved once and routed, and a write by id goes straight
to the id store. The custom store is borrowed read-only from the
read-through parent until the span's first custom write.

The eager post-processors already read and write the span through
SpanContents. The lazy post-processors and the serializer still take a
TagMap: the span builds a view over its stores once, at serialization,
and caches it (about 32 B per span until they move to SpanContents).

TagMap.Internals, the core's trusted side door, is gone: the stores are
the direct path. The constructors drop the unused tagsSize parameter.

Allocation, 7-span web-request trace: +104 B (+1.3%), from the new
DDSpanContext fields (8 B per span) and the first custom store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dougqh
dougqh force-pushed the dougqh/tagmap-split-known-custom branch from c6d9c8c to 074d3db Compare October 23, 2026 04:14
@dougqh dougqh changed the title Split a span's tags into known and custom stores it owns Have the span own its tag stores directly Oct 23, 2026
@dougqh
dougqh changed the base branch from dougqh/tagmap-hash-support to dougqh/tagmap-store-facade October 23, 2026 04:14
@dougqh dougqh changed the title Have the span own its tag stores directly Have the span own its tag stores directly (dense store - part 7) Oct 23, 2026

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: no release notes Changes to exclude from release notes type: refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant