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
31 changes: 31 additions & 0 deletions dd-trace-core/src/jmh/java/datadog/trace/core/DropWriter.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Drift-stable: implements only the five-method {@link Writer} interface, unchanged
* v1.53→master.
*/
final class DropWriter implements Writer {
@Override
public void write(List<DDSpan> trace) {}

@Override
public void start() {}

@Override
public boolean flush() {
return true;
}

@Override
public void close() {}

@Override
public void incrementDropCounts(int spanCount) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package datadog.trace.core;

import static java.util.concurrent.TimeUnit.MICROSECONDS;

import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
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.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.TearDown;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;

/**
* Grounding benchmark for the full span-creation lifecycle: create -> (set tags) -> finish.
*
* <p>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.
*
* <p><b>Read the allocation columns, not just throughput.</b> 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.
*
* <p><b>Deliberately drift-stable so it can be copied onto past release tags and back-checked.</b>
* 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.)
*
* <p>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.
*
* <p>Multi-threaded on purpose ({@code @Threads(8)}); some tracer optimizations only show under
* contention. Use {@code -t 1} for a single-threaded run.
*/
@State(Scope.Benchmark)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@BenchmarkMode(Mode.Throughput)
@Threads(8)
@OutputTimeUnit(MICROSECONDS)
@Fork(value = 3)
public class SpanCreationBenchmark {
private static final String INSTRUMENTATION_NAME = "bench";
private static final String OPERATION_NAME = "servlet.request";

// 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;

@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();
}

@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, 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, 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, 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 <b>builder tag path</b>: 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, 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, 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package datadog.trace.core;

import static java.util.concurrent.TimeUnit.MICROSECONDS;

import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
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.TearDown;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;

/**
* Runs span creation on a <b>virtual thread</b> — the regime the platform-thread {@link
* SpanCreationBenchmark} is blind to.
*
* <p>Why this exists: {@code startSpan}'s thread-local {@code SpanBuilder} reuse (1.55, #9537) is
* deliberately <b>disabled on virtual threads</b> (an {@code isVirtualThread} guard — thread-local
* caching on numerous short-lived virtual threads is an anti-pattern), so on a virtual thread 1.55
* still allocates a builder per {@code startSpan}. The full builder bypass (1.57, #9998) removes
* that allocation for everyone, including virtual threads. On platform threads the reuse already
* ate the allocation, so 1.57 shows nothing there; on virtual threads it should show as a per-span
* allocation drop at 1.57. This bench is where that appears.
*
* <p><b>Requires a JDK with virtual threads (21+) at run time.</b> To keep the jmh source set
* compilable on older toolchains, the virtual thread is started via reflection ({@code
* Thread.startVirtualThread}); {@link #setup()} fails fast on a pre-21 JDK. The per-op vthread
* spawn + join cost is constant across tracer versions, so it cancels in the 1.55→1.57 delta (read
* the delta, not the absolute B/op).
*/
@State(Scope.Benchmark)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@BenchmarkMode(Mode.Throughput)
@Threads(4)
@OutputTimeUnit(MICROSECONDS)
@Fork(value = 3)
public class SpanCreationVirtualThreadBenchmark {
private static final String INSTRUMENTATION_NAME = "bench";
private static final String OPERATION_NAME = "servlet.request";

CoreTracer tracer;
// Thread.startVirtualThread(Runnable) -> Thread, resolved reflectively (JDK 21+).
private MethodHandle startVirtualThread;
// Reused so no per-op capturing-lambda allocation muddies the measurement.
private Runnable spanTask;

@Setup
public void setup() throws Throwable {
this.tracer = CoreTracer.builder().writer(new DropWriter()).build();
this.startVirtualThread =
MethodHandles.publicLookup()
.findStatic(
Thread.class,
"startVirtualThread",
MethodType.methodType(Thread.class, Runnable.class));
this.spanTask =
() -> {
AgentSpan span = tracer.startSpan(INSTRUMENTATION_NAME, OPERATION_NAME);
span.finish();
};
}

@TearDown
public void tearDown() {
this.tracer.close();
}

/** create + finish a bare span on a fresh virtual thread; join. */
@Benchmark
public void bareStartSpanOnVirtualThread() throws Throwable {
Thread vthread = (Thread) startVirtualThread.invokeExact(spanTask);
vthread.join();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package datadog.trace.core;

import static java.util.concurrent.TimeUnit.MICROSECONDS;

import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
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.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;

/**
* Front-half (application-thread) benchmark for <b>multi-span trace assembly</b>: a
* web-server-shaped parent plus N children, each finished, whole trace dropped.
*
* <p>Where {@link SpanCreationBenchmark} measures a single span (front-A), this measures the cost
* that <i>scales with trace size</i> (front-B): every span created copies the tracer's baseline /
* trace-level tags into its own storage, so an N-span trace pays that copy N+1 times. That per-span
* copy is exactly what the map-to-map copy optimization (TagMap 1.0) and the trace/span tag split
* (level-split read-through) target — this bench is how their win shows up as it scales, which a
* single-span bench cannot show.
*
* <p>Sweeping {@code childCount} turns the per-child marginal cost into the slope: the level-split
* win should flatten it (children stop re-copying trace-level tags).
*
* <p>Same conventions as {@link SpanCreationBenchmark}: {@link DropWriter} isolates front-half
* allocation; read alloc ({@code -prof gc}) as the anchor, throughput as directional; logging must
* be forced to WARN or DEBUG-line allocation corrupts the numbers. Drift-stable v1.53→master
* ({@code buildSpan}/{@code asChildOf}/{@code setTag}/{@code finish}, {@link Tags}, {@link
* datadog.trace.common.writer.Writer}) so it can be grafted onto old tags for the historical curve.
*/
@State(Scope.Benchmark)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@BenchmarkMode(Mode.Throughput)
@Threads(8)
@OutputTimeUnit(MICROSECONDS)
@Fork(value = 3)
public class TraceAssemblyBenchmark {
private static final String INSTRUMENTATION_NAME = "bench";
private static final String ROOT_OPERATION = "servlet.request";
private static final String CHILD_OPERATION = "servlet.handler";

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 = 200;

/** Number of child spans under the root — the axis that turns per-child cost into a slope. */
@Param({"1", "5", "20"})
int childCount;

CoreTracer tracer;

@Setup
public void setup() {
this.tracer = CoreTracer.builder().writer(new DropWriter()).build();
}

@TearDown
public void tearDown() {
this.tracer.close();
}

/** Web-server root + {@code childCount} children, each finished; whole trace dropped. */
@Benchmark
public void webServerTrace() {
AgentSpan root = tracer.buildSpan(INSTRUMENTATION_NAME, ROOT_OPERATION).start();
root.setTag(Tags.COMPONENT, COMPONENT_VALUE);
root.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER);
root.setTag(Tags.HTTP_METHOD, HTTP_METHOD_VALUE);
root.setTag(Tags.HTTP_ROUTE, HTTP_ROUTE_VALUE);
root.setTag(Tags.HTTP_URL, HTTP_URL_VALUE);
root.setTag(Tags.HTTP_STATUS, HTTP_STATUS_VALUE);

for (int i = 0; i < childCount; i++) {
AgentSpan child =
tracer.buildSpan(INSTRUMENTATION_NAME, CHILD_OPERATION).asChildOf(root).start();
child.setTag(Tags.COMPONENT, COMPONENT_VALUE);
child.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_INTERNAL);
child.finish();
}

root.finish();
}
}
Loading