diff --git a/dd-trace-core/src/jmh/java/datadog/trace/common/metrics/AdversarialMetricsBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/common/metrics/AdversarialMetricsBenchmark.java index 01ba90097a4..ac5938fcc32 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/common/metrics/AdversarialMetricsBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/common/metrics/AdversarialMetricsBenchmark.java @@ -27,29 +27,35 @@ import org.openjdk.jmh.infra.Blackhole; /** - * Adversarial JMH benchmark designed to stress the metrics subsystem's capacity bounds. + * Adversarial JMH benchmark designed to stress every cardinality + capacity dimension of the + * metrics subsystem at once. * - *
The metrics aggregator is bounded at every layer: + *
The metrics aggregator is supposed to be bounded by design: * *
The benchmark hammers all of these simultaneously with 8 producer threads, unique labels per - * op (so the aggregate cache fills+evicts repeatedly), random durations across a wide range (so - * histograms accept many distinct bins), and random {@code error}/{@code topLevel} flags (so both - * histograms are exercised). After the run, drop counters are printed so you can see how the - * subsystem absorbed the burst. + *
This benchmark hammers all of those bounds simultaneously with 8 producer threads, unique + * labels per op (so handlers cap and the table fills+evicts repeatedly), random durations across a + * wide range (so histograms accept many distinct bins), and random {@code error}/{@code topLevel} + * flags (so both histograms are exercised). After the run, prints the drop counters so you can + * verify the subsystem stayed bounded under attack. * - *
What "OOM the metrics subsystem" would look like if the bounds break: producer-thread - * allocation would grow unbounded (snapshots faster than the inbox can drain produces dropped - * snapshots, not heap growth); aggregator-thread heap would grow if entries weren't capped or - * histograms grew past their dense-store limit. + *
What "OOM the metrics subsystem" looks like if the bounds break: producer-thread allocation + * would grow unbounded (snapshots faster than inbox can drain produces dropped snapshots, not heap + * growth); aggregator-thread heap would grow if entries weren't capped, if handlers grew past their + * cap, or if histograms grew past their dense-store limit. */ @State(Scope.Benchmark) @Warmup(iterations = 2, time = 15, timeUnit = SECONDS) @@ -100,7 +106,7 @@ public void tearDown() { System.err.println( " onStatsAggregateDropped = " + health.aggregateDropped.sum() - + " (snapshots dropped because the aggregate cache was full with no stale entry)"); + + " (snapshots dropped because the AggregateTable was full with no stale entry)"); } @Benchmark @@ -108,9 +114,9 @@ public void publish(ThreadState ts, Blackhole blackhole) { int idx = ts.cursor++; ThreadLocalRandom rng = ThreadLocalRandom.current(); - // Mix indices so labels don't fall into linear order. Distinct labels exceed every reasonable - // working-set bound, so the aggregate cache evicts continuously and most ops force a fresh - // MetricKey construction on the consumer thread. + // Mix indices so labels don't fall into linear order in the handler tables. Distinct labels + // exceed every cap (RESOURCE=512, OPERATION=128, SERVICE=128, peer.hostname=512), so handlers + // saturate fast and most ops resolve to the blocked-by-tracer sentinel. int scrambled = idx * 0x9E3779B1; // golden ratio multiplier String service = "svc-" + (scrambled & 0xFFFF); String operation = "op-" + ((scrambled >>> 8) & 0x3FFFF); diff --git a/dd-trace-core/src/jmh/java/datadog/trace/common/metrics/ClientStatsAggregatorMissPathBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/common/metrics/ClientStatsAggregatorMissPathBenchmark.java new file mode 100644 index 00000000000..2079cbf15ec --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/common/metrics/ClientStatsAggregatorMissPathBenchmark.java @@ -0,0 +1,83 @@ +package datadog.trace.common.metrics; + +import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND; +import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_CLIENT; +import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import datadog.communication.ddagent.DDAgentFeaturesDiscovery; +import datadog.trace.api.WellKnownTags; +import datadog.trace.core.CoreSpan; +import datadog.trace.core.monitor.HealthMetrics; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +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.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Miss-path variant of {@link ClientStatsAggregatorBenchmark}. Each op publishes a single-span + * trace from a pre-built pool where every span has a unique (service, operation, resource) tuple. + * After cardinality budgets fill, fields canonicalize to the {@code blocked_by_tracer} sentinel, + * but the producer still allocates a {@link SpanSnapshot} per op and enqueues it for the aggregator + * -- so the steady state exercises the per-op publish allocations + the consumer's + * canonicalize/match work, not the hit-path-only pattern of the other benchmarks. + * + *
Run with {@code -prof gc} to compare allocation rates against master's {@code
+ * ConflatingMetricsAggregator}.
+ */
+@State(Scope.Benchmark)
+@Warmup(iterations = 1, time = 15, timeUnit = SECONDS)
+@Measurement(iterations = 3, time = 15, timeUnit = SECONDS)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(MICROSECONDS)
+@Fork(value = 1)
+public class ClientStatsAggregatorMissPathBenchmark {
+
+ private static final int POOL_SIZE = 4096;
+
+ private final DDAgentFeaturesDiscovery featuresDiscovery =
+ new ClientStatsAggregatorBenchmark.FixedAgentFeaturesDiscovery(
+ Collections.singleton("peer.hostname"), Collections.emptySet());
+ private final ClientStatsAggregator aggregator =
+ new ClientStatsAggregator(
+ new WellKnownTags("", "", "", "", "", ""),
+ Collections.emptySet(),
+ featuresDiscovery,
+ HealthMetrics.NO_OP,
+ new ClientStatsAggregatorBenchmark.NullSink(),
+ 2048,
+ 2048,
+ false);
+
+ private final List Runs multi-threaded ({@link Threads} = 8 by default; override with {@code -t N}) so the
+ * allocation rate {@code -prof gc} reports reflects multiple producers hitting the shared metrics
+ * aggregator + writer pipeline, and so we can compare total throughput between revisions.
+ *
+ * Reflection is used to swap the tracer's default no-op {@code metricsAggregator} for a real
+ * {@link ClientStatsAggregator} so the metrics pipeline actually runs.
+ *
+ * Two modes via {@code @Param}:
+ *
+ * The handlers are reset on the aggregator thread every reporting cycle via {@link
* #resetCardinalityHandlers()}.
*
+ * EMPTY-as-absent contract: all UTF8 fields are non-null. The optional fields ({@code
+ * serviceSource}, {@code httpMethod}, {@code httpEndpoint}, {@code grpcStatusCode}) carry {@link
+ * UTF8BytesString#EMPTY} when the snapshot had no value; {@link SerializingMetricWriter} tests
+ * against {@code EMPTY} (identity comparison on the singleton) to decide whether to emit each field
+ * on the wire.
+ *
* Deliberate cohesion. This class concentrates the per-field {@code
* PropertyCardinalityHandler}/{@code TagCardinalityHandler} infrastructure, the canonicalized label
- * fields, the encoded {@code peerTags} list used by the serializer, the {@link Canonical} scratch
+ * fields, the encoded {@code peerTags} array used by the serializer, the {@link Canonical} scratch
* buffer, and the mutable counter/histogram aggregate state on a single object. The prior design
* split label fields and aggregate state across separate {@code MetricKey} and {@code
* AggregateMetric} instances, allocating both per unique key on miss; folding them yields one
@@ -66,6 +71,9 @@ final class AggregateEntry extends Hashtable.Entry {
static final long ERROR_TAG = 0x8000000000000000L;
static final long TOP_LEVEL_TAG = 0x4000000000000000L;
+ /** Shared empty array used by entries with no peer tags. */
+ private static final UTF8BytesString[] EMPTY_PEER_TAGS = new UTF8BytesString[0];
+
/**
* Whether cardinality limits substitute the {@code blocked_by_tracer} sentinel when a per-field
* budget is exhausted. Read once at class init from {@link
@@ -133,7 +141,7 @@ final class AggregateEntry extends Hashtable.Entry {
final short httpStatusCode;
final boolean synthetic;
final boolean traceRoot;
- final List Field order intentionally mirrors {@link Canonical#matches} -- UTF8 fields first (highest
- * cardinality first for matches' short-circuit benefit), then the peer-tag list, then the
+ * cardinality first for matches' short-circuit benefit), then the peer-tag array, then the
* primitives. The hash itself is order-stable across all callers; the lockstep ordering is purely
* for readability when reasoning about lookup and equality in tandem.
+ *
+ * {@code peerTags} is taken as a {@code (array, length)} pair so the same routine works for
+ * the {@link Canonical} scratch buffer (where {@code length < array.length}) and the entry's
+ * fixed-size array.
*/
static long hashOf(
UTF8BytesString resource,
@@ -368,7 +384,8 @@ static long hashOf(
short httpStatusCode,
boolean synthetic,
boolean traceRoot,
- List Values are sized to the typical-service workload with headroom; "typical" estimates are noted
- * inline. Raise if a workload routinely hits the sentinel; lower carries proportional memory
- * savings but risks suppressing legitimate distinctions.
+ * Values are sized to cover realistic workload cardinality per 10s reporting window with
+ * headroom -- the prior DDCache-inherited limits (RESOURCE=32, OPERATION=64, ...) were chosen for
+ * memory conservation and were tight enough that a single REST API with a couple hundred routes
+ * would exhaust the budget within seconds. Memory cost with the flat handler tables is ~20 KB
+ * across all 9 handlers -- negligible relative to the {@code maxAggregates}-sized entry table.
*/
final class MetricCardinalityLimits {
private MetricCardinalityLimits() {}
/**
- * Distinct {@code resource.name} values per cycle. Highest-cardinality field by far: DB-query
- * obfuscations, HTTP route templates, custom resources. Typical service: 30-200 unique.
+ * Distinct {@code resource.name} values per cycle. Highest-cardinality field: HTTP route
+ * templates, SQL query templates, custom resources. A web app with one parameterized route per
+ * controller method easily hits low hundreds.
*/
- static final int RESOURCE = 128;
+ static final int RESOURCE = 512;
/**
* Distinct {@code service.name} values per cycle. Local service plus downstream peer-service
- * names. Microservice meshes typically reference 10-50 distinct services.
+ * names; sized for service-mesh hubs that fan out to many downstreams.
*/
- static final int SERVICE = 32;
+ static final int SERVICE = 128;
/**
* Distinct {@code operation.name} values per cycle. Names like {@code http.request}, {@code
- * db.query}, etc. Typical service: 10-30 across integrations.
+ * db.query}, etc. One per integration kind; production services often span 30-60.
*/
- static final int OPERATION = 64;
+ static final int OPERATION = 128;
/**
* Distinct {@code _dd.base_service} override values per cycle. Used rarely; usually empty or one
@@ -37,16 +40,16 @@ private MetricCardinalityLimits() {}
static final int SERVICE_SOURCE = 16;
/**
- * Distinct {@code span.type} values per cycle. {@code DDSpanTypes} catalog is ~30; a single
- * service usually spans 5-10 integration types.
+ * Distinct {@code span.type} values per cycle. {@code DDSpanTypes} catalog has ~30 known values;
+ * a single service typically spans 5-10 integration types.
*/
- static final int TYPE = 16;
+ static final int TYPE = 32;
/**
- * Distinct {@code span.kind} values per cycle. OTel defines exactly 5 (server/client/producer/
- * consumer/internal); 8 still leaves 60% headroom in case a producer invents new kinds.
+ * Distinct {@code span.kind} values per cycle. OTel defines 5 standard kinds (server/client/
+ * producer/consumer/internal); the 16 cap leaves headroom in case producers invent new kinds.
*/
- static final int SPAN_KIND = 8;
+ static final int SPAN_KIND = 16;
/**
* Distinct HTTP method values per cycle. Standard verbs are 7-9; WebDAV/custom adds a few more.
@@ -57,7 +60,7 @@ private MetricCardinalityLimits() {}
* Distinct {@code http.endpoint} values per cycle. Path templates -- same shape as {@code
* RESOURCE} for HTTP-heavy services. Only used when {@code includeEndpointInMetrics} is enabled.
*/
- static final int HTTP_ENDPOINT = 64;
+ static final int HTTP_ENDPOINT = 256;
/**
* Distinct gRPC status code values per cycle. gRPC spec defines exactly 17 codes (0-16); 24
diff --git a/dd-trace-core/src/main/java/datadog/trace/common/metrics/SerializingMetricWriter.java b/dd-trace-core/src/main/java/datadog/trace/common/metrics/SerializingMetricWriter.java
index 03bc1a2f993..e2dc3d6f66b 100644
--- a/dd-trace-core/src/main/java/datadog/trace/common/metrics/SerializingMetricWriter.java
+++ b/dd-trace-core/src/main/java/datadog/trace/common/metrics/SerializingMetricWriter.java
@@ -13,7 +13,6 @@
import datadog.trace.api.git.GitInfo;
import datadog.trace.api.git.GitInfoProvider;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
-import java.util.List;
import java.util.function.Function;
public final class SerializingMetricWriter implements MetricWriter {
@@ -183,8 +182,8 @@ public void add(AggregateEntry entry) {
writer.writeUTF8(entry.getSpanKind());
writer.writeUTF8(PEER_TAGS);
- final List On this branch, peer tags live as a single pre-encoded {@code List On this branch, peer tags live as a pre-encoded {@code UTF8BytesString[]} on the entry
+ * (canonicalization through {@link PeerTagSchema#register} already collapsed identical values), so
+ * equality compares the arrays via {@link Arrays#equals(Object[], Object[])}. The hash side
+ * (computed in {@link AggregateEntry#hashOf}) folds in the same array, so the contract stays
+ * consistent.
*/
public final class AggregateEntryTestUtils {
private AggregateEntryTestUtils() {}
@@ -75,7 +77,7 @@ public static boolean equals(AggregateEntry a, AggregateEntry b) {
&& Objects.equals(a.getServiceSource(), b.getServiceSource())
&& Objects.equals(a.getType(), b.getType())
&& Objects.equals(a.getSpanKind(), b.getSpanKind())
- && a.getPeerTags().equals(b.getPeerTags())
+ && Arrays.equals(a.getPeerTags(), b.getPeerTags())
&& Objects.equals(a.getHttpMethod(), b.getHttpMethod())
&& Objects.equals(a.getHttpEndpoint(), b.getHttpEndpoint())
&& Objects.equals(a.getGrpcStatusCode(), b.getGrpcStatusCode());
diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateTableTest.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateTableTest.java
index 8694892ea84..a739a0f0b62 100644
--- a/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateTableTest.java
+++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateTableTest.java
@@ -8,6 +8,7 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
import datadog.metrics.agent.AgentMeter;
import datadog.metrics.api.statsd.StatsDClient;
@@ -96,6 +97,35 @@ void peerTagPairsParticipateInIdentity() {
assertEquals(3, table.size());
}
+ @Test
+ void cardinalityBlockedValuesCollapseIntoOneEntry() {
+ // SERVICE_HANDLER has a cardinality limit of 128. With 150 distinct service names, services
+ // 129+ canonicalize to the "blocked_by_tracer" sentinel. Because the table hashes from the
+ // canonical (post-handler) form, all blocked services land in the same bucket and merge into
+ // a single entry rather than fragmenting.
+ //
+ // Sentinel substitution is gated on AggregateEntry#LIMITS_ENABLED. With the flag off (the
+ // default), over-cap values get fresh UTF8BytesStrings and flow to distinct buckets, so this
+ // test only meaningfully runs in limits-on mode.
+ assumeTrue(
+ AggregateEntry.LIMITS_ENABLED,
+ "cardinality collapse only fires when the limits flag is enabled");
+
+ AggregateEntry.resetCardinalityHandlers();
+ AggregateTable table = new AggregateTable(256);
+
+ for (int i = 0; i < 150; i++) {
+ AggregateEntry entry = table.findOrInsert(snapshot("svc-" + i, "op", "client"));
+ assertNotNull(entry);
+ entry.recordOneDuration(1L);
+ }
+
+ // 128 in-budget services + 1 collapsed "blocked_by_tracer" entry = 129 total.
+ assertEquals(129, table.size());
+
+ AggregateEntry.resetCardinalityHandlers();
+ }
+
@Test
void capOverrunEvictsStaleEntry() {
AggregateTable table = new AggregateTable(2);
diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/ClientStatsAggregatorBootstrapTest.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/ClientStatsAggregatorBootstrapTest.java
index 67857ff3830..b0572fb8af8 100644
--- a/dd-trace-core/src/test/java/datadog/trace/common/metrics/ClientStatsAggregatorBootstrapTest.java
+++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/ClientStatsAggregatorBootstrapTest.java
@@ -1,7 +1,7 @@
package datadog.trace.common.metrics;
import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@@ -284,14 +284,15 @@ void reconcileSwapsSchemaWhenTagSetChanges() throws Exception {
ArgumentCaptor>> pool = generatePool(POOL_SIZE);
+ private int cursor;
+
+ static List
>> generatePool(int n) {
+ List
>> out = new ArrayList<>(n);
+ for (int i = 0; i < n; i++) {
+ SimpleSpan span =
+ new SimpleSpan(
+ "svc-" + i, "op-" + i, "res-" + i, "type-" + (i & 7), true, true, false, 0, 10, -1);
+ span.setTag(SPAN_KIND, SPAN_KIND_CLIENT);
+ span.setTag("peer.hostname", "host-" + i);
+ out.add(Collections.singletonList(span));
+ }
+ return out;
+ }
+
+ @Benchmark
+ public void benchmark(Blackhole blackhole) {
+ int idx = cursor;
+ cursor = (idx + 1) % POOL_SIZE;
+ blackhole.consume(aggregator.publish(pool.get(idx)));
+ }
+}
diff --git a/dd-trace-core/src/jmh/java/datadog/trace/common/metrics/TracePipelineBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/common/metrics/TracePipelineBenchmark.java
new file mode 100644
index 00000000000..cc1d4a37538
--- /dev/null
+++ b/dd-trace-core/src/jmh/java/datadog/trace/common/metrics/TracePipelineBenchmark.java
@@ -0,0 +1,176 @@
+package datadog.trace.common.metrics;
+
+import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_CLIENT;
+import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_SERVER;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import datadog.trace.api.WellKnownTags;
+import datadog.trace.bootstrap.instrumentation.api.AgentScope;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.common.writer.Writer;
+import datadog.trace.core.CoreTracer;
+import datadog.trace.core.DDSpan;
+import datadog.trace.core.monitor.HealthMetrics;
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.List;
+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;
+
+/**
+ * End-to-end JMH benchmark of a 3-span HTTP-style trace through {@link CoreTracer}: one {@code
+ * span.kind=server} root + two {@code span.kind=client} children, as if a service handled an
+ * incoming request that made two outbound HTTP calls. Children inherit the server span as parent
+ * via implicit scope-based parentage; the root finishes last so {@code PendingTrace.write} ->
+ * {@code tracer.write(trace)} -> metricsAggregator.publish + writer.write (no-op) runs
+ * synchronously on the producing thread.
+ *
+ *
+ *
+ */
+@State(Scope.Benchmark)
+@Warmup(iterations = 2, time = 15, timeUnit = SECONDS)
+@Measurement(iterations = 5, time = 15, timeUnit = SECONDS)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(SECONDS)
+@Threads(8)
+@Fork(value = 2)
+public class TracePipelineBenchmark {
+
+ @Param({"stable", "varied"})
+ String mode;
+
+ private CoreTracer tracer;
+ private ClientStatsAggregator aggregator;
+ private boolean stable;
+
+ @State(Scope.Thread)
+ public static class ThreadState {
+ int cursor;
+ }
+
+ @Setup
+ public void setup() throws Exception {
+ this.stable = "stable".equals(mode);
+ this.tracer = CoreTracer.builder().writer(new NoopWriter()).strictTraceWrites(false).build();
+ this.aggregator =
+ new ClientStatsAggregator(
+ new WellKnownTags("", "", "", "", "", ""),
+ Collections.emptySet(),
+ new ClientStatsAggregatorBenchmark.FixedAgentFeaturesDiscovery(
+ Collections.singleton("peer.hostname"), Collections.emptySet()),
+ HealthMetrics.NO_OP,
+ new ClientStatsAggregatorBenchmark.NullSink(),
+ 2048,
+ 2048,
+ false);
+ this.aggregator.start();
+ // Replace the no-op aggregator the tracer was constructed with. The field is package-private
+ // in datadog.trace.core; reflect since this benchmark lives in the metrics package.
+ Field f = CoreTracer.class.getDeclaredField("metricsAggregator");
+ f.setAccessible(true);
+ f.set(this.tracer, this.aggregator);
+ }
+
+ @TearDown
+ public void tearDown() {
+ aggregator.close();
+ tracer.close();
+ }
+
+ @Benchmark
+ public void threeSpanTrace(ThreadState ts, Blackhole blackhole) {
+ int idx = ts.cursor++;
+ String service = stable ? "svc" : "svc-" + idx;
+ String serverOp = stable ? "servlet.request" : "servlet.request-" + idx;
+ String serverResource = stable ? "GET /widgets/{id}" : "GET /widgets/" + idx;
+ String clientOp = stable ? "http.request" : "http.request-" + idx;
+ String clientResource1 = stable ? "GET /downstream-a" : "GET /downstream-a/" + idx;
+ String clientResource2 = stable ? "GET /downstream-b" : "GET /downstream-b/" + idx;
+ String hostA = stable ? "host-a" : "host-a-" + idx;
+ String hostB = stable ? "host-b" : "host-b-" + idx;
+
+ AgentSpan server = tracer.startSpan("servlet", serverOp);
+ server.setResourceName(serverResource);
+ server.setServiceName(service);
+ server.setTag(SPAN_KIND, SPAN_KIND_SERVER);
+ AgentScope serverScope = tracer.activateSpan(server);
+ try {
+ AgentSpan client1 = tracer.startSpan("okhttp", clientOp);
+ client1.setResourceName(clientResource1);
+ client1.setServiceName(service);
+ client1.setTag(SPAN_KIND, SPAN_KIND_CLIENT);
+ client1.setTag("peer.hostname", hostA);
+ AgentScope client1Scope = tracer.activateSpan(client1);
+ try {
+ // simulated unit of in-call work would go here
+ } finally {
+ client1Scope.close();
+ }
+ client1.finish();
+
+ AgentSpan client2 = tracer.startSpan("okhttp", clientOp);
+ client2.setResourceName(clientResource2);
+ client2.setServiceName(service);
+ client2.setTag(SPAN_KIND, SPAN_KIND_CLIENT);
+ client2.setTag("peer.hostname", hostB);
+ AgentScope client2Scope = tracer.activateSpan(client2);
+ try {
+ // simulated unit of in-call work would go here
+ } finally {
+ client2Scope.close();
+ }
+ client2.finish();
+ } finally {
+ serverScope.close();
+ }
+ // Finishing the root last triggers PendingTrace.write -> tracer.write -> metrics + writer on
+ // this thread, since all child refs have already decremented to zero.
+ server.finish();
+ blackhole.consume(server);
+ }
+
+ private static final class NoopWriter implements Writer {
+ @Override
+ public void write(List