diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/DropWriter.java b/dd-trace-core/src/jmh/java/datadog/trace/core/DropWriter.java new file mode 100644 index 00000000000..727cc00fa57 --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/DropWriter.java @@ -0,0 +1,31 @@ +package datadog.trace.core; + +import datadog.trace.common.writer.Writer; +import java.util.List; + +/** + * No-op {@link Writer}: drops finished traces so span-creation benchmarks measure only the + * application-thread (front-half) allocation — create, tag, finish, PendingTrace completion — with + * no serialization or agent I/O leaking into the {@code -prof gc} number. + * + *
Drift-stable: implements only the five-method {@link Writer} interface, unchanged
+ * v1.53→master.
+ */
+final class DropWriter implements Writer {
+ @Override
+ public void write(List This is the "micro-ish" reference the TagMap 2.0 / SpanPrototype work is measured against. It
+ * pairs a tag-free baseline with two known-tag shapes — a web-server span (7 tags) and a JDBC/DB
+ * client span (9 tags) — so the dense-store and SpanPrototype allocation wins are actually
+ * exercised (a tag-free or custom-tag benchmark would be dense-neutral and show nothing), and so
+ * the marginal per-tag cost is visible across two realistic tag counts. It also covers the builder
+ * tag path ({@code withTag} before {@code start()}, the OTel-bridge shape) alongside the
+ * set-after-start path, so the startSpan/buildSpan lineages can be tracked as they diverge across
+ * releases.
+ *
+ * Read the allocation columns, not just throughput. Run with {@code -prof gc}: {@code
+ * gc.alloc.rate.norm} (B/op) is deterministic run-to-run and is the primary signal; throughput is
+ * thermal/contention-fragile on a laptop and should be treated as directional. Multi-fork
+ * ({@code @Fork(3)}) guards against per-fork inlining bimodality.
+ *
+ * Deliberately drift-stable so it can be copied onto past release tags and back-checked.
+ * It touches only API that is byte-identical from v1.53.0 to master: {@link
+ * CoreTracer#buildSpan(String, CharSequence)} / {@link CoreTracer#startSpan(String, CharSequence)},
+ * {@code AgentSpan.setTag(String, ...)} / {@code finish()}, the {@link Tags} constants, and the
+ * five-method {@code Writer} interface (implemented as a no-op {@link DropWriter}). If you add to
+ * it, keep it inside that stable surface or grafting it onto old tags for the historical curve will
+ * stop compiling. (Source rebuilds only reach ~v1.53 — older tags hit dead build-time dependencies;
+ * deeper history is a published-jar job.) The {@code *ViaPrototype} / {@code
+ * *ViaPrototypeStartSpan} arms are the exception: they exercise {@link SpanPrototype} (new in this
+ * PR) and do not graft onto pre-SpanPrototype tags — the historical table above covers the baseline
+ * arms only.
+ *
+ * Spans are finished against {@link DropWriter} so the create/tag/finish allocation is isolated
+ * from serialization and agent I/O — those live on a different lever and would otherwise leak into
+ * the {@code -prof gc} number via the writer's background threads.
+ *
+ * Multi-threaded on purpose ({@code @Threads(8)}); some tracer optimizations only show under
+ * contention. Use {@code -t 1} for a single-threaded run.
+ *
+ * Historical allocation (B/op, {@code gc.alloc.rate.norm}), this benchmark grafted onto
+ * each release tag and run {@code @Threads(8) -f3 -wi5 -i5 -prof gc} (measured 2026-07). The ~1.59
+ * drop is the TagMap 1.0 shared-Entry default flip; the ~1.61 drop is the interceptor/links
+ * cluster. Net 1.53 → 1.64 is -20% to -31% per arm.
+ *
+ * Throughput (ops/us) from the same runs — noisier, treat as directional only (laptop
+ * thermals + per-fork inlining bimodality; no Δ% given because there is no reliable trend):
+ *
+ * Isolates the constant-application only (not span creation or the {@code afterStart} virtual
+ * chain), so the delta is purely N-stamps vs. bulk-copy. All arms apply tags at the {@code TagMap}
+ * level and skip the per-tag {@code TagInterceptor} dispatch that the real construction seed still
+ * incurs (span.kind, analytics-rate, ... are intercepted). That dispatch is a common cost
+ * on both the old and new production paths, so it cancels in the delta — but it means the absolute
+ * ops/s and the new/old ratio here are a TagMap-level upper bound, not the end-to-end win. (The
+ * interceptor-free bulk-share is what TagInterceptor retirement eventually unlocks; the current
+ * seed intercepts.) Run with {@code -prof gc} — the interesting axes are ops/s and B/op.
+ */
+@State(Scope.Thread)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(SECONDS)
+@Warmup(iterations = 5, time = 2)
+@Measurement(iterations = 5, time = 2)
+@Fork(3)
+@Threads(8)
+public class SpanPrototypeBenchmark {
+
+ // The constant set a typical server span carries, as cached entries (the shared-Entry
+ // hand-optimization the decorators use today).
+ private static final TagMap.Entry COMPONENT = TagMap.Entry.create(Tags.COMPONENT, "netty");
+ private static final TagMap.Entry KIND =
+ TagMap.Entry.create(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER);
+ private static final TagMap.Entry LANGUAGE =
+ TagMap.Entry.create(DDTags.LANGUAGE_TAG_KEY, DDTags.LANGUAGE_TAG_VALUE);
+ private static final TagMap.Entry ANALYTICS =
+ TagMap.Entry.create(DDTags.ANALYTICS_SAMPLE_RATE, 1.0d);
+
+ private SpanPrototype prototype;
+
+ @Setup(Level.Trial)
+ public void setUp() {
+ // Baked once — the same constants, composed through the builder.
+ prototype =
+ SpanPrototype.builder()
+ .initComponentOnly("netty")
+ .initKind(Tags.SPAN_KIND_SERVER)
+ .initTag(DDTags.LANGUAGE_TAG_KEY, DDTags.LANGUAGE_TAG_VALUE)
+ .initTag(ANALYTICS)
+ .build();
+ }
+
+ @Benchmark
+ public TagMap oldPerSpanStamps() {
+ TagMap tags = TagMap.create();
+ tags.set(COMPONENT);
+ tags.set(KIND);
+ tags.set(LANGUAGE);
+ tags.set(ANALYTICS);
+ return tags;
+ }
+
+ @Benchmark
+ public TagMap newBulkApply() {
+ TagMap tags = TagMap.create();
+ tags.putAll(prototype.tags());
+ return tags;
+ }
+
+ @Benchmark
+ public TagMap newConstructionSeed() {
+ return prototype.tags().copy();
+ }
+}
diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java
index 761320a2205..37335e8d2d8 100644
--- a/internal-api/src/main/java/datadog/trace/api/TagMap.java
+++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java
@@ -177,6 +177,17 @@ public static final class Entry extends EntryChange
*/
static final byte ANY = 0;
+ /**
+ * Whether {@code value} is treated as "no tag" — a null, or an empty {@link CharSequence}. Set
+ * paths that honor the tag-filtering contract (e.g. {@code AgentSpan.setTag}, {@link
+ * SpanPrototype}) can gate on this without constructing an Entry — which matters once a dense
+ * store makes per-tag Entry allocation the wrong path.
+ */
+ public static boolean isEmptyValue(Object value) {
+ return value == null
+ || (value instanceof CharSequence && ((CharSequence) value).length() == 0);
+ }
+
/**
* Entry for {@code (tag, value)}, or null when {@code value} is null or an empty {@code
* CharSequence} -- checked by runtime type, so an empty String passed as {@code Object} skips
@@ -184,23 +195,14 @@ public static final class Entry extends EntryChange
*/
@Nullable
public static final Entry create(@Nonnull String tag, Object value) {
- if (value == null) {
- return null;
- }
- if (value instanceof CharSequence && ((CharSequence) value).length() == 0) {
- return null;
- }
- return TagMap.Entry.newAnyEntry(tag, value);
+ return isEmptyValue(value) ? null : TagMap.Entry.newAnyEntry(tag, value);
}
/** If value is non-null, returns a new TagMap.Entry If value is null or empty, returns null */
@Nullable
public static final Entry create(@Nonnull String tag, CharSequence value) {
// NOTE: From the static typing, we know that value is not a primitive box
-
- return (value == null || value.length() == 0)
- ? null
- : TagMap.Entry.newObjectEntry(tag, value);
+ return isEmptyValue(value) ? null : TagMap.Entry.newObjectEntry(tag, value);
}
public static final Entry create(@Nonnull String tag, boolean value) {
diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java
index 237ea38267f..c38d3bcdd8b 100644
--- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java
+++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentTracer.java
@@ -350,6 +350,30 @@ default AgentSpan blackholeSpan() {
*/
SpanBuilder buildSpan(String instrumentationName, CharSequence spanName);
+ /**
+ * Returns a SpanBuilder seeded from a {@link SpanPrototype}: the prototype supplies the
+ * instrumentation name, span type, and constant tags. {@code operationName} overrides the
+ * prototype's when non-null — the explicit value wins, the prototype is the fallback.
+ *
+ * This default seeds identity only; a real tracer should override it to also seed the
+ * prototype's tags (see {@code CoreTracer}). The no-op tracer discards tags, so identity-only
+ * is correct there.
+ */
+ default SpanBuilder buildSpan(@Nonnull SpanPrototype prototype, CharSequence operationName) {
+ return buildSpan(
+ prototype.instrumentationName(),
+ operationName != null ? operationName : prototype.operationName());
+ }
+
+ /**
+ * Creates and starts a span seeded from a {@link SpanPrototype}. This is the
+ * auto-instrumentation entry point mirroring {@link #startSpan(String, CharSequence)}; see
+ * {@link #buildSpan(SpanPrototype, CharSequence)}.
+ */
+ default AgentSpan startSpan(@Nonnull SpanPrototype prototype, CharSequence operationName) {
+ return buildSpan(prototype, operationName).start();
+ }
+
/**
* Returns a SpanBuilder that can be used to produce one and only one span. By imposing the
* single span creation limitation, this method is more efficient than {@link #buildSpan}
@@ -453,6 +477,13 @@ public AgentSpan startSpan(final String instrumentationName, final CharSequence
return NoopSpan.INSTANCE;
}
+ @Override
+ public AgentSpan startSpan(
+ @Nonnull final SpanPrototype prototype, final CharSequence operationName) {
+ // The default routes through buildSpan(String,...), which is null on the noop tracer -> NPE.
+ return NoopSpan.INSTANCE;
+ }
+
@Override
public AgentSpan startSpan(
final String instrumentationName, final CharSequence spanName, final long startTimeMicros) {
diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java
new file mode 100644
index 00000000000..24ccd2b39d8
--- /dev/null
+++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java
@@ -0,0 +1,179 @@
+package datadog.trace.bootstrap.instrumentation.api;
+
+import datadog.trace.api.TagMap;
+
+/**
+ * A baked-once, frozen descriptor of a span's constant initial state — the per-decorator constants
+ * (instrumentation name, span type, {@code span.kind}, component, …) that {@code
+ * BaseDecorator.afterStart} otherwise stamps one entry at a time, per span.
+ *
+ * Composed through {@link #builder()}: authors set identity and constant tags via typed methods
+ * and never touch {@link TagMap} directly. {@link Builder#extends_(SpanPrototype)} inherits a base
+ * prototype (e.g. a SpanType base like {@code HttpServer}) so an integration adds only what's
+ * specific to it. Rides the existing {@code TagMap} API, so it's independent of any deeper TagMap
+ * rework — the internal seed can get faster without changing this surface.
+ *
+ * v1 carries identity + constant tags. Derivation / canonicalization / lifecycle hooks are
+ * deliberately out — grown when the work that needs each arrives, not pre-slotted.
+ */
+public final class SpanPrototype {
+ /** The empty prototype — for spans created without a decorator-provided prototype. */
+ public static final SpanPrototype NONE = builder().build();
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ private final String instrumentationName;
+ private final CharSequence operationName;
+ private final CharSequence spanType;
+ private final CharSequence integrationName;
+ private final TagMap tags; // frozen
+
+ private SpanPrototype(final Builder builder) {
+ this.instrumentationName = builder.instrumentationName;
+ this.operationName = builder.operationName;
+ this.spanType = builder.spanType;
+ this.integrationName = builder.integrationName;
+ this.tags = builder.tags.immutableCopy();
+ }
+
+ public String instrumentationName() {
+ return instrumentationName;
+ }
+
+ public CharSequence operationName() {
+ return operationName;
+ }
+
+ public CharSequence spanType() {
+ return spanType;
+ }
+
+ /**
+ * The integration name to record on the span context ({@code setIntegrationName}), which the
+ * IntegrationAdder serializer step turns into {@code _dd.integration}. Null unless set via {@link
+ * Builder#initComponentAndIntegration}. Mirrors {@code BaseDecorator.afterStart}'s side effect.
+ */
+ public CharSequence integrationName() {
+ return integrationName;
+ }
+
+ /** The frozen constant tags — the internal seed applied at span construction. */
+ public TagMap tags() {
+ return tags;
+ }
+
+ public static final class Builder {
+ private String instrumentationName;
+ private CharSequence operationName;
+ private CharSequence spanType;
+ private CharSequence integrationName;
+ // Internal accumulator — never exposed; authors compose via the typed methods below.
+ private final TagMap tags = TagMap.create();
+
+ private Builder() {}
+
+ /**
+ * Inherit a base prototype's identity and constant tags (e.g. a SpanType base). Subsequent
+ * identity / {@code init*} calls on this builder override the inherited values.
+ */
+ public Builder extends_(final SpanPrototype base) {
+ if (base != null) {
+ if (base.instrumentationName != null) {
+ this.instrumentationName = base.instrumentationName;
+ }
+ if (base.operationName != null) {
+ this.operationName = base.operationName;
+ }
+ if (base.spanType != null) {
+ this.spanType = base.spanType;
+ }
+ if (base.integrationName != null) {
+ this.integrationName = base.integrationName;
+ }
+ this.tags.putAll(base.tags);
+ }
+ return this;
+ }
+
+ public Builder initInstrumentationNames(final String[] instrumentationNames) {
+ return (instrumentationNames == null || instrumentationNames.length == 0)
+ ? this
+ : initInstrumentationName(instrumentationNames[0]);
+ }
+
+ public Builder initInstrumentationName(final String instrumentationName) {
+ this.instrumentationName = instrumentationName;
+ return this;
+ }
+
+ public Builder initOperationName(final CharSequence operationName) {
+ this.operationName = operationName;
+ return this;
+ }
+
+ public Builder initSpanType(final CharSequence spanType) {
+ this.spanType = spanType;
+ return this;
+ }
+
+ /** Sets {@code span.kind}. */
+ public Builder initKind(final CharSequence kind) {
+ return initTag(Tags.SPAN_KIND, kind);
+ }
+
+ /**
+ * Sets the {@code component} tag only. Does NOT touch the integration name — so overriding a
+ * component inherited via {@link #initComponentAndIntegration} with this leaves the inherited
+ * integration name in place (a desync). Use {@link #initComponentAndIntegration} to override
+ * both together.
+ */
+ public Builder initComponentOnly(final CharSequence component) {
+ return initTag(Tags.COMPONENT, component);
+ }
+
+ /**
+ * Sets the {@code component} tag AND records it as the integration name — the {@code
+ * BaseDecorator.afterStart} pairing (component tag + {@code setIntegrationName(component)},
+ * which the IntegrationAdder serializer turns into {@code _dd.integration}). Null/empty is a
+ * no-op for both.
+ */
+ public Builder initComponentAndIntegration(final CharSequence component) {
+ if (!TagMap.Entry.isEmptyValue(component)) {
+ this.tags.set(Tags.COMPONENT, component);
+ this.integrationName = component;
+ }
+ return this;
+ }
+
+ public Builder initTag(final String key, final CharSequence value) {
+ if (!TagMap.Entry.isEmptyValue(value)) {
+ this.tags.set(key, value);
+ }
+ return this;
+ }
+
+ public Builder initTag(final String key, final Object value) {
+ if (!TagMap.Entry.isEmptyValue(value)) {
+ this.tags.set(key, value);
+ }
+ return this;
+ }
+
+ /**
+ * Advanced/internal: reuse an already-built entry — a decorator's cached constant or a metric
+ * entry — rather than re-creating it. Authors should prefer the typed {@code init*} methods.
+ */
+ public Builder initTag(final TagMap.EntryReader entry) {
+ if (entry != null) {
+ this.tags.set(entry);
+ }
+ return this;
+ }
+
+ public SpanPrototype build() {
+ return new SpanPrototype(this);
+ }
+ }
+}
diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java
new file mode 100644
index 00000000000..522a2918036
--- /dev/null
+++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java
@@ -0,0 +1,52 @@
+package datadog.trace.bootstrap.instrumentation.api;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+class SpanPrototypeTest {
+
+ @Test
+ void extendsInheritsBaseIdentityAndTagsThenOverrides() {
+ final SpanPrototype base =
+ SpanPrototype.builder()
+ .initInstrumentationName("base")
+ .initSpanType("base-type")
+ .initKind("server")
+ .build();
+ final SpanPrototype derived =
+ SpanPrototype.builder()
+ .extends_(base)
+ .initComponentOnly("netty")
+ .initSpanType("http")
+ .build();
+
+ assertEquals("base", derived.instrumentationName()); // inherited
+ assertEquals("http", derived.spanType()); // overridden
+ assertEquals("server", derived.tags().getString(Tags.SPAN_KIND)); // inherited tag
+ assertEquals("netty", derived.tags().getString(Tags.COMPONENT)); // added tag
+ }
+
+ @Test
+ void emptyOrNullConstantsAreDroppedNotBaked() {
+ // Match AgentSpan.setTag / the cached-Entry path: a null or empty constant is "no tag", not an
+ // empty tag. A raw tags.set would otherwise bake a tag that per-span stamping never emits.
+ final SpanPrototype proto =
+ SpanPrototype.builder()
+ .initComponentOnly("") // empty -> dropped
+ .initKind("") // empty -> dropped
+ .initTag("empty.cs", "") // empty CharSequence -> dropped
+ .initTag("null.cs", (CharSequence) null) // null -> dropped
+ .initTag("null.obj", (Object) null) // null -> dropped
+ .initTag("kept", "v") // non-empty -> present
+ .build();
+
+ assertNull(proto.tags().getString(Tags.COMPONENT));
+ assertNull(proto.tags().getString(Tags.SPAN_KIND));
+ assertNull(proto.tags().getString("empty.cs"));
+ assertNull(proto.tags().getString("null.cs"));
+ assertNull(proto.tags().getString("null.obj"));
+ assertEquals("v", proto.tags().getString("kept")); // sanity: non-empty still stored
+ }
+}
+ * ver bareStart bareBuild webServer viaBuilder jdbcClient
+ * 1.53 1330.7 1331.1 2058.7 2434.7 1821.3
+ * 1.54 1423.8 1423.8 2131.4 2491.1 2037.8
+ * 1.55 1308.8 1422.0 2098.1 2477.2 2029.4
+ * 1.56 1322.2 1393.0 2131.5 2464.2 2016.6
+ * 1.57 1321.6 1363.3 2063.3 2410.6 2073.8
+ * 1.58 1312.6 1310.5 2018.0 2442.1 2065.3
+ * 1.59 1174.9 1166.1 1869.0 1987.2 1866.0
+ * 1.60 1180.6 1192.1 1858.9 1963.3 1818.0
+ * 1.61 959.2 951.7 1639.2 1774.1 1619.4
+ * 1.62 948.0 948.7 1674.9 1787.1 1614.1
+ * 1.63 927.0 926.7 1626.8 1737.7 1429.8
+ * 1.64 923.3 958.4 1636.6 1751.1 1421.5
+ * Δ% -30.6 -28.0 -20.5 -28.1 -22.0
+ *
+ *
+ *
+ * ver bareStart bareBuild webServer viaBuilder jdbcClient
+ * 1.53 5.69 5.49 4.00 3.99 5.33
+ * 1.54 4.26 4.25 3.47 3.55 3.15
+ * 1.55 3.87 4.13 3.37 3.52 3.14
+ * 1.56 3.92 4.03 3.79 3.33 3.15
+ * 1.57 4.21 4.11 3.31 3.49 3.37
+ * 1.58 4.16 4.19 3.34 3.52 3.27
+ * 1.59 4.20 4.66 3.43 3.52 3.45
+ * 1.60 5.48 5.66 3.42 3.54 3.77
+ * 1.61 4.17 4.31 3.46 3.58 3.46
+ * 1.62 5.53 5.72 4.20 4.37 4.49
+ * 1.63 6.03 5.97 4.66 5.13 4.49
+ * 1.64 5.37 5.26 4.62 5.29 5.06
+ *
+ */
+@State(Scope.Benchmark)
+@Warmup(iterations = 5)
+@Measurement(iterations = 5)
+@BenchmarkMode(Mode.Throughput)
+@Threads(8)
+@OutputTimeUnit(MICROSECONDS)
+// -DTEST_LOG_LEVEL=warn: the jmh classpath picks up dd-trace-core's test logback.xml, whose root
+// defaults to DEBUG -> per-span "Started/Finished span" logging would dominate the -prof gc alloc.
+@Fork(
+ value = 3,
+ jvmArgs = {"-DTEST_LOG_LEVEL=warn"})
+public class SpanCreationBenchmark {
+ private static final String INSTRUMENTATION_NAME = "bench";
+ private static final String SERVER_OPERATION_NAME = "servlet.request";
+ // The DB-shaped span gets its own operation name -- a real jdbc span is never "servlet.request",
+ // and keeping the two shapes distinct by operation avoids conflating them.
+ private static final String JDBC_OPERATION_NAME = "database.query";
+
+ // int tag values are deliberately kept inside Integer's built-in cache (-128..127) so valueOf
+ // returns a shared box and boxing does not allocate — the bench then measures tag storage / path
+ // cost, not incidental boxing (which differs between setTag(int) and the builder's
+ // withTag(Number)).
+
+ // Web-server-shaped known tags — the profile the dense store / SpanPrototype target.
+ private static final String COMPONENT_VALUE = "tomcat-server";
+ private static final String HTTP_METHOD_VALUE = "GET";
+ private static final String HTTP_ROUTE_VALUE = "/owners/{ownerId}";
+ private static final String HTTP_URL_VALUE = "http://localhost:8080/owners/42";
+ private static final int HTTP_STATUS_VALUE = 100; // in-cache; value itself is immaterial here
+ private static final int PEER_PORT_VALUE = 80;
+
+ // JDBC/DB-client-shaped known tags — a higher-tag-count shape (9 vs the web shape's 7), matching
+ // what DatabaseClientDecorator + JDBCDecorator set on a statement span.
+ private static final String DB_COMPONENT_VALUE = "java-jdbc-statement";
+ private static final String DB_TYPE_VALUE = "postgresql";
+ private static final String DB_INSTANCE_VALUE = "petclinic";
+ private static final String DB_USER_VALUE = "app";
+ private static final String DB_OPERATION_VALUE = "SELECT";
+ private static final String DB_STATEMENT_VALUE = "SELECT * FROM owners WHERE id = ?";
+ private static final String DB_PEER_HOSTNAME_VALUE = "db.internal";
+ private static final int DB_PEER_PORT_VALUE = 90; // in-cache; value itself is immaterial here
+
+ CoreTracer tracer;
+
+ // Baked-once prototypes carrying only the type-constant subset each baseline sets individually
+ // (component + span.kind; jdbc also db.type). The dynamic tags are set per-span in both arms, so
+ // the *ViaPrototype vs *Span delta isolates the construction-path seeding of just those
+ // constants.
+ SpanPrototype webProto;
+ SpanPrototype jdbcProto;
+
+ @Setup
+ public void setup() {
+ // DropWriter keeps finish() from pulling in serialization / agent I/O, so -prof gc reflects
+ // span creation + tagging + PendingTrace completion only.
+ this.tracer = CoreTracer.builder().writer(new DropWriter()).build();
+ this.webProto =
+ SpanPrototype.builder()
+ .initInstrumentationName(INSTRUMENTATION_NAME)
+ .initOperationName(SERVER_OPERATION_NAME)
+ .initComponentOnly(COMPONENT_VALUE)
+ .initKind(Tags.SPAN_KIND_SERVER)
+ .build();
+ this.jdbcProto =
+ SpanPrototype.builder()
+ .initInstrumentationName(INSTRUMENTATION_NAME)
+ .initOperationName(JDBC_OPERATION_NAME)
+ .initComponentOnly(DB_COMPONENT_VALUE)
+ .initKind(Tags.SPAN_KIND_CLIENT)
+ .initTag(Tags.DB_TYPE, DB_TYPE_VALUE)
+ .build();
+ }
+
+ @TearDown
+ public void tearDown() {
+ this.tracer.close();
+ }
+
+ /** Baseline: create + finish a bare span via startSpan, no tags. */
+ @Benchmark
+ public void bareStartSpan() {
+ AgentSpan span = tracer.startSpan(INSTRUMENTATION_NAME, SERVER_OPERATION_NAME);
+ span.finish();
+ }
+
+ /** Baseline: create + finish a bare span via the builder path, no tags. */
+ @Benchmark
+ public void bareBuildSpan() {
+ AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, SERVER_OPERATION_NAME).start();
+ span.finish();
+ }
+
+ /** Web-server-shaped span: create -> set the typical known tags -> finish. */
+ @Benchmark
+ public void webServerSpan() {
+ AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, SERVER_OPERATION_NAME).start();
+ span.setTag(Tags.COMPONENT, COMPONENT_VALUE);
+ span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER);
+ span.setTag(Tags.HTTP_METHOD, HTTP_METHOD_VALUE);
+ span.setTag(Tags.HTTP_ROUTE, HTTP_ROUTE_VALUE);
+ span.setTag(Tags.HTTP_URL, HTTP_URL_VALUE);
+ span.setTag(Tags.HTTP_STATUS, HTTP_STATUS_VALUE);
+ span.setTag(Tags.PEER_PORT, PEER_PORT_VALUE);
+ span.finish();
+ }
+
+ /**
+ * Web-server-shaped span via the builder tag path: tags accumulated on the builder with
+ * {@code withTag} and applied at {@code start()}, rather than set on the span afterward. This is
+ * the shape the OTel bridge takes (OTel {@code SpanBuilder.setAttribute} → dd builder), still
+ * live today for manual OTel and OTel-bridge auto-instrumentation. Compare against {@link
+ * #webServerSpan} (same tags, set after start) to track how the startSpan/buildSpan paths diverge
+ * across releases.
+ */
+ @Benchmark
+ public void webServerSpanViaBuilder() {
+ AgentSpan span =
+ tracer
+ .buildSpan(INSTRUMENTATION_NAME, SERVER_OPERATION_NAME)
+ .withTag(Tags.COMPONENT, COMPONENT_VALUE)
+ .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER)
+ .withTag(Tags.HTTP_METHOD, HTTP_METHOD_VALUE)
+ .withTag(Tags.HTTP_ROUTE, HTTP_ROUTE_VALUE)
+ .withTag(Tags.HTTP_URL, HTTP_URL_VALUE)
+ .withTag(Tags.HTTP_STATUS, HTTP_STATUS_VALUE)
+ .withTag(Tags.PEER_PORT, PEER_PORT_VALUE)
+ .start();
+ span.finish();
+ }
+
+ /** JDBC/DB-client-shaped span: create -> set the typical DB known tags (9) -> finish. */
+ @Benchmark
+ public void jdbcClientSpan() {
+ AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, JDBC_OPERATION_NAME).start();
+ span.setTag(Tags.COMPONENT, DB_COMPONENT_VALUE);
+ span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT);
+ span.setTag(Tags.DB_TYPE, DB_TYPE_VALUE);
+ span.setTag(Tags.DB_INSTANCE, DB_INSTANCE_VALUE);
+ span.setTag(Tags.DB_USER, DB_USER_VALUE);
+ span.setTag(Tags.DB_OPERATION, DB_OPERATION_VALUE);
+ span.setTag(Tags.DB_STATEMENT, DB_STATEMENT_VALUE);
+ span.setTag(Tags.PEER_HOSTNAME, DB_PEER_HOSTNAME_VALUE);
+ span.setTag(Tags.PEER_PORT, DB_PEER_PORT_VALUE);
+ span.finish();
+ }
+
+ /**
+ * Web-server-shaped span via {@link SpanPrototype}: the type-constants (component, span.kind)
+ * ride a baked-once prototype seeded at construction; the dynamic http.* / peer.port tags are set
+ * per-span, as real instrumentation does. Compare against {@link #webServerSpan} (identical tags,
+ * all set individually) to read the prototype's construction-path win on a full span.
+ */
+ @Benchmark
+ public void webServerSpanViaPrototype() {
+ AgentSpan span = tracer.buildSpan(webProto, null).start(); // null -> prototype's operationName
+ span.setTag(Tags.HTTP_METHOD, HTTP_METHOD_VALUE);
+ span.setTag(Tags.HTTP_ROUTE, HTTP_ROUTE_VALUE);
+ span.setTag(Tags.HTTP_URL, HTTP_URL_VALUE);
+ span.setTag(Tags.HTTP_STATUS, HTTP_STATUS_VALUE);
+ span.setTag(Tags.PEER_PORT, PEER_PORT_VALUE);
+ span.finish();
+ }
+
+ /**
+ * JDBC/DB-client-shaped span via {@link SpanPrototype}: component, span.kind, and db.type ride
+ * the prototype; the dynamic db.* / peer.* tags are set per-span. Compare against {@link
+ * #jdbcClientSpan}.
+ */
+ @Benchmark
+ public void jdbcClientSpanViaPrototype() {
+ AgentSpan span = tracer.buildSpan(jdbcProto, null).start();
+ span.setTag(Tags.DB_INSTANCE, DB_INSTANCE_VALUE);
+ span.setTag(Tags.DB_USER, DB_USER_VALUE);
+ span.setTag(Tags.DB_OPERATION, DB_OPERATION_VALUE);
+ span.setTag(Tags.DB_STATEMENT, DB_STATEMENT_VALUE);
+ span.setTag(Tags.PEER_HOSTNAME, DB_PEER_HOSTNAME_VALUE);
+ span.setTag(Tags.PEER_PORT, DB_PEER_PORT_VALUE);
+ span.finish();
+ }
+
+ /**
+ * Web-server-shaped span via {@code startSpan(SpanPrototype, ...)} — the builder-free
+ * construction entry (no MultiSpanBuilder allocation), the auto-instrumentation path. Compare
+ * against {@link #webServerSpanViaPrototype} (same prototype, but {@code buildSpan(...).start()}
+ * allocates a builder) to read the builder-free saving, and against {@link #webServerSpan} for
+ * the full win.
+ */
+ @Benchmark
+ public void webServerSpanViaPrototypeStartSpan() {
+ AgentSpan span = tracer.startSpan(webProto, null); // null -> prototype's operationName
+ span.setTag(Tags.HTTP_METHOD, HTTP_METHOD_VALUE);
+ span.setTag(Tags.HTTP_ROUTE, HTTP_ROUTE_VALUE);
+ span.setTag(Tags.HTTP_URL, HTTP_URL_VALUE);
+ span.setTag(Tags.HTTP_STATUS, HTTP_STATUS_VALUE);
+ span.setTag(Tags.PEER_PORT, PEER_PORT_VALUE);
+ span.finish();
+ }
+
+ /** JDBC/DB-client-shaped span via the builder-free {@code startSpan(SpanPrototype, ...)}. */
+ @Benchmark
+ public void jdbcClientSpanViaPrototypeStartSpan() {
+ AgentSpan span = tracer.startSpan(jdbcProto, null);
+ span.setTag(Tags.DB_INSTANCE, DB_INSTANCE_VALUE);
+ span.setTag(Tags.DB_USER, DB_USER_VALUE);
+ span.setTag(Tags.DB_OPERATION, DB_OPERATION_VALUE);
+ span.setTag(Tags.DB_STATEMENT, DB_STATEMENT_VALUE);
+ span.setTag(Tags.PEER_HOSTNAME, DB_PEER_HOSTNAME_VALUE);
+ span.setTag(Tags.PEER_PORT, DB_PEER_PORT_VALUE);
+ span.finish();
+ }
+}
diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java
index c48d8f94df6..abaf9b3fa3e 100644
--- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java
+++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java
@@ -75,6 +75,7 @@
import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration;
import datadog.trace.bootstrap.instrumentation.api.SpanAttributes;
import datadog.trace.bootstrap.instrumentation.api.SpanLink;
+import datadog.trace.bootstrap.instrumentation.api.SpanPrototype;
import datadog.trace.bootstrap.instrumentation.api.TagContext;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import datadog.trace.civisibility.interceptor.CiVisibilityApmProtocolInterceptor;
@@ -132,6 +133,7 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.zip.ZipOutputStream;
+import javax.annotation.Nonnull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -1041,6 +1043,27 @@ public CoreSpanBuilder buildSpan(
return createMultiSpanBuilder(instrumentationName, operationName);
}
+ /**
+ * Seeds identity (instrumentation name, operation, span type) and constant tags from a prototype.
+ * {@code operationName} overrides the prototype's when non-null — the explicit value wins, the
+ * prototype is the fallback. The prototype's tags are seeded during {@link CoreSpanBuilder}
+ * construction just before the builder's own tags, so explicit tags override prototype constants.
+ */
+ @Override
+ public CoreSpanBuilder buildSpan(
+ @Nonnull final SpanPrototype prototype, CharSequence operationName) {
+ if (operationName == null) {
+ operationName = prototype.operationName();
+ }
+ CoreSpanBuilder builder =
+ createMultiSpanBuilder(prototype.instrumentationName(), operationName);
+ builder.spanPrototype = prototype;
+ if (prototype.spanType() != null) {
+ builder.spanType = prototype.spanType();
+ }
+ return builder;
+ }
+
MultiSpanBuilder createMultiSpanBuilder(
final String instrumentationName, final CharSequence operationName) {
return new MultiSpanBuilder(this, instrumentationName, operationName);
@@ -1146,6 +1169,17 @@ public AgentSpan startSpan(
this, instrumentationName, spanName, parent, CoreSpanBuilder.IGNORE_SCOPE, startTimeMicros);
}
+ @Override
+ public AgentSpan startSpan(
+ @Nonnull final SpanPrototype prototype, final CharSequence operationName) {
+ return CoreSpanBuilder.startSpan(
+ this,
+ prototype,
+ operationName != null ? operationName : prototype.operationName(),
+ CoreSpanBuilder.USE_SCOPE,
+ CoreSpanBuilder.AUTO_ASSIGN_TIMESTAMP);
+ }
+
@Override
public AgentScope activateSpan(AgentSpan span) {
return scopeManager.activateSpan(span);
@@ -1588,6 +1622,7 @@ public abstract static class CoreSpanBuilder implements AgentTracer.SpanBuilder
// Builder attributes
// Make sure any fields added here are also reset properly in ReusableSingleSpanBuilder.reset
protected TagMap.Ledger tagLedger;
+ protected SpanPrototype spanPrototype = SpanPrototype.NONE;
protected long timestampMicro;
protected AgentSpanContext parent;
protected String serviceName;
@@ -1626,6 +1661,7 @@ protected static final DDSpan buildSpan(
boolean errorFlag,
CharSequence spanType,
TagMap.Ledger tagLedger,
+ SpanPrototype spanPrototype,
List
+ *
+ *
+ *