From 6125b3776c6daf12fccf128a552461be96aa4ee3 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 30 Jun 2026 17:03:16 -0400 Subject: [PATCH 1/9] Introduce TagContributor + TagExtractor span-tag APIs 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 (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 --- .../instrumentation/api/TagContributor.java | 23 ++++++++++++++ .../instrumentation/api/TagExtractor.java | 30 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java create mode 100644 internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java new file mode 100644 index 00000000000..e3f7877e27b --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java @@ -0,0 +1,23 @@ +package datadog.trace.bootstrap.instrumentation.api; + +/** + * An object that projects its own typed state onto a span — the inverse of a builder that takes + * external {@code setTag} calls. Implemented by typed POJOs we own whose fields map to known tags + * (e.g. {@code DbInfo}, git metadata, extracted context). {@code addTo} is a flat sweep of {@code + * span.setTag(...)}; once the id-arm lands it becomes {@code setTag(KnownTagIds.X, field)}, so it + * compiles to field-loads + positional stores with no {@code keyOf}. + * + *

This is an authoring aid meant to compile to ~zero — the opposite of a Decorator. Apply it at + * a CONCRETE-type, monomorphic call site (the integration's own advice) so {@code addTo} + * devirtualizes and inlines. Do not route many contributors through one shared {@code + * List} sink — that re-megamorphizes and recreates the decorator problem. Prefer + * stateless implementations. + * + *

Targets {@link AgentSpan} for now (so it can also drive span-level state — resource name, + * error, measured — that is not yet expressed as tags). As those span fields migrate into the tag + * model, this surface narrows toward a future {@code addTo(TagMap)}. See {@link TagExtractor} for + * the extrinsic counterpart (foreign objects we do not own). + */ +public interface TagContributor { + void addTo(AgentSpan span); +} diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java new file mode 100644 index 00000000000..4881308a231 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java @@ -0,0 +1,30 @@ +package datadog.trace.bootstrap.instrumentation.api; + +/** + * Extracts tags from a foreign object we do not own (a framework {@code Connection}, request, + * response, etc.) onto a span — the extrinsic counterpart to {@link TagContributor}. This is the + * irreducible "reach into version-specific framework objects" that cannot be expressed as data; it + * stays imperative, but contained in a narrow, single-purpose place. + * + *

Two usage modes, chosen by the source's lifecycle vs. the span: + * + *

    + *
  • read {@code source} and place tags directly — when {@code source} is per-span (a request / + * statement); + *
  • build a memoized typed POJO that is itself a {@link TagContributor} — when {@code source} + * outlives the span (e.g. {@code Connection -> DbInfo}, extracted once and cached). + *
+ * + *

This is an authoring aid meant to compile to ~zero — the opposite of a Decorator. Intended as + * a {@code static final}, non-capturing lambda invoked from the integration's own advice: a + * monomorphic call site the JIT devirtualizes and inlines (strictly better than dispatching through + * the decorator hierarchy). Never route many extractors through one shared site. Targets {@link + * AgentSpan} so it can also drive span-level state (resource name, error, status) during the + * transition; that surface narrows as those fields migrate into the tag model. + * + * @param the foreign source type to extract from + */ +@FunctionalInterface +public interface TagExtractor { + void extract(T source, AgentSpan span); +} From 114e15115833309ab31462d1f332f97a0518c490 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 30 Jun 2026 17:23:23 -0400 Subject: [PATCH 2/9] Add TagProjectionBenchmark: decorator vs extractor/contributor dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../trace/api/TagProjectionBenchmark.java | 373 ++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 internal-api/src/jmh/java/datadog/trace/api/TagProjectionBenchmark.java diff --git a/internal-api/src/jmh/java/datadog/trace/api/TagProjectionBenchmark.java b/internal-api/src/jmh/java/datadog/trace/api/TagProjectionBenchmark.java new file mode 100644 index 00000000000..b37eb514b4f --- /dev/null +++ b/internal-api/src/jmh/java/datadog/trace/api/TagProjectionBenchmark.java @@ -0,0 +1,373 @@ +package datadog.trace.api; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Models projecting a typed source's fields onto a span three ways — the decorator template-method + * pattern vs. the {@code TagExtractor} / {@code TagContributor} shape — to show what compiles away + * and what doesn't. Uses synthetic structural equivalents (a light DCE-safe {@link Sink} stands in + * for the span) so it isolates *dispatch* cost, not span/tag-storage cost; the real interfaces + * target {@code AgentSpan}, but the call-site shape is what this measures. + * + *

{@code mode}: + * + *

    + *
  • mono — one concrete type at the call site. The JIT devirtualizes + inlines all three + * → they should ~tie (the abstraction is free at the decorator's BEST case — charitable, + * since a real program loads many decorators). + *
  • mega — {@link #TYPES} equivalent loaded types, exercised so the call site (and, for + * the decorator, the shared base's template-method calls) go megamorphic. The decorator is + * FORCED here in reality (its template dispatch is structural). Extractor/contributor are + * shown in mega too to prove they are not magic: routed through a shared megamorphic site + * they degrade identically. The real win is that they CAN stay mono (per-integration site) + * and the decorator cannot. + *
+ * + *

Run with {@code -prof gc} (captures should stay flat) and {@code -XX:+PrintInlining} to + * confirm the mechanism: decorator template calls megamorphic/not-inlined under mega; + * extractor/contributor inlined under mono. Throughput is the headline; per your methodology, + * multi-thread reveals alloc pressure and 3+ forks expose bimodal JIT/devirt behavior. + */ +@State(Scope.Thread) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Warmup(iterations = 5, time = 2) +@Measurement(iterations = 5, time = 2) +@Fork(3) +@Threads(8) +public class TagProjectionBenchmark { + static final int TYPES = 8; + + @Param({"mono", "mega"}) + String mode; + + /** Light DCE-safe stand-in for the span — identical trivial work per set across all arms. */ + static final class Sink { + long acc; + + void set(String key, Object value) { + this.acc += key.length() + (value == null ? 0 : value.hashCode()); + } + } + + /** The typed source whose fields are projected (a stand-in for DbInfo). */ + static final class DbPojo { + final String dbType, dbUser, dbInstance, dbHostname; + + DbPojo(String t, String u, String i, String h) { + this.dbType = t; + this.dbUser = u; + this.dbInstance = i; + this.dbHostname = h; + } + } + + // ---- (1) decorator template-method pattern: shared base onConnection() calls abstract hooks + // ---- + abstract static class TemplateDecorator { + abstract String dbType(DbPojo p); + + abstract String dbUser(DbPojo p); + + abstract String dbInstance(DbPojo p); + + abstract String dbHostname(DbPojo p); + + // shared base: calls the abstract template methods -> megamorphic when many subclasses loaded + final void onConnection(Sink sink, DbPojo p) { + String type = dbType(p); + if (type != null) sink.set("db.type", type); + String user = dbUser(p); + if (user != null) sink.set("db.user", user); + String instance = dbInstance(p); + if (instance != null) sink.set("db.instance", instance); + String host = dbHostname(p); + if (host != null) sink.set("peer.hostname", host); + } + } + + // ---- (2) extractor shape: stateless lambda-style, reads source -> sets directly ---- + interface Extractor { + void extract(DbPojo p, Sink sink); + } + + // ---- (3) contributor shape: the source projects itself ---- + interface Contributor { + void addTo(Sink sink); + } + + private DbPojo[] pojos; + private TemplateDecorator[] decorators; + private Extractor[] extractors; + private Contributor[] contributors; + private Sink sink; + private int idx; + + @Setup(Level.Trial) + public void setup() { + this.sink = new Sink(); + this.pojos = new DbPojo[TYPES]; + this.decorators = new TemplateDecorator[TYPES]; + this.extractors = new Extractor[TYPES]; + this.contributors = new Contributor[TYPES]; + for (int i = 0; i < TYPES; i++) { + DbPojo pojo = new DbPojo("h2", "sa", "db" + i, "localhost"); + this.pojos[i] = pojo; + this.decorators[i] = newDecorator(i); + this.extractors[i] = newExtractor(i); + this.contributors[i] = newContributor(pojo, i); + } + } + + // distinct concrete types per index so 'mega' has TYPES loaded implementations to megamorphize + private static TemplateDecorator newDecorator(int i) { + switch (i) { + case 0: + return new TemplateDecorator() { + String dbType(DbPojo p) { + return p.dbType; + } + + String dbUser(DbPojo p) { + return p.dbUser; + } + + String dbInstance(DbPojo p) { + return p.dbInstance; + } + + String dbHostname(DbPojo p) { + return p.dbHostname; + } + }; + case 1: + return new TemplateDecorator() { + String dbType(DbPojo p) { + return p.dbType; + } + + String dbUser(DbPojo p) { + return p.dbUser; + } + + String dbInstance(DbPojo p) { + return p.dbInstance; + } + + String dbHostname(DbPojo p) { + return p.dbHostname; + } + }; + case 2: + return new TemplateDecorator() { + String dbType(DbPojo p) { + return p.dbType; + } + + String dbUser(DbPojo p) { + return p.dbUser; + } + + String dbInstance(DbPojo p) { + return p.dbInstance; + } + + String dbHostname(DbPojo p) { + return p.dbHostname; + } + }; + case 3: + return new TemplateDecorator() { + String dbType(DbPojo p) { + return p.dbType; + } + + String dbUser(DbPojo p) { + return p.dbUser; + } + + String dbInstance(DbPojo p) { + return p.dbInstance; + } + + String dbHostname(DbPojo p) { + return p.dbHostname; + } + }; + case 4: + return new TemplateDecorator() { + String dbType(DbPojo p) { + return p.dbType; + } + + String dbUser(DbPojo p) { + return p.dbUser; + } + + String dbInstance(DbPojo p) { + return p.dbInstance; + } + + String dbHostname(DbPojo p) { + return p.dbHostname; + } + }; + case 5: + return new TemplateDecorator() { + String dbType(DbPojo p) { + return p.dbType; + } + + String dbUser(DbPojo p) { + return p.dbUser; + } + + String dbInstance(DbPojo p) { + return p.dbInstance; + } + + String dbHostname(DbPojo p) { + return p.dbHostname; + } + }; + case 6: + return new TemplateDecorator() { + String dbType(DbPojo p) { + return p.dbType; + } + + String dbUser(DbPojo p) { + return p.dbUser; + } + + String dbInstance(DbPojo p) { + return p.dbInstance; + } + + String dbHostname(DbPojo p) { + return p.dbHostname; + } + }; + default: + return new TemplateDecorator() { + String dbType(DbPojo p) { + return p.dbType; + } + + String dbUser(DbPojo p) { + return p.dbUser; + } + + String dbInstance(DbPojo p) { + return p.dbInstance; + } + + String dbHostname(DbPojo p) { + return p.dbHostname; + } + }; + } + } + + // distinct Extractor lambda expressions -> distinct synthetic types (megamorphic when rotated) + private static Extractor newExtractor(int i) { + switch (i) { + case 0: + return TagProjectionBenchmark::extract; + case 1: + return (p, s) -> extract(p, s); + case 2: + return (DbPojo p, Sink s) -> extract(p, s); + case 3: + return (p, s) -> { + extract(p, s); + }; + case 4: + return (final DbPojo p, final Sink s) -> extract(p, s); + case 5: + return (p, s) -> extract(p, s); + case 6: + return (DbPojo p, Sink s) -> { + extract(p, s); + }; + default: + return (p, s) -> extract(p, s); + } + } + + // the actual projection, shared so all extractor variants do identical work + private static void extract(DbPojo p, Sink sink) { + if (p.dbType != null) sink.set("db.type", p.dbType); + if (p.dbUser != null) sink.set("db.user", p.dbUser); + if (p.dbInstance != null) sink.set("db.instance", p.dbInstance); + if (p.dbHostname != null) sink.set("peer.hostname", p.dbHostname); + } + + private static Contributor newContributor(DbPojo p, int i) { + switch (i) { + case 0: + return s -> extract(p, s); + case 1: + return (Sink s) -> extract(p, s); + case 2: + return s -> { + extract(p, s); + }; + case 3: + return (final Sink s) -> extract(p, s); + case 4: + return s -> extract(p, s); + case 5: + return (Sink s) -> { + extract(p, s); + }; + case 6: + return s -> extract(p, s); + default: + return (Sink s) -> extract(p, s); + } + } + + // mono: always index 0 (one type at the site). mega: rotate -> the site sees all TYPES. + private int next() { + if ("mono".equals(this.mode)) return 0; + int i = this.idx + 1; + if (i >= TYPES) i = 0; + this.idx = i; + return i; + } + + @Benchmark + public long decorator() { + int i = next(); + this.decorators[i].onConnection(this.sink, this.pojos[i]); + return this.sink.acc; + } + + @Benchmark + public long extractor() { + int i = next(); + this.extractors[i].extract(this.pojos[i], this.sink); + return this.sink.acc; + } + + @Benchmark + public long contributor() { + int i = next(); + this.contributors[i].addTo(this.sink); + return this.sink.acc; + } +} From f2878b2f17a424e45267065b47e399ef47fde637 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 30 Jun 2026 20:50:57 -0400 Subject: [PATCH 3/9] Add span-first AgentSpan.setTags(source, extractor) 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 --- .../bootstrap/instrumentation/api/AgentSpan.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java index 0eb67f3ad52..4a92241097f 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java @@ -88,6 +88,18 @@ default boolean isValid() { AgentSpan setAllTags(Map map); + /** + * Sets tags on this span by applying a {@link TagExtractor} to a foreign source — the span-first, + * product-dev-facing form of {@code extractor.extract(source, this)} (matches {@code + * setTag}/{@code setAllTags}). The extra indirection inlines away at a monomorphic call site. + * This is also the seam where the whole extraction can be coarse-locked (one critical section) — + * an implementer may override to do so. + */ + default AgentSpan setTags(final T source, final TagExtractor extractor) { + extractor.extract(source, this); + return this; + } + @Override AgentSpan setTag(String key, Number value); From 28b356adc9f8f525c8edfa9a3d466604d12faa9d Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 8 Jul 2026 10:11:39 -0400 Subject: [PATCH 4/9] Guard AgentSpan.setTags against a null source 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 --- .../trace/bootstrap/instrumentation/api/AgentSpan.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java index 4a92241097f..46257f93aeb 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java @@ -96,7 +96,9 @@ default boolean isValid() { * an implementer may override to do so. */ default AgentSpan setTags(final T source, final TagExtractor extractor) { - extractor.extract(source, this); + if (source != null) { + extractor.extract(source, this); + } return this; } From c5a9b0a16da58a07aafa51ccb8622bcbd1825cf9 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 5 Oct 2026 14:48:42 -0400 Subject: [PATCH 5/9] Mark TagExtractor as a @Strategy and setTags as its @StrategyConsumer 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 --- .../datadog/trace/bootstrap/instrumentation/api/AgentSpan.java | 2 ++ .../trace/bootstrap/instrumentation/api/TagExtractor.java | 3 +++ 2 files changed, 5 insertions(+) diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java index daf16c9652a..f3e74ebb681 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java @@ -11,6 +11,7 @@ import datadog.trace.api.DDTraceId; import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; +import datadog.trace.api.function.StrategyConsumer; import datadog.trace.api.gateway.IGSpanInfo; import datadog.trace.api.gateway.RequestContext; import datadog.trace.api.interceptor.MutableSpan; @@ -115,6 +116,7 @@ default boolean isValid() { * This is also the seam where the whole extraction can be coarse-locked (one critical section) — * an implementer may override to do so. */ + @StrategyConsumer default AgentSpan setTags(final T source, final TagExtractor extractor) { if (source != null) { extractor.extract(source, this); diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java index 4881308a231..b90921985fb 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java @@ -1,5 +1,7 @@ package datadog.trace.bootstrap.instrumentation.api; +import datadog.trace.api.function.Strategy; + /** * Extracts tags from a foreign object we do not own (a framework {@code Connection}, request, * response, etc.) onto a span — the extrinsic counterpart to {@link TagContributor}. This is the @@ -24,6 +26,7 @@ * * @param the foreign source type to extract from */ +@Strategy @FunctionalInterface public interface TagExtractor { void extract(T source, AgentSpan span); From 1c2c39e296e891742d0cc72ab973183f2017a6d6 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 5 Oct 2026 15:42:21 -0400 Subject: [PATCH 6/9] Rename AgentSpan.setTags to setTagsFrom and add a TagContributor overload 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 --- .../instrumentation/api/AgentSpan.java | 23 +++++++++++++++---- .../instrumentation/api/TagContributor.java | 3 ++- .../instrumentation/api/TagExtractor.java | 2 ++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java index f3e74ebb681..3b1239cedec 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java @@ -111,19 +111,32 @@ default boolean isValid() { /** * Sets tags on this span by applying a {@link TagExtractor} to a foreign source — the span-first, - * product-dev-facing form of {@code extractor.extract(source, this)} (matches {@code - * setTag}/{@code setAllTags}). The extra indirection inlines away at a monomorphic call site. - * This is also the seam where the whole extraction can be coarse-locked (one critical section) — - * an implementer may override to do so. + * product-dev-facing form of {@code extractor.extract(source, this)}. Named {@code setTagsFrom}, + * not {@code setTags}, because {@code source} is not itself a tag value: {@code setTag(name, + * value)} stores one value, while this derives any number of tags from {@code source}. The extra + * indirection inlines away at a monomorphic call site. This is also the seam where the whole + * extraction can be coarse-locked (one critical section) — an implementer may override to do so. */ @StrategyConsumer - default AgentSpan setTags(final T source, final TagExtractor extractor) { + default AgentSpan setTagsFrom(final T source, final TagExtractor extractor) { if (source != null) { extractor.extract(source, this); } return this; } + /** + * Sets the tags a {@link TagContributor} projects from its own state — the span-first form of + * {@code contributor.addTo(this)}. Call it with the contributor's concrete type so {@code addTo} + * devirtualizes and inlines. + */ + default AgentSpan setTagsFrom(final TagContributor contributor) { + if (contributor != null) { + contributor.addTo(this); + } + return this; + } + @Override AgentSpan setTag(String key, Number value); diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java index e3f7877e27b..271e21d6f28 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java @@ -16,7 +16,8 @@ *

Targets {@link AgentSpan} for now (so it can also drive span-level state — resource name, * error, measured — that is not yet expressed as tags). As those span fields migrate into the tag * model, this surface narrows toward a future {@code addTo(TagMap)}. See {@link TagExtractor} for - * the extrinsic counterpart (foreign objects we do not own). + * the extrinsic counterpart (foreign objects we do not own). Apply it with {@link + * AgentSpan#setTagsFrom(TagContributor)}. */ public interface TagContributor { void addTo(AgentSpan span); diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java index b90921985fb..12351ffc6bd 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java @@ -24,6 +24,8 @@ * AgentSpan} so it can also drive span-level state (resource name, error, status) during the * transition; that surface narrows as those fields migrate into the tag model. * + *

Apply it with {@link AgentSpan#setTagsFrom(Object, TagExtractor)}. + * * @param the foreign source type to extract from */ @Strategy From 85c6d19991701c2e1e8514d55fc7fc7fdca2da3f Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 5 Oct 2026 15:48:53 -0400 Subject: [PATCH 7/9] Test AgentSpan.setTagsFrom and fix a stale KnownTags reference 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 --- .../instrumentation/api/TagContributor.java | 4 +- .../api/AgentSpanSetTagsFromTest.java | 56 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanSetTagsFromTest.java diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java index 271e21d6f28..41eb6c22ac9 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java @@ -4,8 +4,8 @@ * An object that projects its own typed state onto a span — the inverse of a builder that takes * external {@code setTag} calls. Implemented by typed POJOs we own whose fields map to known tags * (e.g. {@code DbInfo}, git metadata, extracted context). {@code addTo} is a flat sweep of {@code - * span.setTag(...)}; once the id-arm lands it becomes {@code setTag(KnownTagIds.X, field)}, so it - * compiles to field-loads + positional stores with no {@code keyOf}. + * span.setTag(...)}; with the id-keyed setters it becomes {@code setTag(KnownTags.X_ID, field)}, so + * it compiles to field-loads + positional stores with no {@code keyOf}. * *

This is an authoring aid meant to compile to ~zero — the opposite of a Decorator. Apply it at * a CONCRETE-type, monomorphic call site (the integration's own advice) so {@code addTo} diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanSetTagsFromTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanSetTagsFromTest.java new file mode 100644 index 00000000000..8fe823e7719 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanSetTagsFromTest.java @@ -0,0 +1,56 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** {@link AgentSpan#setTagsFrom}: applying a {@link TagExtractor} or a {@link TagContributor}. */ +class AgentSpanSetTagsFromTest { + private final AgentSpan span = mock(AgentSpan.class, CALLS_REAL_METHODS); + private final List calls = new ArrayList<>(); + + @Test + void appliesTheExtractorToTheSourceAndThisSpan() { + TagExtractor extractor = + (source, target) -> { + calls.add(source); + calls.add(target); + }; + + assertSame(span, span.setTagsFrom("source", extractor)); + + assertEquals(2, calls.size()); + assertEquals("source", calls.get(0)); + assertSame(span, calls.get(1)); + } + + @Test + void skipsTheExtractorForANullSource() { + TagExtractor extractor = (source, target) -> calls.add(source); + + assertSame(span, span.setTagsFrom(null, extractor)); + + assertTrue(calls.isEmpty()); + } + + @Test + void appliesTheContributorToThisSpan() { + TagContributor contributor = calls::add; + + assertSame(span, span.setTagsFrom(contributor)); + + assertEquals(1, calls.size()); + assertSame(span, calls.get(0)); + } + + @Test + void skipsANullContributor() { + assertSame(span, span.setTagsFrom((TagContributor) null)); + } +} From 8af312ebf120c239ee5de5b1f67e1a47c2007161 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 5 Oct 2026 16:16:29 -0400 Subject: [PATCH 8/9] Align TagExtractor docs and the benchmark with the refined Strategy model 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 --- .../trace/api/TagProjectionBenchmark.java | 27 ++++++++++++------- .../instrumentation/api/AgentSpan.java | 7 ++--- .../instrumentation/api/TagContributor.java | 4 +-- .../instrumentation/api/TagExtractor.java | 15 ++++++----- 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/internal-api/src/jmh/java/datadog/trace/api/TagProjectionBenchmark.java b/internal-api/src/jmh/java/datadog/trace/api/TagProjectionBenchmark.java index b37eb514b4f..0b449b74ca2 100644 --- a/internal-api/src/jmh/java/datadog/trace/api/TagProjectionBenchmark.java +++ b/internal-api/src/jmh/java/datadog/trace/api/TagProjectionBenchmark.java @@ -29,17 +29,21 @@ * → they should ~tie (the abstraction is free at the decorator's BEST case — charitable, * since a real program loads many decorators). *
  • mega — {@link #TYPES} equivalent loaded types, exercised so the call site (and, for - * the decorator, the shared base's template-method calls) go megamorphic. The decorator is - * FORCED here in reality (its template dispatch is structural). Extractor/contributor are - * shown in mega too to prove they are not magic: routed through a shared megamorphic site - * they degrade identically. The real win is that they CAN stay mono (per-integration site) - * and the decorator cannot. + * the decorator, the shared base's template-method calls) go megamorphic. This models the + * failure mode where the binding isn't visible to the JIT, so devirtualization falls back to + * receiver profiles. All three degrade; the decorator degrades most because it makes one + * dynamic dispatch per hook, where an extractor or contributor makes one in total. * * + *

    Receivers here come from arrays at a private call site, so every arm measures profile-driven + * dispatch. Production instead binds a {@code static final} constant at the call site through a + * small shared consumer ({@code setTagsFrom}), which devirtualizes on the exact type when the + * consumer inlines (see {@link datadog.trace.api.function.Strategy}); the decorator gets the same + * benefit only when its larger template method inlines. These arms don't model that shape. + * *

    Run with {@code -prof gc} (captures should stay flat) and {@code -XX:+PrintInlining} to * confirm the mechanism: decorator template calls megamorphic/not-inlined under mega; - * extractor/contributor inlined under mono. Throughput is the headline; per your methodology, - * multi-thread reveals alloc pressure and 3+ forks expose bimodal JIT/devirt behavior. + * extractor/contributor inlined under mono. 3+ forks expose bimodal JIT/devirtualization behavior. */ @State(Scope.Thread) @BenchmarkMode(Mode.Throughput) @@ -54,6 +58,8 @@ public class TagProjectionBenchmark { @Param({"mono", "mega"}) String mode; + private boolean mono; + /** Light DCE-safe stand-in for the span — identical trivial work per set across all arms. */ static final class Sink { long acc; @@ -118,6 +124,7 @@ interface Contributor { @Setup(Level.Trial) public void setup() { + this.mono = "mono".equals(this.mode); this.sink = new Sink(); this.pojos = new DbPojo[TYPES]; this.decorators = new TemplateDecorator[TYPES]; @@ -132,7 +139,9 @@ public void setup() { } } - // distinct concrete types per index so 'mega' has TYPES loaded implementations to megamorphize + // distinct concrete types per index so 'mega' has TYPES loaded implementations to megamorphize. + // Keep these bodies separate even though they repeat: a shared base class or a shared lambda + // would give the JIT a single implementation to inline, and 'mega' would quietly measure 'mono'. private static TemplateDecorator newDecorator(int i) { switch (i) { case 0: @@ -343,7 +352,7 @@ private static Contributor newContributor(DbPojo p, int i) { // mono: always index 0 (one type at the site). mega: rotate -> the site sees all TYPES. private int next() { - if ("mono".equals(this.mode)) return 0; + if (this.mono) return 0; int i = this.idx + 1; if (i >= TYPES) i = 0; this.idx = i; diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java index 3b1239cedec..80660efd65b 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java @@ -113,9 +113,10 @@ default boolean isValid() { * Sets tags on this span by applying a {@link TagExtractor} to a foreign source — the span-first, * product-dev-facing form of {@code extractor.extract(source, this)}. Named {@code setTagsFrom}, * not {@code setTags}, because {@code source} is not itself a tag value: {@code setTag(name, - * value)} stores one value, while this derives any number of tags from {@code source}. The extra - * indirection inlines away at a monomorphic call site. This is also the seam where the whole - * extraction can be coarse-locked (one critical section) — an implementer may override to do so. + * value)} stores one value, while this derives any number of tags from {@code source}. Kept small + * so it inlines: each call site then sees the exact extractor type (see {@link + * datadog.trace.api.function.Strategy}). This is also the seam where the whole extraction can be + * coarse-locked (one critical section) — an implementer may override to do so. */ @StrategyConsumer default AgentSpan setTagsFrom(final T source, final TagExtractor extractor) { diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java index 41eb6c22ac9..637600b4203 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContributor.java @@ -4,8 +4,8 @@ * An object that projects its own typed state onto a span — the inverse of a builder that takes * external {@code setTag} calls. Implemented by typed POJOs we own whose fields map to known tags * (e.g. {@code DbInfo}, git metadata, extracted context). {@code addTo} is a flat sweep of {@code - * span.setTag(...)}; with the id-keyed setters it becomes {@code setTag(KnownTags.X_ID, field)}, so - * it compiles to field-loads + positional stores with no {@code keyOf}. + * span.setTag(...)}; once id-keyed setters land, it becomes {@code setTag(KnownTags.X_ID, field)}, + * so it compiles to field-loads + positional stores with no {@code keyOf}. * *

    This is an authoring aid meant to compile to ~zero — the opposite of a Decorator. Apply it at * a CONCRETE-type, monomorphic call site (the integration's own advice) so {@code addTo} diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java index 12351ffc6bd..a62e10836b7 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java @@ -17,12 +17,15 @@ * outlives the span (e.g. {@code Connection -> DbInfo}, extracted once and cached). * * - *

    This is an authoring aid meant to compile to ~zero — the opposite of a Decorator. Intended as - * a {@code static final}, non-capturing lambda invoked from the integration's own advice: a - * monomorphic call site the JIT devirtualizes and inlines (strictly better than dispatching through - * the decorator hierarchy). Never route many extractors through one shared site. Targets {@link - * AgentSpan} so it can also drive span-level state (resource name, error, status) during the - * transition; that surface narrows as those fields migrate into the tag model. + *

    This is an authoring aid meant to compile to ~zero — the opposite of a Decorator. It is a + * {@link datadog.trace.api.function.Strategy}: bind it at the call site, preferably as a named + * class held in a {@code static final} field of that concrete class ({@code XExtractor.INSTANCE}); + * a non-capturing lambda constant is accepted but is the weaker, speculative shape. The consumer, + * {@link AgentSpan#setTagsFrom(Object, TagExtractor)}, is small so it inlines and each call site + * sees the exact extractor type. Don't hold extractors in a field or collection of the abstract + * type and dispatch from there. Targets {@link AgentSpan} so it can also drive span-level state + * (resource name, error, status) during the transition; that surface narrows as those fields + * migrate into the tag model. * *

    Apply it with {@link AgentSpan#setTagsFrom(Object, TagExtractor)}. * From 6956c7dd0b507c2d3ca34bcacec81fd9c67c50dd Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 7 Oct 2026 15:51:56 -0400 Subject: [PATCH 9/9] Skip tag extractors and contributors on immutable spans 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 --- .../instrumentation/api/ImmutableSpan.java | 14 ++++++++++++++ .../api/AgentSpanSetTagsFromTest.java | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ImmutableSpan.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ImmutableSpan.java index c71f8b59023..544beac87cd 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ImmutableSpan.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ImmutableSpan.java @@ -62,6 +62,20 @@ public AgentSpan setAllTags(Map map) { return this; } + /** Skips the extractor: its tags would be discarded, so its work and side effects are wasted. */ + @Override + public AgentSpan setTagsFrom(T source, TagExtractor extractor) { + return this; + } + + /** + * Skips the contributor: its tags would be discarded, so its work and side effects are wasted. + */ + @Override + public AgentSpan setTagsFrom(TagContributor contributor) { + return this; + } + @Override public AgentSpan setTag(String key, Number value) { return this; diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanSetTagsFromTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanSetTagsFromTest.java index 8fe823e7719..c03baa7e06b 100644 --- a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanSetTagsFromTest.java +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanSetTagsFromTest.java @@ -53,4 +53,22 @@ void appliesTheContributorToThisSpan() { void skipsANullContributor() { assertSame(span, span.setTagsFrom((TagContributor) null)); } + + @Test + void skipsTheExtractorOnANoopSpan() { + TagExtractor extractor = (source, target) -> calls.add(source); + + assertSame(NoopSpan.INSTANCE, NoopSpan.INSTANCE.setTagsFrom("source", extractor)); + + assertTrue(calls.isEmpty()); + } + + @Test + void skipsTheContributorOnANoopSpan() { + TagContributor contributor = calls::add; + + assertSame(NoopSpan.INSTANCE, NoopSpan.INSTANCE.setTagsFrom(contributor)); + + assertTrue(calls.isEmpty()); + } }