4646import java .util .Collections ;
4747import java .util .List ;
4848import java .util .Random ;
49+ import java .util .concurrent .atomic .AtomicInteger ;
4950import org .openjdk .jmh .annotations .Benchmark ;
5051import org .openjdk .jmh .annotations .Fork ;
5152import org .openjdk .jmh .annotations .Group ;
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