Skip to content

Encode tag ids with a colored slot and collapse dense presence to one occupancy long (phase 2) - #12046

Draft
dougqh wants to merge 7 commits into
dougqh/dense-store-v2from
dougqh/bloom-v2
Draft

Encode tag ids with a colored slot and collapse dense presence to one occupancy long (phase 2)#12046
dougqh wants to merge 7 commits into
dougqh/dense-store-v2from
dougqh/bloom-v2

Conversation

@dougqh

@dougqh dougqh commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Reframe of the dense-store presence layer (was: two-tier presence bloom).

Replaces the earlier two-tier design (per-map group-decl mask + field-decl bloom) with a single global colored slot: the tag-id middle 16 bits carry one graph-colored slot coordinate (SLOT_SHIFT=32, SLOT_MASK=0xFFFF), so the dense store tracks presence with one occupancy long instead of a group mask plus a field bloom. Also adds the trace-level bit (LEVEL_TRACE) and level-bit read-through in the parent-visibility check.

KnownTags stays hand-maintained here (src/main); the tag-registry code generator that produces these colored ids lands in #12047, which relocates the file to src/generated.

Stacked on #12045 (dense store). Draft.

🤖 Generated with Claude Code

dougqh and others added 5 commits July 21, 2026 13:06
A fresh, mutable TagMap can read through to a frozen parent on local
misses, so a span can layer its own tags over a shared, immutable set
(e.g. merged tracer tags) without copying them.

- createFromParent(parent): the only way to attach a parent; the parent
  must be frozen and is fixed at construction (no re-parenting), so
  read-through can treat it as stable. Single-parent by design in phase 1.
- Reads resolve local-first, then the parent; a local entry shadows the
  parent's (local-wins). Removing a parent key locally records a lazy
  tombstone (removedFromParent) so it stops reading through; the tombstone
  set is null until first needed, keeping the hot paths untouched.
- size()/isEmpty() are exact (Map contract) and resolve the parent;
  isDefinitelyEmpty()/estimateSize() are the cheap conservative variants
  for the hot path. copy() preserves the parent and tombstones; forEach
  walks local then parent.

Built on the folded final-class TagMap (#11967); composes cleanly with the
null-tolerant Entry pathway (#11963).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…plit phase 1)

Attach the trace's merged tracer tags to each span's TagMap as a frozen
read-through parent (via TagMap.createFromParent) at span construction,
instead of copying them into every span. The span sees the shared tags on
read and only stores its own local tags, so the common trace-level bundle
is held once per trace rather than duplicated per span.

- CoreTracer builds the frozen merged-tracer-tags parent once; config
  version is kept out of that bundle.
- DDSpanContext attaches the parent at construction (fixed, no re-parenting).
- Adds TagMapReadThroughBenchmark (copy-down vs read-through, -prof gc).

Stacked on the read-through mechanism (#11789), which builds on the folded
final-class TagMap (#11967).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
StringIndex is a compact open-addressed string→index structure (the keyOf
substrate the dense tag store builds on): parallel hash/name arrays, linear
probing, on par with HashSet on lookup at a smaller footprint. Includes unit
tests, a footprint test (jol), and comparison benchmarks (vs HashSet/switch).

No TagMap changes — standalone util. Rebased onto the level-split stack
(consumer #11932) as the layer dense-store sits on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… gate

Re-applies the coverage fix dropped by the branch rebase/restack.
jacocoTestCoverageVerification flags StringIndex at 0.7 instruction
coverage (min 0.8): the instance long[] API (mapLongValues / lookup /
lookupOrDefault) and the Support.numSlots(int[]) static were never
exercised. Add two tests covering them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… capacityFor

Two consistency changes in light of the FlatHashtable strategy family:

- Rename Support -> EmbeddingSupport. StringIndex is the LightMap/object-side
  member (a real object with static factories), and its static-over-raw-arrays
  tier is exactly the "embed the backing arrays in your own fields" role that
  LightMap.EmbeddingSupport names.
- Replace tableSizeFor with capacityFor(n[, loadFactor]) + DEFAULT_LOAD_FACTOR /
  LOW_LOAD_FACTOR, mirroring FlatHashtable's sizing (duplicated for now; the two
  branches are independent, to be unified when the family converges). This also
  tightens the sizing: the old `while (size <= n)` over-allocated 2x at power-of-
  two counts (capacityFor(16) is now 32, was 64) while still targeting load
  factor <= 0.5. capacityFor(0) stays valid (StringIndex allows the empty set).

Updates StringIndexTest (sizing expectations + a rejects test) and the three
benchmarks referencing the tier. Behavior unchanged except the tighter default
table size.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqh dougqh added comp: core Tracer core tag: no release notes Changes to exclude from release notes type: refactoring tag: ai generated Largely based on code generated by an AI or LLM labels Jul 23, 2026
@datadog-prod-us1-6

datadog-prod-us1-6 Bot commented Jul 23, 2026

Copy link
Copy Markdown

Bits has a CI fix ready

🟢 Investigated · 🟢 Fix prepared · ⚪ Validation skipped · 🟠 Ready

Wrapped the dense-tag initialization comment in dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java to match the repository formatter.

Commit fix to this PR


View in Datadog | Reviewed commit d6e7088 · Any feedback? Reach out in #deveng-pr-agent

@datadog-prod-us1-6

This comment has been minimized.

@dougqh
dougqh force-pushed the dougqh/bloom-v2 branch from 85380de to d6e7088 Compare July 23, 2026 01:01
@dougqh dougqh changed the title Add bloom-filter fast-path to the dense tag store Add a two-tier presence bloom to the dense tag store Jul 23, 2026
@dd-octo-sts

dd-octo-sts Bot commented Jul 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 14.02 s 13.90 s [+0.2%; +1.5%] (maybe worse)
startup:insecure-bank:tracing:Agent 12.86 s 12.90 s [-1.1%; +0.4%] (no difference)
startup:petclinic:appsec:Agent 16.98 s 16.87 s [-0.3%; +1.6%] (no difference)
startup:petclinic:iast:Agent 16.98 s 17.02 s [-1.2%; +0.7%] (no difference)
startup:petclinic:profiling:Agent 16.80 s 16.85 s [-1.4%; +0.8%] (no difference)
startup:petclinic:sca:Agent 16.86 s 16.67 s [+0.2%; +2.1%] (maybe worse)
startup:petclinic:tracing:Agent 16.13 s 16.09 s [-0.9%; +1.4%] (no difference)

Commit: 4c905641 · 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 dougqh changed the title Add a two-tier presence bloom to the dense tag store Add a two-tier presence bloom to the dense tag store (phase 2) Jul 23, 2026
@dougqh dougqh changed the title Add a two-tier presence bloom to the dense tag store (phase 2) Encode tag ids with a colored slot and collapse dense presence to one occupancy long (phase 2) Aug 17, 2026
@dougqh
dougqh force-pushed the dougqh/dense-store-v2 branch from d758efe to 992dc14 Compare August 18, 2026 20:14
@dougqh

dougqh commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb9ebca1a0

ℹ️ 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".

// makeTagId(serial=285, slot=13) <conditional>

public static final String HTTP_STATUS_CODE_NAME = "http.status_code";
public static final long HTTP_STATUS_CODE_ID = 0x011E000A00000000L;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Mark HTTP status IDs as intercepted

When an ID-based tag caller uses the advertised sign-bit routing, keyOf("http.status_code") now returns this positive ID, so isIntercepted is false even though TagInterceptor.needsIntercept explicitly handles this tag to populate DDSpanContext.httpStatusCode and apply the 404 resource-name rule. Encode this ID with INTERCEPTED, as is done for the other stored tags that require interceptor side effects.

Useful? React with 👍 / 👎.

Comment on lines +43 to +47
public static final String MEASURED_NAME = "measured";
public static final long MEASURED_ID = 0x8009FFFF00000000L;
// makeTagId(serial=9, slot=NO_SLOT) + intercepted [directive]

public static final String ANALYTICS_SAMPLE_RATE_NAME = "analytics.sample_rate";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Register the actual reserved tag names

The new reserved mappings use conceptual names rather than the keys handled by TagInterceptor: for example, the runtime keys are _dd.measured and _dd1.sr.eausr, so keyOf(DDTags.MEASURED) and keyOf(DDTags.ANALYTICS_SAMPLE_RATE) both return zero instead of these reserved IDs. The same mismatch affects _dd.origin (registered as origin) and service.name (only service is registered), preventing established tag constants from using the advertised name-to-ID/interceptor route.

Useful? React with 👍 / 👎.

dougqh and others added 2 commits August 18, 2026 17:32
Known tags (keyOf resolves to a stored id) are held in insertion-ordered
parallel arrays (knownIds/knownValues) with NO per-tag Entry object — the
allocation lever. Lazily allocated on the first known-tag write; custom tags
stay in the hash buckets. Disjoint by construction (known-ness is global), so
read-through shadow checks stay within-region and the bucket path is unchanged.

- KnownTagCodec (id encoding + resolver) + hand-written KnownTags (keyOf
  substrate over StringIndex).
- The KnownTagCodec is ALWAYS present: CoreTracer registers the real resolver
  unconditionally at init (so keyOf/nameOf name resolution is always live —
  the OTel-name mapping later in the stack depends on this), and on first use
  with nothing registered the codec lazily installs an empty NoKnownTagCodec.
  Either way it locks after first use, so a map can never be built half-bucketed
  then half-dense by a late registration.
- Dense ROUTING is decoupled from resolution and gated separately by the
  const-folded KnownTagCodec.DENSE_STORE, captured from the new
  trace.experimental.dense.tags.enabled Config flag (off by default). TagMap
  computes keyOf only under this gate, so when off the dense branches
  dead-code-eliminate and tag storage is byte-identical to the bucket store.
- Sizing is a generous fixed stopgap (KNOWN_INIT_CAP=12, the per-type max);
  exact per-type sizing comes with the tag registry.

Reconciled onto the level-split stack (fold + read-through + StringIndex);
built on the folded final-class TagMap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reframe of the dense-store presence layer. Replaces the earlier two-tier
(group-decl mask + field-decl bloom) design with a single global colored
slot: the tag-id middle 16 bits carry one graph-colored slot coordinate
(SLOT_SHIFT=32, SLOT_MASK=0xFFFF), so the dense store tracks presence with
one occupancy long instead of a group mask plus a field bloom. Adds the
trace-level bit (LEVEL_TRACE) and level-bit read-through in the parent
visibility check.

KnownTags remains hand-maintained here (src/main); the tag-registry code
generator that produces these colored ids lands in the following commit,
which relocates the file to src/generated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqh
dougqh force-pushed the dougqh/dense-store-v2 branch from 992dc14 to 6948f86 Compare August 19, 2026 12:45
@dougqh

dougqh commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c905641c2

ℹ️ 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".

* rate — only the fast-path hit rate does. Unslotted stored tags ({@link KnownTagCodec#NO_SLOT})
* all fold onto one shared bit ({@code slot & 63}); the scan stays authoritative for them.
*/
private long knownOccupancy;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid enlarging every TagMap for a disabled experiment

perf: When dense tags retain their default-disabled setting, this eager occupancy field and knownTraceLevel still increase every TagMap instance from 48 to 56 bytes on HotSpot, even though none of the new filtering executes; DDSpanContext creates and retains one map per span, so this adds 8 bytes of allocation and corresponding GC traffic per span for the default production path. Verify the enabled/disabled allocation tradeoff with the existing JMH/JFR tooling and keep this state out of each map when dense routing is disabled.

AGENTS.md reference: AGENTS.md:L79-L81

Useful? React with 👍 / 👎.

// makeTagId(serial=299, slot=6) + intercepted <required>

public static final String VERSION_NAME = "version";
public static final long VERSION_ID = 0x012C000C00000004L;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep per-span version out of the trace-level bit

perf: When dd.version is configured, InternalTagsAdder inserts version into each span's local TagMap, while CoreTracer.withTracerTags explicitly removes it from the read-through parent; marking this ID LEVEL_TRACE therefore sets knownTraceLevel on most local maps and prevents parentDenseVisible from taking its span-level fast skip, causing occupancy checks and potentially linear local-ID scans for trace-level parent tags during serialization. Clear this bit for version or make the level marker reflect the map tier where the tag is actually stored, and verify the read-through cost with the existing TagMap benchmark.

AGENTS.md reference: AGENTS.md:L79-L81

Useful? React with 👍 / 👎.

@dougqh
dougqh force-pushed the dougqh/dense-store-v2 branch from 6948f86 to 4556153 Compare August 29, 2026 03:33
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