Skip to content

Commit edd160a

Browse files
authored
fix: Reduce allocations for classic histogram buckets (#2081)
Related to #2075 This reduces allocation and GC pressure in histogram text formatting by eliminating unnecessary intermediate allocations. --------- Signed-off-by: Jay DeLuca <jaydeluca4@gmail.com> Signed-off-by: Ubuntu <jaydeluca4@gmail.com>
1 parent 04bee70 commit edd160a

7 files changed

Lines changed: 170 additions & 84 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.prometheus.metrics.benchmarks;
2+
3+
import io.prometheus.metrics.config.EscapingScheme;
4+
import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter;
5+
import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter;
6+
import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets;
7+
import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
8+
import io.prometheus.metrics.model.snapshots.HistogramSnapshot.HistogramDataPointSnapshot;
9+
import io.prometheus.metrics.model.snapshots.Labels;
10+
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
11+
import java.io.IOException;
12+
import java.io.OutputStream;
13+
import java.util.concurrent.TimeUnit;
14+
import org.openjdk.jmh.annotations.Benchmark;
15+
import org.openjdk.jmh.annotations.Fork;
16+
import org.openjdk.jmh.annotations.Measurement;
17+
import org.openjdk.jmh.annotations.Warmup;
18+
19+
/**
20+
* Benchmarks for writing a classic histogram (10 label combinations × 12 buckets) to text formats.
21+
* Output goes to /dev/null to isolate pure formatting CPU cost with zero IO overhead.
22+
*/
23+
@Fork(3)
24+
@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
25+
@Measurement(iterations = 10, time = 2, timeUnit = TimeUnit.SECONDS)
26+
public class HistogramTextFormatBenchmark {
27+
28+
private static final MetricSnapshots SNAPSHOTS;
29+
30+
static {
31+
double[] upperBounds = {
32+
.005, .01, .025, .05, .1, .25, .5, 1.0, 2.5, 5.0, 10.0, Double.POSITIVE_INFINITY
33+
};
34+
Number[] counts = {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L};
35+
ClassicHistogramBuckets buckets = ClassicHistogramBuckets.of(upperBounds, counts);
36+
37+
HistogramSnapshot.Builder builder =
38+
HistogramSnapshot.builder().name("http_request_duration_seconds");
39+
40+
for (int i = 0; i < 10; i++) {
41+
builder.dataPoint(
42+
HistogramDataPointSnapshot.builder()
43+
.classicHistogramBuckets(buckets)
44+
.labels(Labels.of("status", "value_" + i))
45+
.sum(123.456)
46+
.createdTimestampMillis(1000L)
47+
.build());
48+
}
49+
50+
SNAPSHOTS = MetricSnapshots.of(builder.build());
51+
}
52+
53+
private static final OpenMetricsTextFormatWriter OPEN_METRICS_TEXT_FORMAT_WRITER =
54+
OpenMetricsTextFormatWriter.create();
55+
private static final PrometheusTextFormatWriter PROMETHEUS_TEXT_FORMAT_WRITER =
56+
PrometheusTextFormatWriter.create();
57+
58+
@Benchmark
59+
public OutputStream openMetricsWriteToNull() throws IOException {
60+
OutputStream nullOutputStream = TextFormatUtilBenchmark.NullOutputStream.INSTANCE;
61+
OPEN_METRICS_TEXT_FORMAT_WRITER.write(nullOutputStream, SNAPSHOTS, EscapingScheme.ALLOW_UTF8);
62+
return nullOutputStream;
63+
}
64+
65+
@Benchmark
66+
public OutputStream prometheusWriteToNull() throws IOException {
67+
OutputStream nullOutputStream = TextFormatUtilBenchmark.NullOutputStream.INSTANCE;
68+
PROMETHEUS_TEXT_FORMAT_WRITER.write(nullOutputStream, SNAPSHOTS, EscapingScheme.ALLOW_UTF8);
69+
return nullOutputStream;
70+
}
71+
}

mise.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ run = "./mvnw install -DskipTests -Dcoverage.skip=true"
5959

6060
[tasks."lint"]
6161
description = "Run all lints"
62-
raw_args = true
6362
depends = ["lint:bom"]
63+
raw_args = true
6464
run = "flint run"
6565

6666
[tasks."lint:fix"]
@@ -95,19 +95,19 @@ run = ["hugo --gc --minify --baseURL ${BASE_URL}/", "echo 'ls ./public/api' && l
9595

9696
[tasks."benchmark:quick"]
9797
description = "Run benchmarks with reduced iterations (quick smoke test, ~10 min)"
98-
run = "python3 ./.mise/tasks/update_benchmarks.py --jmh-args '-f 1 -wi 1 -i 3'"
98+
run = "python3 ./.mise/tasks/update_benchmarks.py --jmh-args '-f 1 -wi 1 -i 3 -prof gc'"
9999

100100
[tasks."benchmark:ci"]
101101
description = "Run benchmarks with CI configuration (3 forks, 3 warmup, 5 measurement iterations (~60 min total)"
102-
run = "python3 ./.mise/tasks/update_benchmarks.py --jmh-args '-f 3 -wi 3 -i 5'"
102+
run = "python3 ./.mise/tasks/update_benchmarks.py --jmh-args '-f 3 -wi 3 -i 5 -prof gc'"
103103

104104
[tasks."benchmark:ci-json"]
105105
description = "Run benchmarks with CI configuration and JSON output (for workflow/testing)"
106106
run = """
107107
./mvnw -pl benchmarks -am -DskipTests clean package
108108
JMH_ARGS="${JMH_ARGS:--f 3 -wi 3 -i 5}"
109109
echo "Running benchmarks with args: $JMH_ARGS"
110-
java -jar ./benchmarks/target/benchmarks.jar -rf json -rff benchmark-results.json $JMH_ARGS
110+
java -jar ./benchmarks/target/benchmarks.jar -rf json -rff benchmark-results.json $JMH_ARGS -prof gc
111111
"""
112112

113113
[tasks."benchmark:generate-summary"]

prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,14 +258,15 @@ private void writeClassicHistogramDataPoints(
258258
HistogramSnapshot snapshot,
259259
EscapingScheme scheme)
260260
throws IOException {
261+
String bucketName = name + "_bucket";
261262
for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
262263
ClassicHistogramBuckets buckets = getClassicBuckets(data);
263264
Exemplars exemplars = data.getExemplars();
264265
long cumulativeCount = 0;
265266
for (int i = 0; i < buckets.size(); i++) {
266267
cumulativeCount += buckets.getCount(i);
267268
writeNameAndLabels(
268-
writer, name, "_bucket", data.getLabels(), scheme, "le", buckets.getUpperBound(i));
269+
writer, bucketName, null, data.getLabels(), scheme, "le", buckets.getUpperBound(i));
269270
writeLong(writer, cumulativeCount);
270271
Exemplar exemplar;
271272
if (i == 0) {
@@ -636,7 +637,7 @@ private void writeNameAndLabels(
636637
metricInsideBraces = true;
637638
writer.write('{');
638639
}
639-
writeName(writer, name + (suffix != null ? suffix : ""), NameType.Metric);
640+
writeName(writer, suffix != null ? name + suffix : name, NameType.Metric);
640641
if (!labels.isEmpty() || additionalLabelName != null) {
641642
writeLabels(
642643
writer,

prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,9 @@ private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme sc
157157
throws IOException {
158158
MetricMetadata metadata = snapshot.getMetadata();
159159
writeMetadata(writer, "gauge", metadata, scheme);
160+
String name = getMetadataName(metadata, scheme);
160161
for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) {
161-
writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme);
162+
writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
162163
writeDouble(writer, data.getValue());
163164
if (exemplarsOnAllMetricTypesEnabled) {
164165
writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
@@ -190,20 +191,18 @@ private void writeClassicHistogramBuckets(
190191
List<HistogramSnapshot.HistogramDataPointSnapshot> dataList,
191192
EscapingScheme scheme)
192193
throws IOException {
194+
String name = getMetadataName(metadata, scheme);
195+
String bucketName = name + "_bucket";
196+
String countName = name + countSuffix;
197+
String sumName = name + sumSuffix;
193198
for (HistogramSnapshot.HistogramDataPointSnapshot data : dataList) {
194199
ClassicHistogramBuckets buckets = getClassicBuckets(data);
195200
Exemplars exemplars = data.getExemplars();
196201
long cumulativeCount = 0;
197202
for (int i = 0; i < buckets.size(); i++) {
198203
cumulativeCount += buckets.getCount(i);
199204
writeNameAndLabels(
200-
writer,
201-
getMetadataName(metadata, scheme),
202-
"_bucket",
203-
data.getLabels(),
204-
scheme,
205-
"le",
206-
buckets.getUpperBound(i));
205+
writer, bucketName, null, data.getLabels(), scheme, "le", buckets.getUpperBound(i));
207206
writeLong(writer, cumulativeCount);
208207
Exemplar exemplar;
209208
if (i == 0) {
@@ -215,9 +214,9 @@ private void writeClassicHistogramBuckets(
215214
}
216215
// In OpenMetrics format, histogram _count and _sum are either both present or both absent.
217216
if (data.hasCount() && data.hasSum()) {
218-
writeCountAndSum(writer, metadata, data, countSuffix, sumSuffix, exemplars, scheme);
217+
writeCountAndSum(writer, countName, sumName, data, exemplars, scheme);
219218
}
220-
writeCreated(writer, metadata, data, scheme);
219+
writeCreated(writer, name, data, scheme);
221220
}
222221
}
223222

@@ -235,6 +234,9 @@ void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme
235234
throws IOException {
236235
boolean metadataWritten = false;
237236
MetricMetadata metadata = snapshot.getMetadata();
237+
String name = getMetadataName(metadata, scheme);
238+
String countName = name + "_count";
239+
String sumName = name + "_sum";
238240
for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) {
239241
if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) {
240242
continue;
@@ -252,13 +254,7 @@ void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme
252254
int exemplarIndex = 1;
253255
for (Quantile quantile : data.getQuantiles()) {
254256
writeNameAndLabels(
255-
writer,
256-
getMetadataName(metadata, scheme),
257-
null,
258-
data.getLabels(),
259-
scheme,
260-
"quantile",
261-
quantile.getQuantile());
257+
writer, name, null, data.getLabels(), scheme, "quantile", quantile.getQuantile());
262258
writeDouble(writer, quantile.getValue());
263259
if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) {
264260
exemplarIndex = (exemplarIndex + 1) % exemplars.size();
@@ -268,8 +264,8 @@ void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme
268264
}
269265
}
270266
// Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics.
271-
writeCountAndSum(writer, metadata, data, "_count", "_sum", exemplars, scheme);
272-
writeCreated(writer, metadata, data, scheme);
267+
writeCountAndSum(writer, countName, sumName, data, exemplars, scheme);
268+
writeCreated(writer, name, data, scheme);
273269
}
274270
}
275271

@@ -290,9 +286,10 @@ private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingSch
290286
throws IOException {
291287
MetricMetadata metadata = snapshot.getMetadata();
292288
writeMetadata(writer, "stateset", metadata, scheme);
289+
String name = getMetadataName(metadata, scheme);
293290
for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) {
294291
for (int i = 0; i < data.size(); i++) {
295-
writer.write(getMetadataName(metadata, scheme));
292+
writer.write(name);
296293
writer.write('{');
297294
Labels labels = data.getLabels();
298295
for (int j = 0; j < labels.size(); j++) {
@@ -307,7 +304,7 @@ private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingSch
307304
if (!labels.isEmpty()) {
308305
writer.write(",");
309306
}
310-
writer.write(getMetadataName(metadata, scheme));
307+
writer.write(name);
311308
writer.write("=\"");
312309
writeEscapedString(writer, data.getName(i));
313310
writer.write("\"} ");
@@ -325,8 +322,9 @@ private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingSchem
325322
throws IOException {
326323
MetricMetadata metadata = snapshot.getMetadata();
327324
writeMetadata(writer, "unknown", metadata, scheme);
325+
String name = getMetadataName(metadata, scheme);
328326
for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) {
329-
writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme);
327+
writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
330328
writeDouble(writer, data.getValue());
331329
if (exemplarsOnAllMetricTypesEnabled) {
332330
writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
@@ -338,16 +336,14 @@ private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingSchem
338336

339337
private void writeCountAndSum(
340338
Writer writer,
341-
MetricMetadata metadata,
339+
String countName,
340+
String sumName,
342341
DistributionDataPointSnapshot data,
343-
String countSuffix,
344-
String sumSuffix,
345342
Exemplars exemplars,
346343
EscapingScheme scheme)
347344
throws IOException {
348345
if (data.hasCount()) {
349-
writeNameAndLabels(
350-
writer, getMetadataName(metadata, scheme), countSuffix, data.getLabels(), scheme);
346+
writeNameAndLabels(writer, countName, null, data.getLabels(), scheme);
351347
writeLong(writer, data.getCount());
352348
if (exemplarsOnAllMetricTypesEnabled) {
353349
writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme);
@@ -356,19 +352,12 @@ private void writeCountAndSum(
356352
}
357353
}
358354
if (data.hasSum()) {
359-
writeNameAndLabels(
360-
writer, getMetadataName(metadata, scheme), sumSuffix, data.getLabels(), scheme);
355+
writeNameAndLabels(writer, sumName, null, data.getLabels(), scheme);
361356
writeDouble(writer, data.getSum());
362357
writeScrapeTimestampAndExemplar(writer, data, null, scheme);
363358
}
364359
}
365360

366-
private void writeCreated(
367-
Writer writer, MetricMetadata metadata, DataPointSnapshot data, EscapingScheme scheme)
368-
throws IOException {
369-
writeCreated(writer, getMetadataName(metadata, scheme), data, scheme);
370-
}
371-
372361
private void writeCreated(
373362
Writer writer, String baseName, DataPointSnapshot data, EscapingScheme scheme)
374363
throws IOException {
@@ -409,7 +398,7 @@ private void writeNameAndLabels(
409398
metricInsideBraces = true;
410399
writer.write('{');
411400
}
412-
writeName(writer, name + (suffix != null ? suffix : ""), NameType.Metric);
401+
writeName(writer, suffix != null ? name + suffix : name, NameType.Metric);
413402
if (!labels.isEmpty() || additionalLabelName != null) {
414403
writeLabels(
415404
writer,

0 commit comments

Comments
 (0)