Skip to content

Take the span's tag lock once in setTagsFrom (tag api - part 1) - #13030

Draft
dougqh wants to merge 1 commit into
dougqh/prototype-fast-pathfrom
dougqh/settagsfrom-lock-coarsening
Draft

dougqh wants to merge 1 commit into
dougqh/prototype-fast-pathfrom
dougqh/settagsfrom-lock-coarsening

Conversation

@dougqh

@dougqh dougqh commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

What Does This Do

setTagsFrom now takes the span's tag lock once for a whole batch of tags, instead of once per setTag, while keeping the span's tag semantics.

  • SpanData is a new interface: what a TagContributor or TagExtractor may read and set on a span.
    • The setTag overloads by name and by id, plus setTag(EntryReader). A null value removes the tag, and the tag interceptor runs.
    • getTag.
    • The span-level fields that aren't tags yet: setResourceName, setError and setMeasured.
    • It's fluent. AgentSpan extends SpanData, with covariant AgentSpan returns.
  • TagContributor.addTo and TagExtractor.extract take a SpanData instead of an AgentSpan. Nothing in production implements either yet.
  • DDSpanContext is a SpanData. Its setTag family returns this instead of void, and it gains setResourceName(CharSequence) and setError(boolean) at default priority.
  • DDSpan.setTagsFrom delegates to DDSpanContext.setTagsFrom. That takes the tag lock once and hands the contributor or extractor a HeldSpanData, which DDSpanContext provides.
    • HeldSpanData forwards to new package-private *HoldingLock variants of setTag, removeTag and getTag: the same bodies without the synchronized.
    • It lives only for the setTagsFrom call, and asserts that the lock is still held.
    • It's @NoEscape. Contributor call sites are monomorphic by design (@Strategy), so it should be scalar-replaced.
    • The public setters don't change. Interception still runs outside the lock for single setTag calls.

Motivation

One lock per batch. A decorator's run of setTag calls takes and releases the span's tag lock once per call. JDK 17 can't coarsen those locks across the interceptor's calls. A held outer lock doesn't help either, because a recursive monitor enter still does an atomic update.

The vocabulary keeps the contracts apart.

No adapter on hot shared paths. The post-processor chain is megamorphic, so an adapter handed down it would always escape. That's why SpanStorage passes DDSpanContext itself. Contributor sites are monomorphic, which is what makes a short-lived wrapper viable here.

Additional Notes

  • Running interception under the lock follows an existing precedent: setAllTags already intercepts while holding it, and interceptor callbacks re-enter the span's locking setters, since the lock is reentrant.
  • SetTagsFromBenchmark is new, but its numbers are still to come. It compares 9 jdbc tags (8 known by id, including an int and the intercepted span.kind, plus 1 custom) one by one, through a contributor, and through an extractor, on a reused span and on a fresh span. -prof gc checks that HeldSpanData is scalar-replaced.
    • In an earlier prototype, the same coarsening measured −38% on a reused span and −8 to −11% on a fresh span, single-threaded, and was within noise at 8 threads.
  • Tests: DDSpanSetTagsFromTest checks that:
    • a batch sets what the same setTag calls set;
    • interception runs inside a batch (resource.name and span.kind), and reads see the batch's own writes;
    • the span-level setters work;
    • a null source or contributor is skipped;
    • a kept SpanData used after the call trips the assertion.
  • Found along the way: TagMap implements Map but doesn't override equals, so two maps with the same contents aren't equal. It's pre-existing and not fixed here; the test compares plain map copies.
  • Stacked on Presize dense tag stores and seed span prototypes in bulk (dense store - part 9) #12926, the top of the dense-store stack, so it's measured on the final storage.

🤖 Generated with Claude Code

SpanData is what a TagContributor or TagExtractor may read and set on a
span: the setTag overloads (by name and by id) and getTag, with the
span's tag semantics (a null value removes the tag, the interceptor
runs), plus setResourceName, setError and setMeasured. AgentSpan extends
it with covariant returns, and the contributor and extractor now take a
SpanData.

DDSpanContext is a SpanData too: its setTag family returns this. Its
setTagsFrom takes the tag lock once and hands the contributor a
HeldSpanData that lives only for that call: it forwards to new
package-private *HoldingLock variants of setTag, removeTag and getTag
(the same bodies without the synchronized), and asserts the lock is
still held. Contributor call sites are monomorphic by design, so the
wrapper should be scalar-replaced. The public setters are unchanged.

SetTagsFromBenchmark compares a jdbc span's nine tags one by one,
through a contributor and through an extractor, on a reused and a fresh
span.

Co-Authored-By: Claude Opus 5.5 <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 Oct 23, 2026
@datadog-datadog-prod-us1

This comment has been minimized.

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