Skip to content

Commit e7a25c4

Browse files
committed
Add benchmarks for basic prebuilt MeterFilter implementations
Signed-off-by: etki <[email protected]>
1 parent 169f38a commit e7a25c4

10 files changed

+1249
-0
lines changed

benchmarks/benchmarks-core/build.gradle

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,13 @@ jmh {
4040
duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE
4141
zip64 = true
4242
}
43+
44+
tasks.register('benchmarks', JavaExec) {
45+
doFirst {
46+
logging.captureStandardOutput(LogLevel.QUIET)
47+
logging.captureStandardError(LogLevel.QUIET)
48+
}
49+
50+
classpath = sourceSets.jmh.runtimeClasspath
51+
mainClass = 'io.micrometer.benchmark.BenchmarkSupport'
52+
}
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
/*
2+
* Copyright 2025 VMware, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.benchmark;
17+
18+
import io.micrometer.common.lang.Nullable;
19+
import org.openjdk.jmh.annotations.Mode;
20+
import org.openjdk.jmh.profile.GCProfiler;
21+
import org.openjdk.jmh.profile.LinuxPerfAsmProfiler;
22+
import org.openjdk.jmh.profile.LinuxPerfNormProfiler;
23+
import org.openjdk.jmh.runner.Runner;
24+
import org.openjdk.jmh.runner.RunnerException;
25+
import org.openjdk.jmh.runner.options.ChainedOptionsBuilder;
26+
import org.openjdk.jmh.runner.options.Options;
27+
import org.openjdk.jmh.runner.options.OptionsBuilder;
28+
import org.openjdk.jmh.runner.options.TimeValue;
29+
30+
import java.util.Arrays;
31+
import java.util.BitSet;
32+
import java.util.Random;
33+
import java.util.concurrent.TimeUnit;
34+
35+
public class BenchmarkSupport {
36+
37+
// Making things deterministic
38+
private static final Random RANDOM = new Random(0x5EAL);
39+
40+
// A generic number of test instances to use. Making sure it's small
41+
// enough for L2 even for moderately sized objects, yet big enough
42+
// to confuse JIT.
43+
public static final int DEFAULT_POOL_SIZE = 1 << 12;
44+
45+
// A bitmask that would prevent an incremented number escaping
46+
// DEFAULT_POOL_SIZE by a simple AND operation.
47+
public static final int DEFAULT_MASK = DEFAULT_POOL_SIZE - 1;
48+
49+
// To prevent any kind of JIT assumption that a specific instance
50+
// corresponds to a specific sample (shouldn't happen, but anyway),
51+
// we need to walk them at a different rate. 2 is a bad choice,
52+
// because it will discard half of the samples, but for every
53+
// n = 2^k the value of 3 will be a divisor of either n + 1 or
54+
// 2n + 1.
55+
public static final int SAMPLE_STEP = 3;
56+
57+
private BenchmarkSupport() {
58+
}
59+
60+
/**
61+
* When you need to break existing patterns in an array.
62+
*/
63+
public static <T> T[] shuffle(T[] values) {
64+
for (int i = 0; i < values.length; i++) {
65+
int target = RANDOM.nextInt(values.length);
66+
T buffer = values[target];
67+
values[target] = values[i];
68+
values[i] = buffer;
69+
}
70+
71+
return values;
72+
}
73+
74+
/**
75+
* Create a bit mask with [count] of [options] bits set. A simple method to select a
76+
* combination of unique items using each bit as the presence mark for every item.
77+
* @param count Number of bits to be set.
78+
* @param options Number of all possible options.
79+
*/
80+
public static BitSet selection(int count, int options) {
81+
if (count > options) {
82+
throw new IllegalArgumentException(
83+
"Requested count " + count + " is bigger than list of available options (" + options + ")");
84+
}
85+
86+
if (count < 0) {
87+
throw new IllegalArgumentException(
88+
"Number of selected options must be non-negative, " + count + " provided");
89+
}
90+
91+
BitSet reply = new BitSet(options);
92+
93+
if (count == options) {
94+
for (int i = 0; i < count; i++) {
95+
reply.set(i);
96+
}
97+
return reply;
98+
}
99+
100+
for (int flag = 0; flag < count; flag++) {
101+
// with [remaining] positions left, we select a _disabled_
102+
// bit N to install, meaning we have to pretend that all
103+
// _enabled_ bits don't exist at all.
104+
105+
int remaining = options - flag;
106+
int next = RANDOM.nextInt(remaining);
107+
int skipped = 0;
108+
109+
for (int bit = 0; bit < options; bit++) {
110+
if (reply.get(bit)) {
111+
skipped++;
112+
continue;
113+
}
114+
115+
if (bit == skipped + next) {
116+
reply.set(bit);
117+
break;
118+
}
119+
}
120+
121+
// bit can be as much as options - 1 in the previous loop, thus >=
122+
if (skipped + next >= options) {
123+
String message = String.format(
124+
"Failed to set the bit: while looking to set option #%d out of total %d, disabled bit #%d should have been enabled, however, %d bits were skipped in %s",
125+
flag, options, next, skipped, reply);
126+
throw new IllegalStateException(message);
127+
}
128+
}
129+
130+
return reply;
131+
}
132+
133+
public static BitSet selection(int minimum, int maximum, int options) {
134+
return selection(RANDOM.nextInt(minimum, maximum + 1), options);
135+
}
136+
137+
public static BitSet selection(int options) {
138+
return selection(RANDOM.nextInt(options + 1), options);
139+
}
140+
141+
public static class ModeUniformDistribution {
142+
143+
private ModeUniformDistribution() {
144+
}
145+
146+
/**
147+
* <p>
148+
* A silly mock distribution to test the reaction of the code to different input
149+
* sizes. With the specified probability, the [mode] value will be returned,
150+
* otherwise a value between [minimum] and [maximum] (inclusively, but without the
151+
* mode) will be selected uniformly.
152+
* </p>
153+
*
154+
* <p>
155+
* The reason for not taking any nonsilly distribution is the generic case modeled
156+
* here, a case with a distinct mode, but without the probability of other values
157+
* falling off sharply with the distance from the mode. This allows both to see
158+
* how code reacts to the position of that mode and to confuse the JIT regarding
159+
* any assumptions (beyond mode value) and machinery like predictors. Using just a
160+
* bell-like distribution here would either shrink the mode or reduce output to
161+
* 3-5 values that would totally dominate everything else, so it requires a tight
162+
* bell mixed with a uniform distribution to blend in all possibilities. This is
163+
* exactly what this method does, with replacing the distribution by explicit
164+
* boost for a specific value to keep things less error-prone (no one ever will
165+
* plot these values or look at them in the debugger again).
166+
* </p>
167+
* @param minimum The minimum amount that can be selected, inclusive.
168+
* @param maximum The maximum amount that can be selected, inclusive.
169+
* @param mode The value that appears with increased probability.
170+
* @param probability The probability of selecting the value instead of using
171+
* uniform distribution.
172+
* @return Value that differs from uniform distribution by a more often selected
173+
* mode, according to passed probability.
174+
*/
175+
public static int sample(int minimum, int maximum, int mode, double probability) {
176+
if (mode < minimum || mode > maximum) {
177+
throw new IllegalArgumentException(
178+
"Provided mode " + mode + " isn't in the min/max interval [" + minimum + ", " + maximum + "]");
179+
}
180+
181+
if (RANDOM.nextDouble() < probability) {
182+
return mode;
183+
}
184+
185+
// excluding mode here, so upper bound is maximum, not maximum + 1
186+
int selection = RANDOM.nextInt(minimum, maximum);
187+
188+
// compensating for absent mode
189+
if (selection >= mode) {
190+
return selection + 1;
191+
}
192+
193+
return selection;
194+
}
195+
196+
}
197+
198+
public static ChainedOptionsBuilder defaults() {
199+
ChainedOptionsBuilder options = new OptionsBuilder().forks(1)
200+
// 0.5m warmups
201+
.warmupIterations(6)
202+
.warmupTime(new TimeValue(5L, TimeUnit.SECONDS))
203+
// 9.5m benchmarks
204+
.measurementIterations(57)
205+
.measurementTime(new TimeValue(5L, TimeUnit.SECONDS))
206+
.mode(Mode.AverageTime)
207+
.timeUnit(TimeUnit.NANOSECONDS)
208+
.shouldDoGC(true)
209+
.addProfiler(GCProfiler.class);
210+
211+
// Please forgive me
212+
if (System.getProperty("os.name", "_fallback_").toLowerCase().contains("linux")) {
213+
options.addProfiler(LinuxPerfAsmProfiler.class).addProfiler(LinuxPerfNormProfiler.class);
214+
}
215+
216+
return options;
217+
}
218+
219+
/**
220+
* Runs benchmarks in specified classes with defaults (10s iterations, 0.5m warmup,
221+
* 9.5m benchmark, GC & perf profilers). As usual, be aware that defaults might not
222+
* suit your case, use overrides when necessary.
223+
*/
224+
public static void run(String[] patterns, @Nullable ChainedOptionsBuilder override) throws RunnerException {
225+
ChainedOptionsBuilder options = defaults();
226+
227+
for (String pattern : patterns) {
228+
options.include(pattern);
229+
}
230+
231+
Options defaults = options.build();
232+
233+
new Runner(override == null ? defaults : override.parent(defaults).build()).run();
234+
}
235+
236+
/**
237+
* @see #run(String[], ChainedOptionsBuilder)
238+
*/
239+
public static void run(String[] patterns) throws RunnerException {
240+
run(patterns, null);
241+
}
242+
243+
/**
244+
* @see #run(String[], ChainedOptionsBuilder)
245+
*/
246+
public static void run(Class<?>[] sources, @Nullable ChainedOptionsBuilder override) throws RunnerException {
247+
String[] patterns = Arrays.stream(sources).map(Class::getCanonicalName).toArray(String[]::new);
248+
249+
run(patterns, override);
250+
}
251+
252+
/**
253+
* @see #run(String[], ChainedOptionsBuilder)
254+
*/
255+
public static void run(Class<?> source, @Nullable ChainedOptionsBuilder override) throws RunnerException {
256+
run(new Class<?>[] { source }, override);
257+
}
258+
259+
/**
260+
* @see #run(String[], ChainedOptionsBuilder)
261+
*/
262+
public static void run(Class<?>... sources) throws RunnerException {
263+
run(sources, null);
264+
}
265+
266+
/**
267+
* @see #run(String[], ChainedOptionsBuilder)
268+
*/
269+
public static void run(Class<?> source) throws RunnerException {
270+
run(source, null);
271+
}
272+
273+
public static void main(String[] includes) throws RunnerException {
274+
ChainedOptionsBuilder builder = defaults()
275+
// Only 2m benchmarks, we're launching everything at once here
276+
.measurementIterations(24)
277+
.measurementTime(new TimeValue(5L, TimeUnit.SECONDS));
278+
279+
for (String pattern : includes) {
280+
builder.include(pattern);
281+
}
282+
283+
Options options = builder.build();
284+
285+
Runner runner = new Runner(options);
286+
287+
if (includes.length == 0) {
288+
System.out.println("Specify benchmark patterns as CLI arguments");
289+
runner.list();
290+
}
291+
else {
292+
runner.run();
293+
}
294+
}
295+
296+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2025 VMware, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.benchmark.core.instrument;
17+
18+
import io.micrometer.benchmark.BenchmarkSupport;
19+
import io.micrometer.benchmark.core.instrument.config.filter.FilterBenchmarkSupport;
20+
import io.micrometer.core.instrument.Meter;
21+
import io.micrometer.core.instrument.Tag;
22+
import org.openjdk.jmh.annotations.*;
23+
import org.openjdk.jmh.runner.RunnerException;
24+
25+
import java.util.List;
26+
import java.util.concurrent.TimeUnit;
27+
28+
public class MeterBenchmarks {
29+
30+
private MeterBenchmarks() {
31+
}
32+
33+
@Fork(value = 1)
34+
@Warmup(iterations = 6, time = 10, timeUnit = TimeUnit.SECONDS)
35+
@Measurement(iterations = 54, time = 10, timeUnit = TimeUnit.SECONDS)
36+
@BenchmarkMode(Mode.AverageTime)
37+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
38+
@State(Scope.Benchmark)
39+
public static class GetTags {
40+
41+
private static final int COUNT = BenchmarkSupport.DEFAULT_POOL_SIZE;
42+
43+
private static final int MASK = BenchmarkSupport.DEFAULT_MASK;
44+
45+
@Param({ "0", "1", "2", "4", "8", "16", "32", "64" })
46+
public int mode;
47+
48+
private Meter.Id[] identifiers;
49+
50+
private int iteration;
51+
52+
@Setup
53+
public void setUp() {
54+
identifiers = FilterBenchmarkSupport.distributed(mode).limit(COUNT).toArray(Meter.Id[]::new);
55+
}
56+
57+
@Benchmark
58+
public List<Tag> baseline() {
59+
return identifiers[iteration++ & MASK].getTags();
60+
}
61+
62+
public static void main(String[] args) throws RunnerException {
63+
BenchmarkSupport.run(GetTags.class);
64+
}
65+
66+
}
67+
68+
public static void main(String[] args) throws RunnerException {
69+
BenchmarkSupport.run(MeterBenchmarks.class);
70+
}
71+
72+
}

0 commit comments

Comments
 (0)