From eabce403e8e17a8a3ea6022339e57b55a401971b Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 22 Oct 2026 09:31:13 -0400 Subject: [PATCH 1/4] Bucket known tags by slot in TagMap TagMap bucketed a known tag by folding its serial, so which tags shared one of the 16 buckets was luck, and each shared bucket costs a BucketGroup (48 B). Slot coloring already gives tags that appear together on a span type distinct slots, so bucketing a known tag by its slot keeps them in distinct buckets more often. Trace-level tags reuse span-level slot numbers, so the level bit moves them to the other half; known tags without a slot and custom tags keep their hashes. Co-Authored-By: Claude Opus 5.5 --- .../main/java/datadog/trace/api/TagMap.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index be07fadbeeb..0b42cb3f9d2 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -497,14 +497,27 @@ static long customHash(String tag) { } /** - * Folds a tag hash to the 32-bit bucket hash. A tag id's serial is in bits 63-48, so it is - * folded down from there rather than from bit 32, where a bucket mask would discard it. + * Folds a tag hash to the 32-bit bucket hash, whose low bits pick the bucket. * - *

Never zero: BucketGroup treats a zero hash as a vacant slot. The fold can produce zero - * when a serial equals the id's flag bits (serial 4 with the trace-level bit, 4), so zero maps - * to the same nonzero sentinel {@link #_hash} uses. + *

A known tag with a slot is bucketed by its slot: slot coloring gives tags that appear + * together on a span type distinct slots, so they mostly land in distinct buckets and need no + * BucketGroup. Trace-level tags reuse the span-level slot numbers, so their level bit moves + * them to the other half of the buckets. The serial sits above the slot, so two known tags + * never share a bucket hash. + * + *

A known tag without a slot folds its serial (bits 63-48) down instead, and a custom tag + * uses its name hash. + * + *

Never zero: BucketGroup treats a zero hash as a vacant slot. A slotted known tag's hash + * carries its nonzero serial; the other folds map zero to the same nonzero sentinel {@link + * #_hash} uses. */ static int bucketHash(long tagHash) { + int slot = (int) (tagHash >>> 32) & 0xFFFF; + if ((tagHash >>> 32) != 0 && slot != KnownTagCodec.NO_SLOT) { + int level = (int) (tagHash & KnownTagCodec.LEVEL_TRACE) << 1; // trace-level: + 8 + return ((int) (tagHash >>> 48) << 16) | (slot ^ level); + } int hash = (int) (tagHash >>> 48) ^ (int) tagHash; return hash == 0 ? 0xDD06 : hash; } From 19b2e886c3774ab874b8b07c7ac45925519ceb86 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 22 Oct 2026 09:43:10 -0400 Subject: [PATCH 2/4] Add whole-span and whole-trace allocation benchmarks SpanScenarioBenchmark builds spans and traces the way today's decorators do (prototype in afterStart, then request or connection tags by name): bare spans, a server span, the server span via the builder, a jdbc span, and a server trace with 1, 5 or 20 jdbc children. TraceShapeBenchmark builds one web-request-shaped trace three ways: every tag by name, the decorators' calls, and the newest span APIs. Both finish what they start, so allocation per op covers whole spans and traces. Single-span results can hide per-child costs that only the trace scenarios show. TraceShapeBenchmark is the same file #12918 adds. Co-Authored-By: Claude Opus 5.5 --- .../trace/core/SpanScenarioBenchmark.java | 219 ++++++++++++++ .../trace/core/TraceShapeBenchmark.java | 276 ++++++++++++++++++ 2 files changed, 495 insertions(+) create mode 100644 dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java create mode 100644 dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java new file mode 100644 index 00000000000..5030e61a15c --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java @@ -0,0 +1,219 @@ +package datadog.trace.core; + +import static java.util.concurrent.TimeUnit.MICROSECONDS; + +import datadog.context.ContextScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +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; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Span and trace scenarios built the way today's decorators build them: a {@link SpanPrototype} + * applied in {@code afterStart}, then the request or connection tags by name. Each op finishes what + * it starts, so allocation per op covers the whole span or trace. + * + *

+ * + *

Single-span and multi-span results can move in opposite directions: a change can make a + * trace's root cheaper while charging every child, so read the {@code webServerTrace} rows too. + * Values such as the client port and the status fall outside {@code Integer}'s box cache on + * purpose. + * + *

Passing {@code -jvmArgsAppend} on the command line replaces the {@code @Fork} arguments below, + * including {@code -DTEST_LOG_LEVEL=warn}; keep it, or debug logging dominates the allocation. + */ +@State(Scope.Benchmark) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@BenchmarkMode(Mode.Throughput) +@Threads(8) +@OutputTimeUnit(MICROSECONDS) +@Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn") +public class SpanScenarioBenchmark { + static final String SERVER_INSTRUMENTATION = "tomcat-server"; + static final CharSequence SERVER_OPERATION = UTF8BytesString.create("servlet.request"); + static final CharSequence SERVER_COMPONENT = UTF8BytesString.create("tomcat-server"); + static final CharSequence WEB = UTF8BytesString.create("web"); + static final String USER_AGENT = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0"; + static final String METHOD = "GET"; + static final String URL = "http://localhost:8080/owners/42"; + static final String HOSTNAME = "localhost"; + static final String CLIENT_IP = "127.0.0.1"; + static final int CLIENT_PORT = 54321; // ephemeral, outside the box cache + static final String ROUTE = "/owners/{ownerId}"; + static final String SERVER_RESOURCE_NAME = "GET /owners/{ownerId}"; + static final CharSequence SERVER_RESOURCE = UTF8BytesString.create(SERVER_RESOURCE_NAME); + static final int STATUS = 200; // outside the box cache + + static final String JDBC_INSTRUMENTATION = "java-jdbc-prepared_statement"; + static final CharSequence JDBC_OPERATION = UTF8BytesString.create("database.query"); + static final CharSequence JDBC_RENAMED_OPERATION = UTF8BytesString.create("postgresql.query"); + static final CharSequence JAVA_JDBC = UTF8BytesString.create("java-jdbc"); + static final CharSequence JDBC_PREPARED_STATEMENT = + UTF8BytesString.create("java-jdbc-prepared_statement"); + static final CharSequence SQL = UTF8BytesString.create("sql"); + static final CharSequence STATEMENT = UTF8BytesString.create("SELECT * FROM owners WHERE id = ?"); + + static final String BARE_INSTRUMENTATION = "bench"; + static final CharSequence BARE_OPERATION = UTF8BytesString.create("bare.op"); + + /** The trace size, taken only by {@code webServerTrace} so the single-span scenarios run once. */ + @State(Scope.Benchmark) + public static class Shape { + @Param({"1", "5", "20"}) + int children; + } + + CoreTracer tracer; + SpanPrototype serverProto; + SpanPrototype jdbcProto; + + @Setup + public void setup(Blackhole blackhole) { + this.tracer = CoreTracer.builder().writer(new DropWriter(blackhole)).build(); + // the decorators' prototype chains: Base (type, component) -> Server / Client (kind, language) + this.serverProto = + SpanPrototype.builder() + .initInstrumentationName(SERVER_INSTRUMENTATION) + .initOperationName(SERVER_OPERATION) + .initSpanType(WEB) + .initComponentAndIntegration(SERVER_COMPONENT) + .initKind(Tags.SPAN_KIND_SERVER) + .initTag("language", "jvm") + .build(); + this.jdbcProto = + SpanPrototype.builder() + .initInstrumentationName(JDBC_INSTRUMENTATION) + .initOperationName(JDBC_OPERATION) + .initSpanType(SQL) + .initComponentAndIntegration(JAVA_JDBC) + .initKind(Tags.SPAN_KIND_CLIENT) + .build(); + } + + @TearDown + public void tearDown() { + this.tracer.close(); + } + + @Benchmark + public void bareStartSpan() { + tracer.startSpan(BARE_INSTRUMENTATION, BARE_OPERATION).finish(); + } + + @Benchmark + public void bareBuildSpan() { + tracer.buildSpan(BARE_INSTRUMENTATION, BARE_OPERATION).start().finish(); + } + + @Benchmark + public void webServerSpan() { + AgentSpan server = startServer(); + server.setHttpStatusCode(STATUS); + server.finish(); + } + + @Benchmark + public void webServerSpanViaBuilder() { + AgentSpan server = + tracer + .buildSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION) + .withSpanType(WEB) + .withTag(Tags.COMPONENT, SERVER_COMPONENT) + .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER) + .withTag("language", "jvm") + .withTag(Tags.HTTP_USER_AGENT, USER_AGENT) + .withTag(Tags.HTTP_METHOD, METHOD) + .withTag(Tags.HTTP_URL, URL) + .withTag(Tags.HTTP_HOSTNAME, HOSTNAME) + .withTag(Tags.PEER_HOST_IPV4, CLIENT_IP) + .withTag(Tags.PEER_PORT, CLIENT_PORT) + .withTag(Tags.HTTP_ROUTE, ROUTE) + .withResourceName(SERVER_RESOURCE_NAME) + .start(); + server.setHttpStatusCode(STATUS); + server.finish(); + } + + @Benchmark + public void jdbcClientSpan() { + jdbc().finish(); + } + + @Benchmark + public void webServerTrace(Shape shape) { + AgentSpan server = startServer(); + try (ContextScope serverScope = tracer.activateSpan(server)) { + for (int i = 0; i < shape.children; ++i) { + AgentSpan jdbc = jdbc(); + ContextScope jdbcScope = tracer.activateSpan(jdbc); + jdbcScope.close(); + jdbc.finish(); + } + } + server.setHttpStatusCode(STATUS); + server.finish(); + } + + /** HttpServerDecorator: startSpan, afterStart (prototype), onRequest, then the route. */ + AgentSpan startServer() { + AgentSpan server = tracer.startSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION); + server.applyOverwriting(serverProto); + server.setTag(Tags.HTTP_USER_AGENT, USER_AGENT); + server.setTag(Tags.HTTP_METHOD, METHOD); + server.setTag(Tags.HTTP_URL, URL); + server.setTag(Tags.HTTP_HOSTNAME, HOSTNAME); + server.setResourceName(SERVER_RESOURCE); // HttpResourceDecorator.withServerPath + server.setTag(Tags.PEER_HOST_IPV4, CLIENT_IP); + server.setTag(Tags.PEER_PORT, CLIENT_PORT); // setPeerPort + server.setTag(Tags.HTTP_ROUTE, ROUTE); // the framework, once routing resolves + return server; + } + + /** + * JDBCDecorator over a postgres prepared statement: startSpan, afterStart (prototype), + * onConnection, onPreparedStatement. + */ + AgentSpan jdbc() { + AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); + jdbc.applyOverwriting(jdbcProto); + jdbc.setTag(Tags.DB_TYPE, "postgresql"); // processDatabaseType + jdbc.setServiceName("postgresql", JAVA_JDBC); // postProcessServiceAndOperationName + jdbc.setOperationName(JDBC_RENAMED_OPERATION); + jdbc.setTag(Tags.DB_POOL_NAME, "HikariPool-1"); + jdbc.setTag(Tags.DB_USER, "app"); // DatabaseClientDecorator.onConnection + jdbc.setTag(Tags.DB_INSTANCE, "petclinic"); + jdbc.setTag(Tags.PEER_HOSTNAME, "db.internal"); + jdbc.setResourceName(STATEMENT); // withQueryInfo + jdbc.setTag(Tags.DB_OPERATION, "SELECT"); + jdbc.spanContext().setIntegrationName(JDBC_PREPARED_STATEMENT); + jdbc.setTag(Tags.COMPONENT, JDBC_PREPARED_STATEMENT); + return jdbc; + } +} diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java new file mode 100644 index 00000000000..5556fa26139 --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java @@ -0,0 +1,276 @@ +package datadog.trace.core; + +import static java.util.concurrent.TimeUnit.MICROSECONDS; + +import datadog.context.ContextScope; +import datadog.trace.api.KnownTags; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +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; +import org.openjdk.jmh.infra.Blackhole; + +/** + * One whole trace per op, shaped like a web request: a server root, five jdbc prepared-statement + * children and one {@code @Trace} internal span, all finished. Divide by 7 for per-span figures. + * + *

For retrospective comparison across tracer versions, each version keeps its own copy of this + * benchmark, so the arms reflect that version's code: + * + *

+ * + *

Tag values are realistic on purpose: the client port, the database port and the status fall + * outside {@code Integer}'s box cache, so primitive boxing shows up. + * + *

This copy: 1.68 snapshot (decorators apply a prototype in {@code afterStart}, #12037; peer + * tags by id, #12715). + */ +@State(Scope.Benchmark) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@BenchmarkMode(Mode.Throughput) +@Threads(8) +@OutputTimeUnit(MICROSECONDS) +@Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn") +public class TraceShapeBenchmark { + static final int JDBC_SPANS = 5; + + // server: HttpServerDecorator (tomcat) + static final String SERVER_INSTRUMENTATION = "tomcat-server"; + static final CharSequence SERVER_OPERATION = UTF8BytesString.create("servlet.request"); + static final CharSequence SERVER_COMPONENT = UTF8BytesString.create("tomcat-server"); + static final CharSequence WEB = UTF8BytesString.create("web"); + static final String USER_AGENT = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0"; + static final String METHOD = "GET"; + static final String URL = "http://localhost:8080/owners/42"; + static final String HOSTNAME = "localhost"; + static final String CLIENT_IP = "127.0.0.1"; + static final int CLIENT_PORT = 54321; // ephemeral, outside the box cache + static final String ROUTE = "/owners/{ownerId}"; + static final CharSequence SERVER_RESOURCE = UTF8BytesString.create("GET /owners/{ownerId}"); + static final int STATUS = 200; // outside the box cache + + // jdbc: JDBCDecorator over a postgres prepared statement + static final String JDBC_INSTRUMENTATION = "java-jdbc-prepared_statement"; + static final CharSequence JDBC_OPERATION = UTF8BytesString.create("database.query"); + static final CharSequence JDBC_RENAMED_OPERATION = UTF8BytesString.create("postgresql.query"); + static final CharSequence JAVA_JDBC = UTF8BytesString.create("java-jdbc"); + static final CharSequence JDBC_PREPARED_STATEMENT = + UTF8BytesString.create("java-jdbc-prepared_statement"); + static final CharSequence SQL = UTF8BytesString.create("sql"); + static final String DB_TYPE = "postgresql"; + static final String DB_SERVICE = "postgresql"; + static final String DB_POOL = "HikariPool-1"; + static final String DB_USER = "app"; + static final String DB_INSTANCE = "petclinic"; + static final String DB_HOST = "db.internal"; + static final String DB_OPERATION = "SELECT"; + static final CharSequence STATEMENT = UTF8BytesString.create("SELECT * FROM owners WHERE id = ?"); + + // internal: TraceDecorator (@Trace) + static final String TRACE_INSTRUMENTATION = "trace"; + static final CharSequence TRACE_OPERATION = UTF8BytesString.create("trace.annotation"); + static final CharSequence TRACE_COMPONENT = UTF8BytesString.create("trace"); + static final CharSequence TRACE_RESOURCE = UTF8BytesString.create("OwnerService.findOwner"); + + CoreTracer tracer; + + SpanPrototype serverProto; + SpanPrototype jdbcProto; + SpanPrototype traceProto; + + @Setup + public void setup(Blackhole blackhole) { + this.tracer = CoreTracer.builder().writer(new DropWriter(blackhole)).build(); + // the decorators' prototype chains: Base (type, component) -> Server / Client (kind, language) + this.serverProto = + SpanPrototype.builder() + .initInstrumentationName(SERVER_INSTRUMENTATION) + .initOperationName(SERVER_OPERATION) + .initSpanType(WEB) + .initComponentAndIntegration(SERVER_COMPONENT) + .initKind(Tags.SPAN_KIND_SERVER) + .initTag("language", "jvm") + .build(); + this.jdbcProto = + SpanPrototype.builder() + .initInstrumentationName(JDBC_INSTRUMENTATION) + .initOperationName(JDBC_OPERATION) + .initSpanType(SQL) + .initComponentAndIntegration(JAVA_JDBC) + .initKind(Tags.SPAN_KIND_CLIENT) + .build(); + this.traceProto = + SpanPrototype.builder() + .initInstrumentationName(TRACE_INSTRUMENTATION) + .initOperationName(TRACE_OPERATION) + .initComponentAndIntegration(TRACE_COMPONENT) + .build(); + } + + @TearDown + public void tearDown() { + this.tracer.close(); + } + + // ---- legacy: every tag by name ---- + + @Benchmark + public void legacy() { + AgentSpan server = tracer.startSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION); + server.setSpanType(WEB); + server.setTag(Tags.COMPONENT, SERVER_COMPONENT); + server.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER); + server.setTag("language", "jvm"); + legacyRequest(server); + try (ContextScope scope = tracer.activateSpan(server)) { + for (int i = 0; i < JDBC_SPANS; ++i) { + AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); + jdbc.setSpanType(SQL); + jdbc.setTag(Tags.COMPONENT, JAVA_JDBC); + jdbc.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + legacyJdbc(jdbc); + finishScoped(jdbc); + } + AgentSpan internal = tracer.startSpan(TRACE_INSTRUMENTATION, TRACE_OPERATION); + internal.setTag(Tags.COMPONENT, TRACE_COMPONENT); + internal.setResourceName(TRACE_RESOURCE); + finishScoped(internal); + } + server.setHttpStatusCode(STATUS); + server.finish(); + } + + static void legacyRequest(AgentSpan server) { + server.setTag(Tags.HTTP_USER_AGENT, USER_AGENT); + server.setTag(Tags.HTTP_METHOD, METHOD); + server.setTag(Tags.HTTP_URL, URL); + server.setTag(Tags.HTTP_HOSTNAME, HOSTNAME); + server.setResourceName(SERVER_RESOURCE); + server.setTag(Tags.PEER_HOST_IPV4, CLIENT_IP); + server.setTag(Tags.PEER_PORT, CLIENT_PORT); + server.setTag(Tags.HTTP_ROUTE, ROUTE); + } + + static void legacyJdbc(AgentSpan jdbc) { + jdbc.setTag(Tags.DB_TYPE, DB_TYPE); + jdbc.setServiceName(DB_SERVICE, JAVA_JDBC); + jdbc.setOperationName(JDBC_RENAMED_OPERATION); + jdbc.setTag(Tags.DB_POOL_NAME, DB_POOL); + jdbc.setTag(Tags.DB_USER, DB_USER); + jdbc.setTag(Tags.DB_INSTANCE, DB_INSTANCE); + jdbc.setTag(Tags.PEER_HOSTNAME, DB_HOST); + jdbc.setResourceName(STATEMENT); + jdbc.setTag(Tags.DB_OPERATION, DB_OPERATION); + jdbc.setTag(Tags.COMPONENT, JDBC_PREPARED_STATEMENT); + } + + // ---- normal: what this version's decorators do ---- + + @Benchmark + public void normal() { + // HttpServerDecorator: startSpan, afterStart, onRequest, then onResponseStatus at the end + AgentSpan server = tracer.startSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION); + server.applyOverwriting(serverProto); // BaseDecorator.afterStart + server.setTag(Tags.HTTP_USER_AGENT, USER_AGENT); + server.setTag(Tags.HTTP_METHOD, METHOD); + server.setTag(Tags.HTTP_URL, URL); + server.setTag(Tags.HTTP_HOSTNAME, HOSTNAME); + server.setResourceName(SERVER_RESOURCE); // HttpResourceDecorator.withServerPath + server.setTag(Tags.PEER_HOST_IPV4, CLIENT_IP); + server.setTag(Tags.PEER_PORT, CLIENT_PORT); // setPeerPort + server.setTag(Tags.HTTP_ROUTE, ROUTE); // the framework, once routing resolves + try (ContextScope scope = tracer.activateSpan(server)) { + for (int i = 0; i < JDBC_SPANS; ++i) { + // AbstractPreparedStatementInstrumentation: startSpan, afterStart, onConnection, + // onPreparedStatement + AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); + jdbc.applyOverwriting(jdbcProto); + jdbc.setTag(Tags.DB_TYPE, DB_TYPE); // processDatabaseType + jdbc.setServiceName(DB_SERVICE, JAVA_JDBC); // postProcessServiceAndOperationName + jdbc.setOperationName(JDBC_RENAMED_OPERATION); + jdbc.setTag(Tags.DB_POOL_NAME, DB_POOL); + jdbc.setTag(Tags.DB_USER, DB_USER); // DatabaseClientDecorator.onConnection + jdbc.setTag(Tags.DB_INSTANCE, DB_INSTANCE); + jdbc.setTag(Tags.PEER_HOSTNAME, DB_HOST); + jdbc.setResourceName(STATEMENT); // withQueryInfo + jdbc.setTag(Tags.DB_OPERATION, DB_OPERATION); + jdbc.spanContext().setIntegrationName(JDBC_PREPARED_STATEMENT); + jdbc.setTag(Tags.COMPONENT, JDBC_PREPARED_STATEMENT); + finishScoped(jdbc); + } + AgentSpan internal = tracer.startSpan(TRACE_INSTRUMENTATION, TRACE_OPERATION); + internal.applyOverwriting(traceProto); + internal.setResourceName(TRACE_RESOURCE); + finishScoped(internal); + } + server.setHttpStatusCode(STATUS); // onResponseStatus + server.finish(); + } + + // ---- newApi: construction from a prototype, tags by id ---- + + @Benchmark + public void newApi() { + AgentSpan server = tracer.startSpan(serverProto, SERVER_OPERATION); + server.setTag(KnownTags.HTTP_USERAGENT_ID, USER_AGENT); + server.setTag(KnownTags.HTTP_METHOD_ID, METHOD); + server.setTag(KnownTags.HTTP_URL_ID, URL); + server.setTag(KnownTags.HTTP_HOSTNAME_ID, HOSTNAME); + server.setResourceName(SERVER_RESOURCE); + server.setTag(KnownTags.PEER_IPV4_ID, CLIENT_IP); + server.setTag(KnownTags.PEER_PORT_ID, CLIENT_PORT); + server.setTag(KnownTags.HTTP_ROUTE_ID, ROUTE); + try (ContextScope scope = tracer.activateSpan(server)) { + for (int i = 0; i < JDBC_SPANS; ++i) { + AgentSpan jdbc = tracer.startSpan(jdbcProto, JDBC_OPERATION); + jdbc.setTag(KnownTags.DB_TYPE_ID, DB_TYPE); + jdbc.setServiceName(DB_SERVICE, JAVA_JDBC); + jdbc.setOperationName(JDBC_RENAMED_OPERATION); + jdbc.setTag(KnownTags.DB_POOL_NAME_ID, DB_POOL); + jdbc.setTag(KnownTags.DB_USER_ID, DB_USER); + jdbc.setTag(KnownTags.DB_INSTANCE_ID, DB_INSTANCE); + jdbc.setTag(KnownTags.PEER_HOSTNAME_ID, DB_HOST); + jdbc.setResourceName(STATEMENT); + jdbc.setTag(KnownTags.DB_OPERATION_ID, DB_OPERATION); + jdbc.spanContext().setIntegrationName(JDBC_PREPARED_STATEMENT); + jdbc.setTag(KnownTags.COMPONENT_ID, JDBC_PREPARED_STATEMENT); + finishScoped(jdbc); + } + AgentSpan internal = tracer.startSpan(traceProto, TRACE_OPERATION); + internal.setResourceName(TRACE_RESOURCE); + finishScoped(internal); + } + server.setHttpStatusCode(STATUS); + server.finish(); + } + + /** The advice's exit: activate around the call, then close and finish. */ + void finishScoped(AgentSpan span) { + ContextScope scope = tracer.activateSpan(span); + scope.close(); + span.finish(); + } +} From 6e65a3119af1bfc6b0f61355847ee5960499ae29 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 22 Oct 2026 11:40:30 -0400 Subject: [PATCH 3/4] Vary TraceShapeBenchmark's trace size; test bucketHash TraceShapeBenchmark takes the number of jdbc children (1, 5, 20), so each arm shows how a trace's cost grows with its span count: single-span and small-trace results can hide costs that land on every child span. It covers what SpanScenarioBenchmark's trace scenario added, and the rest of that benchmark overlapped SpanCreationBenchmark (same scenario names, different semantics), so SpanScenarioBenchmark goes. TagMapBucketHashTest pins down the bucket hash: known tags get distinct, nonzero hashes, a slotted tag lands in its slot's bucket (trace-level in the other half), and tags without a slot and custom tags keep their hashes. The inline comment now says the level bit flips bit 3. Co-Authored-By: Claude Opus 5.5 --- .../trace/core/SpanScenarioBenchmark.java | 219 ------------------ .../trace/core/TraceShapeBenchmark.java | 20 +- .../main/java/datadog/trace/api/TagMap.java | 2 +- .../trace/api/TagMapBucketHashTest.java | 67 ++++++ 4 files changed, 82 insertions(+), 226 deletions(-) delete mode 100644 dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapBucketHashTest.java diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java deleted file mode 100644 index 5030e61a15c..00000000000 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/SpanScenarioBenchmark.java +++ /dev/null @@ -1,219 +0,0 @@ -package datadog.trace.core; - -import static java.util.concurrent.TimeUnit.MICROSECONDS; - -import datadog.context.ContextScope; -import datadog.trace.bootstrap.instrumentation.api.AgentSpan; -import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; -import datadog.trace.bootstrap.instrumentation.api.Tags; -import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; -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; -import org.openjdk.jmh.infra.Blackhole; - -/** - * Span and trace scenarios built the way today's decorators build them: a {@link SpanPrototype} - * applied in {@code afterStart}, then the request or connection tags by name. Each op finishes what - * it starts, so allocation per op covers the whole span or trace. - * - *

- * - *

Single-span and multi-span results can move in opposite directions: a change can make a - * trace's root cheaper while charging every child, so read the {@code webServerTrace} rows too. - * Values such as the client port and the status fall outside {@code Integer}'s box cache on - * purpose. - * - *

Passing {@code -jvmArgsAppend} on the command line replaces the {@code @Fork} arguments below, - * including {@code -DTEST_LOG_LEVEL=warn}; keep it, or debug logging dominates the allocation. - */ -@State(Scope.Benchmark) -@Warmup(iterations = 5) -@Measurement(iterations = 5) -@BenchmarkMode(Mode.Throughput) -@Threads(8) -@OutputTimeUnit(MICROSECONDS) -@Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn") -public class SpanScenarioBenchmark { - static final String SERVER_INSTRUMENTATION = "tomcat-server"; - static final CharSequence SERVER_OPERATION = UTF8BytesString.create("servlet.request"); - static final CharSequence SERVER_COMPONENT = UTF8BytesString.create("tomcat-server"); - static final CharSequence WEB = UTF8BytesString.create("web"); - static final String USER_AGENT = - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0"; - static final String METHOD = "GET"; - static final String URL = "http://localhost:8080/owners/42"; - static final String HOSTNAME = "localhost"; - static final String CLIENT_IP = "127.0.0.1"; - static final int CLIENT_PORT = 54321; // ephemeral, outside the box cache - static final String ROUTE = "/owners/{ownerId}"; - static final String SERVER_RESOURCE_NAME = "GET /owners/{ownerId}"; - static final CharSequence SERVER_RESOURCE = UTF8BytesString.create(SERVER_RESOURCE_NAME); - static final int STATUS = 200; // outside the box cache - - static final String JDBC_INSTRUMENTATION = "java-jdbc-prepared_statement"; - static final CharSequence JDBC_OPERATION = UTF8BytesString.create("database.query"); - static final CharSequence JDBC_RENAMED_OPERATION = UTF8BytesString.create("postgresql.query"); - static final CharSequence JAVA_JDBC = UTF8BytesString.create("java-jdbc"); - static final CharSequence JDBC_PREPARED_STATEMENT = - UTF8BytesString.create("java-jdbc-prepared_statement"); - static final CharSequence SQL = UTF8BytesString.create("sql"); - static final CharSequence STATEMENT = UTF8BytesString.create("SELECT * FROM owners WHERE id = ?"); - - static final String BARE_INSTRUMENTATION = "bench"; - static final CharSequence BARE_OPERATION = UTF8BytesString.create("bare.op"); - - /** The trace size, taken only by {@code webServerTrace} so the single-span scenarios run once. */ - @State(Scope.Benchmark) - public static class Shape { - @Param({"1", "5", "20"}) - int children; - } - - CoreTracer tracer; - SpanPrototype serverProto; - SpanPrototype jdbcProto; - - @Setup - public void setup(Blackhole blackhole) { - this.tracer = CoreTracer.builder().writer(new DropWriter(blackhole)).build(); - // the decorators' prototype chains: Base (type, component) -> Server / Client (kind, language) - this.serverProto = - SpanPrototype.builder() - .initInstrumentationName(SERVER_INSTRUMENTATION) - .initOperationName(SERVER_OPERATION) - .initSpanType(WEB) - .initComponentAndIntegration(SERVER_COMPONENT) - .initKind(Tags.SPAN_KIND_SERVER) - .initTag("language", "jvm") - .build(); - this.jdbcProto = - SpanPrototype.builder() - .initInstrumentationName(JDBC_INSTRUMENTATION) - .initOperationName(JDBC_OPERATION) - .initSpanType(SQL) - .initComponentAndIntegration(JAVA_JDBC) - .initKind(Tags.SPAN_KIND_CLIENT) - .build(); - } - - @TearDown - public void tearDown() { - this.tracer.close(); - } - - @Benchmark - public void bareStartSpan() { - tracer.startSpan(BARE_INSTRUMENTATION, BARE_OPERATION).finish(); - } - - @Benchmark - public void bareBuildSpan() { - tracer.buildSpan(BARE_INSTRUMENTATION, BARE_OPERATION).start().finish(); - } - - @Benchmark - public void webServerSpan() { - AgentSpan server = startServer(); - server.setHttpStatusCode(STATUS); - server.finish(); - } - - @Benchmark - public void webServerSpanViaBuilder() { - AgentSpan server = - tracer - .buildSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION) - .withSpanType(WEB) - .withTag(Tags.COMPONENT, SERVER_COMPONENT) - .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER) - .withTag("language", "jvm") - .withTag(Tags.HTTP_USER_AGENT, USER_AGENT) - .withTag(Tags.HTTP_METHOD, METHOD) - .withTag(Tags.HTTP_URL, URL) - .withTag(Tags.HTTP_HOSTNAME, HOSTNAME) - .withTag(Tags.PEER_HOST_IPV4, CLIENT_IP) - .withTag(Tags.PEER_PORT, CLIENT_PORT) - .withTag(Tags.HTTP_ROUTE, ROUTE) - .withResourceName(SERVER_RESOURCE_NAME) - .start(); - server.setHttpStatusCode(STATUS); - server.finish(); - } - - @Benchmark - public void jdbcClientSpan() { - jdbc().finish(); - } - - @Benchmark - public void webServerTrace(Shape shape) { - AgentSpan server = startServer(); - try (ContextScope serverScope = tracer.activateSpan(server)) { - for (int i = 0; i < shape.children; ++i) { - AgentSpan jdbc = jdbc(); - ContextScope jdbcScope = tracer.activateSpan(jdbc); - jdbcScope.close(); - jdbc.finish(); - } - } - server.setHttpStatusCode(STATUS); - server.finish(); - } - - /** HttpServerDecorator: startSpan, afterStart (prototype), onRequest, then the route. */ - AgentSpan startServer() { - AgentSpan server = tracer.startSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION); - server.applyOverwriting(serverProto); - server.setTag(Tags.HTTP_USER_AGENT, USER_AGENT); - server.setTag(Tags.HTTP_METHOD, METHOD); - server.setTag(Tags.HTTP_URL, URL); - server.setTag(Tags.HTTP_HOSTNAME, HOSTNAME); - server.setResourceName(SERVER_RESOURCE); // HttpResourceDecorator.withServerPath - server.setTag(Tags.PEER_HOST_IPV4, CLIENT_IP); - server.setTag(Tags.PEER_PORT, CLIENT_PORT); // setPeerPort - server.setTag(Tags.HTTP_ROUTE, ROUTE); // the framework, once routing resolves - return server; - } - - /** - * JDBCDecorator over a postgres prepared statement: startSpan, afterStart (prototype), - * onConnection, onPreparedStatement. - */ - AgentSpan jdbc() { - AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); - jdbc.applyOverwriting(jdbcProto); - jdbc.setTag(Tags.DB_TYPE, "postgresql"); // processDatabaseType - jdbc.setServiceName("postgresql", JAVA_JDBC); // postProcessServiceAndOperationName - jdbc.setOperationName(JDBC_RENAMED_OPERATION); - jdbc.setTag(Tags.DB_POOL_NAME, "HikariPool-1"); - jdbc.setTag(Tags.DB_USER, "app"); // DatabaseClientDecorator.onConnection - jdbc.setTag(Tags.DB_INSTANCE, "petclinic"); - jdbc.setTag(Tags.PEER_HOSTNAME, "db.internal"); - jdbc.setResourceName(STATEMENT); // withQueryInfo - jdbc.setTag(Tags.DB_OPERATION, "SELECT"); - jdbc.spanContext().setIntegrationName(JDBC_PREPARED_STATEMENT); - jdbc.setTag(Tags.COMPONENT, JDBC_PREPARED_STATEMENT); - return jdbc; - } -} diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java index 5556fa26139..46e2887084e 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java @@ -14,6 +14,7 @@ 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; @@ -23,8 +24,13 @@ import org.openjdk.jmh.infra.Blackhole; /** - * One whole trace per op, shaped like a web request: a server root, five jdbc prepared-statement - * children and one {@code @Trace} internal span, all finished. Divide by 7 for per-span figures. + * One whole trace per op, shaped like a web request: a server root, {@code children} jdbc + * prepared-statement children and one {@code @Trace} internal span, all finished. A trace has + * {@code children + 2} spans. + * + *

{@code children} varies the trace size, because single-span and small-trace results can hide + * costs that land on every child span: a change can make the root cheaper while charging each + * child, which only the larger traces show. * *

For retrospective comparison across tracer versions, each version keeps its own copy of this * benchmark, so the arms reflect that version's code: @@ -54,7 +60,9 @@ @OutputTimeUnit(MICROSECONDS) @Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn") public class TraceShapeBenchmark { - static final int JDBC_SPANS = 5; + /** The number of jdbc children, so the trace has {@code children + 2} spans. */ + @Param({"1", "5", "20"}) + int children; // server: HttpServerDecorator (tomcat) static final String SERVER_INSTRUMENTATION = "tomcat-server"; @@ -146,7 +154,7 @@ public void legacy() { server.setTag("language", "jvm"); legacyRequest(server); try (ContextScope scope = tracer.activateSpan(server)) { - for (int i = 0; i < JDBC_SPANS; ++i) { + for (int i = 0; i < children; ++i) { AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); jdbc.setSpanType(SQL); jdbc.setTag(Tags.COMPONENT, JAVA_JDBC); @@ -203,7 +211,7 @@ public void normal() { server.setTag(Tags.PEER_PORT, CLIENT_PORT); // setPeerPort server.setTag(Tags.HTTP_ROUTE, ROUTE); // the framework, once routing resolves try (ContextScope scope = tracer.activateSpan(server)) { - for (int i = 0; i < JDBC_SPANS; ++i) { + for (int i = 0; i < children; ++i) { // AbstractPreparedStatementInstrumentation: startSpan, afterStart, onConnection, // onPreparedStatement AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); @@ -244,7 +252,7 @@ public void newApi() { server.setTag(KnownTags.PEER_PORT_ID, CLIENT_PORT); server.setTag(KnownTags.HTTP_ROUTE_ID, ROUTE); try (ContextScope scope = tracer.activateSpan(server)) { - for (int i = 0; i < JDBC_SPANS; ++i) { + for (int i = 0; i < children; ++i) { AgentSpan jdbc = tracer.startSpan(jdbcProto, JDBC_OPERATION); jdbc.setTag(KnownTags.DB_TYPE_ID, DB_TYPE); jdbc.setServiceName(DB_SERVICE, JAVA_JDBC); diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index 0b42cb3f9d2..3e9fe756d88 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -515,7 +515,7 @@ static long customHash(String tag) { static int bucketHash(long tagHash) { int slot = (int) (tagHash >>> 32) & 0xFFFF; if ((tagHash >>> 32) != 0 && slot != KnownTagCodec.NO_SLOT) { - int level = (int) (tagHash & KnownTagCodec.LEVEL_TRACE) << 1; // trace-level: + 8 + int level = (int) (tagHash & KnownTagCodec.LEVEL_TRACE) << 1; // trace-level: flip bit 3 return ((int) (tagHash >>> 48) << 16) | (slot ^ level); } int hash = (int) (tagHash >>> 48) ^ (int) tagHash; diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapBucketHashTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapBucketHashTest.java new file mode 100644 index 00000000000..7a58dbe3534 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapBucketHashTest.java @@ -0,0 +1,67 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; + +/** How {@link TagMap.Entry#bucketHash} places known and custom tags in the buckets. */ +class TagMapBucketHashTest { + private static final int BUCKET_MASK = 15; // TagMap's 16 buckets + + /** Every known tag id the registry generated. */ + static List knownTagIds() throws IllegalAccessException { + List ids = new ArrayList<>(); + for (Field field : KnownTags.class.getFields()) { + if (Modifier.isStatic(field.getModifiers()) + && field.getType() == long.class + && field.getName().endsWith("_ID")) { + ids.add(field.getLong(null)); + } + } + return ids; + } + + @Test + void knownTagsGetDistinctNonzeroBucketHashes() throws IllegalAccessException { + List ids = knownTagIds(); + assertFalse(ids.isEmpty()); + Set hashes = new HashSet<>(); + for (long id : ids) { + int hash = TagMap.Entry.bucketHash(id); + assertNotEquals(0, hash, "zero marks a vacant BucketGroup slot"); + assertTrue( + hashes.add(hash), "two known tags share a bucket hash: " + KnownTagCodec.nameOf(id)); + } + } + + @Test + void aSlottedKnownTagIsBucketedBySlot() throws IllegalAccessException { + for (long id : knownTagIds()) { + int slot = KnownTagCodec.slotOf(id); + if (slot == KnownTagCodec.NO_SLOT) { + continue; + } + // trace-level tags reuse the span-level slot numbers, so they move to the other half + int expected = (slot ^ (KnownTagCodec.isTraceLevel(id) ? 8 : 0)) & BUCKET_MASK; + assertEquals(expected, TagMap.Entry.bucketHash(id) & BUCKET_MASK, KnownTagCodec.nameOf(id)); + } + } + + @Test + void tagsWithoutASlotAndCustomTagsKeepTheirHashes() { + long noSlot = ((long) 5 << 48) | ((long) KnownTagCodec.NO_SLOT << 32); + assertEquals((int) (noSlot >>> 48) ^ (int) noSlot, TagMap.Entry.bucketHash(noSlot)); + + long custom = TagMap.Entry.customHash("my.custom.tag"); + assertEquals((int) custom == 0 ? 0xDD06 : (int) custom, TagMap.Entry.bucketHash(custom)); + } +} From 2271e4f4fd011b0b56aa5fc70a1c99b673d53fbd Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 22 Oct 2026 17:50:26 -0400 Subject: [PATCH 4/4] Add a whole-request benchmark for the header code paths HeaderPathBenchmark runs one request per op through the propagation paths: extract the inbound headers with the default propagator, start the server span from what was extracted, run a few children, inject into an outbound call, finish everything. Inbound headers vary: none, Datadog, W3C (with dd, OpenTelemetry and third-party tracestate members), both, and both plus baggage. The existing propagation benchmarks each measure one step, so they can't see costs the extracted state causes later on every span; this one can. A setup check fails the run if nothing is extracted or injected. Co-Authored-By: Claude Opus 5.5 --- .../trace/core/HeaderPathBenchmark.java | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java new file mode 100644 index 00000000000..d73d130d069 --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java @@ -0,0 +1,226 @@ +package datadog.trace.core; + +import static datadog.context.Context.root; +import static datadog.trace.bootstrap.instrumentation.api.AgentSpan.fromContext; +import static java.util.concurrent.TimeUnit.MICROSECONDS; + +import datadog.context.Context; +import datadog.context.ContextScope; +import datadog.context.propagation.CarrierSetter; +import datadog.context.propagation.CarrierVisitor; +import datadog.context.propagation.Propagators; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; +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; +import org.openjdk.jmh.infra.Blackhole; + +/** + * One whole request per op, through the header code paths: extract the inbound headers with the + * default propagator, start the server span from what was extracted, run a few children, make one + * outbound HTTP call that injects its headers, then finish everything. + * + *

The existing propagation benchmarks each measure one step (extracting into a context, + * injecting from a prebuilt one, parsing one header value). They can't see costs that the extracted + * state causes later, on every span of the trace: for example, a {@code tracestate} that leads each + * span to allocate sampling state. Measuring the whole request catches those. + * + *

{@code headers} selects the inbound headers, under the default propagation styles: + * + *

+ */ +@State(Scope.Benchmark) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@BenchmarkMode(Mode.Throughput) +@Threads(8) +@OutputTimeUnit(MICROSECONDS) +@Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn") +public class HeaderPathBenchmark { + static final int JDBC_SPANS = 3; + + static final String TRACE_ID_HEX = "66a0f6c800000000a3ce929d0e0e4736"; + static final String TRACE_ID_LOW = "11803532876627986230"; // a3ce929d0e0e4736 + static final String PARENT_ID = "67667974448284343"; // 00f067aa0ba902b7 + static final String PARENT_ID_HEX = "00f067aa0ba902b7"; + + static final String SERVER_INSTRUMENTATION = "tomcat-server"; + static final CharSequence SERVER_OPERATION = UTF8BytesString.create("servlet.request"); + static final String JDBC_INSTRUMENTATION = "java-jdbc-prepared_statement"; + static final CharSequence JDBC_OPERATION = UTF8BytesString.create("database.query"); + static final String CLIENT_INSTRUMENTATION = "okhttp"; + static final CharSequence CLIENT_OPERATION = UTF8BytesString.create("okhttp.request"); + + static final CarrierVisitor> GETTER = + new CarrierVisitor>() { + @Override + public void forEachKeyValue( + Map carrier, BiConsumer visitor) { + carrier.forEach(visitor); + } + }; + static final CarrierSetter> SETTER = Map::put; + + @Param({"none", "datadog", "tracecontext", "both", "bothBaggage"}) + String headers; + + CoreTracer tracer; + Map inbound; + SpanPrototype serverProto; + SpanPrototype jdbcProto; + SpanPrototype clientProto; + + /** One outbound carrier per thread, reused across requests. */ + @State(Scope.Thread) + public static class Outbound { + final Map headers = new HashMap<>(); + } + + @Setup + public void setup(Blackhole blackhole) { + this.tracer = CoreTracer.builder().writer(new DropWriter(blackhole)).build(); + this.inbound = inboundHeaders(headers); + this.serverProto = + SpanPrototype.builder() + .initInstrumentationName(SERVER_INSTRUMENTATION) + .initOperationName(SERVER_OPERATION) + .initSpanType("web") + .initComponentAndIntegration("tomcat-server") + .initKind(Tags.SPAN_KIND_SERVER) + .initTag("language", "jvm") + .build(); + this.jdbcProto = + SpanPrototype.builder() + .initInstrumentationName(JDBC_INSTRUMENTATION) + .initOperationName(JDBC_OPERATION) + .initSpanType("sql") + .initComponentAndIntegration("java-jdbc") + .initKind(Tags.SPAN_KIND_CLIENT) + .build(); + this.clientProto = + SpanPrototype.builder() + .initInstrumentationName(CLIENT_INSTRUMENTATION) + .initOperationName(CLIENT_OPERATION) + .initSpanType("http") + .initComponentAndIntegration("okhttp") + .initKind(Tags.SPAN_KIND_CLIENT) + .build(); + checkPropagation(); + } + + /** + * Fails fast if the headers aren't actually extracted and injected, which would measure nothing. + */ + void checkPropagation() { + Context parent = Propagators.defaultPropagator().extract(root(), inbound, GETTER); + if (!headers.equals("none") && fromContext(parent) == null) { + throw new IllegalStateException("nothing extracted from " + headers + " headers"); + } + Outbound outbound = new Outbound(); + request(outbound); + if (!outbound.headers.containsKey("x-datadog-trace-id")) { + throw new IllegalStateException("nothing injected: " + outbound.headers); + } + } + + @TearDown + public void tearDown() { + this.tracer.close(); + } + + static Map inboundHeaders(String style) { + Map h = new HashMap<>(); + h.put("host", "localhost:8080"); + h.put("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/120.0"); + h.put("accept", "application/json"); + if (style.equals("datadog") || style.startsWith("both")) { + h.put("x-datadog-trace-id", TRACE_ID_LOW); + h.put("x-datadog-parent-id", PARENT_ID); + h.put("x-datadog-sampling-priority", "1"); + h.put("x-datadog-tags", "_dd.p.dm=-0,_dd.p.tid=66a0f6c800000000"); + } + if (style.equals("tracecontext") || style.startsWith("both")) { + h.put("traceparent", "00-" + TRACE_ID_HEX + "-" + PARENT_ID_HEX + "-01"); + h.put( + "tracestate", + "dd=s:1;p:" + + PARENT_ID_HEX + + ";t.dm:-0;t.tid:66a0f6c800000000," + + "ot=th:8;rv:9b8233f7e3a151,rojo=00f067aa0ba902b7"); + } + if (style.equals("bothBaggage")) { + h.put("baggage", "userId=alice,serverNode=DF%2028,isProduction=false"); + } + return h; + } + + @Benchmark + public void request(Outbound outbound) { + outbound.headers.clear(); + // HttpServerDecorator.extract / startSpan + Context parent = Propagators.defaultPropagator().extract(root(), inbound, GETTER); + AgentSpan extractedSpan = fromContext(parent); + AgentSpanContext extracted = extractedSpan == null ? null : extractedSpan.spanContext(); + AgentSpan server = tracer.startSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION, extracted); + server.setMeasured(true); + server.applyOverwriting(serverProto); + server.setTag(Tags.HTTP_METHOD, "GET"); + server.setTag(Tags.HTTP_URL, "http://localhost:8080/owners/42"); + server.setTag(Tags.HTTP_ROUTE, "/owners/{ownerId}"); + server.setTag(Tags.PEER_PORT, 54321); + + try (ContextScope serverScope = parent.with(server).attach()) { + for (int i = 0; i < JDBC_SPANS; ++i) { + AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); + jdbc.applyOverwriting(jdbcProto); + jdbc.setTag(Tags.DB_TYPE, "postgresql"); + jdbc.setTag(Tags.DB_INSTANCE, "petclinic"); + jdbc.setTag(Tags.PEER_HOSTNAME, "db.internal"); + jdbc.setResourceName("SELECT * FROM owners WHERE id = ?"); + try (ContextScope s = tracer.activateSpan(jdbc)) { + // the statement executes + } + jdbc.finish(); + } + + // the outbound call: start the client span, inject its headers, finish + AgentSpan client = tracer.startSpan(CLIENT_INSTRUMENTATION, CLIENT_OPERATION); + client.applyOverwriting(clientProto); + client.setTag(Tags.HTTP_METHOD, "GET"); + client.setTag(Tags.HTTP_URL, "http://inventory.internal/items/7"); + client.setTag(Tags.PEER_HOSTNAME, "inventory.internal"); + client.setTag(Tags.PEER_PORT, 8080); + Propagators.defaultPropagator() + .inject(Context.current().with(client), outbound.headers, SETTER); + client.setHttpStatusCode(200); + client.finish(); + } + server.setHttpStatusCode(200); + server.finish(); + } +}