Skip to content

Commit 3af8e13

Browse files
dougqhclaude
andcommitted
Introduce SpanPrototype: baked-once constant span-tag descriptor
The builder API (extends_/init*) plus its per-mechanism microbenchmark and a pure-API test, split out from the combined span-prototype work so the abstraction lands independently of the decorator demo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2c3db21 commit 3af8e13

3 files changed

Lines changed: 261 additions & 0 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package datadog.trace.api;
2+
3+
import static java.util.concurrent.TimeUnit.SECONDS;
4+
5+
import datadog.trace.bootstrap.instrumentation.api.SpanPrototype;
6+
import datadog.trace.bootstrap.instrumentation.api.Tags;
7+
import org.openjdk.jmh.annotations.Benchmark;
8+
import org.openjdk.jmh.annotations.BenchmarkMode;
9+
import org.openjdk.jmh.annotations.Fork;
10+
import org.openjdk.jmh.annotations.Level;
11+
import org.openjdk.jmh.annotations.Measurement;
12+
import org.openjdk.jmh.annotations.Mode;
13+
import org.openjdk.jmh.annotations.OutputTimeUnit;
14+
import org.openjdk.jmh.annotations.Scope;
15+
import org.openjdk.jmh.annotations.Setup;
16+
import org.openjdk.jmh.annotations.State;
17+
import org.openjdk.jmh.annotations.Threads;
18+
import org.openjdk.jmh.annotations.Warmup;
19+
20+
/**
21+
* Per-mechanism benchmark for {@link SpanPrototype}: the constant-tag application a span pays at
22+
* start. Compares the three phases of the mechanism, holding the resulting tag set identical:
23+
*
24+
* <ul>
25+
* <li><b>oldPerSpanStamps</b> — a fresh {@code TagMap} filled by N individual {@code set(entry)}
26+
* calls, as {@code BaseDecorator.afterStart} does today (once per span).
27+
* <li><b>newBulkApply</b> — a fresh map + one {@code putAll} of the baked-once prototype (the
28+
* afterStart-consolidation on-ramp).
29+
* <li><b>newConstructionSeed</b> — the span's map is <em>born</em> as a {@code copy()} of the
30+
* frozen prototype (clone-at-birth; what increment 2's construction wiring unlocks).
31+
* </ul>
32+
*
33+
* <p>Isolates the constant-application only (not span creation or the {@code afterStart} virtual
34+
* chain), so the delta is purely N-stamps vs. bulk-copy. Run with {@code -prof gc} — the
35+
* interesting axes are ops/s and B/op.
36+
*/
37+
@State(Scope.Thread)
38+
@BenchmarkMode(Mode.Throughput)
39+
@OutputTimeUnit(SECONDS)
40+
@Warmup(iterations = 5, time = 2)
41+
@Measurement(iterations = 5, time = 2)
42+
@Fork(3)
43+
@Threads(8)
44+
public class SpanPrototypeBenchmark {
45+
46+
// The constant set a typical server span carries, as cached entries (the shared-Entry
47+
// hand-optimization the decorators use today).
48+
private static final TagMap.Entry COMPONENT = TagMap.Entry.create(Tags.COMPONENT, "netty");
49+
private static final TagMap.Entry KIND =
50+
TagMap.Entry.create(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER);
51+
private static final TagMap.Entry LANGUAGE =
52+
TagMap.Entry.create(DDTags.LANGUAGE_TAG_KEY, DDTags.LANGUAGE_TAG_VALUE);
53+
private static final TagMap.Entry ANALYTICS =
54+
TagMap.Entry.create(DDTags.ANALYTICS_SAMPLE_RATE, 1.0d);
55+
56+
private SpanPrototype prototype;
57+
58+
@Setup(Level.Trial)
59+
public void setUp() {
60+
// Baked once — the same constants, composed through the builder.
61+
prototype =
62+
SpanPrototype.builder()
63+
.initComponent("netty")
64+
.initKind(Tags.SPAN_KIND_SERVER)
65+
.initTag(DDTags.LANGUAGE_TAG_KEY, DDTags.LANGUAGE_TAG_VALUE)
66+
.initTag(ANALYTICS)
67+
.build();
68+
}
69+
70+
@Benchmark
71+
public TagMap oldPerSpanStamps() {
72+
TagMap tags = TagMap.create();
73+
tags.set(COMPONENT);
74+
tags.set(KIND);
75+
tags.set(LANGUAGE);
76+
tags.set(ANALYTICS);
77+
return tags;
78+
}
79+
80+
@Benchmark
81+
public TagMap newBulkApply() {
82+
TagMap tags = TagMap.create();
83+
tags.putAll(prototype.tags());
84+
return tags;
85+
}
86+
87+
@Benchmark
88+
public TagMap newConstructionSeed() {
89+
return prototype.tags().copy();
90+
}
91+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package datadog.trace.bootstrap.instrumentation.api;
2+
3+
import datadog.trace.api.TagMap;
4+
5+
/**
6+
* A baked-once, frozen descriptor of a span's constant initial state — the per-decorator constants
7+
* (instrumentation name, span type, {@code span.kind}, component, …) that {@code
8+
* BaseDecorator.afterStart} otherwise stamps one entry at a time, per span.
9+
*
10+
* <p>Composed through {@link #builder()}: authors set identity and constant tags via typed methods
11+
* and never touch {@link TagMap} directly. {@link Builder#extends_(SpanPrototype)} inherits a base
12+
* prototype (e.g. a SpanType base like {@code HttpServer}) so an integration adds only what's
13+
* specific to it. Rides the existing {@code TagMap} API, so it's independent of any deeper TagMap
14+
* rework — the internal seed can get faster without changing this surface.
15+
*
16+
* <p>v1 carries identity + constant tags. Derivation / canonicalization / lifecycle hooks are
17+
* deliberately out — grown when the work that needs each arrives, not pre-slotted.
18+
*/
19+
public final class SpanPrototype {
20+
/** The empty prototype — for spans created without a decorator-provided prototype. */
21+
public static final SpanPrototype NONE = builder().build();
22+
23+
public static Builder builder() {
24+
return new Builder();
25+
}
26+
27+
private final String instrumentationName;
28+
private final CharSequence operationName;
29+
private final CharSequence spanType;
30+
private final TagMap tags; // frozen
31+
32+
private SpanPrototype(final Builder builder) {
33+
this.instrumentationName = builder.instrumentationName;
34+
this.operationName = builder.operationName;
35+
this.spanType = builder.spanType;
36+
this.tags = builder.tags.immutableCopy();
37+
}
38+
39+
public String instrumentationName() {
40+
return instrumentationName;
41+
}
42+
43+
public CharSequence operationName() {
44+
return operationName;
45+
}
46+
47+
public CharSequence spanType() {
48+
return spanType;
49+
}
50+
51+
/** The frozen constant tags — the internal seed applied at span construction. */
52+
public TagMap tags() {
53+
return tags;
54+
}
55+
56+
public static final class Builder {
57+
private String instrumentationName;
58+
private CharSequence operationName;
59+
private CharSequence spanType;
60+
// Internal accumulator — never exposed; authors compose via the typed methods below.
61+
private final TagMap tags = TagMap.create();
62+
63+
private Builder() {}
64+
65+
/**
66+
* Inherit a base prototype's identity and constant tags (e.g. a SpanType base). Subsequent
67+
* identity / {@code init*} calls on this builder override the inherited values.
68+
*/
69+
public Builder extends_(final SpanPrototype base) {
70+
if (base != null) {
71+
if (base.instrumentationName != null) {
72+
this.instrumentationName = base.instrumentationName;
73+
}
74+
if (base.operationName != null) {
75+
this.operationName = base.operationName;
76+
}
77+
if (base.spanType != null) {
78+
this.spanType = base.spanType;
79+
}
80+
this.tags.putAll(base.tags);
81+
}
82+
return this;
83+
}
84+
85+
public Builder instrumentationName(final String[] instrumentationNames) {
86+
return (instrumentationNames == null || instrumentationNames.length == 0)
87+
? this
88+
: instrumentationName(instrumentationNames[0]);
89+
}
90+
91+
public Builder instrumentationName(final String instrumentationName) {
92+
this.instrumentationName = instrumentationName;
93+
return this;
94+
}
95+
96+
public Builder operationName(final CharSequence operationName) {
97+
this.operationName = operationName;
98+
return this;
99+
}
100+
101+
public Builder spanType(final CharSequence spanType) {
102+
this.spanType = spanType;
103+
return this;
104+
}
105+
106+
/** Sets {@code span.kind}. */
107+
public Builder initKind(final CharSequence kind) {
108+
return initTag(Tags.SPAN_KIND, kind);
109+
}
110+
111+
/** Sets {@code component}. */
112+
public Builder initComponent(final CharSequence component) {
113+
return initTag(Tags.COMPONENT, component);
114+
}
115+
116+
public Builder initTag(final String key, final CharSequence value) {
117+
if (value != null) {
118+
this.tags.set(key, value);
119+
}
120+
return this;
121+
}
122+
123+
public Builder initTag(final String key, final Object value) {
124+
if (value != null) {
125+
this.tags.set(key, value);
126+
}
127+
return this;
128+
}
129+
130+
/**
131+
* Advanced/internal: reuse an already-built entry — a decorator's cached constant or a metric
132+
* entry — rather than re-creating it. Authors should prefer the typed {@code init*} methods.
133+
*/
134+
public Builder initTag(final TagMap.EntryReader entry) {
135+
if (entry != null) {
136+
this.tags.set(entry);
137+
}
138+
return this;
139+
}
140+
141+
public SpanPrototype build() {
142+
return new SpanPrototype(this);
143+
}
144+
}
145+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package datadog.trace.bootstrap.instrumentation.api;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
class SpanPrototypeTest {
8+
9+
@Test
10+
void extendsInheritsBaseIdentityAndTagsThenOverrides() {
11+
final SpanPrototype base =
12+
SpanPrototype.builder()
13+
.instrumentationName("base")
14+
.spanType("base-type")
15+
.initKind("server")
16+
.build();
17+
final SpanPrototype derived =
18+
SpanPrototype.builder().extends_(base).initComponent("netty").spanType("http").build();
19+
20+
assertEquals("base", derived.instrumentationName()); // inherited
21+
assertEquals("http", derived.spanType()); // overridden
22+
assertEquals("server", derived.tags().getString(Tags.SPAN_KIND)); // inherited tag
23+
assertEquals("netty", derived.tags().getString(Tags.COMPONENT)); // added tag
24+
}
25+
}

0 commit comments

Comments
 (0)