From fc03dc13e7bb658ca78e7699fdb7fbcb5160b2a5 Mon Sep 17 00:00:00 2001 From: Jens Wilke Date: Mon, 21 Jul 2025 16:22:20 +0700 Subject: [PATCH 1/3] Improve CounterDataPoint.inc by introducing a specialized class, Counter.DataPointIgnoringExemplars, which can be used. Also makes Counter.DataPoint visible and reduces foodprint by making it static. --- .../metrics/core/metrics/Counter.java | 44 +++++++++++++++++-- .../metrics/core/metrics/CounterTest.java | 17 ++++++- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java index 8ed9b0001..48d033000 100644 --- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java +++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java @@ -101,7 +101,7 @@ protected DataPoint newDataPoint() { if (isExemplarsEnabled()) { return new DataPoint(new ExemplarSampler(exemplarSamplerConfig)); } else { - return new DataPoint(null); + return new DataPointIgnoringExemplars(); } } @@ -112,7 +112,7 @@ static String stripTotalSuffix(String name) { return name; } - class DataPoint implements CounterDataPoint { + public static class DataPoint implements CounterDataPoint { private final DoubleAdder doubleValue = new DoubleAdder(); // LongAdder is 20% faster than DoubleAdder. So let's use the LongAdder for long observations, @@ -168,7 +168,11 @@ public void incWithExemplar(double amount, Labels labels) { } } - private void validateAndAdd(long amount) { + protected boolean isExemplarsEnabled() { + return exemplarSampler != null; + } + + protected void validateAndAdd(long amount) { if (amount < 0) { throw new IllegalArgumentException( "Negative increment " + amount + " is illegal for Counter metrics."); @@ -202,6 +206,40 @@ private CounterSnapshot.CounterDataPointSnapshot collect(Labels labels) { } } + /** + * Specialized data point, which is used in case no exemplar support is enabled. + * Applications can cast to this data time to speed up counter increment operations. + */ + public static final class DataPointIgnoringExemplars extends DataPoint { + + private DataPointIgnoringExemplars() { + super(null); + } + + /** + * This is one of the most used metric. Override for speed. Direct shortcut to + * the {@code LongAdder.add} method with just only validating the argument. This + * override is actually not really needed because the override of {@link #isExemplarsEnabled()} + * and inlining has the same effect in the end, however, for everyone inspecting + * the code it makes it obvious that the increment a long counter code path is as short + * as possible. + */ + @Override + public void inc(long amount) { + validateAndAdd(amount); + } + + /** + * Override for speed. Since final, the JVM will inline code for this + * method and all Exemplar code will be left out. + */ + @Override + protected boolean isExemplarsEnabled() { + return false; + } + + } + public static Builder builder() { return new Builder(PrometheusProperties.get()); } diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java index b5ca3be15..b788766e4 100644 --- a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java +++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java @@ -319,8 +319,6 @@ void incWithExemplar2() { public void testExemplarSamplerDisabled() { Counter counter = Counter.builder() - // .withExemplarSampler((inc, prev) -> {throw new RuntimeException("unexpected call to - // exemplar sampler");}) .name("count_total") .withoutExemplars() .build(); @@ -330,6 +328,21 @@ public void testExemplarSamplerDisabled() { assertThat(getData(counter).getExemplar()).isNull(); } + @Test + public void testExemplarSamplerDisabledReturnsFastCounter() { + Counter counter = + Counter.builder() + .name("count_total") + .withoutExemplars() + .build(); + Counter.DataPointIgnoringExemplars fasterCounter = (Counter.DataPointIgnoringExemplars) + counter.labelValues(); + assertThat(getData(counter).getValue()).isEqualTo(0); + fasterCounter.inc(1); + assertThat(getData(counter).getValue()).isEqualTo(1); + assertThat(fasterCounter.isExemplarsEnabled()).isFalse(); + } + @Test public void testConstLabelsFirst() { assertThatExceptionOfType(IllegalArgumentException.class) From 5c257feebcd514b76e776daa235ac6b8859f99d5 Mon Sep 17 00:00:00 2001 From: Jens Wilke Date: Mon, 21 Jul 2025 17:10:40 +0700 Subject: [PATCH 2/3] Remove redundant field. Also include inc() to the specialised methods. --- .../metrics/core/metrics/Counter.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java index 48d033000..0632ab8b3 100644 --- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java +++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java @@ -32,13 +32,12 @@ public class Counter extends StatefulMetric implements CounterDataPoint { - private final boolean exemplarsEnabled; private final ExemplarSamplerConfig exemplarSamplerConfig; private Counter(Builder builder, PrometheusProperties prometheusProperties) { super(builder); MetricsProperties[] properties = getMetricProperties(builder, prometheusProperties); - exemplarsEnabled = getConfigProperty(properties, MetricsProperties::getExemplarsEnabled); + boolean exemplarsEnabled = getConfigProperty(properties, MetricsProperties::getExemplarsEnabled); if (exemplarsEnabled) { exemplarSamplerConfig = new ExemplarSamplerConfig(prometheusProperties.getExemplarProperties(), 1); @@ -93,7 +92,7 @@ protected CounterSnapshot collect(List labels, List metricDat @Override protected boolean isExemplarsEnabled() { - return exemplarsEnabled; + return exemplarSamplerConfig != null; } @Override @@ -118,7 +117,7 @@ public static class DataPoint implements CounterDataPoint { // LongAdder is 20% faster than DoubleAdder. So let's use the LongAdder for long observations, // and DoubleAdder for double observations. If the user doesn't observe any double at all, // we will be using the LongAdder and get the best performance. - private final LongAdder longValue = new LongAdder(); + protected final LongAdder longValue = new LongAdder(); private final long createdTimeMillis = System.currentTimeMillis(); private final ExemplarSampler exemplarSampler; // null if isExemplarsEnabled() is false @@ -188,7 +187,7 @@ private void validateAndAdd(double amount) { doubleValue.add(amount); } - private CounterSnapshot.CounterDataPointSnapshot collect(Labels labels) { + protected CounterSnapshot.CounterDataPointSnapshot collect(Labels labels) { // Read the exemplar first. Otherwise, there is a race condition where you might // see an Exemplar for a value that's not counted yet. // If there are multiple Exemplars (by default it's just one), use the newest. @@ -229,6 +228,17 @@ public void inc(long amount) { validateAndAdd(amount); } + /** + * Counter increment, probably the most used metric update method. Specialise and skip + * validation. Basically, the JVM JIT is doing this for us and inlining the code. However, + * for people that are curious about performance and inspecting the code, it might be good + * to be assured that this goes directly to the `LongAdder` method. + */ + @Override + public void inc() { + longValue.increment(); + } + /** * Override for speed. Since final, the JVM will inline code for this * method and all Exemplar code will be left out. From 717aff7ff5b022dad0e0025f02c6442a222c8a10 Mon Sep 17 00:00:00 2001 From: Jens Wilke Date: Mon, 21 Jul 2025 18:37:49 +0700 Subject: [PATCH 3/3] make withoutExamplars() take precedence for counter --- .../java/io/prometheus/metrics/core/metrics/Counter.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java index 0632ab8b3..091e684c2 100644 --- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java +++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java @@ -38,7 +38,11 @@ private Counter(Builder builder, PrometheusProperties prometheusProperties) { super(builder); MetricsProperties[] properties = getMetricProperties(builder, prometheusProperties); boolean exemplarsEnabled = getConfigProperty(properties, MetricsProperties::getExemplarsEnabled); - if (exemplarsEnabled) { + // exemplars might be enabled specifically for a metric, however, if the code + // says withoutExemplars they should stay disabled. + boolean notTurnedOffWithinCode = + builder == null || builder.exemplarsEnabled == null || builder.exemplarsEnabled; + if (exemplarsEnabled && notTurnedOffWithinCode) { exemplarSamplerConfig = new ExemplarSamplerConfig(prometheusProperties.getExemplarProperties(), 1); } else {