Skip to content

Fold OptimizedTagMap into a final class TagMap - #11967

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 4 commits into
masterfrom
dougqh/fold-optimized-tagmap
Jul 20, 2026
Merged

Fold OptimizedTagMap into a final class TagMap#11967
gh-worker-dd-mergequeue-cf854d[bot] merged 4 commits into
masterfrom
dougqh/fold-optimized-tagmap

Conversation

@dougqh

@dougqhdougqh commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

What Does This Do

TagMap was an interface with a single implementation, OptimizedTagMap. The split was vestigial scaffolding from when a second (HashMap-backed) implementation existed. This folds the impl into one public final class TagMap and drops the interface.

  • Interface abstract declarations removed; OptimizedTagMap's bodies become TagMap's methods.
  • Nested types that were implicitly public static in the interface (EntryChange, EntryRemoval, EntryReader, Entry, Ledger) are now written out explicitly as public static.
  • Static factories (create/fromMap/ledger/…) and EMPTY become explicit public static members; the EmptyHolder lazy-init note is updated now that there is no interface↔impl class-init cycle.
  • putAll(TagMap) loses its instanceof dispatch (always true with one class) and calls the fast path directly.

Motivation

Code simplicity — not performance. A single final class is monomorphic by construction, but CHA already devirtualized the sole impl, so no steady-state change is expected. Public API is preserved, so callers are unchanged.

Additional Notes

Stacked on #11963.

From Claude: folded per the long-standing "simplify TagMap" plan; verified public-API-preserving and monomorphism-neutral.

🤖 Generated with Claude Code

@dougqhdougqh added tag: ai generated Largely based on code generated by an AI or LLM tag: no release note comp: core Tracer core type: refactoring labels Jul 15, 2026
@dougqh
dougqh marked this pull request as ready for review July 15, 2026 17:41
@dougqh
dougqh requested a review from a team as a code ownerJuly 15, 2026 17:41

@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:8d735dec7e

ℹ️ 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 threadinternal-api/src/main/java/datadog/trace/api/TagMap.java

@datadog-datadog-prod-us1-2datadog-datadog-prod-us1-2Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Datadog Autotest: PASS

More details

48 adversarial scenarios validated the four riskiest areas of this fold: EMPTY direct class-init (safe — private constructor reads no statics), putAll(TagMap) with mismatched 1-vs-16 bucket arrays (correct — loop bounds min-clamped, EMPTY always empty), compute* delegation from Map.super vs. old TagMap.super (identical behavior), and freeze/immutableCopy invariants. No regressions found.

Was this helpful? React 👍 or 👎

📊 Validated against 48 scenarios · Open Bits AI session

🤖 Datadog Autotest · Commit 8d735de · What is Autotest? · Any feedback? Reach out in #autotest

@datadog-datadog-prod-us1-2

This comment has been minimized.

@dd-octo-sts

dd-octo-stsBot commented Jul 15, 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.02 s13.95 s[-0.4%; +1.4%] (no difference)
startup:insecure-bank:tracing:Agent12.96 s12.95 s[-0.8%; +0.9%] (no difference)
startup:petclinic:appsec:Agent15.79 s16.70 s[-11.2%; +0.2%] (unstable)
startup:petclinic:iast:Agent16.82 s16.84 s[-0.8%; +0.6%] (no difference)
startup:petclinic:profiling:Agent16.60 s16.75 s[-2.0%; +0.2%] (no difference)
startup:petclinic:sca:Agent16.91 s16.84 s[-0.5%; +1.4%] (no difference)
startup:petclinic:tracing:Agent16.10 s16.14 s[-1.3%; +0.7%] (no difference)

Commit:0737ca6b · 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 added a commit that referenced this pull request Jul 15, 2026
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>
dougqh added a commit that referenced this pull request Jul 15, 2026
…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>
Base automatically changed from dougqh/tagmap-null-entry-tolerance to masterJuly 15, 2026 20:25

@datadog-datadog-prod-us1-2datadog-datadog-prod-us1-2Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Datadog Autotest: PASS

More details

76 synthetic scenarios covering EMPTY initialization, putAll, compute/computeIfAbsent/computeIfPresent, Entry.create with all types, freeze/copy, and ledger operations all pass cleanly. The one non-trivial behavioral change — Entry.create(String, Object) now returning null for empty CharSequence typed as Object — is intentional, documented, and aligns existing callers (e.g. GitInfo) whose comments already stated that empty values should be treated as absent.

Was this helpful? React 👍 or 👎

📊 Validated against 76 scenarios · Open Bits AI session

🤖 Datadog Autotest · Commit 5f8632d · What is Autotest? · Any feedback? Reach out in #autotest

dougqh added a commit that referenced this pull request Jul 15, 2026
…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>
Comment threadinternal-api/src/main/java/datadog/trace/api/TagMap.java Outdated
Comment threadinternal-api/src/main/java/datadog/trace/api/TagMap.java Outdated
dougqhand others added 4 commits July 16, 2026 12:53
TagMap was an interface with a single implementation, OptimizedTagMap. The
split was vestigial scaffolding from when a second (HashMap-backed) impl
existed; with one impl it is false generalization. Collapse them into one
`public final class TagMap`:
- The interface's abstract method declarations are removed; OptimizedTagMap's
bodies become TagMap's methods.
- Nested types that were implicitly `public static` in the interface
(EntryChange, EntryRemoval, EntryReader, Entry, Ledger) are now written out
explicitly as `public static`.
- Static factories (create/fromMap/ledger/...) and the EMPTY constant become
explicit `public static` members; the EmptyHolder lazy-init note is updated
now that there is no interface<->impl class-init cycle.
- putAll(TagMap) loses its `instanceof` dispatch (always true once there is one
class) and calls the fast path directly.
No behavior change; motivation is code simplicity, not performance (a single
final class is monomorphic by construction, but CHA already devirtualized the
sole impl). Public API is preserved, so callers are unchanged; the 3 tests that
referenced OptimizedTagMap now reference TagMap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Post-fold tidy, all TagMap-scoped:
- Remove EmptyHolder: with one class there is no interface<->impl class-init
cycle to break, and the private constructor reads no statics, so EMPTY is a
direct `new TagMap(new Object[1], 0)` initializer.
- Static factories (create/fromMap/ledger/...) are now `public static final`
(not expressible on the old interface).
- assertSize/assertNotEmpty/assertEmpty/checkIntegrity test helpers dropped
their now-always-true `instanceof TagMap` guard + redundant cast.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The interface -> final-class fold removed the TagMap interface decls, which
carried the @Nullable/@nonnull param annotations from #11963. Re-home them
onto the now-concrete methods: @nonnull tag keys and strict-setter values,
@nullable on the set(EntryReader)/getAndSet(Entry) sinks (+ the getAndSet
contract javadoc). The null-tolerance behavior was already preserved by the
fold; this restores the self-describing contract on the write surface.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per review: these factories NPE on a null map (map.size()/putAll), so the
input is non-null by contract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dougqh
dougqhforce-pushed the dougqh/fold-optimized-tagmap branch from 6abe29d to 0737ca6CompareJuly 16, 2026 16:56
@PerfectSlayerPerfectSlayer added tag: no release notes Changes to exclude from release notes and removed tag: no release note labels Jul 20, 2026
dougqh added a commit that referenced this pull request Jul 20, 2026
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>
dougqh added a commit that referenced this pull request Jul 20, 2026
…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>

@sarahchen6sarahchen6 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@dougqh
dougqh added this pull request to the merge queueJul 20, 2026
@dd-octo-sts

Copy link
Copy Markdown
Contributor

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351Bot commented Jul 20, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-07-20 16:42:59 UTC ℹ️ Start processing command /merge


2026-07-20 16:43:04 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 2h (p90).


2026-07-20 17:41:40 UTC ℹ️ MergeQueue: This merge request was merged

@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Jul 20, 2026
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854dBot merged commit 4936611 into masterJul 20, 2026
592 of 594 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854dBot deleted the dougqh/fold-optimized-tagmap branch July 20, 2026 17:41
@github-actionsgithub-actionsBot added this to the 1.65.0 milestone Jul 20, 2026
dougqh added a commit that referenced this pull request Jul 21, 2026
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>
dougqh added a commit that referenced this pull request Jul 21, 2026
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>
dougqh added a commit that referenced this pull request Jul 21, 2026
…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>
dougqh added a commit that referenced this pull request Jul 21, 2026
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>
dougqh added a commit that referenced this pull request Jul 21, 2026
…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>
dougqh added a commit that referenced this pull request Jul 21, 2026
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>
dougqh added a commit that referenced this pull request Jul 21, 2026
…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>
dougqh added a commit that referenced this pull request Jul 21, 2026
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>
dougqh added a commit that referenced this pull request Jul 21, 2026
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>
dougqh added a commit that referenced this pull request Jul 21, 2026
…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>
dougqh added a commit that referenced this pull request Jul 21, 2026
…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>
gh-worker-dd-mergequeue-cf854dBot pushed a commit that referenced this pull request Aug 17, 2026
…hase 1a) (#11789)
Add TagMap read-through support (level-split phase 1 mechanism)
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>
Mark parent field @VisibleForTesting and size the tombstone HashSet small
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Collapse redundant isDefinitelyEmpty() call in isEmpty()
A live read-through parent is never definitely-empty: createFromParent
drops an empty parent, copy() only forwards an already-vetted parent, and
the parent is frozen so it can't become empty. So the size==0 /
no-tombstones branch of isEmpty() always returned false via the parent
chain walk — collapse it to a documented `return false`.
Addresses mcculls' review comment on #11789.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merge branch 'master' into dougqh/tagmap-read-through
Merge branch 'master' into dougqh/tagmap-read-through
Merge branch 'master' into dougqh/tagmap-read-through
Merge branch 'master' into dougqh/tagmap-read-through
Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
gh-worker-dd-mergequeue-cf854dBot pushed a commit that referenced this pull request Aug 17, 2026
…plit) (phase 1a) (#11932)
Add TagMap read-through support (level-split phase 1 mechanism)
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>
Wire mergedTracerTags as a read-through parent at span build (level-split 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>
Mark parent field @VisibleForTesting and size the tombstone HashSet small
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Collapse redundant isDefinitelyEmpty() call in isEmpty()
A live read-through parent is never definitely-empty: createFromParent
drops an empty parent, copy() only forwards an already-vetted parent, and
the parent is frozen so it can't become empty. So the size==0 /
no-tombstones branch of isEmpty() always returned false via the parent
chain walk — collapse it to a documented `return false`.
Addresses mcculls' review comment on #11789.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merge branch 'dougqh/tagmap-read-through' into dougqh/tagmap-read-through-consumer
Merge branch 'master' into dougqh/tagmap-read-through
Merge branch 'master' into dougqh/tagmap-read-through
Merge branch 'dougqh/tagmap-read-through' into dougqh/tagmap-read-through-consumer
Merge branch 'master' into dougqh/tagmap-read-through
Merge branch 'dougqh/tagmap-read-through' into dougqh/tagmap-read-through-consumer
Make TagMap fillMap/fillStringMap read-through aware
Both bulk-fill helpers walked only this.buckets, so a parent-backed
TagMap materialized through them dropped tags visible only via the
parent chain and ignored shadowing/tombstones -- unlike putAll, forEach,
and iteration, which already honor the read-through union.
Branch to the visible-union walk (forEach) when a parent is attached,
mirroring putAllOptimizedMap; keep the untouched local-only loop for the
common no-parent case so it pays zero overhead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop observationally-empty read-through parents (exact isEmpty)
createFromParent used isDefinitelyEmpty(), which only checks each
level's local size and ignores shadowing/tombstones. With multi-level
read-through, a frozen intermediate can be observationally empty (no
local entries, every inherited key tombstoned) yet report
isDefinitelyEmpty() == false because a farther ancestor still holds
entries. Attaching such a parent then let a child take isEmpty()'s
no-tombstone fast path and return false while size(), lookup, and
iteration all report empty -- a Map-contract violation.
Use exact isEmpty() so semantically empty parents are dropped, which
also restores the invariant isEmpty()'s fast path relies on: an
attached parent always contributes at least one visible entry. The
hot path is unaffected -- isEmpty() short-circuits on size != 0, only
walking tombstones in the rare all-tombstoned case this fix targets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merge remote-tracking branch 'origin/master' into dougqh/tagmap-read-through-consumer
# Conflicts:
#	internal-api/src/main/java/datadog/trace/api/TagMap.java
#	internal-api/src/test/java/datadog/trace/api/TagMapReadThroughTest.java
Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
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.

3 participants

@dougqh@sarahchen6@PerfectSlayer