Skip to content

Commit 68327bb

Browse files
committed
Avoid capturing lambda allocation when retrieving existing meters
In particular focuses on Timer, LongTaskTimer, and Counter as these are the types expected to be retrieved (non-functional) the most. Avoiding this allocation makes the retrieval more performant and generates less garbage, causing less GC pressure.
1 parent 84322ca commit 68327bb

File tree

6 files changed

+96
-42
lines changed

6 files changed

+96
-42
lines changed

benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/MeterRegistrationBenchmark.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
@Warmup(iterations = 2)
3434
@Measurement(iterations = 5)
3535
@BenchmarkMode(Mode.AverageTime)
36-
@OutputTimeUnit(TimeUnit.MICROSECONDS)
36+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
3737
@Threads(2)
3838
public class MeterRegistrationBenchmark {
3939

@@ -49,7 +49,7 @@ public static void main(String[] args) throws RunnerException {
4949

5050
Meter.MeterProvider<Counter> counterMeterProvider = Counter.builder("jmh.existing").withRegistry(registry);
5151

52-
Tags tags = Tags.of("key", "value");
52+
Tags tags = Tags.of("k1", "v1");
5353

5454
@Setup
5555
public void setup() {
@@ -89,8 +89,8 @@ public Meter registerStale() {
8989
}
9090

9191
@Benchmark
92-
public Meter registerExisting() {
93-
return registry.counter("jmh.existing", "k1", "v1");
92+
public Meter registerExistingCounter() {
93+
return registry.counter("jmh.existing", tags);
9494
}
9595

9696
@Benchmark

benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/ObservationBenchmark.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ public boolean supportsContext(Observation.Context context) {
8080
};
8181
}
8282

83-
@TearDown
84-
public void tearDown() {
85-
System.out.println("Meters:");
86-
System.out.println(meterRegistry.getMetersAsString());
87-
}
83+
// @TearDown
84+
// public void tearDown() {
85+
// System.out.println("Meters:");
86+
// System.out.println(meterRegistry.getMetersAsString());
87+
// }
8888

8989
@Benchmark
9090
public void baseline() {

implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/types/DynatraceTimerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class DynatraceTimerTest {
4747

4848
private static final DistributionStatisticConfig DISTRIBUTION_STATISTIC_CONFIG = DistributionStatisticConfig.NONE;
4949

50-
private static final PauseDetector PAUSE_DETECTOR = new NoPauseDetector();
50+
private static final PauseDetector PAUSE_DETECTOR = NoPauseDetector.INSTANCE;
5151

5252
@Test
5353
void testTimerCount() {

micrometer-core/src/main/java/io/micrometer/core/instrument/MeterRegistry.java

Lines changed: 69 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import io.micrometer.core.instrument.search.RequiredSearch;
3232
import io.micrometer.core.instrument.search.Search;
3333
import io.micrometer.core.instrument.util.TimeUtils;
34+
import org.jspecify.annotations.NonNull;
35+
import org.jspecify.annotations.NullUnmarked;
3436
import org.jspecify.annotations.Nullable;
3537

3638
import java.time.Duration;
@@ -106,7 +108,7 @@ public abstract class MeterRegistry {
106108

107109
/**
108110
* write/remove guarded by meterMapLock, read in
109-
* {@link #getOrCreateMeter(DistributionStatisticConfig, BiFunction, Meter.Id, Function)}
111+
* {@link #getOrCreateMeter(DistributionStatisticConfig, PauseDetector, NewMeterSupplier, Meter.Id, Function)}
110112
* is unguarded
111113
*/
112114
private final Map<Meter.Id, Meter> preFilterIdToMeterMap = new HashMap<>();
@@ -135,7 +137,7 @@ public abstract class MeterRegistry {
135137

136138
private final AtomicBoolean closed = new AtomicBoolean();
137139

138-
private PauseDetector pauseDetector = new NoPauseDetector();
140+
private PauseDetector pauseDetector = NoPauseDetector.INSTANCE;
139141

140142
private @Nullable HighCardinalityTagsDetector highCardinalityTagsDetector;
141143

@@ -338,9 +340,12 @@ private String getBaseTimeUnitStr() {
338340
* Only used by {@link Counter#builder(String)}.
339341
* @param id The identifier for this counter.
340342
* @return A new or existing counter.
343+
* @implNote Avoids allocation in the case the meter is already registered,
344+
* particularly capturing lambdas.
341345
*/
342346
Counter counter(Meter.Id id) {
343-
return registerMeterIfNecessary(Counter.class, id, this::newCounter, NoopCounter::new);
347+
return registerMeterIfNecessary(Counter.class, id, null, null,
348+
(registry, mappedId, mappedConfig, pd) -> registry.newCounter(mappedId), NoopCounter::new);
344349
}
345350

346351
/**
@@ -352,22 +357,27 @@ Counter counter(Meter.Id id) {
352357
* @return A new or existing gauge.
353358
*/
354359
<T> Gauge gauge(Meter.Id id, @Nullable T obj, ToDoubleFunction<T> valueFunction) {
355-
return registerMeterIfNecessary(Gauge.class, id, id2 -> newGauge(id2, obj, valueFunction), NoopGauge::new);
360+
return registerMeterIfNecessary(Gauge.class, id,
361+
(registry, mappedId) -> registry.newGauge(mappedId, obj, valueFunction), NoopGauge::new);
356362
}
357363

358364
/**
359-
* Only used by {@link Timer#builder(String)}.
365+
* Only used internally.
360366
* @param id The identifier for this timer.
361367
* @param distributionStatisticConfig Configuration that governs how distribution
362368
* statistics are computed.
369+
* @param specificPauseDetector explicit pause detector to use that may be different
370+
* from the registry-configured one
363371
* @return A new or existing timer.
372+
* @implNote Avoids allocation in the case the meter is already registered,
373+
* particularly capturing lambdas.
364374
*/
365375
Timer timer(Meter.Id id, DistributionStatisticConfig distributionStatisticConfig,
366-
PauseDetector pauseDetectorOverride) {
367-
return registerMeterIfNecessary(Timer.class, id, distributionStatisticConfig, (id2, filteredConfig) -> {
368-
Meter.Id withUnit = id2.withBaseUnit(getBaseTimeUnitStr());
369-
return newTimer(withUnit, filteredConfig.merge(defaultHistogramConfig()), pauseDetectorOverride);
370-
}, NoopTimer::new);
376+
PauseDetector specificPauseDetector) {
377+
return registerMeterIfNecessary(Timer.class, id, distributionStatisticConfig, specificPauseDetector,
378+
(registry, mappedId, mappedConfig, pd) -> registry
379+
.newTimer(mappedId.withBaseUnit(registry.getBaseTimeUnitStr()), mappedConfig, pd),
380+
NoopTimer::new);
371381
}
372382

373383
/**
@@ -378,8 +388,9 @@ Timer timer(Meter.Id id, DistributionStatisticConfig distributionStatisticConfig
378388
* @return A new or existing distribution summary.
379389
*/
380390
DistributionSummary summary(Meter.Id id, DistributionStatisticConfig distributionStatisticConfig, double scale) {
381-
return registerMeterIfNecessary(DistributionSummary.class, id, distributionStatisticConfig, (id2,
382-
filteredConfig) -> newDistributionSummary(id2, filteredConfig.merge(defaultHistogramConfig()), scale),
391+
return registerMeterIfNecessary(
392+
DistributionSummary.class, id, distributionStatisticConfig, null, (registry, mappedId, filteredConfig,
393+
pd) -> registry.newDistributionSummary(mappedId, filteredConfig, scale),
383394
NoopDistributionSummary::new);
384395
}
385396

@@ -392,7 +403,8 @@ DistributionSummary summary(Meter.Id id, DistributionStatisticConfig distributio
392403
* @return The meter.
393404
*/
394405
Meter register(Meter.Id id, Meter.Type type, Iterable<Measurement> measurements) {
395-
return registerMeterIfNecessary(Meter.class, id, id2 -> newMeter(id2, type, measurements), NoopMeter::new);
406+
return registerMeterIfNecessary(Meter.class, id,
407+
(registry, mappedId) -> registry.newMeter(mappedId, type, measurements), NoopMeter::new);
396408
}
397409

398410
/**
@@ -607,14 +619,15 @@ public <T extends Collection<?>> T gaugeCollectionSize(String name, Iterable<Tag
607619
}
608620

609621
private <M extends Meter> M registerMeterIfNecessary(Class<M> meterClass, Meter.Id id,
610-
Function<Meter.Id, M> builder, Function<Meter.Id, M> noopBuilder) {
611-
return registerMeterIfNecessary(meterClass, id, null, (id2, conf) -> builder.apply(id2), noopBuilder);
622+
BiFunction<MeterRegistry, Meter.Id, M> meterSupplier, Function<Meter.Id, M> noopBuilder) {
623+
return registerMeterIfNecessary(meterClass, id, null, null,
624+
(registry, mappedId, conf, pd) -> meterSupplier.apply(registry, mappedId), noopBuilder);
612625
}
613626

614627
private <M extends Meter> M registerMeterIfNecessary(Class<M> meterClass, Meter.Id id,
615-
@Nullable DistributionStatisticConfig config, BiFunction<Meter.Id, DistributionStatisticConfig, M> builder,
616-
Function<Meter.Id, M> noopBuilder) {
617-
Meter m = getOrCreateMeter(config, builder, id, noopBuilder);
628+
@Nullable DistributionStatisticConfig config, @Nullable PauseDetector specificPauseDetector,
629+
NewMeterSupplier<M> meterSupplier, Function<Meter.Id, M> noopBuilder) {
630+
Meter m = getOrCreateMeter(config, specificPauseDetector, meterSupplier, id, noopBuilder);
618631

619632
if (!meterClass.isInstance(m)) {
620633
throw new IllegalArgumentException(
@@ -643,8 +656,29 @@ private Meter.Id mapId(Meter.Id id) {
643656
return mappedId;
644657
}
645658

659+
@FunctionalInterface
660+
// Brittle, but this is internal. We pass nulls for some meter types as explained in
661+
// JavaDoc.
662+
@NullUnmarked
663+
private interface NewMeterSupplier<M extends Meter> {
664+
665+
/**
666+
* Create a new meter with the given parameters. The DistributionStatisticConfig
667+
* and PauseDetector will be null for meter types that do not take them.
668+
* @param registry the registry from which to make the new meter
669+
* @param id the ID of the meter to create
670+
* @param distributionStatisticConfig optional distribution config for types that
671+
* take it
672+
* @param pauseDetector optional pause detector for types that take it
673+
* @return a new meter
674+
*/
675+
@NonNull M create(MeterRegistry registry, Meter.Id id, DistributionStatisticConfig distributionStatisticConfig,
676+
PauseDetector pauseDetector);
677+
678+
}
679+
646680
private Meter getOrCreateMeter(@Nullable DistributionStatisticConfig config,
647-
BiFunction<Meter.Id, /* Nullable Generic */ DistributionStatisticConfig, ? extends Meter> builder,
681+
@Nullable PauseDetector specificPauseDetector, NewMeterSupplier<? extends Meter> meterSupplier,
648682
Meter.Id originalId, Function<Meter.Id, ? extends Meter> noopBuilder) {
649683

650684
Meter m = preFilterIdToMeterMap.get(originalId);
@@ -684,9 +718,10 @@ private Meter getOrCreateMeter(@Nullable DistributionStatisticConfig config,
684718
config = filteredConfig;
685719
}
686720
}
721+
config = config.merge(defaultHistogramConfig());
687722
}
688723

689-
m = builder.apply(mappedId, config);
724+
m = meterSupplier.create(this, mappedId, config, specificPauseDetector);
690725

691726
Meter.Id synAssoc = mappedId.syntheticAssociation();
692727
if (synAssoc != null) {
@@ -1066,13 +1101,14 @@ public LongTaskTimer longTaskTimer(String name, Iterable<Tag> tags) {
10661101
* Only used by {@link LongTaskTimer#builder(String)}.
10671102
* @param id The identifier for this long task timer.
10681103
* @return A new or existing long task timer.
1104+
* @implNote Avoids allocation in the case the meter is already registered,
1105+
* particularly capturing lambdas.
10691106
*/
10701107
LongTaskTimer longTaskTimer(Meter.Id id, DistributionStatisticConfig distributionStatisticConfig) {
1071-
return registerMeterIfNecessary(LongTaskTimer.class, id, distributionStatisticConfig,
1072-
(id2, filteredConfig) -> {
1073-
Meter.Id withUnit = id2.withBaseUnit(getBaseTimeUnitStr());
1074-
return newLongTaskTimer(withUnit, filteredConfig.merge(defaultHistogramConfig()));
1075-
}, NoopLongTaskTimer::new);
1108+
return registerMeterIfNecessary(LongTaskTimer.class, id, distributionStatisticConfig, null,
1109+
(registry, mappedId, mappedConfig, pd) -> registry
1110+
.newLongTaskTimer(mappedId.withBaseUnit(registry.getBaseTimeUnitStr()), mappedConfig),
1111+
NoopLongTaskTimer::new);
10761112
}
10771113

10781114
/**
@@ -1116,7 +1152,8 @@ public <T extends Number> FunctionCounter counter(String name, Iterable<Tag> tag
11161152
*/
11171153
<T> FunctionCounter counter(Meter.Id id, T obj, ToDoubleFunction<T> countFunction) {
11181154
return registerMeterIfNecessary(FunctionCounter.class, id,
1119-
id2 -> newFunctionCounter(id2, obj, countFunction), NoopFunctionCounter::new);
1155+
(registry, mappedId) -> registry.newFunctionCounter(mappedId, obj, countFunction),
1156+
NoopFunctionCounter::new);
11201157
}
11211158

11221159
/**
@@ -1157,10 +1194,10 @@ public <T> FunctionTimer timer(String name, Iterable<Tag> tags, T obj, ToLongFun
11571194
*/
11581195
<T> FunctionTimer timer(Meter.Id id, T obj, ToLongFunction<T> countFunction,
11591196
ToDoubleFunction<T> totalTimeFunction, TimeUnit totalTimeFunctionUnit) {
1160-
return registerMeterIfNecessary(FunctionTimer.class, id, id2 -> {
1161-
Meter.Id withUnit = id2.withBaseUnit(getBaseTimeUnitStr());
1162-
return newFunctionTimer(withUnit, obj, countFunction, totalTimeFunction, totalTimeFunctionUnit);
1163-
}, NoopFunctionTimer::new);
1197+
return registerMeterIfNecessary(FunctionTimer.class, id,
1198+
(registry, mappedId) -> registry.newFunctionTimer(mappedId.withBaseUnit(getBaseTimeUnitStr()), obj,
1199+
countFunction, totalTimeFunction, totalTimeFunctionUnit),
1200+
NoopFunctionTimer::new);
11641201
}
11651202

11661203
/**
@@ -1198,7 +1235,8 @@ public <T> TimeGauge timeGauge(String name, Iterable<Tag> tags, T obj, TimeUnit
11981235
<T> TimeGauge timeGauge(Meter.Id id, @Nullable T obj, TimeUnit timeFunctionUnit,
11991236
ToDoubleFunction<T> timeFunction) {
12001237
return registerMeterIfNecessary(TimeGauge.class, id,
1201-
id2 -> newTimeGauge(id2, obj, timeFunctionUnit, timeFunction), NoopTimeGauge::new);
1238+
(registry, mappedId) -> registry.newTimeGauge(mappedId, obj, timeFunctionUnit, timeFunction),
1239+
NoopTimeGauge::new);
12021240
}
12031241

12041242
}

micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/pause/NoPauseDetector.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,22 @@
1515
*/
1616
package io.micrometer.core.instrument.distribution.pause;
1717

18+
/**
19+
* No-op implementation of a {@link PauseDetector}.
20+
*/
1821
public class NoPauseDetector implements PauseDetector {
1922

23+
/**
24+
* Singleton instance of {@link NoPauseDetector}.
25+
* @since 1.16.0
26+
*/
27+
public static final NoPauseDetector INSTANCE = new NoPauseDetector();
28+
29+
/**
30+
* @deprecated use {@link #INSTANCE} instead.
31+
*/
32+
@Deprecated
33+
public NoPauseDetector() {
34+
}
35+
2036
}

micrometer-core/src/test/java/io/micrometer/core/instrument/AbstractTimerTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ static class MyTimer extends AbstractTimer {
7575

7676
MyTimer() {
7777
super(new Meter.Id("name", Tags.empty(), null, null, Meter.Type.TIMER), Clock.SYSTEM,
78-
DistributionStatisticConfig.DEFAULT, new NoPauseDetector(), TimeUnit.SECONDS, false);
78+
DistributionStatisticConfig.DEFAULT, NoPauseDetector.INSTANCE, TimeUnit.SECONDS, false);
7979
}
8080

8181
@Override

0 commit comments

Comments
 (0)