Skip to content

Commit 6e9d64d

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

9 files changed

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