diff --git a/sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java b/sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java index d3e8d50f435..13dc0667134 100644 --- a/sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java +++ b/sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java @@ -30,6 +30,7 @@ import java.util.Collections; import java.util.List; import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Group; @@ -73,6 +74,13 @@ * record activities over more distinct series. The highest contention scenario is cardinality=1, * threads=4. Any scenario with threads=1 has zero contention. * + *

To make the cardinality dimension meaningful under contention, each thread traverses the + * series in its own independent (per-thread shuffled) order — see {@link ThreadState}. This models + * independent threads recording to arbitrary series. A naive shared {@code i % cardinality} index + * would instead march all threads through the same series in lockstep (contention self-synchronizes + * them), collapsing high-cardinality multi-thread runs into a single rotating hotspot that behaves + * like cardinality=1. + * *

It's useful to characterize the performance of the metrics system under contention, as some * high-performance applications may have many threads trying to record to the same series. It's * also useful to characterize the performance of the metrics system under low contention, as some @@ -125,6 +133,9 @@ public static class BenchmarkState { List attributesList; Span span; io.opentelemetry.context.Scope contextScope; + // Hands out a distinct seed to each recording thread's ThreadState so threads traverse the + // series in independent orders (see ThreadState). + final AtomicInteger threadSeedSequence = new AtomicInteger(); @Setup @SuppressWarnings("MustBeClosedChecker") @@ -184,6 +195,38 @@ public void tearDown() { } } + /** + * Per-thread series traversal order. Each recording thread shuffles {@code [0, cardinality)} with + * a distinct seed, so that at any given record the threads are recording to different + * series rather than marching through the same series in lockstep. Without this, the shared + * sequential {@code i % cardinality} index plus contention's self-synchronizing effect collapses + * the high-cardinality, multi-thread cases into a single rotating hotspot (effectively + * cardinality=1 contention), which does not reflect real-world recording where independent + * threads touch arbitrary series. + */ + @State(Scope.Thread) + public static class ThreadState { + int[] order; + + @Setup + public void setup(BenchmarkState benchmarkState) { + int cardinality = benchmarkState.cardinality; + order = new int[cardinality]; + for (int i = 0; i < cardinality; i++) { + order[i] = i; + } + // Distinct seed per thread => independent permutations => no cross-thread lockstep. + Random random = + new Random(INITIAL_SEED + benchmarkState.threadSeedSequence.getAndIncrement()); + for (int i = cardinality - 1; i > 0; i--) { + int j = random.nextInt(i + 1); + int tmp = order[i]; + order[i] = order[j]; + order[j] = tmp; + } + } + } + @Benchmark @Group("threads1") @GroupThreads(1) @@ -191,8 +234,8 @@ public void tearDown() { @Warmup(iterations = 3, time = 1) @Measurement(iterations = 10, time = 1) @OperationsPerInvocation(RECORDS_PER_INVOCATION) - public void record_SingleThread(BenchmarkState benchmarkState) { - record(benchmarkState); + public void record_SingleThread(BenchmarkState benchmarkState, ThreadState threadState) { + record(benchmarkState, threadState); } @Benchmark @@ -202,14 +245,15 @@ public void record_SingleThread(BenchmarkState benchmarkState) { @Warmup(iterations = 3, time = 1) @Measurement(iterations = 10, time = 1) @OperationsPerInvocation(RECORDS_PER_INVOCATION) - public void record_MultipleThreads(BenchmarkState benchmarkState) { - record(benchmarkState); + public void record_MultipleThreads(BenchmarkState benchmarkState, ThreadState threadState) { + record(benchmarkState, threadState); } - private static void record(BenchmarkState benchmarkState) { + private static void record(BenchmarkState benchmarkState, ThreadState threadState) { + // Per-thread series order: at a given i, different threads hit different series (no lockstep). + int[] order = threadState.order; for (int i = 0; i < RECORDS_PER_INVOCATION; i++) { - Attributes attributes = - benchmarkState.attributesList.get(i % benchmarkState.attributesList.size()); + Attributes attributes = benchmarkState.attributesList.get(order[i % order.length]); long value = benchmarkState.measurements.get(i % benchmarkState.measurements.size()); benchmarkState.instrument.record(value, attributes); }