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