Add TagContributor / TagExtractor span-tag APIs#11817
Draft
dougqh wants to merge 3 commits into
Draft
Conversation
Foundational contract for dissolving Decorators (see decorator-dissolution.md). API types only -- no wiring or consumers yet. - TagContributor (intrinsic): a typed POJO we own projects its own state onto a span via addTo(AgentSpan). DbInfo / git-meta / ExtractedContext are the precedents; addTo is a flat setTag sweep that becomes setTag(KnownTagIds.X, field) once the id-arm lands. - TagExtractor<T> (extrinsic, @FunctionalInterface): extracts tags from a foreign object we don't own (Connection/Request) onto a span. Intended as a static-final non-capturing lambda invoked from the integration's own advice (a monomorphic site the JIT devirtualizes + inlines) -- the opposite of the decorator's shared megamorphic dispatch. Two modes: place tags directly, or build a memoized POJO that is itself a TagContributor. Both target AgentSpan for now so they can drive span-level state (resource name, error, status) during the transition; the sink narrows toward TagMap as those fields migrate into the tag model. Javadoc bakes in the compile-to-~zero litmus + the monomorphic-site / no-shared-sink discipline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Representative benchmark for the decorator-dissolution thesis: project a
typed source's fields onto a span three ways (decorator template-method
pattern / TagExtractor-shaped lambda / TagContributor-shaped), to show
what the JIT compiles away and what survives. Synthetic structural
equivalents + a light DCE-safe sink isolate *dispatch* cost.
@Param mode {mono, mega}: mono = one type (charitable to the decorator);
mega = 8 equivalent loaded types exercised so the call site / shared-base
template calls go megamorphic. Methodology: @threads(8), @fork(3),
Throughput + -prof gc (dougqh's standard — multi-thread reveals alloc
pressure, 3+ forks expose bimodal JIT/devirt).
Results (ops/s, 3f x 8t): mono ~ties (1.26-1.66B, abstraction free);
mega decorator 426M (-72%) vs extractor 914M (-27%) / contributor 1026M
(-38%). Zero alloc in all arms (pure dispatch). PrintInlining confirms
the mechanism: decorator-mono inlines all 4 template calls; decorator-
mega leaves all 4 as `virtual call` (megamorphic); extractor-mega has ONE
virtual call + inlined body. The template-method pattern pays one
megamorphic dispatch PER template method; extractor/contributor pay one
total -> the decorator's cost scales with field count (worse for the 7+
HttpServer hooks), while extractor/contributor can stay monomorphic
per-site (~3-4x faster in reality).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
|
🎯 Code Coverage (details) 🔗 Commit SHA: f2878b2 | Docs | Datadog PR Page | Give us feedback! |
Contributor
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
The span-first, product-dev-facing form of extractor.extract(source, this) (matches setTag / setAllTags). The indirection inlines away at a monomorphic call site, and it's the seam where an implementer can coarse-lock the whole extraction. Part of the TagExtractor API; consumed by the JDBC canary and subsequent extractor migrations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft — API contract + justifying benchmark. Consumers stack on top: #11820 (JDBC), #11822 (peer-connection), #11823 (dbUser peel).
Foundation for incrementally dissolving the span Decorators (deep virtual hierarchy:
BaseDecorator → {Client,Server}Decorator → {Http,Database,…}* → ~149 concrete) into composable, JIT-friendly tag projection.What
TagContributor(intrinsic) — a typed POJO we own projects its own state onto a span viaaddTo(AgentSpan).DBInfo/ git-meta /ExtractedContextare the precedents.TagExtractor<T>(extrinsic,@FunctionalInterface) — extracts tags from a foreign object we don't own (InetSocketAddress/Request) onto a span. Realized as astatic finalnon-capturing extractor invoked from the integration's own (monomorphic) advice site.AgentSpan.setTags(source, extractor)— the span-first, product-facing form (span.setTags(dbInfo, EXTRACTOR), matchingsetTag/setAllTags). The extra indirection inlines away at a monomorphic site; it's also the seam an implementer can override to coarse-lock the whole extraction.TagProjectionBenchmark— justifies the direction (decorator vs extractor vs contributor dispatch).Both interfaces target
AgentSpanfor now (so they can also drive span-level state — resource name, error, status — during the transition); the sink narrows towardTagMapas those fields migrate into the tag model.Convention established by the consumers: extractors are promoted to named singleton classes (
XExtractor implements TagExtractor<T>+ private-ctorINSTANCE) rather than inline lambdas — for a name at the call site, a home for pure statics + caches (class static, not lambda-captured → stays non-capturing), and composability the inheritance chain lacked. The one rubric line: no capturing lambdas (allocates per call + breaks memoization on any lazy variant).Why (benchmark,
@Threads(8) @Fork(3), Throughput +-prof gc)The template-method decorator makes one dynamic dispatch per hook; an extractor/contributor makes one total.
-XX:+PrintInliningconfirms the mechanism: decorator-mega leaves all 4 template calls asvirtual call(megamorphic, not inlined); extractor-mega is onevirtual call+ inlined body.Two stacked wins: a robust floor (fewer dynamic dispatches, N→1 — holds on the interpreter/C1/cold path, no JIT needed) and a JIT ceiling (devirt + inline → ~zero when monomorphic). By construction the decorator's dispatch is structurally megamorphic; extractors are per-integration monomorphic → ~3× in the real-world comparison (extractor-mono vs decorator-mega).
/techdebtdeferred until ready-for-review (draft).🤖 Generated with Claude Code