diff --git a/internal-api/src/jmh/java/datadog/trace/api/SpanPrototypeBenchmark.java b/internal-api/src/jmh/java/datadog/trace/api/SpanPrototypeBenchmark.java new file mode 100644 index 00000000000..2ef2b182dd4 --- /dev/null +++ b/internal-api/src/jmh/java/datadog/trace/api/SpanPrototypeBenchmark.java @@ -0,0 +1,91 @@ +package datadog.trace.api; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; +import datadog.trace.bootstrap.instrumentation.api.Tags; +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.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; + +/** + * Per-mechanism benchmark for {@link SpanPrototype}: the constant-tag application a span pays at + * start. Compares the three phases of the mechanism, holding the resulting tag set identical: + * + *
Isolates the constant-application only (not span creation or the {@code afterStart} virtual + * chain), so the delta is purely N-stamps vs. bulk-copy. 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() + .initComponent("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/bootstrap/instrumentation/api/SpanPrototype.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java new file mode 100644 index 00000000000..6a0d9f53e4c --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototype.java @@ -0,0 +1,145 @@ +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 TagMap tags; // frozen + + private SpanPrototype(final Builder builder) { + this.instrumentationName = builder.instrumentationName; + this.operationName = builder.operationName; + this.spanType = builder.spanType; + this.tags = builder.tags.immutableCopy(); + } + + public String instrumentationName() { + return instrumentationName; + } + + public CharSequence operationName() { + return operationName; + } + + public CharSequence spanType() { + return spanType; + } + + /** 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; + // 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; + } + this.tags.putAll(base.tags); + } + return this; + } + + public Builder instrumentationName(final String[] instrumentationNames) { + return (instrumentationNames == null || instrumentationNames.length == 0) + ? this + : instrumentationName(instrumentationNames[0]); + } + + public Builder instrumentationName(final String instrumentationName) { + this.instrumentationName = instrumentationName; + return this; + } + + public Builder operationName(final CharSequence operationName) { + this.operationName = operationName; + return this; + } + + public Builder spanType(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 {@code component}. */ + public Builder initComponent(final CharSequence component) { + return initTag(Tags.COMPONENT, component); + } + + public Builder initTag(final String key, final CharSequence value) { + if (value != null) { + this.tags.set(key, value); + } + return this; + } + + public Builder initTag(final String key, final Object value) { + if (value != null) { + 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..fa5a20c1b1f --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/SpanPrototypeTest.java @@ -0,0 +1,25 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class SpanPrototypeTest { + + @Test + void extendsInheritsBaseIdentityAndTagsThenOverrides() { + final SpanPrototype base = + SpanPrototype.builder() + .instrumentationName("base") + .spanType("base-type") + .initKind("server") + .build(); + final SpanPrototype derived = + SpanPrototype.builder().extends_(base).initComponent("netty").spanType("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 + } +}