From c62dc62f4f6a9fac9e06ca6c691182174c6642f8 Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Wed, 21 Jan 2026 16:30:39 -0600 Subject: [PATCH 1/3] Rework MetricRecordBenchmark --- .../sdk/metrics/MetricRecordBenchmark.java | 243 ++++++++++++++++++ .../sdk/metrics/MetricsBenchmarks.java | 112 -------- .../io/opentelemetry/sdk/metrics/TestSdk.java | 91 ------- 3 files changed, 243 insertions(+), 203 deletions(-) create mode 100644 sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricRecordBenchmark.java delete mode 100644 sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricsBenchmarks.java delete mode 100644 sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/TestSdk.java diff --git a/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricRecordBenchmark.java b/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricRecordBenchmark.java new file mode 100644 index 00000000000..54232d4a260 --- /dev/null +++ b/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricRecordBenchmark.java @@ -0,0 +1,243 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.sdk.metrics.InstrumentType.COUNTER; +import static io.opentelemetry.sdk.metrics.InstrumentType.GAUGE; +import static io.opentelemetry.sdk.metrics.InstrumentType.HISTOGRAM; +import static io.opentelemetry.sdk.metrics.InstrumentType.UP_DOWN_COUNTER; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.common.export.MemoryMode; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Group; +import org.openjdk.jmh.annotations.GroupThreads; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Notes on interpreting the data: + * + *

The benchmark has two dimensions which partially overlap: cardinality and thread count. + * Cardinality dictates how many unique attribute sets (i.e. series) are recorded to, and thread + * count dictates how many threads are simultaneously recording to those series. In all cases, the + * record path needs to look up an aggregation handle for the series corresponding to the + * measurement's {@link Attributes} in a {@link java.util.concurrent.ConcurrentHashMap}. That will + * be the case until otel adds support for bound + * instruments. The cardinality dictates the size of this map, which has some impact on + * performance. However, by far the dominant bottleneck is contention. That is, how many threads are + * simultaneously trying to record to the same series? Increasing the threads increases contention. + * Increasing cardinality decreases contention, as the threads are now spreading their record + * activities over more distinct series. The highest contention scenario is cardinality=1, + * threads=4. Any scenario with threads=1 has zero contention. + * + *

It's useful to characterize the performance of the metrics system under contention, as some + * high-performance applications may have many threads trying to record to the same series. It's + * also useful to characterize the performance of the metrics system under low contention, as some + * high-performance applications may not frequently be trying to concurrently record to the same + * series yet still care about the overhead of each record operation. + * + *

{@link AggregationTemporality} can impact performance because additional concurrency controls + * are needed to ensure there are no duplicate, partial, or lost writes while resetting the set of + * timeseries each collection. + */ +public class MetricRecordBenchmark { + + private static final int recordCount = 10 * 1024; + + @State(Scope.Benchmark) + public static class ThreadState { + + @Param InstrumentTypeAndAggregation instrumentTypeAndAggregation; + + @Param AggregationTemporality aggregationTemporality; + + @Param({"1", "100"}) + int cardinality; + + // The following parameters are excluded from the benchmark to reduce combinatorial explosion + // but can optionally be enabled for adhoc evaluation. + + // InstrumentValueType doesn't materially impact performance. Uncomment to evaluate. + // @Param + // InstrumentValueType instrumentValueType; + InstrumentValueType instrumentValueType = InstrumentValueType.LONG; + + // MemoryMode almost exclusively impacts collect from a performance standpoint. Uncomment to + // evaluate. + // @Param + // MemoryMode memoryMode; + MemoryMode memoryMode = MemoryMode.REUSABLE_DATA; + + // Exemplars can impact performance, but we skip evaluation to limit test cases. Uncomment to + // evaluate. + // @Param({"true", "false"}) + // boolean exemplars; + boolean exemplars = false; + + OpenTelemetrySdk openTelemetry; + Instrument instrument; + List measurements; + List attributesList; + MetricsTestOperationBuilder.Operation op; + Span span; + io.opentelemetry.context.Scope contextScope; + + @Setup + @SuppressWarnings("MustBeClosedChecker") + public void setup() { + InstrumentType instrumentType = instrumentTypeAndAggregation.instrumentType; + Aggregation aggregation = instrumentTypeAndAggregation.aggregation; + + openTelemetry = + OpenTelemetrySdk.builder() + .setTracerProvider(SdkTracerProvider.builder().setSampler(Sampler.alwaysOn()).build()) + .setMeterProvider( + SdkMeterProvider.builder() + .registerMetricReader( + InMemoryMetricReader.builder() + .setAggregationTemporalitySelector(unused -> aggregationTemporality) + .setDefaultAggregationSelector( + DefaultAggregationSelector.getDefault() + .with(instrumentType, aggregation)) + .setMemoryMode(memoryMode) + .build()) + .setExemplarFilter( + exemplars ? ExemplarFilter.traceBased() : ExemplarFilter.alwaysOff()) + .build()) + .build(); + + Meter meter = openTelemetry.getMeter("benchmark"); + instrument = getInstrument(meter, instrumentType, instrumentValueType); + Tracer tracer = openTelemetry.getTracer("benchmark"); + span = tracer.spanBuilder("benchmark").startSpan(); + // We suppress warnings on closing here, as we rely on tests to make sure context is closed. + contextScope = span.makeCurrent(); + + Random random = new Random(); + attributesList = new ArrayList<>(cardinality); + AttributeKey key = AttributeKey.stringKey("key"); + String last = "aaaaaaaaaaaaaaaaaaaaaaaaaa"; + for (int i = 0; i < cardinality; i++) { + char[] chars = last.toCharArray(); + chars[random.nextInt(last.length())] = (char) (random.nextInt(26) + 'a'); + last = new String(chars); + attributesList.add(Attributes.of(key, last)); + } + Collections.shuffle(attributesList); + + measurements = new ArrayList<>(recordCount); + for (int i = 0; i < recordCount; i++) { + measurements.add((long) random.nextInt(2000)); + } + Collections.shuffle(measurements); + } + + @TearDown + public void tearDown() { + contextScope.close(); + span.end(); + openTelemetry.shutdown(); + } + } + + @Benchmark + @Group("threads1") + @GroupThreads(1) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 5, time = 1) + public void record_1Thread(ThreadState threadState) { + record(threadState); + } + + @Benchmark + @Group("threads4") + @GroupThreads(4) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 5, time = 1) + public void record_4Threads(ThreadState threadState) { + record(threadState); + } + + private static void record(ThreadState threadState) { + for (int i = 0; i < recordCount; i++) { + Attributes attributes = threadState.attributesList.get(i % threadState.attributesList.size()); + long value = threadState.measurements.get(i % threadState.measurements.size()); + threadState.instrument.record(value, attributes); + } + } + + @SuppressWarnings("ImmutableEnumChecker") + public enum InstrumentTypeAndAggregation { + COUNTER_SUM(COUNTER, Aggregation.sum()), + UP_DOWN_COUNTER_SUM(UP_DOWN_COUNTER, Aggregation.sum()), + GAUGE_LAST_VALUE(GAUGE, Aggregation.lastValue()), + HISTOGRAM_EXPLICIT(HISTOGRAM, Aggregation.explicitBucketHistogram()), + HISTOGRAM_BASE2_EXPONENTIAL(HISTOGRAM, Aggregation.base2ExponentialBucketHistogram()); + + InstrumentTypeAndAggregation(InstrumentType instrumentType, Aggregation aggregation) { + this.instrumentType = instrumentType; + this.aggregation = aggregation; + } + + private final InstrumentType instrumentType; + private final Aggregation aggregation; + } + + private interface Instrument { + void record(long value, Attributes attributes); + } + + private static Instrument getInstrument( + Meter meter, InstrumentType instrumentType, InstrumentValueType instrumentValueType) { + String name = "instrument"; + switch (instrumentType) { + case COUNTER: + return instrumentValueType == InstrumentValueType.DOUBLE + ? meter.counterBuilder(name).ofDoubles().build()::add + : meter.counterBuilder(name).build()::add; + case UP_DOWN_COUNTER: + return instrumentValueType == InstrumentValueType.DOUBLE + ? meter.upDownCounterBuilder(name).ofDoubles().build()::add + : meter.upDownCounterBuilder(name).build()::add; + case HISTOGRAM: + return instrumentValueType == InstrumentValueType.DOUBLE + ? meter.histogramBuilder(name).build()::record + : meter.histogramBuilder(name).ofLongs().build()::record; + case GAUGE: + return instrumentValueType == InstrumentValueType.DOUBLE + ? meter.gaugeBuilder(name).build()::set + : meter.gaugeBuilder(name).ofLongs().build()::set; + case OBSERVABLE_COUNTER: + case OBSERVABLE_UP_DOWN_COUNTER: + case OBSERVABLE_GAUGE: + } + throw new IllegalArgumentException(); + } +} diff --git a/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricsBenchmarks.java b/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricsBenchmarks.java deleted file mode 100644 index c181f5dd1ba..00000000000 --- a/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricsBenchmarks.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.metrics; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.api.metrics.Meter; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.Tracer; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Param; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.TearDown; -import org.openjdk.jmh.annotations.Threads; -import org.openjdk.jmh.annotations.Warmup; -import org.openjdk.jmh.infra.ThreadParams; - -@BenchmarkMode({Mode.AverageTime}) -@OutputTimeUnit(TimeUnit.NANOSECONDS) -@Warmup(iterations = 5, time = 1) -@Measurement(iterations = 10, time = 1) -@Fork(1) -public class MetricsBenchmarks { - - private static final List ATTRIBUTES_LIST; - - static { - int keys = 5; - int valuesPerKey = 20; - - ATTRIBUTES_LIST = new ArrayList<>(); - for (int key = 0; key < keys; key++) { - AttributesBuilder builder = Attributes.builder(); - for (int value = 0; value < valuesPerKey; value++) { - builder.put("key_" + key, "value_" + value); - } - ATTRIBUTES_LIST.add(builder.build()); - } - } - - @State(Scope.Benchmark) - public static class ThreadState { - - @Param TestSdk sdk; - - @Param MetricsTestOperationBuilder opBuilder; - - MetricsTestOperationBuilder.Operation op; - Span span; - io.opentelemetry.context.Scope contextScope; - final Attributes sharedLabelSet = Attributes.builder().put("KEY", "VALUE").build(); - Attributes threadUniqueLabelSet; - - @Setup - @SuppressWarnings("MustBeClosedChecker") - public void setup(ThreadParams threadParams) { - Meter meter = sdk.getMeter(); - Tracer tracer = sdk.getTracer(); - span = tracer.spanBuilder("benchmark").startSpan(); - // We suppress warnings on closing here, as we rely on tests to make sure context is closed. - contextScope = span.makeCurrent(); - op = opBuilder.build(meter); - threadUniqueLabelSet = - Attributes.builder().put("KEY", String.valueOf(threadParams.getThreadIndex())).build(); - } - - @TearDown - public void tearDown() { - contextScope.close(); - span.end(); - } - } - - @Benchmark - @Threads(1) - public void recordToMultipleAttributes(ThreadState threadState) { - for (Attributes attributes : ATTRIBUTES_LIST) { - threadState.op.perform(attributes); - } - } - - @Benchmark - @Threads(1) - public void oneThread(ThreadState threadState) { - threadState.op.perform(threadState.sharedLabelSet); - } - - @Benchmark - @Threads(8) - public void eightThreadsCommonLabelSet(ThreadState threadState) { - threadState.op.perform(threadState.sharedLabelSet); - } - - @Benchmark - @Threads(8) - public void eightThreadsSeparateLabelSets(ThreadState threadState) { - threadState.op.perform(threadState.threadUniqueLabelSet); - } -} diff --git a/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/TestSdk.java b/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/TestSdk.java deleted file mode 100644 index 64bcc03ad6b..00000000000 --- a/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/TestSdk.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.metrics; - -import io.opentelemetry.api.metrics.Meter; -import io.opentelemetry.api.metrics.MeterProvider; -import io.opentelemetry.api.trace.Tracer; -import io.opentelemetry.sdk.common.Clock; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.sdk.trace.samplers.Sampler; - -@SuppressWarnings("ImmutableEnumChecker") -public enum TestSdk { - API_ONLY( - new SdkBuilder() { - @Override - Meter build() { - return MeterProvider.noop().get("io.opentelemetry.sdk.metrics"); - } - }), - SDK_NO_EXEMPLARS( - new SdkBuilder() { - @Override - Meter build() { - return SdkMeterProvider.builder() - .setClock(Clock.getDefault()) - .setResource(Resource.empty()) - // Must register reader for real SDK. - .registerMetricReader(InMemoryMetricReader.create()) - .setExemplarFilter(ExemplarFilter.alwaysOff()) - .build() - .get("io.opentelemetry.sdk.metrics"); - } - }), - SDK_CUMULATIVE( - new SdkBuilder() { - @Override - Meter build() { - return SdkMeterProvider.builder() - .setClock(Clock.getDefault()) - .setResource(Resource.empty()) - // Must register reader for real SDK. - .registerMetricReader(InMemoryMetricReader.create()) - .build() - .get("io.opentelemetry.sdk.metrics"); - } - }), - SDK_DELTA( - new SdkBuilder() { - @Override - Meter build() { - return SdkMeterProvider.builder() - .setClock(Clock.getDefault()) - .setResource(Resource.empty()) - // Must register reader for real SDK. - .registerMetricReader(InMemoryMetricReader.createDelta()) - .build() - .get("io.opentelemetry.sdk.metrics"); - } - }); - - private final SdkBuilder sdkBuilder; - - TestSdk(SdkBuilder sdkBuilder) { - this.sdkBuilder = sdkBuilder; - } - - public Meter getMeter() { - return sdkBuilder.build(); - } - - public Tracer getTracer() { - return SdkBuilder.buildTracer(); - } - - private abstract static class SdkBuilder { - abstract Meter build(); - - private static Tracer buildTracer() { - return SdkTracerProvider.builder() - .setSampler(Sampler.alwaysOn()) - .build() - .get("io.opentelemetry.sdk.metrics"); - } - } -} From 1b4ce35f0302ea1b43974682537961905593c312 Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Wed, 21 Jan 2026 16:50:03 -0600 Subject: [PATCH 2/3] Publish MetricRecordBenchmark --- .github/workflows/benchmark.yml | 11 ++- sdk/all/build.gradle.kts | 2 + .../sdk}/MetricRecordBenchmark.java | 17 ++-- .../metrics/MetricsTestOperationBuilder.java | 89 ------------------- 4 files changed, 20 insertions(+), 99 deletions(-) rename sdk/{metrics/src/jmh/java/io/opentelemetry/sdk/metrics => all/src/jmh/java/io/opentelemetry/sdk}/MetricRecordBenchmark.java (93%) delete mode 100644 sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricsTestOperationBuilder.java diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 9c08c4c2f74..6d4c21b4a64 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -42,10 +42,15 @@ jobs: env: DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + # TODO (jack-berg): Select or build appropriate benchmarks for other key areas: + # - Log SDK record & export + # - Trace SDK record & export + # - Metric SDK export + # - Noop implementation - name: Run Benchmark run: | - cd sdk/trace/build - java -jar libs/opentelemetry-sdk-trace-*-jmh.jar -rf json SpanBenchmark SpanPipelineBenchmark ExporterBenchmark + cd sdk/all/build + java -jar libs/opentelemetry-sdk-*-jmh.jar -rf json MetricRecordBenchmark - name: Use CLA approved github bot run: .github/scripts/use-cla-approved-bot.sh @@ -54,7 +59,7 @@ jobs: uses: benchmark-action/github-action-benchmark@4bdcce38c94cec68da58d012ac24b7b1155efe8b # v1.20.7 with: tool: 'jmh' - output-file-path: sdk/trace/build/jmh-result.json + output-file-path: sdk/all/build/jmh-result.json gh-pages-branch: benchmarks github-token: ${{ secrets.GITHUB_TOKEN }} benchmark-data-dir-path: "benchmarks" diff --git a/sdk/all/build.gradle.kts b/sdk/all/build.gradle.kts index 88d18b78ac7..b3852139ade 100644 --- a/sdk/all/build.gradle.kts +++ b/sdk/all/build.gradle.kts @@ -22,4 +22,6 @@ dependencies { testAnnotationProcessor("com.google.auto.value:auto-value") testImplementation(project(":sdk:testing")) + + jmh(project(":sdk:testing")) } diff --git a/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricRecordBenchmark.java b/sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java similarity index 93% rename from sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricRecordBenchmark.java rename to sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java index 54232d4a260..ac022707cc2 100644 --- a/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricRecordBenchmark.java +++ b/sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.sdk.metrics; +package io.opentelemetry.sdk; import static io.opentelemetry.sdk.metrics.InstrumentType.COUNTER; import static io.opentelemetry.sdk.metrics.InstrumentType.GAUGE; @@ -15,8 +15,12 @@ import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; -import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.common.export.MemoryMode; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.ExemplarFilter; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.InstrumentValueType; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; @@ -49,10 +53,10 @@ * be the case until otel adds support for bound * instruments. The cardinality dictates the size of this map, which has some impact on - * performance. However, by far the dominant bottleneck is contention. That is, how many threads are - * simultaneously trying to record to the same series? Increasing the threads increases contention. - * Increasing cardinality decreases contention, as the threads are now spreading their record - * activities over more distinct series. The highest contention scenario is cardinality=1, + * performance. However, by far the dominant bottleneck is contention. That is, the number of + * threads simultaneously trying to record to the same series. Increasing the threads increases + * contention. Increasing cardinality decreases contention, as the threads are now spreading their + * record activities over more distinct series. The highest contention scenario is cardinality=1, * threads=4. Any scenario with threads=1 has zero contention. * *

It's useful to characterize the performance of the metrics system under contention, as some @@ -103,7 +107,6 @@ public static class ThreadState { Instrument instrument; List measurements; List attributesList; - MetricsTestOperationBuilder.Operation op; Span span; io.opentelemetry.context.Scope contextScope; diff --git a/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricsTestOperationBuilder.java b/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricsTestOperationBuilder.java deleted file mode 100644 index a810048290b..00000000000 --- a/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricsTestOperationBuilder.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.metrics; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.metrics.DoubleCounter; -import io.opentelemetry.api.metrics.DoubleHistogram; -import io.opentelemetry.api.metrics.LongCounter; -import io.opentelemetry.api.metrics.LongHistogram; -import io.opentelemetry.api.metrics.Meter; -import java.util.concurrent.ThreadLocalRandom; - -/** - * This enum allows for iteration over all of the operations that we want to benchmark. To ensure - * that the enum cannot change state, each enum holds a builder function- passing a meter in will - * return a wrapper for both bound and unbound versions of that operation which can then be used in - * a benchmark. - */ -@SuppressWarnings("ImmutableEnumChecker") -public enum MetricsTestOperationBuilder { - LongCounterAdd( - meter -> { - return new Operation() { - final LongCounter metric = meter.counterBuilder("long_counter").build(); - - @Override - public void perform(Attributes labels) { - metric.add(5L, labels); - } - }; - }), - DoubleCounterAdd( - meter -> { - return new Operation() { - final DoubleCounter metric = meter.counterBuilder("double_counter").ofDoubles().build(); - - @Override - public void perform(Attributes labels) { - metric.add(5.0d, labels); - } - }; - }), - DoubleHistogramRecord( - meter -> { - return new Operation() { - final DoubleHistogram metric = - meter.histogramBuilder("double_histogram_recorder").build(); - - @Override - public void perform(Attributes labels) { - // We record different values to try to hit more areas of the histogram buckets. - metric.record(ThreadLocalRandom.current().nextDouble(0, 20_000d), labels); - } - }; - }), - LongHistogramRecord( - meter -> { - return new Operation() { - final LongHistogram metric = - meter.histogramBuilder("long_value_recorder").ofLongs().build(); - - @Override - public void perform(Attributes labels) { - metric.record(ThreadLocalRandom.current().nextLong(0, 20_000L), labels); - } - }; - }); - - private final OperationBuilder builder; - - MetricsTestOperationBuilder(OperationBuilder builder) { - this.builder = builder; - } - - public Operation build(Meter meter) { - return this.builder.build(meter); - } - - private interface OperationBuilder { - Operation build(Meter meter); - } - - interface Operation { - void perform(Attributes labels); - } -} From 77262b268213b69c7a8abd102384832f31449a84 Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Mon, 26 Jan 2026 14:00:29 -0600 Subject: [PATCH 3/3] feedback --- .../io/opentelemetry/sdk/MetricRecordBenchmark.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java b/sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java index ac022707cc2..d51c8171ea3 100644 --- a/sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java +++ b/sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java @@ -71,7 +71,8 @@ */ public class MetricRecordBenchmark { - private static final int recordCount = 10 * 1024; + private static final int INITIAL_SEED = 513423236; + private static final int RECORD_COUNT = 10 * 1024; @State(Scope.Benchmark) public static class ThreadState { @@ -141,7 +142,7 @@ public void setup() { // We suppress warnings on closing here, as we rely on tests to make sure context is closed. contextScope = span.makeCurrent(); - Random random = new Random(); + Random random = new Random(INITIAL_SEED); attributesList = new ArrayList<>(cardinality); AttributeKey key = AttributeKey.stringKey("key"); String last = "aaaaaaaaaaaaaaaaaaaaaaaaaa"; @@ -153,8 +154,8 @@ public void setup() { } Collections.shuffle(attributesList); - measurements = new ArrayList<>(recordCount); - for (int i = 0; i < recordCount; i++) { + measurements = new ArrayList<>(RECORD_COUNT); + for (int i = 0; i < RECORD_COUNT; i++) { measurements.add((long) random.nextInt(2000)); } Collections.shuffle(measurements); @@ -189,7 +190,7 @@ public void record_4Threads(ThreadState threadState) { } private static void record(ThreadState threadState) { - for (int i = 0; i < recordCount; i++) { + for (int i = 0; i < RECORD_COUNT; i++) { Attributes attributes = threadState.attributesList.get(i % threadState.attributesList.size()); long value = threadState.measurements.get(i % threadState.measurements.size()); threadState.instrument.record(value, attributes);