Skip to content

Commit 71d75b2

Browse files
committed
initial impl complete
1 parent 6502e3b commit 71d75b2

22 files changed

Lines changed: 959 additions & 199 deletions

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

Lines changed: 150 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@
1212

1313
import io.opentelemetry.api.common.AttributeKey;
1414
import 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;
1531
import io.opentelemetry.api.metrics.Meter;
1632
import io.opentelemetry.api.trace.Span;
1733
import io.opentelemetry.api.trace.Tracer;
@@ -52,6 +68,8 @@
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.
@@ -62,16 +80,16 @@
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
}

sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleCounter.java

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,57 @@
1010
import io.opentelemetry.api.incubator.metrics.BoundDoubleCounter;
1111
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleCounter;
1212
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleCounterBuilder;
13+
import io.opentelemetry.context.Context;
1314
import io.opentelemetry.sdk.metrics.internal.descriptor.Advice;
1415
import io.opentelemetry.sdk.metrics.internal.descriptor.InstrumentDescriptor;
16+
import io.opentelemetry.sdk.metrics.internal.state.BoundStorageHandle;
1517
import io.opentelemetry.sdk.metrics.internal.state.WriteableMetricStorage;
1618
import java.util.List;
19+
import javax.annotation.Nullable;
1720

18-
final class ExtendedSdkDoubleCounter extends SdkDoubleCounter implements ExtendedDoubleCounter {
21+
final class ExtendedSdkDoubleCounter extends SdkDoubleCounter
22+
implements ExtendedDoubleCounter, BoundDoubleCounter {
23+
24+
// Non-null only when this is a bound instance returned from bind(); null for the instrument
25+
// itself. When set, the add() methods record straight to this handle instead of resolving the
26+
// series from the storage on each call.
27+
@Nullable private final BoundStorageHandle boundHandle;
1928

2029
private ExtendedSdkDoubleCounter(
2130
InstrumentDescriptor descriptor, SdkMeter sdkMeter, WriteableMetricStorage storage) {
31+
this(descriptor, sdkMeter, storage, null);
32+
}
33+
34+
private ExtendedSdkDoubleCounter(
35+
InstrumentDescriptor descriptor,
36+
SdkMeter sdkMeter,
37+
WriteableMetricStorage storage,
38+
@Nullable BoundStorageHandle boundHandle) {
2239
super(descriptor, sdkMeter, storage);
40+
this.boundHandle = boundHandle;
2341
}
2442

2543
@Override
2644
public BoundDoubleCounter bind(Attributes attributes) {
27-
// TODO(bound-instruments): implement against WriteableMetricStorage in the implementation phase
28-
// (includes fresh-eyes work on DeltaSynchronousMetricStorage).
29-
throw new UnsupportedOperationException("bind is not yet implemented");
45+
return new ExtendedSdkDoubleCounter(
46+
getDescriptor(), sdkMeter, storage, storage.bind(attributes));
47+
}
48+
49+
@Override
50+
public void add(double value) {
51+
add(value, Context.current());
52+
}
53+
54+
@Override
55+
public void add(double value, Context context) {
56+
if (!validateNonNegative(value)) {
57+
return;
58+
}
59+
if (boundHandle != null) {
60+
boundHandle.recordDouble(value, context);
61+
} else {
62+
storage.recordDouble(value, Attributes.empty(), context);
63+
}
3064
}
3165

3266
static final class ExtendedSdkDoubleCounterBuilder extends SdkDoubleCounterBuilder

sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleGauge.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,52 @@
1111
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleGauge;
1212
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder;
1313
import io.opentelemetry.api.incubator.metrics.ExtendedLongGaugeBuilder;
14+
import io.opentelemetry.context.Context;
1415
import io.opentelemetry.sdk.metrics.internal.descriptor.InstrumentDescriptor;
16+
import io.opentelemetry.sdk.metrics.internal.state.BoundStorageHandle;
1517
import io.opentelemetry.sdk.metrics.internal.state.WriteableMetricStorage;
1618
import java.util.List;
19+
import javax.annotation.Nullable;
1720

18-
final class ExtendedSdkDoubleGauge extends SdkDoubleGauge implements ExtendedDoubleGauge {
21+
final class ExtendedSdkDoubleGauge extends SdkDoubleGauge
22+
implements ExtendedDoubleGauge, BoundDoubleGauge {
23+
24+
// Non-null only when this is a bound instance returned from bind(); null for the instrument
25+
// itself. When set, the set() methods record straight to this handle instead of resolving the
26+
// series from the storage on each call.
27+
@Nullable private final BoundStorageHandle boundHandle;
1928

2029
private ExtendedSdkDoubleGauge(
2130
InstrumentDescriptor descriptor, SdkMeter sdkMeter, WriteableMetricStorage storage) {
31+
this(descriptor, sdkMeter, storage, null);
32+
}
33+
34+
private ExtendedSdkDoubleGauge(
35+
InstrumentDescriptor descriptor,
36+
SdkMeter sdkMeter,
37+
WriteableMetricStorage storage,
38+
@Nullable BoundStorageHandle boundHandle) {
2239
super(descriptor, sdkMeter, storage);
40+
this.boundHandle = boundHandle;
2341
}
2442

2543
@Override
2644
public BoundDoubleGauge bind(Attributes attributes) {
27-
// TODO(bound-instruments): implement against WriteableMetricStorage in the implementation phase
28-
// (includes fresh-eyes work on DeltaSynchronousMetricStorage).
29-
throw new UnsupportedOperationException("bind is not yet implemented");
45+
return new ExtendedSdkDoubleGauge(getDescriptor(), sdkMeter, storage, storage.bind(attributes));
46+
}
47+
48+
@Override
49+
public void set(double value) {
50+
set(value, Context.current());
51+
}
52+
53+
@Override
54+
public void set(double value, Context context) {
55+
if (boundHandle != null) {
56+
boundHandle.recordDouble(value, context);
57+
} else {
58+
storage.recordDouble(value, Attributes.empty(), context);
59+
}
3060
}
3161

3262
static final class ExtendedSdkDoubleGaugeBuilder extends SdkDoubleGaugeBuilder

0 commit comments

Comments
 (0)