1212
1313import io .opentelemetry .api .common .AttributeKey ;
1414import io .opentelemetry .api .common .Attributes ;
15+ import io .opentelemetry .api .incubator .metrics .BoundDoubleCounter ;
16+ import io .opentelemetry .api .incubator .metrics .BoundDoubleGauge ;
17+ import io .opentelemetry .api .incubator .metrics .BoundDoubleHistogram ;
18+ import io .opentelemetry .api .incubator .metrics .BoundDoubleUpDownCounter ;
19+ import io .opentelemetry .api .incubator .metrics .BoundLongCounter ;
20+ import io .opentelemetry .api .incubator .metrics .BoundLongGauge ;
21+ import io .opentelemetry .api .incubator .metrics .BoundLongHistogram ;
22+ import io .opentelemetry .api .incubator .metrics .BoundLongUpDownCounter ;
23+ import io .opentelemetry .api .incubator .metrics .ExtendedDoubleCounter ;
24+ import io .opentelemetry .api .incubator .metrics .ExtendedDoubleGauge ;
25+ import io .opentelemetry .api .incubator .metrics .ExtendedDoubleHistogram ;
26+ import io .opentelemetry .api .incubator .metrics .ExtendedDoubleUpDownCounter ;
27+ import io .opentelemetry .api .incubator .metrics .ExtendedLongCounter ;
28+ import io .opentelemetry .api .incubator .metrics .ExtendedLongGauge ;
29+ import io .opentelemetry .api .incubator .metrics .ExtendedLongHistogram ;
30+ import io .opentelemetry .api .incubator .metrics .ExtendedLongUpDownCounter ;
1531import io .opentelemetry .api .metrics .Meter ;
1632import io .opentelemetry .api .trace .Span ;
1733import io .opentelemetry .api .trace .Tracer ;
5268 * {@link Aggregation}, including all relevant combinations for synchronous instruments.
5369 * <li>{@link BenchmarkState#aggregationTemporality}
5470 * <li>{@link BenchmarkState#cardinality}
71+ * <li>{@link BenchmarkState#bound} whether recording goes through bound instruments ({@code
72+ * Extended*#bind(Attributes)}) or unbound instruments.
5573 * <li>thread count
5674 * <li>{@link BenchmarkState#instrumentValueType}, {@link BenchmarkState#memoryMode}, and {@link
5775 * BenchmarkState#exemplars} are disabled to reduce combinatorial explosion.
6280 *
6381 * <p>The cardinality and thread count dimensions partially overlap. Cardinality dictates how many
6482 * unique attribute sets (i.e. series) are recorded to, and thread count dictates how many threads
65- * are simultaneously recording to those series. In all cases , the record path needs to look up an
66- * aggregation handle for the series corresponding to the measurement's {@link Attributes} in a
67- * {@link java.util.concurrent.ConcurrentHashMap}. That will be the case until otel adds support for
68- * <a href="https://github.com/open-telemetry/opentelemetry-specification/issues/4126"> bound
69- * instruments</a> . The cardinality dictates the size of this map, which has some impact on
70- * performance. However, by far the dominant bottleneck is contention. That is, the number of
71- * threads simultaneously trying to record to the same series. Increasing the threads increases
72- * contention. Increasing cardinality decreases contention, as the threads are now spreading their
73- * record activities over more distinct series. The highest contention scenario is cardinality=1,
74- * threads=4. Any scenario with threads=1 has zero contention.
83+ * are simultaneously recording to those series. For unbound instruments , the record path looks up
84+ * an aggregation handle for the series corresponding to the measurement's {@link Attributes} in a
85+ * {@link java.util.concurrent.ConcurrentHashMap}; for bound instruments ({@link
86+ * BenchmarkState# bound}) that handle is resolved once at bind time, so the record path skips the
87+ * lookup and attribute processing entirely . The cardinality dictates the size of this map, which
88+ * has some impact on performance. However, by far the dominant bottleneck is contention. That is,
89+ * the number of threads simultaneously trying to record to the same series. Increasing the threads
90+ * increases contention. Increasing cardinality decreases contention, as the threads are now
91+ * spreading their record activities over more distinct series. The highest contention scenario is
92+ * cardinality=1, threads=4. Any scenario with threads=1 has zero contention.
7593 *
7694 * <p>It's useful to characterize the performance of the metrics system under contention, as some
7795 * high-performance applications may have many threads trying to record to the same series. It's
@@ -99,6 +117,12 @@ public static class BenchmarkState {
99117 @ Param ({"1" , "128" })
100118 int cardinality ;
101119
120+ // Whether to record through bound instruments (Extended*#bind(Attributes)), which resolve the
121+ // timeseries once up front, or unbound instruments, which look up the timeseries by Attributes
122+ // on every record.
123+ @ Param ({"false" , "true" })
124+ boolean bound ;
125+
102126 // The following parameters are excluded from the benchmark to reduce combinatorial explosion
103127 // but can optionally be enabled for adhoc evaluation.
104128
@@ -120,7 +144,10 @@ public static class BenchmarkState {
120144 boolean exemplars = false ;
121145
122146 OpenTelemetrySdk openTelemetry ;
147+ // Populated when bound == false.
123148 private Instrument instrument ;
149+ // Populated when bound == true; parallel to attributesList (one bound instrument per series).
150+ private List <BoundInstrument > boundInstruments ;
124151 List <Long > measurements ;
125152 List <Attributes > attributesList ;
126153 Span span ;
@@ -151,7 +178,6 @@ public void setup() {
151178 .build ();
152179
153180 Meter meter = openTelemetry .getMeter ("benchmark" );
154- instrument = getInstrument (meter , instrumentType , instrumentValueType );
155181 Tracer tracer = openTelemetry .getTracer ("benchmark" );
156182 span = tracer .spanBuilder ("benchmark" ).startSpan ();
157183 // We suppress warnings on closing here, as we rely on tests to make sure context is closed.
@@ -169,6 +195,13 @@ public void setup() {
169195 }
170196 Collections .shuffle (attributesList );
171197
198+ if (bound ) {
199+ boundInstruments =
200+ bindInstruments (meter , instrumentType , instrumentValueType , attributesList );
201+ } else {
202+ instrument = getInstrument (meter , instrumentType , instrumentValueType );
203+ }
204+
172205 measurements = new ArrayList <>(RECORDS_PER_INVOCATION );
173206 for (int i = 0 ; i < RECORDS_PER_INVOCATION ; i ++) {
174207 measurements .add ((long ) random .nextInt (2000 ));
@@ -207,11 +240,21 @@ public void record_MultipleThreads(BenchmarkState benchmarkState) {
207240 }
208241
209242 private static void record (BenchmarkState benchmarkState ) {
210- for (int i = 0 ; i < RECORDS_PER_INVOCATION ; i ++) {
211- Attributes attributes =
212- benchmarkState .attributesList .get (i % benchmarkState .attributesList .size ());
213- long value = benchmarkState .measurements .get (i % benchmarkState .measurements .size ());
214- benchmarkState .instrument .record (value , attributes );
243+ if (benchmarkState .bound ) {
244+ // Bound: record straight to the pre-resolved bound instrument for the series, indexed in
245+ // lockstep with attributesList — no per-record Attributes lookup.
246+ List <BoundInstrument > boundInstruments = benchmarkState .boundInstruments ;
247+ for (int i = 0 ; i < RECORDS_PER_INVOCATION ; i ++) {
248+ long value = benchmarkState .measurements .get (i % benchmarkState .measurements .size ());
249+ boundInstruments .get (i % boundInstruments .size ()).record (value );
250+ }
251+ } else {
252+ for (int i = 0 ; i < RECORDS_PER_INVOCATION ; i ++) {
253+ Attributes attributes =
254+ benchmarkState .attributesList .get (i % benchmarkState .attributesList .size ());
255+ long value = benchmarkState .measurements .get (i % benchmarkState .measurements .size ());
256+ benchmarkState .instrument .record (value , attributes );
257+ }
215258 }
216259 }
217260
@@ -262,4 +305,95 @@ private static Instrument getInstrument(
262305 }
263306 throw new IllegalArgumentException ();
264307 }
308+
309+ @ FunctionalInterface
310+ private interface BoundInstrument {
311+ void record (long value );
312+ }
313+
314+ /**
315+ * Builds the instrument, then binds one {@link BoundInstrument} per series in {@code
316+ * attributesList}, returned in the same order so the record loop can index it in lockstep.
317+ */
318+ private static List <BoundInstrument > bindInstruments (
319+ Meter meter ,
320+ InstrumentType instrumentType ,
321+ InstrumentValueType instrumentValueType ,
322+ List <Attributes > attributesList ) {
323+ boolean isDouble = instrumentValueType == InstrumentValueType .DOUBLE ;
324+ String name = "instrument" ;
325+ List <BoundInstrument > result = new ArrayList <>(attributesList .size ());
326+ switch (instrumentType ) {
327+ case COUNTER :
328+ if (isDouble ) {
329+ ExtendedDoubleCounter instrument =
330+ (ExtendedDoubleCounter ) meter .counterBuilder (name ).ofDoubles ().build ();
331+ for (Attributes attributes : attributesList ) {
332+ BoundDoubleCounter bound = instrument .bind (attributes );
333+ result .add (bound ::add );
334+ }
335+ } else {
336+ ExtendedLongCounter instrument = (ExtendedLongCounter ) meter .counterBuilder (name ).build ();
337+ for (Attributes attributes : attributesList ) {
338+ BoundLongCounter bound = instrument .bind (attributes );
339+ result .add (bound ::add );
340+ }
341+ }
342+ return result ;
343+ case UP_DOWN_COUNTER :
344+ if (isDouble ) {
345+ ExtendedDoubleUpDownCounter instrument =
346+ (ExtendedDoubleUpDownCounter ) meter .upDownCounterBuilder (name ).ofDoubles ().build ();
347+ for (Attributes attributes : attributesList ) {
348+ BoundDoubleUpDownCounter bound = instrument .bind (attributes );
349+ result .add (bound ::add );
350+ }
351+ } else {
352+ ExtendedLongUpDownCounter instrument =
353+ (ExtendedLongUpDownCounter ) meter .upDownCounterBuilder (name ).build ();
354+ for (Attributes attributes : attributesList ) {
355+ BoundLongUpDownCounter bound = instrument .bind (attributes );
356+ result .add (bound ::add );
357+ }
358+ }
359+ return result ;
360+ case HISTOGRAM :
361+ if (isDouble ) {
362+ ExtendedDoubleHistogram instrument =
363+ (ExtendedDoubleHistogram ) meter .histogramBuilder (name ).build ();
364+ for (Attributes attributes : attributesList ) {
365+ BoundDoubleHistogram bound = instrument .bind (attributes );
366+ result .add (bound ::record );
367+ }
368+ } else {
369+ ExtendedLongHistogram instrument =
370+ (ExtendedLongHistogram ) meter .histogramBuilder (name ).ofLongs ().build ();
371+ for (Attributes attributes : attributesList ) {
372+ BoundLongHistogram bound = instrument .bind (attributes );
373+ result .add (bound ::record );
374+ }
375+ }
376+ return result ;
377+ case GAUGE :
378+ if (isDouble ) {
379+ ExtendedDoubleGauge instrument = (ExtendedDoubleGauge ) meter .gaugeBuilder (name ).build ();
380+ for (Attributes attributes : attributesList ) {
381+ BoundDoubleGauge bound = instrument .bind (attributes );
382+ result .add (bound ::set );
383+ }
384+ } else {
385+ ExtendedLongGauge instrument =
386+ (ExtendedLongGauge ) meter .gaugeBuilder (name ).ofLongs ().build ();
387+ for (Attributes attributes : attributesList ) {
388+ BoundLongGauge bound = instrument .bind (attributes );
389+ result .add (bound ::set );
390+ }
391+ }
392+ return result ;
393+ case OBSERVABLE_COUNTER :
394+ case OBSERVABLE_UP_DOWN_COUNTER :
395+ case OBSERVABLE_GAUGE :
396+ }
397+ throw new IllegalArgumentException ();
398+ }
265399}
0 commit comments