Skip to content

Commit 591d6ad

Browse files
committed
perf: optimize classic-only histogram observe path
Signed-off-by: Arnab Nandy <arnab_nandy7@yahoo.com>
1 parent 08cf925 commit 591d6ad

3 files changed

Lines changed: 99 additions & 2 deletions

File tree

docs/apidiffs/current_vs_latest/prometheus-metrics-core.txt

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Histogram.java

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@ public class DataPoint implements DistributionDataPoint {
205205
private final LongAdder nativeZeroCount = new LongAdder();
206206
private final LongAdder count = new LongAdder();
207207
private final DoubleAdder sum = new DoubleAdder();
208+
private final long[] classicOnlyBuckets;
209+
private long classicOnlyCount;
210+
private double classicOnlySum;
208211
private volatile int nativeSchema =
209212
nativeInitialSchema; // integer in [-4, 8] or CLASSIC_HISTOGRAM
210213
private volatile double nativeZeroThreshold = Histogram.this.nativeMinZeroThreshold;
@@ -223,16 +226,27 @@ private DataPoint() {
223226
for (int i = 0; i < classicUpperBounds.length; i++) {
224227
classicBuckets[i] = new LongAdder();
225228
}
229+
classicOnlyBuckets = new long[classicUpperBounds.length];
226230
maybeScheduleNextReset();
227231
}
228232

229233
@Override
230234
public double getSum() {
235+
if (isClassicOnly()) {
236+
synchronized (this) {
237+
return classicOnlySum;
238+
}
239+
}
231240
return sum.sum();
232241
}
233242

234243
@Override
235244
public long getCount() {
245+
if (isClassicOnly()) {
246+
synchronized (this) {
247+
return classicOnlyCount;
248+
}
249+
}
236250
return count.sum();
237251
}
238252

@@ -242,7 +256,9 @@ public void observe(double value) {
242256
// See https://github.com/prometheus/client_golang/issues/1275 on ignoring NaN observations.
243257
return;
244258
}
245-
if (!buffer.append(value)) {
259+
if (isClassicOnly()) {
260+
doObserveClassicOnly(value);
261+
} else if (!buffer.append(value)) {
246262
doObserve(value, false);
247263
}
248264
if (exemplarSampler != null) {
@@ -256,14 +272,32 @@ public void observeWithExemplar(double value, Labels labels) {
256272
// See https://github.com/prometheus/client_golang/issues/1275 on ignoring NaN observations.
257273
return;
258274
}
259-
if (!buffer.append(value)) {
275+
if (isClassicOnly()) {
276+
doObserveClassicOnly(value);
277+
} else if (!buffer.append(value)) {
260278
doObserve(value, false);
261279
}
262280
if (exemplarSampler != null) {
263281
exemplarSampler.observeWithExemplar(value, labels);
264282
}
265283
}
266284

285+
private boolean isClassicOnly() {
286+
return Histogram.this.nativeInitialSchema == CLASSIC_HISTOGRAM;
287+
}
288+
289+
private synchronized void doObserveClassicOnly(double value) {
290+
for (int i = 0; i < classicUpperBounds.length; ++i) {
291+
// The last bucket is +Inf, so we always increment.
292+
if (value <= classicUpperBounds[i]) {
293+
classicOnlyBuckets[i]++;
294+
break;
295+
}
296+
}
297+
classicOnlySum += value;
298+
classicOnlyCount++;
299+
}
300+
267301
private void doObserve(double value, boolean fromBuffer) {
268302
// classicUpperBounds is an empty array if this is a native histogram only.
269303
for (int i = 0; i < classicUpperBounds.length; ++i) {
@@ -301,6 +335,16 @@ private void doObserve(double value, boolean fromBuffer) {
301335

302336
private HistogramSnapshot.HistogramDataPointSnapshot collect(Labels labels) {
303337
Exemplars exemplars = exemplarSampler != null ? exemplarSampler.collect() : Exemplars.EMPTY;
338+
if (isClassicOnly()) {
339+
synchronized (this) {
340+
return new HistogramSnapshot.HistogramDataPointSnapshot(
341+
ClassicHistogramBuckets.of(classicUpperBounds, classicOnlyBuckets),
342+
classicOnlySum,
343+
labels,
344+
exemplars,
345+
createdTimeMillis);
346+
}
347+
}
304348
return buffer.run(
305349
expectedCount -> count.sum() == expectedCount,
306350
() -> {

prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/HistogramTest.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,6 +1565,57 @@ void testObserveMultithreaded()
15651565
assertThat(executor.awaitTermination(5, TimeUnit.SECONDS)).isTrue();
15661566
}
15671567

1568+
@Test
1569+
void testClassicOnlyObserveMultithreaded()
1570+
throws InterruptedException, ExecutionException, TimeoutException {
1571+
Histogram histogram =
1572+
Histogram.builder().name("test").classicOnly().labelNames("status").build();
1573+
int nThreads = 8;
1574+
DistributionDataPoint obs = histogram.labelValues("200");
1575+
ExecutorService executor = Executors.newFixedThreadPool(nThreads);
1576+
CompletionService<List<HistogramSnapshot>> completionService =
1577+
new ExecutorCompletionService<>(executor);
1578+
CountDownLatch startSignal = new CountDownLatch(nThreads);
1579+
for (int t = 0; t < nThreads; t++) {
1580+
completionService.submit(
1581+
() -> {
1582+
List<HistogramSnapshot> snapshots = new ArrayList<>();
1583+
startSignal.countDown();
1584+
startSignal.await();
1585+
for (int i = 0; i < 10; i++) {
1586+
for (int j = 0; j < 1000; j++) {
1587+
obs.observe(1.1);
1588+
}
1589+
snapshots.add(histogram.collect());
1590+
}
1591+
return snapshots;
1592+
});
1593+
}
1594+
long maxCount = 0;
1595+
for (int i = 0; i < nThreads; i++) {
1596+
Future<List<HistogramSnapshot>> future = completionService.take();
1597+
List<HistogramSnapshot> snapshots = future.get(5, TimeUnit.SECONDS);
1598+
long count = 0;
1599+
for (HistogramSnapshot snapshot : snapshots) {
1600+
assertThat(snapshot.getDataPoints().size()).isOne();
1601+
HistogramSnapshot.HistogramDataPointSnapshot data =
1602+
snapshot.getDataPoints().stream().findFirst().orElseThrow(RuntimeException::new);
1603+
assertThat(data.getCount()).isGreaterThanOrEqualTo(count + 1000);
1604+
assertThat(data.getSum()).isCloseTo(data.getCount() * 1.1, offset(0.0000001));
1605+
count = data.getCount();
1606+
}
1607+
if (count > maxCount) {
1608+
maxCount = count;
1609+
}
1610+
}
1611+
assertThat(maxCount).isEqualTo(nThreads * 10_000L);
1612+
assertThat(obs.getCount()).isEqualTo(nThreads * 10_000L);
1613+
assertThat(obs.getSum()).isCloseTo(nThreads * 10_000L * 1.1, offset(0.0000001));
1614+
assertThat(nThreads * 10_000).isEqualTo(getBucket(histogram, 2.5, "status", "200").getCount());
1615+
executor.shutdown();
1616+
assertThat(executor.awaitTermination(5, TimeUnit.SECONDS)).isTrue();
1617+
}
1618+
15681619
@Test
15691620
void testNativeResetDuration() {
15701621
// Test that nativeResetDuration can be configured without error and the histogram

0 commit comments

Comments
 (0)