Skip to content

Add per-operation self-tuning dense-store sizing (phase 2) - #12056

Draft
dougqh wants to merge 14 commits into
dougqh/generator-v2from
dougqh/sizing-hint
Draft

Add per-operation self-tuning dense-store sizing (phase 2)#12056
dougqh wants to merge 14 commits into
dougqh/generator-v2from
dougqh/sizing-hint

Conversation

@dougqh

Copy link
Copy Markdown
Contributor

What

Sizes each span's dense TagMap store from a per-operation sizing hint instead of a generic default, then feeds the actual known-tag count back on span finish so the hint converges to that operation's real high-water mark. Over-provisioning shrinks toward the operation's true shape without any hand-tuned per-type tables.

This is the SizingHint layer of the dense-store stack, sitting between the generator (dougqh/generator-v2) and the id-keyed TagMap.set(long) work (dougqh/tag-id-api, #11901).

How

  • FlatHashtable — open-addressed find-or-create over self-contained entries; static-polymorphism via a concrete-typed Helper singleton so the JIT devirtualizes/inlines hash/matches/create per call site. Single-reference publish → torn-free under racy access.
  • SizingHint / SizingHelper — opaque per-operation hint (label + cached hash + tuned size) and its String-key helper.
  • SizingHintTable — process-wide, pure-static two-lane (entry vs child span) registry keyed by operation name. Fixed-capacity and deliberately lock-free/racy: a lost update or double-mint only mis-sizes an array (over/under-provision) for a span or two — it never corrupts tag data.
  • WiringCoreTracer.buildSpanContext resolves the hint by operation name + entry-ness and hands it to DDSpanContext, which sizes its TagMap from it and records the final size back via DDSpan.finishAndAddToTrace.

Testing

  • FlatHashtableTest, SizingHintTableTest (incl. the self-tuning recordSize loop and content-keying so a UTF8BytesString operation name resolves to the same hint as its String form).
  • Benchmarks: FlatHashtableBenchmark; a buildMapSized arm in DenseStoreAllocBenchmark.
  • spotlessJavaCheck, :internal-api:spotbugsMain, :dd-trace-core:spotbugsMain all green.

Notes

  • No behavior change to tag semantics — this only sizes the backing store.
  • The three delegating test-only DDSpanContext ctors are marked @Deprecated @VisibleForTesting to steer new code to the full ctor.

🤖 Generated with Claude Code

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). Off-by-default: dormant until a resolver
registers, so production is byte-identical.
- CoreTracer flips it live behind `-Ddd.trace.dense.tags.enabled` for A/B.
- 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>
@dougqhdougqh 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-datadog-prod-us1-2

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

Copy link
Copy Markdown
Contributor

🎯 Code Coverage (details)
Patch Coverage: 84.62%
Overall Coverage: 57.43% (+0.02%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: b424214 | Docs | View more details | Give us feedback!

@dd-octo-sts

dd-octo-stsBot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

SuiteStatus
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
ScenarioCandidatemasterΔ (95% CI of mean)
startup:insecure-bank:iast:Agent14.70 s14.60 s[-0.2%; +1.5%] (no difference)
startup:insecure-bank:tracing:Agent13.61 s13.71 s[-1.4%; -0.1%] (maybe better)
startup:petclinic:appsec:Agent16.87 s16.75 s[-0.1%; +1.6%] (no difference)
startup:petclinic:iast:Agent16.90 s16.92 s[-1.0%; +0.7%] (no difference)
startup:petclinic:profiling:Agent16.58 s16.61 s[-1.4%; +1.0%] (no difference)
startup:petclinic:sca:Agent17.03 s16.90 s[-0.3%; +1.8%] (no difference)
startup:petclinic:tracing:Agent16.25 s16.19 s[-0.6%; +1.3%] (no difference)

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

@datadog-datadog-prod-us1-2

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

Copy link
Copy Markdown
Contributor

Bits has a CI fix ready

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

Registered appsec.agentic_onboarding in AppSec configuration and read it with an empty default during Config initialization, so the setting is emitted in configuration telemetry.

Commit fix to this PR


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

@dougqhdougqh changed the title Add per-operation self-tuning dense-store sizing (SizingHint)Add per-operation self-tuning dense-store sizing (phase 2)Jul 26, 2026
dougqhand others added 2 commits August 17, 2026 19:17
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>
Introduce the tag-registry code generator (buildSrc plugin + tag-conventions
YAML) and use it to assign the colored slots from the previous commit by
graph-coloring the tag co-occurrence graph (each concrete span type's
resolved set plus the <trace> clique), packing all stored tags into
SLOT_COUNT=16 slots. Relocates the hand-maintained KnownTags from src/main to
the generated src/generated source set and renames the codec accessors to
their generated form (serialNum/makeTagId). Adds the generator verify task so
CI fails if the checked-in KnownTags drifts from the conventions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqh
dougqhforce-pushed the dougqh/generator-v2 branch from e6800b8 to 9b7c3d1CompareAugust 17, 2026 23:20
@dougqh
dougqhforce-pushed the dougqh/sizing-hint branch from 956f2c9 to 8fb6bc3CompareAugust 18, 2026 12:36
@dougqh

Copy link
Copy Markdown
ContributorAuthor

@codex review

@chatgpt-codex-connectorchatgpt-codex-connectorBot 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:8fb6bc38bb

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

// resolved hint sizes the span's TagMap and self-tunes on finish; null (no/unkeyable
// operation name) falls back to the generic default capacity.
final boolean entrySpan = !(resolvedParentSpanContext instanceof DDSpanContext);
final SizingHint sizingHint = SizingHintTable.hintFor(operationName, entrySpan);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Skip sizing work while dense tags are disabled

perf: When dd.trace.dense.tags.enabled is absent—the documented default at CoreTracer lines 662–668—every span still performs this operation-name conversion and global-table probe, and the table retains up to 1,024 names even though KnownTagCodec routes no tags into the dense store. This adds unconditional work to span creation with no possible sizing benefit, so resolve hints only when the dense-tag feature is enabled.

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

Useful? React with 👍 / 👎.

Comment on lines 423 to +426
this.unsafeTags =
readThroughParent != null
? TagMap.createFromParent(readThroughParent)
: TagMap.create(capacity);
: sizingHint != null ? TagMap.create(sizingHint) : TagMap.create(capacity);

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 Apply the hint when constructing read-through tag maps

perf: When mergedTracerTagsNeedsIntercept is false, ConfigSnapshot supplies a non-null mergedTracerTags, so this branch always calls createFromParent and discards the sizing hint. This remains true for an empty parent because createFromParent drops it only after this choice, leaving the map at the fixed capacity of 12; consequently the normal shared-parent path does not receive the per-operation sizing introduced by this change. Add a parent-aware creation path that also applies the hint.

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

Useful? React with 👍 / 👎.

private void finishAndAddToTrace(final long durationNano) {
// ensure a min duration of 1
if (DURATION_NANO_UPDATER.compareAndSet(this, 0, Math.max(1, durationNano))) {
context.recordDenseSize();

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 Record sizing for phased-finish spans

perf: The feedback call exists only in finishAndAddToTrace, but spans completed through phasedFinish() followed by publish()—including the gRPC, Netty, WebFlux, and async-servlet paths in this repository—publish directly without entering this method. Those operations therefore never tune their hints and repeatedly start from the seed capacity; invoke the feedback from a terminal path shared by both finish modes.

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

Useful? React with 👍 / 👎.

private void finishAndAddToTrace(final long durationNano) {
// ensure a min duration of 1
if (DURATION_NANO_UPDATER.compareAndSet(this, 0, Math.max(1, durationNano))) {
context.recordDenseSize();

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 Include serialization-time tags in the recorded size

perf: Recording here occurs before serialization, while DDSpanContext.processTagsAndBaggage later runs the lazy processors that append dense known tags such as _dd.integration, _dd.svc_src, and _dd.tracer_host. The learned high-water mark therefore systematically excludes those entries, so affected spans can still grow and copy their dense arrays during serialization on every occurrence; record after the last tag-processing additions instead.

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

Useful? React with 👍 / 👎.

Comment on lines +1446 to +1447
if (!hint.capped && this.knownCount > hint.size) {
hint.size = this.knownCount; // monotonic-max; benign racy plain-int write

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 Make sizing-hint updates truly monotonic

perf: When two spans of the same operation finish concurrently, both can pass this comparison using the same old value; if a span with 10 tags writes first and one with 5 tags writes afterward, the hint decreases from 10 to 5 despite the claimed monotonic-max behavior. Because size is also a plain non-volatile field, prompt visibility to span-creation threads is not guaranteed. This can make later spans repeatedly under-allocate and grow, so use an atomic max/CAS or equivalent synchronization.

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

Useful? React with 👍 / 👎.

Comment on lines +91 to +95
if (entrySpan) {
entrySize++; // racy approximate count
} else {
childSize++;
}

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 Increment the lane count only for a new slot

perf: When many first spans for the same operation race, they can all miss the initial get; after one inserts the hint, getOrCreate returns that existing hint to the remaining callers, but every caller still increments this counter. A sufficiently concurrent cold start can therefore consume the 512-entry budget while storing only one distinct operation, forcing all later operations onto the overflow hint. Have insertion report whether the caller actually claimed a new slot and increment only in that case.

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

Useful? React with 👍 / 👎.

dougqh added a commit that referenced this pull request Aug 18, 2026
…oint, lane count, read-through sizing
- Do no harm: resolve a SizingHint only when KnownTagCodec.isActive() (the dense
store is live). With dd.trace.dense.tags.enabled off (the default), skip the
operationName.toString() + global-table probe entirely -- a hint buys nothing
when known tags don't take the dense path.
- Record point: move recordDenseSize() from DDSpan.finishAndAddToTrace to
DDSpanContext.processTagsAndBaggage (at serialization). That is the single
terminal point that runs for BOTH finish modes -- plain finish() and
phasedFinish()+publish() (gRPC/Netty/WebFlux) -- and runs AFTER the lazy tag
post-processors append serialization-time tags (_dd.integration, host, ...),
so the recorded high-water mark is the span's true final footprint.
- Read-through sizing: add TagMap.createFromParent(parent, hint) so the LOCAL
dense store of a read-through map is sized from the hint too; route it in
DDSpanContext. Sizing now fires on the common child / shared-parent path
(previously the hint was discarded whenever mergedTracerTags was non-null).
- Lane count: FlatHashtable.getOrCreate reports genuine inserts; SizingHintTable
bumps the per-lane cardinality count only on a real insert, so a cold-start
burst of one operation can't spend the budget on a single name.
- Docs: correct the 'monotonic-max' over-claim to best-effort racy max (a
concurrent finish can transiently lower size; benign -- only mis-sizes an array).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqh

Copy link
Copy Markdown
ContributorAuthor

@codex review

@chatgpt-codex-connectorchatgpt-codex-connectorBot 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:390a3432d1

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

Comment on lines +80 to +81
jvmArgsAppend = {
"-DTEST_LOG_LEVEL=warn",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Enable dense tags in the trace-sizing benchmark

perf: This benchmark documents its results as “dense on” and attributes the allocation delta to SizingHint, but its fork omits -Ddd.trace.dense.tags.enabled=true; CoreTracer therefore leaves KnownTagCodec inactive and assigns sizingHint = null for every span. Unlike SpanCreationBenchmark, these runs exercise the legacy tag path, so the committed historical results cannot validate this change; add the dense-store flag to this fork.

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

Useful? React with 👍 / 👎.

Comment on lines +25 to +26
public void write(List<DDSpan> trace) {
blackhole.consume(trace);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exercise serialization before dropping benchmark traces

perf: Both new span/trace benchmarks use this writer, but write only consumes the trace and never calls DDSpanContext.processTagsAndBaggage, which is the sole path invoking recordDenseSize. Consequently, even in SpanCreationBenchmark, where dense tags are enabled, every hint remains at the seed size of 1 and every heavy span repeats the grow/copy sequence; the benchmarks therefore do not measure the advertised steady-state self-tuning behavior. Use a benchmark writer that executes metadata processing, or explicitly train the hints before measurement.

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

Useful? React with 👍 / 👎.

Comment on lines +2207 to +2209
if (KnownTagCodec.isActive()) {
final boolean entrySpan = !(resolvedParentSpanContext instanceof DDSpanContext);
sizingHint = SizingHintTable.hintFor(operationName, entrySpan);

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 Re-key sizing when the operation name changes

perf: When an integration changes a span's operation after start(), this stores the hint for the initial name and later records the final tag count back into that stale hint. This is systematic for the OTel shim: OtelTracer.spanBuilder starts every span as SPAN_KIND_INTERNAL, while OtelSpan.onSpanFinished computes the real operation name, so heterogeneous OTel spans share an internal hint per lane and lean spans eventually allocate for the largest shape while final operation names never learn. Re-resolve the hint when setOperationName changes the name, or defer selecting it until the final name is known.

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

Useful? React with 👍 / 👎.

Comment on lines +69 to +70
for (int i = 0; i < 4096 && firstOverflow == null; i++) {
SizingHint hint = SizingHintTable.hintFor("registry.flood." + i, true);

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 Isolate the test that exhausts the global hint table

If this test runs before freshHintIsSeededAndUncapped or sizingHintFeedsAndTunesTheDenseStore—for example under randomized or parallel JUnit execution—it permanently exhausts the process-wide entry lane, so those tests receive the capped overflow hint with size 8 instead of a fresh size-1 hint and fail. Distinct operation names do not isolate tests once the global cardinality counter is exhausted; reset/use an isolated registry or explicitly guarantee that this destructive test runs last.

Useful? React with 👍 / 👎.

Comment on lines +75 to +76
final String key =
operationName.toString(); // O(1) for String / UTF8BytesString (see class doc)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bound retained operation-name bytes

perf: The cardinality limit bounds the number of keys, but not their byte size: hintFor converts an arbitrary public-API CharSequence to a String, and each created SizingHint keeps that string strongly for the process lifetime. Operation names are only truncated later by TraceUtils.normalizeOperationName in the DD-intake interceptor, so an application producing up to 512 entry and 512 child spans with very large dynamic names can make this supposedly bounded registry permanently retain hundreds of megabytes or more. Reject, truncate, or avoid caching oversized names before inserting them.

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

Useful? React with 👍 / 👎.

Replaces the raw Boolean.getBoolean("dd.trace.dense.tags.enabled") system-property
read with a proper trace.dense.tags.enabled Config flag, captured once into the
DENSE_TAGS_ENABLED static constant so it constant-propagates on the span-creation path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dougqh added a commit that referenced this pull request Aug 18, 2026
…oint, lane count, read-through sizing
- Do no harm: resolve a SizingHint only when KnownTagCodec.isActive() (the dense
store is live). With dd.trace.dense.tags.enabled off (the default), skip the
operationName.toString() + global-table probe entirely -- a hint buys nothing
when known tags don't take the dense path.
- Record point: move recordDenseSize() from DDSpan.finishAndAddToTrace to
DDSpanContext.processTagsAndBaggage (at serialization). That is the single
terminal point that runs for BOTH finish modes -- plain finish() and
phasedFinish()+publish() (gRPC/Netty/WebFlux) -- and runs AFTER the lazy tag
post-processors append serialization-time tags (_dd.integration, host, ...),
so the recorded high-water mark is the span's true final footprint.
- Read-through sizing: add TagMap.createFromParent(parent, hint) so the LOCAL
dense store of a read-through map is sized from the hint too; route it in
DDSpanContext. Sizing now fires on the common child / shared-parent path
(previously the hint was discarded whenever mergedTracerTags was non-null).
- Lane count: FlatHashtable.getOrCreate reports genuine inserts; SizingHintTable
bumps the per-lane cardinality count only on a real insert, so a cold-start
burst of one operation can't spend the budget on a single name.
- Docs: correct the 'monotonic-max' over-claim to best-effort racy max (a
concurrent finish can transiently lower size; benign -- only mis-sizes an array).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqh
dougqhforce-pushed the dougqh/sizing-hint branch from 390a343 to 9f8ff10CompareAugust 18, 2026 14:12
@dougqh

Copy link
Copy Markdown
ContributorAuthor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit:9f8ff1039d

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

dougqhand others added 7 commits August 18, 2026 10:43
Expose the known-tag id on EntryReader: TagMap.Entry resolves it lazily via
KnownTagCodec.keyOf (same memoized-field idiom as lazyTagHash; 0L means unknown
tag / inactive codec, so the sentinel is Long.MIN_VALUE). The dense reader
flyweight already has the id in hand at emit time and records it directly,
skipping the keyOf resolve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ting
Address two Codex robustness findings on the generator:
- generateKnownTags now clears its owned destination tree before regenerating,
so a report/source file retired by a later revision cannot linger and fail
verifyKnownTags with no way for the fix task to remove it.
- All String.format calls in the report/emitter use Locale.ROOT, so %d output
no longer localizes on machines whose default locale uses non-ASCII digits,
preserving the byte-identical-output invariant verifyKnownTags relies on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address the Codex finding that the new experimental dense-tags flag was stored
but never surfaced in Config.toString (repo convention requires new configs
appear there for diagnostics). Surface it only when it diverges from the
default, so normal config dumps stay uncluttered, and compare against a new
DEFAULT_TRACE_DENSE_TAGS_ENABLED constant (also now the getBoolean default) so
the condition stays correct if the default ever changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address the Codex finding that gradle/spotless.gradle excluded src/generated/**,
bypassing google-java-format on committed generated code. Drop the exclude and
make KnownTagsEmitter emit google-java-format-clean output (blank line before the
keyOf static initializer; wrap the long id-assignment), so spotlessCheck and the
byte-identical verifyKnownTags gate both pass on the emitted file. internal-api is
the only module with a committed src/generated Java tree, so the exclude removal
is otherwise a no-op.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a per-namespace name to the tag registry: each tag may declare an
`open-telemetry-name` (replacing the vestigial `aliases` list). The generator
now:
- parses it off both YAMLs into the model,
- validates it (an OTel name may not collide with a canonical tag name nor be
claimed by two tags -> build fails loudly rather than silently picking one),
- emits it into the keyOf table so keyOf(otelName) resolves to the canonical
tag's id (inbound, many->one), and
- emits a reverse switch so openTelemetryNameOf(tagId) recovers it (outbound).
KnownTagCodec gains datadogTagOf(tagId) (== nameOf, the canonical name) and
openTelemetryTagOf(tagId) (the OTel name, or null). nameOf is unchanged and
still returns the Datadog name -- outbound is namespace-specific, not
normalized. Serializer applicability (when a span renders under OTel names,
fallback policy) and additional namespaces are a follow-on concern.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reconciles #11901's SizingHint half onto the colored generator-v2 base as
its own layer. A per-operation SizingHint (kept in a bounded, self-tuning
SizingHintTable keyed by operation name, two lanes for entry vs child spans)
sizes a span's dense TagMap at create and records the observed known-tag
high-water mark back on finish, so the reused hint converges to the
operation's real size. Erases the fixed KNOWN_INIT_CAP dense-array floor tax
that regressed bare/small spans in the #12047-vs-1.65 A/B; the id-keyed write
API stacks on top of this as a separate layer.
New: SizingHint, SizingHelper, SizingHintTable, FlatHashtable (+ tests, jmh).
TagMap: create(SizingHint), recordSize, denseCapHint. DDSpanContext threads a
sizingHint through the primary ctor + recordDenseSize on finish; DDSpan hooks
it at finishAndAddToTrace; CoreTracer resolves the per-operation hint/lane.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Near-no-op DropWriter (blackhole-consuming Writer) plus two front-half
benchmarks used to measure the dense-store / per-operation-sizing
(SizingHint) allocation behavior with the dense flag on:
- SpanCreationBenchmark: single span create -> (set tags) -> finish,
bare/web/jdbc arms; the drift-stable old-API shape for version A/Bs.
- TraceCreationBenchmark: a whole trace (local root + children) so the
entry-vs-child sizing win, which a single-span bench can't reach,
surfaces and scales with childCount.
Read gc.alloc.rate.norm (B/op, deterministic); throughput is directional.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dougqhand others added 3 commits August 18, 2026 12:07
…oint, lane count, read-through sizing
- Do no harm: resolve a SizingHint only when KnownTagCodec.isActive() (the dense
store is live). With dd.trace.dense.tags.enabled off (the default), skip the
operationName.toString() + global-table probe entirely -- a hint buys nothing
when known tags don't take the dense path.
- Record point: move recordDenseSize() from DDSpan.finishAndAddToTrace to
DDSpanContext.processTagsAndBaggage (at serialization). That is the single
terminal point that runs for BOTH finish modes -- plain finish() and
phasedFinish()+publish() (gRPC/Netty/WebFlux) -- and runs AFTER the lazy tag
post-processors append serialization-time tags (_dd.integration, host, ...),
so the recorded high-water mark is the span's true final footprint.
- Read-through sizing: add TagMap.createFromParent(parent, hint) so the LOCAL
dense store of a read-through map is sized from the hint too; route it in
DDSpanContext. Sizing now fires on the common child / shared-parent path
(previously the hint was discarded whenever mergedTracerTags was non-null).
- Lane count: FlatHashtable.getOrCreate reports genuine inserts; SizingHintTable
bumps the per-lane cardinality count only on a real insert, so a cold-start
burst of one operation can't spend the budget on a single name.
- Docs: correct the 'monotonic-max' over-claim to best-effort racy max (a
concurrent finish can transiently lower size; benign -- only mis-sizes an array).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqh
dougqhforce-pushed the dougqh/sizing-hint branch from 9f8ff10 to b424214CompareAugust 18, 2026 16:12
@dougqh
dougqhforce-pushed the dougqh/generator-v2 branch from 19222e1 to 33f32c4CompareAugust 18, 2026 20:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: coreTracer coretag: ai generatedLargely based on code generated by an AI or LLMtag: no release notesChanges to exclude from release notestype: refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@dougqh