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..0b449b74ca2 --- /dev/null +++ b/internal-api/src/jmh/java/datadog/trace/api/TagProjectionBenchmark.java @@ -0,0 +1,382 @@ +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}: + * + *
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. 3+ forks expose bimodal JIT/devirtualization 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;
+
+ 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;
+
+ 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.mono = "mono".equals(this.mode);
+ 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.
+ // 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:
+ 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 (this.mono) 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;
+ }
+}
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 94c0f80572a..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
@@ -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;
@@ -108,6 +109,35 @@ default boolean isValid() {
AgentSpan setAllTags(Map 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 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). 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
new file mode 100644
index 00000000000..a62e10836b7
--- /dev/null
+++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagExtractor.java
@@ -0,0 +1,38 @@
+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
+ * 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:
+ *
+ * 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)}.
+ *
+ * @param
+ *
+ *
+ *