Skip to content

Commit fd0ffde

Browse files
authored
Rework and publish metric benchmarks (#8000)
1 parent 77b2f02 commit fd0ffde

6 files changed

Lines changed: 257 additions & 295 deletions

File tree

.github/workflows/benchmark.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,15 @@ jobs:
4242
env:
4343
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
4444

45+
# TODO (jack-berg): Select or build appropriate benchmarks for other key areas:
46+
# - Log SDK record & export
47+
# - Trace SDK record & export
48+
# - Metric SDK export
49+
# - Noop implementation
4550
- name: Run Benchmark
4651
run: |
47-
cd sdk/trace/build
48-
java -jar libs/opentelemetry-sdk-trace-*-jmh.jar -rf json SpanBenchmark SpanPipelineBenchmark ExporterBenchmark
52+
cd sdk/all/build
53+
java -jar libs/opentelemetry-sdk-*-jmh.jar -rf json MetricRecordBenchmark
4954
5055
- name: Use CLA approved github bot
5156
run: .github/scripts/use-cla-approved-bot.sh
@@ -54,7 +59,7 @@ jobs:
5459
uses: benchmark-action/github-action-benchmark@4bdcce38c94cec68da58d012ac24b7b1155efe8b # v1.20.7
5560
with:
5661
tool: 'jmh'
57-
output-file-path: sdk/trace/build/jmh-result.json
62+
output-file-path: sdk/all/build/jmh-result.json
5863
gh-pages-branch: benchmarks
5964
github-token: ${{ secrets.GITHUB_TOKEN }}
6065
benchmark-data-dir-path: "benchmarks"

sdk/all/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,6 @@ dependencies {
2222
testAnnotationProcessor("com.google.auto.value:auto-value")
2323

2424
testImplementation(project(":sdk:testing"))
25+
26+
jmh(project(":sdk:testing"))
2527
}
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk;
7+
8+
import static io.opentelemetry.sdk.metrics.InstrumentType.COUNTER;
9+
import static io.opentelemetry.sdk.metrics.InstrumentType.GAUGE;
10+
import static io.opentelemetry.sdk.metrics.InstrumentType.HISTOGRAM;
11+
import static io.opentelemetry.sdk.metrics.InstrumentType.UP_DOWN_COUNTER;
12+
13+
import io.opentelemetry.api.common.AttributeKey;
14+
import io.opentelemetry.api.common.Attributes;
15+
import io.opentelemetry.api.metrics.Meter;
16+
import io.opentelemetry.api.trace.Span;
17+
import io.opentelemetry.api.trace.Tracer;
18+
import io.opentelemetry.sdk.common.export.MemoryMode;
19+
import io.opentelemetry.sdk.metrics.Aggregation;
20+
import io.opentelemetry.sdk.metrics.ExemplarFilter;
21+
import io.opentelemetry.sdk.metrics.InstrumentType;
22+
import io.opentelemetry.sdk.metrics.InstrumentValueType;
23+
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
24+
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
25+
import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector;
26+
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
27+
import io.opentelemetry.sdk.trace.SdkTracerProvider;
28+
import io.opentelemetry.sdk.trace.samplers.Sampler;
29+
import java.util.ArrayList;
30+
import java.util.Collections;
31+
import java.util.List;
32+
import java.util.Random;
33+
import org.openjdk.jmh.annotations.Benchmark;
34+
import org.openjdk.jmh.annotations.Fork;
35+
import org.openjdk.jmh.annotations.Group;
36+
import org.openjdk.jmh.annotations.GroupThreads;
37+
import org.openjdk.jmh.annotations.Measurement;
38+
import org.openjdk.jmh.annotations.Param;
39+
import org.openjdk.jmh.annotations.Scope;
40+
import org.openjdk.jmh.annotations.Setup;
41+
import org.openjdk.jmh.annotations.State;
42+
import org.openjdk.jmh.annotations.TearDown;
43+
import org.openjdk.jmh.annotations.Warmup;
44+
45+
/**
46+
* Notes on interpreting the data:
47+
*
48+
* <p>The benchmark has two dimensions which partially overlap: cardinality and thread count.
49+
* Cardinality dictates how many unique attribute sets (i.e. series) are recorded to, and thread
50+
* count dictates how many threads are simultaneously recording to those series. In all cases, the
51+
* record path needs to look up an aggregation handle for the series corresponding to the
52+
* measurement's {@link Attributes} in a {@link java.util.concurrent.ConcurrentHashMap}. That will
53+
* be the case until otel adds support for <a
54+
* href="https://github.com/open-telemetry/opentelemetry-specification/issues/4126">bound
55+
* instruments</a>. The cardinality dictates the size of this map, which has some impact on
56+
* performance. However, by far the dominant bottleneck is contention. That is, the number of
57+
* threads simultaneously trying to record to the same series. Increasing the threads increases
58+
* contention. Increasing cardinality decreases contention, as the threads are now spreading their
59+
* record activities over more distinct series. The highest contention scenario is cardinality=1,
60+
* threads=4. Any scenario with threads=1 has zero contention.
61+
*
62+
* <p>It's useful to characterize the performance of the metrics system under contention, as some
63+
* high-performance applications may have many threads trying to record to the same series. It's
64+
* also useful to characterize the performance of the metrics system under low contention, as some
65+
* high-performance applications may not frequently be trying to concurrently record to the same
66+
* series yet still care about the overhead of each record operation.
67+
*
68+
* <p>{@link AggregationTemporality} can impact performance because additional concurrency controls
69+
* are needed to ensure there are no duplicate, partial, or lost writes while resetting the set of
70+
* timeseries each collection.
71+
*/
72+
public class MetricRecordBenchmark {
73+
74+
private static final int INITIAL_SEED = 513423236;
75+
private static final int RECORD_COUNT = 10 * 1024;
76+
77+
@State(Scope.Benchmark)
78+
public static class ThreadState {
79+
80+
@Param InstrumentTypeAndAggregation instrumentTypeAndAggregation;
81+
82+
@Param AggregationTemporality aggregationTemporality;
83+
84+
@Param({"1", "100"})
85+
int cardinality;
86+
87+
// The following parameters are excluded from the benchmark to reduce combinatorial explosion
88+
// but can optionally be enabled for adhoc evaluation.
89+
90+
// InstrumentValueType doesn't materially impact performance. Uncomment to evaluate.
91+
// @Param
92+
// InstrumentValueType instrumentValueType;
93+
InstrumentValueType instrumentValueType = InstrumentValueType.LONG;
94+
95+
// MemoryMode almost exclusively impacts collect from a performance standpoint. Uncomment to
96+
// evaluate.
97+
// @Param
98+
// MemoryMode memoryMode;
99+
MemoryMode memoryMode = MemoryMode.REUSABLE_DATA;
100+
101+
// Exemplars can impact performance, but we skip evaluation to limit test cases. Uncomment to
102+
// evaluate.
103+
// @Param({"true", "false"})
104+
// boolean exemplars;
105+
boolean exemplars = false;
106+
107+
OpenTelemetrySdk openTelemetry;
108+
Instrument instrument;
109+
List<Long> measurements;
110+
List<Attributes> attributesList;
111+
Span span;
112+
io.opentelemetry.context.Scope contextScope;
113+
114+
@Setup
115+
@SuppressWarnings("MustBeClosedChecker")
116+
public void setup() {
117+
InstrumentType instrumentType = instrumentTypeAndAggregation.instrumentType;
118+
Aggregation aggregation = instrumentTypeAndAggregation.aggregation;
119+
120+
openTelemetry =
121+
OpenTelemetrySdk.builder()
122+
.setTracerProvider(SdkTracerProvider.builder().setSampler(Sampler.alwaysOn()).build())
123+
.setMeterProvider(
124+
SdkMeterProvider.builder()
125+
.registerMetricReader(
126+
InMemoryMetricReader.builder()
127+
.setAggregationTemporalitySelector(unused -> aggregationTemporality)
128+
.setDefaultAggregationSelector(
129+
DefaultAggregationSelector.getDefault()
130+
.with(instrumentType, aggregation))
131+
.setMemoryMode(memoryMode)
132+
.build())
133+
.setExemplarFilter(
134+
exemplars ? ExemplarFilter.traceBased() : ExemplarFilter.alwaysOff())
135+
.build())
136+
.build();
137+
138+
Meter meter = openTelemetry.getMeter("benchmark");
139+
instrument = getInstrument(meter, instrumentType, instrumentValueType);
140+
Tracer tracer = openTelemetry.getTracer("benchmark");
141+
span = tracer.spanBuilder("benchmark").startSpan();
142+
// We suppress warnings on closing here, as we rely on tests to make sure context is closed.
143+
contextScope = span.makeCurrent();
144+
145+
Random random = new Random(INITIAL_SEED);
146+
attributesList = new ArrayList<>(cardinality);
147+
AttributeKey<String> key = AttributeKey.stringKey("key");
148+
String last = "aaaaaaaaaaaaaaaaaaaaaaaaaa";
149+
for (int i = 0; i < cardinality; i++) {
150+
char[] chars = last.toCharArray();
151+
chars[random.nextInt(last.length())] = (char) (random.nextInt(26) + 'a');
152+
last = new String(chars);
153+
attributesList.add(Attributes.of(key, last));
154+
}
155+
Collections.shuffle(attributesList);
156+
157+
measurements = new ArrayList<>(RECORD_COUNT);
158+
for (int i = 0; i < RECORD_COUNT; i++) {
159+
measurements.add((long) random.nextInt(2000));
160+
}
161+
Collections.shuffle(measurements);
162+
}
163+
164+
@TearDown
165+
public void tearDown() {
166+
contextScope.close();
167+
span.end();
168+
openTelemetry.shutdown();
169+
}
170+
}
171+
172+
@Benchmark
173+
@Group("threads1")
174+
@GroupThreads(1)
175+
@Fork(1)
176+
@Warmup(iterations = 5, time = 1)
177+
@Measurement(iterations = 5, time = 1)
178+
public void record_1Thread(ThreadState threadState) {
179+
record(threadState);
180+
}
181+
182+
@Benchmark
183+
@Group("threads4")
184+
@GroupThreads(4)
185+
@Fork(1)
186+
@Warmup(iterations = 5, time = 1)
187+
@Measurement(iterations = 5, time = 1)
188+
public void record_4Threads(ThreadState threadState) {
189+
record(threadState);
190+
}
191+
192+
private static void record(ThreadState threadState) {
193+
for (int i = 0; i < RECORD_COUNT; i++) {
194+
Attributes attributes = threadState.attributesList.get(i % threadState.attributesList.size());
195+
long value = threadState.measurements.get(i % threadState.measurements.size());
196+
threadState.instrument.record(value, attributes);
197+
}
198+
}
199+
200+
@SuppressWarnings("ImmutableEnumChecker")
201+
public enum InstrumentTypeAndAggregation {
202+
COUNTER_SUM(COUNTER, Aggregation.sum()),
203+
UP_DOWN_COUNTER_SUM(UP_DOWN_COUNTER, Aggregation.sum()),
204+
GAUGE_LAST_VALUE(GAUGE, Aggregation.lastValue()),
205+
HISTOGRAM_EXPLICIT(HISTOGRAM, Aggregation.explicitBucketHistogram()),
206+
HISTOGRAM_BASE2_EXPONENTIAL(HISTOGRAM, Aggregation.base2ExponentialBucketHistogram());
207+
208+
InstrumentTypeAndAggregation(InstrumentType instrumentType, Aggregation aggregation) {
209+
this.instrumentType = instrumentType;
210+
this.aggregation = aggregation;
211+
}
212+
213+
private final InstrumentType instrumentType;
214+
private final Aggregation aggregation;
215+
}
216+
217+
private interface Instrument {
218+
void record(long value, Attributes attributes);
219+
}
220+
221+
private static Instrument getInstrument(
222+
Meter meter, InstrumentType instrumentType, InstrumentValueType instrumentValueType) {
223+
String name = "instrument";
224+
switch (instrumentType) {
225+
case COUNTER:
226+
return instrumentValueType == InstrumentValueType.DOUBLE
227+
? meter.counterBuilder(name).ofDoubles().build()::add
228+
: meter.counterBuilder(name).build()::add;
229+
case UP_DOWN_COUNTER:
230+
return instrumentValueType == InstrumentValueType.DOUBLE
231+
? meter.upDownCounterBuilder(name).ofDoubles().build()::add
232+
: meter.upDownCounterBuilder(name).build()::add;
233+
case HISTOGRAM:
234+
return instrumentValueType == InstrumentValueType.DOUBLE
235+
? meter.histogramBuilder(name).build()::record
236+
: meter.histogramBuilder(name).ofLongs().build()::record;
237+
case GAUGE:
238+
return instrumentValueType == InstrumentValueType.DOUBLE
239+
? meter.gaugeBuilder(name).build()::set
240+
: meter.gaugeBuilder(name).ofLongs().build()::set;
241+
case OBSERVABLE_COUNTER:
242+
case OBSERVABLE_UP_DOWN_COUNTER:
243+
case OBSERVABLE_GAUGE:
244+
}
245+
throw new IllegalArgumentException();
246+
}
247+
}

sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricsBenchmarks.java

Lines changed: 0 additions & 112 deletions
This file was deleted.

0 commit comments

Comments
 (0)