Skip to content

Commit bf27aaf

Browse files
Add CompositeScalarReplacementBenchmark
1 parent eb4257a commit bf27aaf

File tree

1 file changed

+123
-0
lines changed

1 file changed

+123
-0
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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+
17+
package io.micrometer.benchmark.core;
18+
19+
import io.micrometer.core.instrument.Counter;
20+
import io.micrometer.core.instrument.MeterRegistry;
21+
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
22+
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
23+
import org.openjdk.jmh.annotations.*;
24+
import org.openjdk.jmh.profile.GCProfiler;
25+
import org.openjdk.jmh.runner.Runner;
26+
import org.openjdk.jmh.runner.RunnerException;
27+
import org.openjdk.jmh.runner.options.OptionsBuilder;
28+
29+
import java.util.concurrent.TimeUnit;
30+
31+
/**
32+
* This benchmark simulates an issue when "Scalar Replacement" optimization (see later)
33+
* using {@link CompositeMeterRegistry} can break.
34+
* <p>
35+
* This benchmark reproduces the issue using two {@link CompositeMeterRegistry} instances:
36+
* one of them is empty, the other one is not (a {@link MeterRegistry} is added to it).
37+
* <p>
38+
* For example, the user uses a {@link CompositeMeterRegistry} that is injected into the
39+
* components of the application but a component somewhere (that they may or may not
40+
* control) uses {@code Metrics}, a static, global {@link CompositeMeterRegistry}. The
41+
* user don't care about the global registry and don't add any {@link MeterRegistry} to
42+
* it.
43+
* <p>
44+
* Another similar scenario (that this benchmark does not simulate) is when the user uses
45+
* both a {@link CompositeMeterRegistry} (that is injected into the components of the
46+
* application) and the global registry in {@code Metrics} but meter registrations and
47+
* recordings happen in the app before any {@link MeterRegistry} would be added either to the
48+
* global or the composite registry. This is an "invalid" scenario, users should configure registries
49+
* properly before they would be used but this scenario causes the same issue that the
50+
* previous one does. This is possible even with one registry, if "enough" recordings happen before any
51+
* {@link MeterRegistry} would be added to the composite/global.
52+
* <p>
53+
* The issue this benchmark reproduces is described in
54+
* <a href="https://github.com/micrometer-metrics/micrometer/issues/6811">#6811</a>:
55+
* {@code AbstractCompositeMeter} maintains a {@code Map} for its children. This map was
56+
* {@code Collections.emptyMap()} when an instance created and replaced to
57+
* {@code IdentityHashMap} as Meters were added. This means that in the scenarios above,
58+
* the Meters of the non-empty {@link CompositeMeterRegistry} use {@code IdentityHashMap}
59+
* and Meters of the empty one use {@code Collections.emptyMap()}. This apparently breaks
60+
* "Scalar Replacement" (a JIT optimization that can eliminate allocations). Before the
61+
* fix in {@code AbstractCompositeMeter} (replacing {@code Collections.emptyMap()} to an
62+
* empty {@code IdentityHashMap}) {@link #compositeBaseline()} and
63+
* {@link #emptyCompositeBaseline()} did not measure significant amount of allocations
64+
* (virtually 0) but {@link #compositeAndEmptyComposite()} did. After the fix, allocations
65+
* were eliminated.
66+
*
67+
* @see "https://github.com/micrometer-metrics/micrometer/issues/6811"
68+
*/
69+
@Fork(1)
70+
@Warmup(iterations = 1)
71+
@Measurement(iterations = 1)
72+
@BenchmarkMode(Mode.AverageTime)
73+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
74+
@State(Scope.Benchmark)
75+
public class CompositeScalarReplacementBenchmark {
76+
77+
private SimpleMeterRegistry simpleMeterRegistry;
78+
79+
private Counter compositeCounter;
80+
81+
private Counter emptyCompositeCounter;
82+
83+
@Setup
84+
public void setup() {
85+
simpleMeterRegistry = new SimpleMeterRegistry();
86+
87+
MeterRegistry composite = new CompositeMeterRegistry().add(simpleMeterRegistry);
88+
compositeCounter = composite.counter("compositeCounter");
89+
90+
MeterRegistry emptyComposite = new CompositeMeterRegistry();
91+
emptyCompositeCounter = emptyComposite.counter("emptyCompositeCounter");
92+
93+
System.out.println("\nMeters at setup:\n" + simpleMeterRegistry.getMetersAsString());
94+
}
95+
96+
@TearDown
97+
public void tearDown() {
98+
System.out.println("\nMeters at tearDown:\n" + simpleMeterRegistry.getMetersAsString());
99+
}
100+
101+
@Benchmark
102+
public void compositeBaseline() {
103+
compositeCounter.increment();
104+
}
105+
106+
@Benchmark
107+
public void emptyCompositeBaseline() {
108+
emptyCompositeCounter.increment();
109+
}
110+
111+
@Benchmark
112+
public void compositeAndEmptyComposite() {
113+
compositeCounter.increment();
114+
emptyCompositeCounter.increment();
115+
}
116+
117+
public static void main(String[] args) throws RunnerException {
118+
new Runner(new OptionsBuilder().include(CompositeScalarReplacementBenchmark.class.getSimpleName())
119+
.addProfiler(GCProfiler.class)
120+
.build()).run();
121+
}
122+
123+
}

0 commit comments

Comments
 (0)