Repository navigation
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>
This comment has been minimized.
This comment has been minimized.
🟢 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>
A null source is now a no-op, so a TagExtractor can assume a non-null source and integrations don't each repeat the null-check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TagExtractor is a stateless policy meant to be a static final, non-capturing extractor bound at the call site; setTags is the small consumer that must inline for each call site to see the exact extractor type. The markers record that contract for review and future checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…load setTags(source, extractor) sat one letter from setTag(name, value), though the source is not a tag value: setTag stores one value, while this derives any number of tags from the source. It also collided in spirit with LLMObsSpan.setTags(Map). setTagsFrom reads as "set tags from this source" and now covers both projections: setTagsFrom(source, extractor) for foreign objects and setTagsFrom(contributor) for typed objects we own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers both overloads: the extractor gets the source and this span, the contributor gets this span, a null source or contributor is skipped, and each returns the span. TagContributor's Javadoc named a KnownTagIds class that doesn't exist; the generated class is KnownTags. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…odel TagExtractor now points to @strategy: prefer a named class held in a static final of its concrete type, with a non-capturing lambda accepted as the weaker shape, and setTagsFrom as the small consumer that must inline. The benchmark Javadoc drops the "decorator is structurally megamorphic" premise: 'mega' models the failure mode where the binding isn't visible to the JIT, and the arms measure profile-driven dispatch, not the static-final-through-an-inlined-consumer shape that ships. Also restores TagContributor's future tense for id-keyed setters, warns against deduplicating the benchmark's deliberately repeated bodies, drops a conversational note, and reads the mode once in setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8af312ebf1
ℹ️ 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".
There was a problem hiding this comment.
More details
Immutable spans still discard projected tag writes. Callback execution matches the new methods’ documented delegation behavior, so the reviewer’s no-op callback concern does not establish a correctness regression.
🤖 Bits Code Review · Commit 8af312e · @DataDog review to ask questions
NoopSpan (via ImmutableSpan) discards every tag, but inherited the setTagsFrom defaults, so an extractor or contributor still ran its metadata access and side effects with tracing disabled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What Does This Do
Adds two span-tag APIs and a benchmark that justifies them. This is the API contract only (draft).
The APIs:
TagContributor(intrinsic): a typed POJO we own projects its own state onto a span viaaddTo(AgentSpan).DBInfo, git metadata andExtractedContextare the precedents.TagExtractor<T>(extrinsic,@FunctionalInterface): extracts tags from a foreign object we don't own (InetSocketAddress,Request) onto a span. It's a@Strategypreferably an inline non-capturing or a single instance held in astatic finalfield.AgentSpan.setTagsFrom(...): the span-first, product-facing forms,span.setTagsFrom(connection, EXTRACTOR)for an extractor andspan.setTagsFrom(dbInfo)for a contributor. NamedsetTagsFromrather thansetTagsbecause the source isn't itself a tag value:setTag(name, value)stores one value, while these derive any number of tags from the source. 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: compares decorator, extractor and contributor dispatch.Motivation
This is the foundation for incrementally dissolving the span decorators, a deep virtual hierarchy (
BaseDecorator → {Client,Server}Decorator → {Http,Database,…}* → ~149 concrete), into composable, JIT-friendly tag projection.It establishes a style we know compiles well (call-time binding, one dispatch, a small consumer) ahead of
setTagitself inlining well. TodaysetTaggoes throughDDSpan,DDSpanContext,TagInterceptor's name switch andTagMap's lookup, which limits the end-to-end gain. That's why the benchmark uses a lightweight sink: it isolates dispatch rather than lettingsetTag's cost swamp it. The payoff compounds as the write path gets cheaper: id-keyed setters, id-keyed interception, the dense store.The template-method decorator makes one dynamic dispatch per hook; an extractor or contributor makes one in total. Benchmark (
@Threads(8) @Fork(3), throughput,-prof gc):-XX:+PrintInliningconfirms the mechanism: decorator-mega leaves all 4 template calls asvirtual call(megamorphic, not inlined); extractor-mega is onevirtual callplus an inlined body.@Threads(8), which adds noise to a benchmark that allocates nothing: the three mono arms should nearly tie but spread about 30%. So the extractor and contributor aren't ranked against each other here; only the decorator's mega drop clearly stands out.What the benchmark supports is a robust floor: fewer dynamic dispatches (N→1), which holds on the interpreter, C1 and the cold path with no JIT needed. That's what the
megaarm shows, and it models the failure mode where the binding isn't visible to the JIT. When it is visible, both shapes can compile to straight-line code: an extractor bound as astatic finalthrough the tinysetTagsFromdevirtualizes on its exact type oncesetTagsFrominlines, and a decorator's template calls devirtualize the same way if its larger template method inlines (seeStrategy). The extractor's practical edge there is that its consumer is small enough to inline reliably.The arms read receivers from arrays at a private call site, so they measure profile-driven dispatch rather than the shape that ships (a
static finalconstant through a shared, inlined consumer). The mono-vs-mega ratios are illustrative, not a prediction of production gains.Additional Notes
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.XExtractor implements TagExtractor<T>with a private-constructorINSTANCE) rather than inline lambdas. That gives a name at the call site, a home for pure statics and caches (class statics, not lambda captures, so it stays non-capturing), and the composability the inheritance chain lacked. The one rubric line: no capturing lambdas (they allocate per call and break memoization in any lazy variant)./techdebtis deferred until the PR is ready for review.Contributor Checklist
type:and (comp:orinst:) labels in addition to any other useful labelsclose,fix, or any linking keywords when referencing an issueUse
solvesinstead, and assign the PR milestone to the issueJira ticket: N/A
🤖 Generated with Claude Code