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/3] 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/3] 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/3] 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);