Skip to content

Commit 01ae43b

Browse files
dougqhclaude
andcommitted
Add TraceCreationBenchmark; production-shape front-half span benchmarks
Add TraceCreationBenchmark (full-trace front-half: a web-request arm and a parameterized fan-out arm, children created under an active root scope) as a reference others can key core-optimization gains and regressions off of. Production-shape both front-half benchmarks' fork config (dd.service/env/version + 6 global dd.tags) so mergedTracerTags is a realistically-sized shared bundle; a bare tracer leaves it near-empty and hides any win that scales with tags-per-span. Flag SpanCreationBenchmark's historical table as a pre-config (bare-tracer) baseline, not comparable going forward. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b994882 commit 01ae43b

2 files changed

Lines changed: 235 additions & 1 deletion

File tree

dd-trace-core/src/jmh/java/datadog/trace/core/SpanCreationBenchmark.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@
5656
* drop is the TagMap 1.0 shared-Entry default flip; the ~1.61 drop is the interceptor/links
5757
* cluster. Net 1.53 &rarr; 1.64 is -20% to -31% per arm.
5858
*
59+
* <p><b>Note — baseline reset.</b> The table below was measured on a <em>bare</em> ({@code
60+
* CoreTracer.builder().build()}) tracer. As of this commit the fork is production-shaped ({@code
61+
* dd.service}/{@code dd.env}/{@code dd.version} + 6 global {@code dd.tags}), which enlarges {@code
62+
* mergedTracerTags} — the trace-level bundle every span merges — so tag-scaling wins are actually
63+
* exercised. That raises absolute B/op across all arms, so the numbers below are a <em>pre-config
64+
* baseline, not comparable</em> to runs on this branch onward; re-baseline going forward rather
65+
* than stitching new points onto this curve.
66+
*
5967
* <pre>
6068
* ver bareStart bareBuild webServer viaBuilder jdbcClient
6169
* 1.53 1330.7 1331.1 2058.7 2434.7 1821.3
@@ -98,7 +106,18 @@
98106
@BenchmarkMode(Mode.Throughput)
99107
@Threads(8)
100108
@OutputTimeUnit(MICROSECONDS)
101-
@Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn")
109+
@Fork(
110+
value = 3,
111+
jvmArgsAppend = {
112+
"-DTEST_LOG_LEVEL=warn",
113+
// Production-shaped tracer config so mergedTracerTags is a realistically-sized shared bundle
114+
// (env + 6 global DD_TAGS + runtime-id/language) — the trace-level tags every span merges.
115+
// A bare tracer leaves it near-empty, hiding any win that scales with tags-per-span.
116+
"-Ddd.service=petclinic",
117+
"-Ddd.env=staging",
118+
"-Ddd.version=1.2.3",
119+
"-Ddd.tags=team:apm,dc:us1,cluster:prod-1,owner:tracing,tier:backend,region:us-east-1"
120+
})
102121
public class SpanCreationBenchmark {
103122
private static final String INSTRUMENTATION_NAME = "bench";
104123
private static final String SERVER_OPERATION_NAME = "servlet.request";
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package datadog.trace.core;
2+
3+
import static java.util.concurrent.TimeUnit.MICROSECONDS;
4+
5+
import datadog.trace.bootstrap.instrumentation.api.AgentScope;
6+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
7+
import datadog.trace.bootstrap.instrumentation.api.Tags;
8+
import org.openjdk.jmh.annotations.Benchmark;
9+
import org.openjdk.jmh.annotations.BenchmarkMode;
10+
import org.openjdk.jmh.annotations.Fork;
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.Param;
15+
import org.openjdk.jmh.annotations.Scope;
16+
import org.openjdk.jmh.annotations.Setup;
17+
import org.openjdk.jmh.annotations.State;
18+
import org.openjdk.jmh.annotations.TearDown;
19+
import org.openjdk.jmh.annotations.Threads;
20+
import org.openjdk.jmh.annotations.Warmup;
21+
import org.openjdk.jmh.infra.Blackhole;
22+
23+
/**
24+
* Front-half allocation/throughput for creating a whole <em>trace</em> (a local-root span plus its
25+
* children), as opposed to the single-span {@link SpanCreationBenchmark}. One op = one complete
26+
* trace: root started, activated, children created-and-finished under it, then root finished.
27+
*
28+
* <p>Purpose — a reference others can key off when implementing core optimizations:
29+
*
30+
* <ul>
31+
* <li><b>Win visibility.</b> Optimizations that touch per-span cost (trace-level tag sharing,
32+
* context propagation, pending-trace bookkeeping) accrue <em>per span</em>, so a trace of
33+
* {@code 1+childCount} spans surfaces the per-trace payoff a single-span bench cannot.
34+
* <li><b>Regression guard.</b> A documented baseline (below) lets a later change to core show up
35+
* as a delta — "did this addition move overhead?" — rather than passing silently.
36+
* </ul>
37+
*
38+
* <p>Isolation: children are created under an active root scope, so this exercises the real
39+
* parent-context propagation and trace-tag inheritance a single-span bench cannot reach. A no-op
40+
* {@link DropWriter} drops finished traces (handing them to a {@link Blackhole} so the JIT can't
41+
* dead-code the finish()-triggered write) so only application-thread (front-half) allocation lands
42+
* in the {@code -prof gc} number, with no serialization or agent I/O.
43+
*
44+
* <p>The tracer is production-shaped via {@code @Fork} jvmArgs (service/env/version + global {@code
45+
* dd.tags}) so {@code mergedTracerTags} is a realistically-sized shared bundle — the same config as
46+
* {@link SpanCreationBenchmark}, keeping numbers comparable across the two.
47+
*
48+
* <p>Read {@code gc.alloc.rate.norm} (B/op, deterministic) as the primary signal; throughput is
49+
* directional-only (laptop thermals + per-fork inlining bimodality).
50+
*
51+
* <p><b>Historical results</b> (populate from a committed run on the branch this file lives on;
52+
* matches the {@link SpanCreationBenchmark} header convention):
53+
*
54+
* <pre>
55+
* date commit arm alloc B/op thrpt ops/us
56+
* ---- ------ --- ---------- ------------
57+
* (TBD — fill from first green run on this branch)
58+
* </pre>
59+
*/
60+
@State(Scope.Benchmark)
61+
@Warmup(iterations = 5)
62+
@Measurement(iterations = 5)
63+
@BenchmarkMode(Mode.Throughput)
64+
@Threads(8)
65+
@OutputTimeUnit(MICROSECONDS)
66+
@Fork(
67+
value = 3,
68+
jvmArgsAppend = {
69+
"-DTEST_LOG_LEVEL=warn",
70+
// Production-shaped tracer config so mergedTracerTags is a realistically-sized shared bundle
71+
// (env + 6 global DD_TAGS + runtime-id/language) — the trace-level tags every span merges.
72+
// Same config as SpanCreationBenchmark so the two benchmarks' numbers stay comparable.
73+
"-Ddd.service=petclinic",
74+
"-Ddd.env=staging",
75+
"-Ddd.version=1.2.3",
76+
"-Ddd.tags=team:apm,dc:us1,cluster:prod-1,owner:tracing,tier:backend,region:us-east-1"
77+
})
78+
public class TraceCreationBenchmark {
79+
private static final String INSTRUMENTATION_NAME = "bench";
80+
private static final String SERVER_OPERATION_NAME = "servlet.request";
81+
private static final String JDBC_OPERATION_NAME = "database.query";
82+
private static final String HTTP_CLIENT_OPERATION_NAME = "http.request";
83+
private static final String INTERNAL_OPERATION_NAME = "internal.work";
84+
85+
// Web-server (local root) tag shape — mirrors SpanCreationBenchmark.webServerSpan.
86+
private static final String COMPONENT_VALUE = "tomcat-server";
87+
private static final String HTTP_METHOD_VALUE = "GET";
88+
private static final String HTTP_ROUTE_VALUE = "/owners/{ownerId}";
89+
private static final String HTTP_URL_VALUE = "http://localhost:8080/owners/42";
90+
private static final int HTTP_STATUS_VALUE = 100; // in Integer cache; boxing does not allocate
91+
private static final int PEER_PORT_VALUE = 80;
92+
93+
// JDBC-client child tag shape — mirrors SpanCreationBenchmark.jdbcClientSpan.
94+
private static final String DB_COMPONENT_VALUE = "java-jdbc-statement";
95+
private static final String DB_TYPE_VALUE = "postgresql";
96+
private static final String DB_INSTANCE_VALUE = "petclinic";
97+
private static final String DB_USER_VALUE = "app";
98+
private static final String DB_OPERATION_VALUE = "SELECT";
99+
private static final String DB_STATEMENT_VALUE = "SELECT * FROM owners WHERE id = ?";
100+
private static final String DB_PEER_HOSTNAME_VALUE = "db.internal";
101+
private static final int DB_PEER_PORT_VALUE = 90; // in Integer cache; boxing does not allocate
102+
103+
// HTTP-client child tag shape.
104+
private static final String HTTP_CLIENT_COMPONENT_VALUE = "apache-httpclient";
105+
private static final String HTTP_CLIENT_URL_VALUE = "http://billing.internal/charge";
106+
private static final String HTTP_CLIENT_PEER_HOSTNAME_VALUE = "billing.internal";
107+
private static final int HTTP_CLIENT_PEER_PORT_VALUE = 90; // in Integer cache; no alloc
108+
109+
// Internal child tag shape (a light span, few tags).
110+
private static final String INTERNAL_COMPONENT_VALUE = "spring-scheduler";
111+
112+
/** Fan-out width for {@link #fanoutTrace()} — root + this many jdbc-shaped children. */
113+
@Param({"1", "5", "10"})
114+
int childCount;
115+
116+
CoreTracer tracer;
117+
118+
@Setup
119+
public void setup(Blackhole blackhole) {
120+
this.tracer = CoreTracer.builder().writer(new DropWriter(blackhole)).build();
121+
}
122+
123+
@TearDown
124+
public void tearDown() {
125+
this.tracer.close();
126+
}
127+
128+
/**
129+
* Fixed, realistic web request: a server local root with a JDBC-client, an HTTP-client, and an
130+
* internal child. One headline number that mirrors the shape of a real request. Not affected by
131+
* {@link #childCount} (JMH still runs it once per param value — read any single row).
132+
*/
133+
@Benchmark
134+
public void webRequestTrace() {
135+
AgentSpan root = tracer.buildSpan(INSTRUMENTATION_NAME, SERVER_OPERATION_NAME).start();
136+
root.setTag(Tags.COMPONENT, COMPONENT_VALUE);
137+
root.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER);
138+
root.setTag(Tags.HTTP_METHOD, HTTP_METHOD_VALUE);
139+
root.setTag(Tags.HTTP_ROUTE, HTTP_ROUTE_VALUE);
140+
root.setTag(Tags.HTTP_URL, HTTP_URL_VALUE);
141+
root.setTag(Tags.HTTP_STATUS, HTTP_STATUS_VALUE);
142+
root.setTag(Tags.PEER_PORT, PEER_PORT_VALUE);
143+
144+
AgentScope scope = tracer.activateSpan(root);
145+
try {
146+
jdbcChild();
147+
httpClientChild();
148+
internalChild();
149+
} finally {
150+
scope.close();
151+
}
152+
root.finish();
153+
}
154+
155+
/**
156+
* Fan-out trace: a server local root with {@link #childCount} JDBC-shaped children. The scaling
157+
* axis — per-trace cost as span count grows exposes how per-span optimizations compound.
158+
*/
159+
@Benchmark
160+
public void fanoutTrace() {
161+
AgentSpan root = tracer.buildSpan(INSTRUMENTATION_NAME, SERVER_OPERATION_NAME).start();
162+
root.setTag(Tags.COMPONENT, COMPONENT_VALUE);
163+
root.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER);
164+
root.setTag(Tags.HTTP_METHOD, HTTP_METHOD_VALUE);
165+
root.setTag(Tags.HTTP_ROUTE, HTTP_ROUTE_VALUE);
166+
root.setTag(Tags.HTTP_URL, HTTP_URL_VALUE);
167+
root.setTag(Tags.HTTP_STATUS, HTTP_STATUS_VALUE);
168+
root.setTag(Tags.PEER_PORT, PEER_PORT_VALUE);
169+
170+
AgentScope scope = tracer.activateSpan(root);
171+
try {
172+
for (int i = 0; i < childCount; i++) {
173+
jdbcChild();
174+
}
175+
} finally {
176+
scope.close();
177+
}
178+
root.finish();
179+
}
180+
181+
/** A JDBC-client child of the currently-active span. */
182+
private void jdbcChild() {
183+
AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, JDBC_OPERATION_NAME).start();
184+
span.setTag(Tags.COMPONENT, DB_COMPONENT_VALUE);
185+
span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT);
186+
span.setTag(Tags.DB_TYPE, DB_TYPE_VALUE);
187+
span.setTag(Tags.DB_INSTANCE, DB_INSTANCE_VALUE);
188+
span.setTag(Tags.DB_USER, DB_USER_VALUE);
189+
span.setTag(Tags.DB_OPERATION, DB_OPERATION_VALUE);
190+
span.setTag(Tags.DB_STATEMENT, DB_STATEMENT_VALUE);
191+
span.setTag(Tags.PEER_HOSTNAME, DB_PEER_HOSTNAME_VALUE);
192+
span.setTag(Tags.PEER_PORT, DB_PEER_PORT_VALUE);
193+
span.finish();
194+
}
195+
196+
/** An HTTP-client child of the currently-active span. */
197+
private void httpClientChild() {
198+
AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, HTTP_CLIENT_OPERATION_NAME).start();
199+
span.setTag(Tags.COMPONENT, HTTP_CLIENT_COMPONENT_VALUE);
200+
span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT);
201+
span.setTag(Tags.HTTP_METHOD, HTTP_METHOD_VALUE);
202+
span.setTag(Tags.HTTP_URL, HTTP_CLIENT_URL_VALUE);
203+
span.setTag(Tags.HTTP_STATUS, HTTP_STATUS_VALUE);
204+
span.setTag(Tags.PEER_HOSTNAME, HTTP_CLIENT_PEER_HOSTNAME_VALUE);
205+
span.setTag(Tags.PEER_PORT, HTTP_CLIENT_PEER_PORT_VALUE);
206+
span.finish();
207+
}
208+
209+
/** A light internal child of the currently-active span. */
210+
private void internalChild() {
211+
AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, INTERNAL_OPERATION_NAME).start();
212+
span.setTag(Tags.COMPONENT, INTERNAL_COMPONENT_VALUE);
213+
span.finish();
214+
}
215+
}

0 commit comments

Comments
 (0)