Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import datadog.trace.bootstrap.instrumentation.api.AgentScope;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities;
import datadog.trace.bootstrap.instrumentation.api.SpanPrototype;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import java.lang.reflect.Method;
import java.net.Inet4Address;
Expand Down Expand Up @@ -48,6 +49,10 @@ public String apply(Class<?> clazz) {
// Deliberately not volatile, reading null and repeating the calculation is safe
private TagMap.Entry cachedComponentEntry = null;

// Deliberately not volatile, same benign-race reasoning as cachedComponentEntry: baking twice is
// safe because the constant tags are identical.
private SpanPrototype cachedPrototype = null;

protected BaseDecorator() {
final Config config = Config.get();
final String[] instrumentationNames = instrumentationNames();
Expand Down Expand Up @@ -90,6 +95,36 @@ protected boolean traceAnalyticsDefault() {
return false;
}

/**
* The baked-once {@link SpanPrototype} for this decorator: the constant identity + tags {@link
* #afterStart} would otherwise stamp one at a time. Composed across the hierarchy via {@link
* #prototypeBuilder()} and cached (lazily, to respect the same static-init ordering caution as
* {@link #componentEntry()}).
*
* <p>Not yet wired into span creation — this is the provider surface; the seed hook comes next.
*/
public final SpanPrototype prototype() {
SpanPrototype prototype = cachedPrototype;
if (prototype == null) {
cachedPrototype = prototype = prototypeBuilder().build();
}
return prototype;
}

/**
* Contributes this decorator's constant identity + tags to the prototype under construction.
* Overrides must call {@code super.prototypeBuilder()} first, then add their own — mirroring the
* {@link #afterStart} super-chain, but run once at bake time rather than per span. Authors
* compose via the typed builder and never see {@link TagMap}.
*/
protected SpanPrototype.Builder prototypeBuilder() {
return SpanPrototype.builder()
.instrumentationName(instrumentationNames())
.spanType(spanType())
.initComponent(component())
.initTag(traceAnalyticsEntry);
}

public AgentSpan afterStart(final AgentSpan span) {
if (spanType() != null) {
span.setSpanType(spanType());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import datadog.trace.api.TagMap;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.SpanPrototype;
import datadog.trace.bootstrap.instrumentation.api.Tags;

public abstract class ClientDecorator extends BaseDecorator {
Expand Down Expand Up @@ -43,4 +44,9 @@ public AgentSpan afterStart(final AgentSpan span) {
span.setMeasured(true);
return super.afterStart(span);
}

@Override
protected SpanPrototype.Builder prototypeBuilder() {
return super.prototypeBuilder().initKind(spanKind());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import datadog.trace.api.DDTags;
import datadog.trace.api.TagMap;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.SpanPrototype;
import datadog.trace.bootstrap.instrumentation.api.Tags;

public abstract class ServerDecorator extends BaseDecorator {
Expand All @@ -18,4 +19,11 @@ public AgentSpan afterStart(final AgentSpan span) {

return super.afterStart(span);
}

@Override
protected SpanPrototype.Builder prototypeBuilder() {
return super.prototypeBuilder()
.initKind(Tags.SPAN_KIND_SERVER)
.initTag(DDTags.LANGUAGE_TAG_KEY, DDTags.LANGUAGE_TAG_VALUE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package datadog.trace.bootstrap.instrumentation.decorator;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;

import datadog.trace.api.DDTags;
import datadog.trace.api.TagMap;
import datadog.trace.bootstrap.instrumentation.api.SpanPrototype;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import org.junit.jupiter.api.Test;

class SpanPrototypeTest {

@Test
void serverPrototypeComposesBaseAndServerConstants() {
final SpanPrototype prototype = new TestServerDecorator().prototype();
final TagMap tags = prototype.tags();

// Identity (BaseDecorator)
assertEquals("test", prototype.instrumentationName());
assertEquals("test-type", prototype.spanType());
// BaseDecorator contribution
assertEquals("test-component", tags.getString(Tags.COMPONENT));
// ServerDecorator contribution
assertEquals(Tags.SPAN_KIND_SERVER, tags.getString(Tags.SPAN_KIND));
assertEquals(DDTags.LANGUAGE_TAG_VALUE, tags.getString(DDTags.LANGUAGE_TAG_KEY));
}

@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
}

@Test
void clientPrototypeComposesBaseAndClientConstants() {
final TagMap tags = new TestClientDecorator().prototype().tags();

assertEquals("test-component", tags.getString(Tags.COMPONENT));
assertEquals(Tags.SPAN_KIND_CLIENT, tags.getString(Tags.SPAN_KIND));
}

@Test
void prototypeIsBakedOnce() {
final TestServerDecorator decorator = new TestServerDecorator();
assertSame(decorator.prototype(), decorator.prototype());
}

static final class TestServerDecorator extends ServerDecorator {
@Override
protected String[] instrumentationNames() {
return new String[] {"test"};
}

@Override
protected CharSequence spanType() {
return "test-type";
}

@Override
protected CharSequence component() {
return "test-component";
}
}

static final class TestClientDecorator extends ClientDecorator {
@Override
protected String[] instrumentationNames() {
return new String[] {"test"};
}

@Override
protected CharSequence spanType() {
return "test-type";
}

@Override
protected CharSequence component() {
return "test-component";
}

@Override
protected String service() {
return "test-service";
}
}
}
Original file line number Diff line number Diff line change
@@ -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:
*
* <ul>
* <li><b>oldPerSpanStamps</b> — a fresh {@code TagMap} filled by N individual {@code set(entry)}
* calls, as {@code BaseDecorator.afterStart} does today (once per span).
* <li><b>newBulkApply</b> — a fresh map + one {@code putAll} of the baked-once prototype (the
* afterStart-consolidation on-ramp).
* <li><b>newConstructionSeed</b> — the span's map is <em>born</em> as a {@code copy()} of the
* frozen prototype (clone-at-birth; what increment 2's construction wiring unlocks).
* </ul>
*
* <p>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();
}
}
Loading