Skip to content

Commit c748bf3

Browse files
committed
Try tweaking benchmark
1 parent 4da6ca0 commit c748bf3

1 file changed

Lines changed: 53 additions & 10 deletions

File tree

sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import java.util.Collections;
4747
import java.util.List;
4848
import java.util.Random;
49+
import java.util.concurrent.atomic.AtomicInteger;
4950
import org.openjdk.jmh.annotations.Benchmark;
5051
import org.openjdk.jmh.annotations.Fork;
5152
import org.openjdk.jmh.annotations.Group;
@@ -91,6 +92,13 @@
9192
* spreading their record activities over more distinct series. The highest contention scenario is
9293
* cardinality=1, threads=4. Any scenario with threads=1 has zero contention.
9394
*
95+
* <p>To make the cardinality dimension meaningful under contention, each thread traverses the series
96+
* in its own independent (per-thread shuffled) order — see {@link ThreadState}. This models
97+
* independent threads recording to arbitrary series. A naive shared {@code i % cardinality} index
98+
* would instead march all threads through the same series in lockstep (contention self-synchronizes
99+
* them), collapsing high-cardinality multi-thread runs into a single rotating hotspot that behaves
100+
* like cardinality=1.
101+
*
94102
* <p>It's useful to characterize the performance of the metrics system under contention, as some
95103
* high-performance applications may have many threads trying to record to the same series. It's
96104
* also useful to characterize the performance of the metrics system under low contention, as some
@@ -153,6 +161,9 @@ public static class BenchmarkState {
153161
List<Attributes> attributesList;
154162
Span span;
155163
io.opentelemetry.context.Scope contextScope;
164+
// Hands out a distinct seed to each recording thread's ThreadState so threads traverse the
165+
// series in independent orders (see ThreadState).
166+
final AtomicInteger threadSeedSequence = new AtomicInteger();
156167

157168
@Setup
158169
@SuppressWarnings("MustBeClosedChecker")
@@ -218,15 +229,46 @@ public void tearDown() {
218229
}
219230
}
220231

232+
/**
233+
* Per-thread series traversal order. Each recording thread shuffles {@code [0, cardinality)} with
234+
* a distinct seed, so that at any given record the threads are recording to <em>different</em>
235+
* series rather than marching through the same series in lockstep. Without this, the shared
236+
* sequential {@code i % cardinality} index plus contention's self-synchronizing effect collapses
237+
* the high-cardinality, multi-thread cases into a single rotating hotspot (effectively
238+
* cardinality=1 contention), which does not reflect real-world recording where independent threads
239+
* touch arbitrary series.
240+
*/
241+
@State(Scope.Thread)
242+
public static class ThreadState {
243+
int[] order;
244+
245+
@Setup
246+
public void setup(BenchmarkState benchmarkState) {
247+
int cardinality = benchmarkState.cardinality;
248+
order = new int[cardinality];
249+
for (int i = 0; i < cardinality; i++) {
250+
order[i] = i;
251+
}
252+
// Distinct seed per thread => independent permutations => no cross-thread lockstep.
253+
Random random = new Random(INITIAL_SEED + benchmarkState.threadSeedSequence.getAndIncrement());
254+
for (int i = cardinality - 1; i > 0; i--) {
255+
int j = random.nextInt(i + 1);
256+
int tmp = order[i];
257+
order[i] = order[j];
258+
order[j] = tmp;
259+
}
260+
}
261+
}
262+
221263
@Benchmark
222264
@Group("threads1")
223265
@GroupThreads(1)
224266
@Fork(3)
225267
@Warmup(iterations = 3, time = 1)
226268
@Measurement(iterations = 10, time = 1)
227269
@OperationsPerInvocation(RECORDS_PER_INVOCATION)
228-
public void record_SingleThread(BenchmarkState benchmarkState) {
229-
record(benchmarkState);
270+
public void record_SingleThread(BenchmarkState benchmarkState, ThreadState threadState) {
271+
record(benchmarkState, threadState);
230272
}
231273

232274
@Benchmark
@@ -236,23 +278,24 @@ public void record_SingleThread(BenchmarkState benchmarkState) {
236278
@Warmup(iterations = 3, time = 1)
237279
@Measurement(iterations = 10, time = 1)
238280
@OperationsPerInvocation(RECORDS_PER_INVOCATION)
239-
public void record_MultipleThreads(BenchmarkState benchmarkState) {
240-
record(benchmarkState);
281+
public void record_MultipleThreads(BenchmarkState benchmarkState, ThreadState threadState) {
282+
record(benchmarkState, threadState);
241283
}
242284

243-
private static void record(BenchmarkState benchmarkState) {
285+
private static void record(BenchmarkState benchmarkState, ThreadState threadState) {
286+
// Per-thread series order: at a given i, different threads hit different series (no lockstep).
287+
int[] order = threadState.order;
244288
if (benchmarkState.bound) {
245-
// Bound: record straight to the pre-resolved bound instrument for the series, indexed in
246-
// lockstep with attributesList — no per-record Attributes lookup.
289+
// Bound: record straight to the pre-resolved bound instrument for the series — no per-record
290+
// Attributes lookup.
247291
List<BoundInstrument> boundInstruments = benchmarkState.boundInstruments;
248292
for (int i = 0; i < RECORDS_PER_INVOCATION; i++) {
249293
long value = benchmarkState.measurements.get(i % benchmarkState.measurements.size());
250-
boundInstruments.get(i % boundInstruments.size()).record(value);
294+
boundInstruments.get(order[i % order.length]).record(value);
251295
}
252296
} else {
253297
for (int i = 0; i < RECORDS_PER_INVOCATION; i++) {
254-
Attributes attributes =
255-
benchmarkState.attributesList.get(i % benchmarkState.attributesList.size());
298+
Attributes attributes = benchmarkState.attributesList.get(order[i % order.length]);
256299
long value = benchmarkState.measurements.get(i % benchmarkState.measurements.size());
257300
benchmarkState.instrument.record(value, attributes);
258301
}

0 commit comments

Comments
 (0)