From 96a006122b96393f9cd3f4842728ffd7df3bfc6d Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Thu, 30 Apr 2026 15:57:15 -0400 Subject: [PATCH 1/6] Create name just one Signed-off-by: Jay DeLuca --- .../HistogramTextFormatBenchmark.java | 103 ++++++++++++++++++ mise.toml | 5 +- .../OpenMetrics2TextFormatWriter.java | 5 +- .../OpenMetricsTextFormatWriter.java | 8 +- .../PrometheusTextFormatWriter.java | 8 +- 5 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java diff --git a/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java new file mode 100644 index 000000000..e77af64eb --- /dev/null +++ b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java @@ -0,0 +1,103 @@ +package io.prometheus.metrics.benchmarks; + +import io.prometheus.metrics.config.EscapingScheme; +import io.prometheus.metrics.expositionformats.ExpositionFormatWriter; +import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; +import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter; +import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets; +import io.prometheus.metrics.model.snapshots.HistogramSnapshot; +import io.prometheus.metrics.model.snapshots.HistogramSnapshot.HistogramDataPointSnapshot; +import io.prometheus.metrics.model.snapshots.Labels; +import io.prometheus.metrics.model.snapshots.MetricSnapshots; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; + +/** + * Benchmarks for writing a classic histogram (10 label combinations × 12 buckets) to text formats. + * + *
+ * Benchmark                                                        Mode  Cnt  Score  Error  Units
+ * HistogramTextFormatBenchmark.openMetricsWriteToByteArray         thrpt
+ * HistogramTextFormatBenchmark.openMetricsWriteToNull              thrpt
+ * HistogramTextFormatBenchmark.prometheusWriteToByteArray          thrpt
+ * HistogramTextFormatBenchmark.prometheusWriteToNull               thrpt
+ * 
+ */ +public class HistogramTextFormatBenchmark { + + private static final MetricSnapshots SNAPSHOTS; + + static { + double[] upperBounds = { + .005, .01, .025, .05, .1, .25, .5, 1.0, 2.5, 5.0, 10.0, Double.POSITIVE_INFINITY + }; + Number[] counts = {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L}; + ClassicHistogramBuckets buckets = ClassicHistogramBuckets.of(upperBounds, counts); + + HistogramSnapshot.Builder builder = + HistogramSnapshot.builder().name("http_request_duration_seconds"); + + for (int i = 0; i < 10; i++) { + builder.dataPoint( + HistogramDataPointSnapshot.builder() + .classicHistogramBuckets(buckets) + .labels(Labels.of("status", "value_" + i)) + .sum(123.456) + .createdTimestampMillis(1000L) + .build()); + } + + SNAPSHOTS = MetricSnapshots.of(builder.build()); + } + + private static final ExpositionFormatWriter OPEN_METRICS_TEXT_FORMAT_WRITER = + OpenMetricsTextFormatWriter.create(); + private static final ExpositionFormatWriter PROMETHEUS_TEXT_FORMAT_WRITER = + PrometheusTextFormatWriter.create(); + + @State(Scope.Benchmark) + public static class WriterState { + + final ByteArrayOutputStream byteArrayOutputStream; + + public WriterState() { + this.byteArrayOutputStream = new ByteArrayOutputStream(); + } + } + + @Benchmark + public OutputStream openMetricsWriteToByteArray(WriterState writerState) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = writerState.byteArrayOutputStream; + byteArrayOutputStream.reset(); + OPEN_METRICS_TEXT_FORMAT_WRITER.write( + byteArrayOutputStream, SNAPSHOTS, EscapingScheme.ALLOW_UTF8); + return byteArrayOutputStream; + } + + @Benchmark + public OutputStream openMetricsWriteToNull() throws IOException { + OutputStream nullOutputStream = TextFormatUtilBenchmark.NullOutputStream.INSTANCE; + OPEN_METRICS_TEXT_FORMAT_WRITER.write(nullOutputStream, SNAPSHOTS, EscapingScheme.ALLOW_UTF8); + return nullOutputStream; + } + + @Benchmark + public OutputStream prometheusWriteToByteArray(WriterState writerState) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = writerState.byteArrayOutputStream; + byteArrayOutputStream.reset(); + PROMETHEUS_TEXT_FORMAT_WRITER.write( + byteArrayOutputStream, SNAPSHOTS, EscapingScheme.ALLOW_UTF8); + return byteArrayOutputStream; + } + + @Benchmark + public OutputStream prometheusWriteToNull() throws IOException { + OutputStream nullOutputStream = TextFormatUtilBenchmark.NullOutputStream.INSTANCE; + PROMETHEUS_TEXT_FORMAT_WRITER.write(nullOutputStream, SNAPSHOTS, EscapingScheme.ALLOW_UTF8); + return nullOutputStream; + } +} diff --git a/mise.toml b/mise.toml index 70cfecbaa..cf5cb8657 100644 --- a/mise.toml +++ b/mise.toml @@ -59,7 +59,6 @@ run = "./mvnw install -DskipTests -Dcoverage.skip=true" [tasks."lint"] description = "Run all lints" -raw_args = true depends = ["lint:bom"] run = "flint run" @@ -95,11 +94,11 @@ run = ["hugo --gc --minify --baseURL ${BASE_URL}/", "echo 'ls ./public/api' && l [tasks."benchmark:quick"] description = "Run benchmarks with reduced iterations (quick smoke test, ~10 min)" -run = "python3 ./.mise/tasks/update_benchmarks.py --jmh-args '-f 1 -wi 1 -i 3'" +run = "python3 ./.mise/tasks/update_benchmarks.py --jmh-args '-f 1 -wi 1 -i 3 -prof gc'" [tasks."benchmark:ci"] description = "Run benchmarks with CI configuration (3 forks, 3 warmup, 5 measurement iterations (~60 min total)" -run = "python3 ./.mise/tasks/update_benchmarks.py --jmh-args '-f 3 -wi 3 -i 5'" +run = "python3 ./.mise/tasks/update_benchmarks.py --jmh-args '-f 3 -wi 3 -i 5 -prof gc'" [tasks."benchmark:ci-json"] description = "Run benchmarks with CI configuration and JSON output (for workflow/testing)" diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java index 963b18507..19e1e4c93 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java @@ -258,6 +258,7 @@ private void writeClassicHistogramDataPoints( HistogramSnapshot snapshot, EscapingScheme scheme) throws IOException { + String bucketName = name + "_bucket"; for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) { ClassicHistogramBuckets buckets = getClassicBuckets(data); Exemplars exemplars = data.getExemplars(); @@ -265,7 +266,7 @@ private void writeClassicHistogramDataPoints( for (int i = 0; i < buckets.size(); i++) { cumulativeCount += buckets.getCount(i); writeNameAndLabels( - writer, name, "_bucket", data.getLabels(), scheme, "le", buckets.getUpperBound(i)); + writer, bucketName, null, data.getLabels(), scheme, "le", buckets.getUpperBound(i)); writeLong(writer, cumulativeCount); Exemplar exemplar; if (i == 0) { @@ -636,7 +637,7 @@ private void writeNameAndLabels( metricInsideBraces = true; writer.write('{'); } - writeName(writer, name + (suffix != null ? suffix : ""), NameType.Metric); + writeName(writer, suffix != null ? name + suffix : name, NameType.Metric); if (!labels.isEmpty() || additionalLabelName != null) { writeLabels( writer, diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java index 1bc7e101f..4cf2068ae 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java @@ -190,6 +190,8 @@ private void writeClassicHistogramBuckets( List dataList, EscapingScheme scheme) throws IOException { + String name = getMetadataName(metadata, scheme); + String bucketName = name + "_bucket"; for (HistogramSnapshot.HistogramDataPointSnapshot data : dataList) { ClassicHistogramBuckets buckets = getClassicBuckets(data); Exemplars exemplars = data.getExemplars(); @@ -198,8 +200,8 @@ private void writeClassicHistogramBuckets( cumulativeCount += buckets.getCount(i); writeNameAndLabels( writer, - getMetadataName(metadata, scheme), - "_bucket", + bucketName, + null, data.getLabels(), scheme, "le", @@ -409,7 +411,7 @@ private void writeNameAndLabels( metricInsideBraces = true; writer.write('{'); } - writeName(writer, name + (suffix != null ? suffix : ""), NameType.Metric); + writeName(writer, suffix != null ? name + suffix : name, NameType.Metric); if (!labels.isEmpty() || additionalLabelName != null) { writeLabels( writer, diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java index b40dcfdf2..d388810ca 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java @@ -204,6 +204,8 @@ private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingS throws IOException { MetricMetadata metadata = snapshot.getMetadata(); writeMetadata(writer, "", "histogram", metadata, scheme); + String name = getMetadataName(metadata, scheme); + String bucketName = name + "_bucket"; for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) { ClassicHistogramBuckets buckets = getClassicBuckets(data); long cumulativeCount = 0; @@ -211,8 +213,8 @@ private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingS cumulativeCount += buckets.getCount(i); writeNameAndLabels( writer, - getMetadataName(metadata, scheme), - "_bucket", + bucketName, + null, data.getLabels(), scheme, "le", @@ -405,7 +407,7 @@ private void writeNameAndLabels( metricInsideBraces = true; writer.write('{'); } - writeName(writer, name + (suffix != null ? suffix : ""), NameType.Metric); + writeName(writer, suffix != null ? name + suffix : name, NameType.Metric); if (!labels.isEmpty() || additionalLabelName != null) { writeLabels( writer, labels, additionalLabelName, additionalLabelValue, metricInsideBraces, scheme); From 29f7c73f550745bae75a988d51124fb1891eb938 Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Thu, 30 Apr 2026 16:52:25 -0400 Subject: [PATCH 2/6] add benchmark Signed-off-by: Jay DeLuca --- .../HistogramTextFormatBenchmark.java | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java index e77af64eb..e2cce19db 100644 --- a/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java +++ b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java @@ -20,11 +20,27 @@ * Benchmarks for writing a classic histogram (10 label combinations × 12 buckets) to text formats. * *
- * Benchmark                                                        Mode  Cnt  Score  Error  Units
- * HistogramTextFormatBenchmark.openMetricsWriteToByteArray         thrpt
- * HistogramTextFormatBenchmark.openMetricsWriteToNull              thrpt
- * HistogramTextFormatBenchmark.prometheusWriteToByteArray          thrpt
- * HistogramTextFormatBenchmark.prometheusWriteToNull               thrpt
+ * Benchmark                                                                     Mode  Cnt       Score        Error   Units
+ * HistogramTextFormatBenchmark.openMetricsWriteToByteArray                     thrpt    3   37567.116 ±  14566.571   ops/s
+ * HistogramTextFormatBenchmark.openMetricsWriteToByteArray:gc.alloc.rate       thrpt    3    1466.289 ±    568.584  MB/sec
+ * HistogramTextFormatBenchmark.openMetricsWriteToByteArray:gc.alloc.rate.norm  thrpt    3   40928.019 ±      0.006    B/op
+ * HistogramTextFormatBenchmark.openMetricsWriteToByteArray:gc.count            thrpt    3     147.000               counts
+ * HistogramTextFormatBenchmark.openMetricsWriteToByteArray:gc.time             thrpt    3      77.000                   ms
+ * HistogramTextFormatBenchmark.openMetricsWriteToNull                          thrpt    3   36179.016 ±   1149.646   ops/s
+ * HistogramTextFormatBenchmark.openMetricsWriteToNull:gc.alloc.rate            thrpt    3    1412.112 ±     44.791  MB/sec
+ * HistogramTextFormatBenchmark.openMetricsWriteToNull:gc.alloc.rate.norm       thrpt    3   40928.019 ±      0.001    B/op
+ * HistogramTextFormatBenchmark.openMetricsWriteToNull:gc.count                 thrpt    3     142.000               counts
+ * HistogramTextFormatBenchmark.openMetricsWriteToNull:gc.time                  thrpt    3      74.000                   ms
+ * HistogramTextFormatBenchmark.prometheusWriteToByteArray                      thrpt    3   36616.472 ±   5189.952   ops/s
+ * HistogramTextFormatBenchmark.prometheusWriteToByteArray:gc.alloc.rate        thrpt    3    1434.773 ±    203.524  MB/sec
+ * HistogramTextFormatBenchmark.prometheusWriteToByteArray:gc.alloc.rate.norm   thrpt    3   41088.019 ±      0.003    B/op
+ * HistogramTextFormatBenchmark.prometheusWriteToByteArray:gc.count             thrpt    3     144.000               counts
+ * HistogramTextFormatBenchmark.prometheusWriteToByteArray:gc.time              thrpt    3      73.000                   ms
+ * HistogramTextFormatBenchmark.prometheusWriteToNull                           thrpt    3   36357.284 ±   4298.616   ops/s
+ * HistogramTextFormatBenchmark.prometheusWriteToNull:gc.alloc.rate             thrpt    3    1424.614 ±    168.607  MB/sec
+ * HistogramTextFormatBenchmark.prometheusWriteToNull:gc.alloc.rate.norm        thrpt    3   41088.019 ±      0.003    B/op
+ * HistogramTextFormatBenchmark.prometheusWriteToNull:gc.count                  thrpt    3     143.000               counts
+ * HistogramTextFormatBenchmark.prometheusWriteToNull:gc.time                   thrpt    3      73.000                   ms
  * 
*/ public class HistogramTextFormatBenchmark { From 94448484a3a7a47e9af06e47890918f42dda14d7 Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Fri, 1 May 2026 08:11:43 -0400 Subject: [PATCH 3/6] more allocations avoided Signed-off-by: Jay DeLuca --- .../HistogramTextFormatBenchmark.java | 81 ++++++++++++------- .../OpenMetrics2TextFormatWriter.java | 5 ++ .../OpenMetricsTextFormatWriter.java | 54 ++++++------- .../PrometheusTextFormatWriter.java | 55 +++++++------ .../expositionformats/TextFormatUtil.java | 24 ++++-- 5 files changed, 134 insertions(+), 85 deletions(-) diff --git a/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java index e2cce19db..0ef1bbc2a 100644 --- a/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java +++ b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java @@ -1,7 +1,6 @@ package io.prometheus.metrics.benchmarks; import io.prometheus.metrics.config.EscapingScheme; -import io.prometheus.metrics.expositionformats.ExpositionFormatWriter; import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter; import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets; @@ -9,39 +8,31 @@ import io.prometheus.metrics.model.snapshots.HistogramSnapshot.HistogramDataPointSnapshot; import io.prometheus.metrics.model.snapshots.Labels; import io.prometheus.metrics.model.snapshots.MetricSnapshots; +import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; /** - * Benchmarks for writing a classic histogram (10 label combinations × 12 buckets) to text formats. + * Benchmarks for writing a classic histogram (10 label combinations × 12 buckets) to text + * formats. * - *
- * Benchmark                                                                     Mode  Cnt       Score        Error   Units
- * HistogramTextFormatBenchmark.openMetricsWriteToByteArray                     thrpt    3   37567.116 ±  14566.571   ops/s
- * HistogramTextFormatBenchmark.openMetricsWriteToByteArray:gc.alloc.rate       thrpt    3    1466.289 ±    568.584  MB/sec
- * HistogramTextFormatBenchmark.openMetricsWriteToByteArray:gc.alloc.rate.norm  thrpt    3   40928.019 ±      0.006    B/op
- * HistogramTextFormatBenchmark.openMetricsWriteToByteArray:gc.count            thrpt    3     147.000               counts
- * HistogramTextFormatBenchmark.openMetricsWriteToByteArray:gc.time             thrpt    3      77.000                   ms
- * HistogramTextFormatBenchmark.openMetricsWriteToNull                          thrpt    3   36179.016 ±   1149.646   ops/s
- * HistogramTextFormatBenchmark.openMetricsWriteToNull:gc.alloc.rate            thrpt    3    1412.112 ±     44.791  MB/sec
- * HistogramTextFormatBenchmark.openMetricsWriteToNull:gc.alloc.rate.norm       thrpt    3   40928.019 ±      0.001    B/op
- * HistogramTextFormatBenchmark.openMetricsWriteToNull:gc.count                 thrpt    3     142.000               counts
- * HistogramTextFormatBenchmark.openMetricsWriteToNull:gc.time                  thrpt    3      74.000                   ms
- * HistogramTextFormatBenchmark.prometheusWriteToByteArray                      thrpt    3   36616.472 ±   5189.952   ops/s
- * HistogramTextFormatBenchmark.prometheusWriteToByteArray:gc.alloc.rate        thrpt    3    1434.773 ±    203.524  MB/sec
- * HistogramTextFormatBenchmark.prometheusWriteToByteArray:gc.alloc.rate.norm   thrpt    3   41088.019 ±      0.003    B/op
- * HistogramTextFormatBenchmark.prometheusWriteToByteArray:gc.count             thrpt    3     144.000               counts
- * HistogramTextFormatBenchmark.prometheusWriteToByteArray:gc.time              thrpt    3      73.000                   ms
- * HistogramTextFormatBenchmark.prometheusWriteToNull                           thrpt    3   36357.284 ±   4298.616   ops/s
- * HistogramTextFormatBenchmark.prometheusWriteToNull:gc.alloc.rate             thrpt    3    1424.614 ±    168.607  MB/sec
- * HistogramTextFormatBenchmark.prometheusWriteToNull:gc.alloc.rate.norm        thrpt    3   41088.019 ±      0.003    B/op
- * HistogramTextFormatBenchmark.prometheusWriteToNull:gc.count                  thrpt    3     143.000               counts
- * HistogramTextFormatBenchmark.prometheusWriteToNull:gc.time                   thrpt    3      73.000                   ms
- * 
+ *

Two variants per format: + * + *

    + *
  • {@code writeToByteArray} — OutputStream path, new BufferedWriter created per call. + *
  • {@code reusingWriter} — Writer path, BufferedWriter reused across calls. + *
+ * + *

Baseline (before allocation optimizations): ~41 KB/op for both Prometheus and OpenMetrics + * formats, dominated by the per-call BufferedWriter buffer (~16 KB) and number-to-string + * conversions. */ public class HistogramTextFormatBenchmark { @@ -70,9 +61,9 @@ public class HistogramTextFormatBenchmark { SNAPSHOTS = MetricSnapshots.of(builder.build()); } - private static final ExpositionFormatWriter OPEN_METRICS_TEXT_FORMAT_WRITER = + private static final OpenMetricsTextFormatWriter OPEN_METRICS_TEXT_FORMAT_WRITER = OpenMetricsTextFormatWriter.create(); - private static final ExpositionFormatWriter PROMETHEUS_TEXT_FORMAT_WRITER = + private static final PrometheusTextFormatWriter PROMETHEUS_TEXT_FORMAT_WRITER = PrometheusTextFormatWriter.create(); @State(Scope.Benchmark) @@ -85,6 +76,26 @@ public WriterState() { } } + @State(Scope.Benchmark) + public static class ReusableWriterState { + + final ByteArrayOutputStream openMetricsByteArrayOutputStream; + final ByteArrayOutputStream prometheusByteArrayOutputStream; + final BufferedWriter openMetricsWriter; + final BufferedWriter prometheusWriter; + + public ReusableWriterState() { + this.openMetricsByteArrayOutputStream = new ByteArrayOutputStream(); + this.prometheusByteArrayOutputStream = new ByteArrayOutputStream(); + this.openMetricsWriter = + new BufferedWriter( + new OutputStreamWriter(openMetricsByteArrayOutputStream, StandardCharsets.UTF_8)); + this.prometheusWriter = + new BufferedWriter( + new OutputStreamWriter(prometheusByteArrayOutputStream, StandardCharsets.UTF_8)); + } + } + @Benchmark public OutputStream openMetricsWriteToByteArray(WriterState writerState) throws IOException { ByteArrayOutputStream byteArrayOutputStream = writerState.byteArrayOutputStream; @@ -101,6 +112,14 @@ public OutputStream openMetricsWriteToNull() throws IOException { return nullOutputStream; } + @Benchmark + public Writer openMetricsReusingWriter(ReusableWriterState state) throws IOException { + state.openMetricsByteArrayOutputStream.reset(); + OPEN_METRICS_TEXT_FORMAT_WRITER.write( + state.openMetricsWriter, SNAPSHOTS, EscapingScheme.ALLOW_UTF8); + return state.openMetricsWriter; + } + @Benchmark public OutputStream prometheusWriteToByteArray(WriterState writerState) throws IOException { ByteArrayOutputStream byteArrayOutputStream = writerState.byteArrayOutputStream; @@ -116,4 +135,12 @@ public OutputStream prometheusWriteToNull() throws IOException { PROMETHEUS_TEXT_FORMAT_WRITER.write(nullOutputStream, SNAPSHOTS, EscapingScheme.ALLOW_UTF8); return nullOutputStream; } + + @Benchmark + public Writer prometheusReusingWriter(ReusableWriterState state) throws IOException { + state.prometheusByteArrayOutputStream.reset(); + PROMETHEUS_TEXT_FORMAT_WRITER.write( + state.prometheusWriter, SNAPSHOTS, EscapingScheme.ALLOW_UTF8); + return state.prometheusWriter; + } } diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java index 19e1e4c93..ddd764786 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java @@ -144,6 +144,11 @@ public OpenMetrics2Properties getOpenMetrics2Properties() { public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme scheme) throws IOException { Writer writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); + write(writer, metricSnapshots, scheme); + } + + public void write(Writer writer, MetricSnapshots metricSnapshots, EscapingScheme scheme) + throws IOException { MetricSnapshots merged = TextFormatUtil.mergeDuplicates(metricSnapshots); for (MetricSnapshot s : merged) { MetricSnapshot snapshot = SnapshotEscaper.escapeMetricSnapshot(s, scheme); diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java index 4cf2068ae..eeebad40c 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java @@ -114,6 +114,11 @@ public String getContentType() { public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme scheme) throws IOException { Writer writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); + write(writer, metricSnapshots, scheme); + } + + public void write(Writer writer, MetricSnapshots metricSnapshots, EscapingScheme scheme) + throws IOException { MetricSnapshots merged = TextFormatUtil.mergeDuplicates(metricSnapshots); for (MetricSnapshot s : merged) { MetricSnapshot snapshot = SnapshotEscaper.escapeMetricSnapshot(s, scheme); @@ -157,8 +162,9 @@ private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme sc throws IOException { MetricMetadata metadata = snapshot.getMetadata(); writeMetadata(writer, "gauge", metadata, scheme); + String name = getMetadataName(metadata, scheme); for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme); + writeNameAndLabels(writer, name, null, data.getLabels(), scheme); writeDouble(writer, data.getValue()); if (exemplarsOnAllMetricTypesEnabled) { writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); @@ -192,6 +198,8 @@ private void writeClassicHistogramBuckets( throws IOException { String name = getMetadataName(metadata, scheme); String bucketName = name + "_bucket"; + String countName = name + countSuffix; + String sumName = name + sumSuffix; for (HistogramSnapshot.HistogramDataPointSnapshot data : dataList) { ClassicHistogramBuckets buckets = getClassicBuckets(data); Exemplars exemplars = data.getExemplars(); @@ -217,9 +225,9 @@ private void writeClassicHistogramBuckets( } // In OpenMetrics format, histogram _count and _sum are either both present or both absent. if (data.hasCount() && data.hasSum()) { - writeCountAndSum(writer, metadata, data, countSuffix, sumSuffix, exemplars, scheme); + writeCountAndSum(writer, countName, sumName, data, exemplars, scheme); } - writeCreated(writer, metadata, data, scheme); + writeCreated(writer, name, data, scheme); } } @@ -237,6 +245,9 @@ void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme throws IOException { boolean metadataWritten = false; MetricMetadata metadata = snapshot.getMetadata(); + String name = getMetadataName(metadata, scheme); + String countName = name + "_count"; + String sumName = name + "_sum"; for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { continue; @@ -254,13 +265,7 @@ void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme int exemplarIndex = 1; for (Quantile quantile : data.getQuantiles()) { writeNameAndLabels( - writer, - getMetadataName(metadata, scheme), - null, - data.getLabels(), - scheme, - "quantile", - quantile.getQuantile()); + writer, name, null, data.getLabels(), scheme, "quantile", quantile.getQuantile()); writeDouble(writer, quantile.getValue()); if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) { exemplarIndex = (exemplarIndex + 1) % exemplars.size(); @@ -270,8 +275,8 @@ void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme } } // Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics. - writeCountAndSum(writer, metadata, data, "_count", "_sum", exemplars, scheme); - writeCreated(writer, metadata, data, scheme); + writeCountAndSum(writer, countName, sumName, data, exemplars, scheme); + writeCreated(writer, name, data, scheme); } } @@ -292,9 +297,10 @@ private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingSch throws IOException { MetricMetadata metadata = snapshot.getMetadata(); writeMetadata(writer, "stateset", metadata, scheme); + String name = getMetadataName(metadata, scheme); for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) { for (int i = 0; i < data.size(); i++) { - writer.write(getMetadataName(metadata, scheme)); + writer.write(name); writer.write('{'); Labels labels = data.getLabels(); for (int j = 0; j < labels.size(); j++) { @@ -309,7 +315,7 @@ private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingSch if (!labels.isEmpty()) { writer.write(","); } - writer.write(getMetadataName(metadata, scheme)); + writer.write(name); writer.write("=\""); writeEscapedString(writer, data.getName(i)); writer.write("\"} "); @@ -327,8 +333,9 @@ private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingSchem throws IOException { MetricMetadata metadata = snapshot.getMetadata(); writeMetadata(writer, "unknown", metadata, scheme); + String name = getMetadataName(metadata, scheme); for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme); + writeNameAndLabels(writer, name, null, data.getLabels(), scheme); writeDouble(writer, data.getValue()); if (exemplarsOnAllMetricTypesEnabled) { writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); @@ -340,16 +347,14 @@ private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingSchem private void writeCountAndSum( Writer writer, - MetricMetadata metadata, + String countName, + String sumName, DistributionDataPointSnapshot data, - String countSuffix, - String sumSuffix, Exemplars exemplars, EscapingScheme scheme) throws IOException { if (data.hasCount()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), countSuffix, data.getLabels(), scheme); + writeNameAndLabels(writer, countName, null, data.getLabels(), scheme); writeLong(writer, data.getCount()); if (exemplarsOnAllMetricTypesEnabled) { writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme); @@ -358,19 +363,12 @@ private void writeCountAndSum( } } if (data.hasSum()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), sumSuffix, data.getLabels(), scheme); + writeNameAndLabels(writer, sumName, null, data.getLabels(), scheme); writeDouble(writer, data.getSum()); writeScrapeTimestampAndExemplar(writer, data, null, scheme); } } - private void writeCreated( - Writer writer, MetricMetadata metadata, DataPointSnapshot data, EscapingScheme scheme) - throws IOException { - writeCreated(writer, getMetadataName(metadata, scheme), data, scheme); - } - private void writeCreated( Writer writer, String baseName, DataPointSnapshot data, EscapingScheme scheme) throws IOException { diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java index d388810ca..2b68919c9 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java @@ -112,10 +112,15 @@ public String getContentType() { @Override public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme scheme) throws IOException { + Writer writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); + write(writer, metricSnapshots, scheme); + } + + public void write(Writer writer, MetricSnapshots metricSnapshots, EscapingScheme scheme) + throws IOException { // See https://prometheus.io/docs/instrumenting/exposition_formats/ // "unknown", "gauge", "counter", "stateset", "info", "histogram", "gaugehistogram", and // "summary". - Writer writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); MetricSnapshots merged = TextFormatUtil.mergeDuplicates(metricSnapshots); for (MetricSnapshot s : merged) { MetricSnapshot snapshot = escapeMetricSnapshot(s, scheme); @@ -193,8 +198,9 @@ private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme sc throws IOException { MetricMetadata metadata = snapshot.getMetadata(); writeMetadata(writer, "", "gauge", metadata, scheme); + String name = getMetadataName(metadata, scheme); for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme); + writeNameAndLabels(writer, name, null, data.getLabels(), scheme); writeDouble(writer, data.getValue()); writeScrapeTimestampAndNewline(writer, data); } @@ -206,6 +212,8 @@ private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingS writeMetadata(writer, "", "histogram", metadata, scheme); String name = getMetadataName(metadata, scheme); String bucketName = name + "_bucket"; + String countName = name + "_count"; + String sumName = name + "_sum"; for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) { ClassicHistogramBuckets buckets = getClassicBuckets(data); long cumulativeCount = 0; @@ -224,14 +232,12 @@ private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingS } if (!snapshot.isGaugeHistogram()) { if (data.hasCount()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_count", data.getLabels(), scheme); + writeNameAndLabels(writer, countName, null, data.getLabels(), scheme); writeLong(writer, data.getCount()); writeScrapeTimestampAndNewline(writer, data); } if (data.hasSum()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_sum", data.getLabels(), scheme); + writeNameAndLabels(writer, sumName, null, data.getLabels(), scheme); writeDouble(writer, data.getSum()); writeScrapeTimestampAndNewline(writer, data); } @@ -257,6 +263,9 @@ private void writeGaugeCountSum( throws IOException { // Prometheus text format does not support gaugehistogram's _gcount and _gsum. // So we append _gcount and _gsum as gauge metrics. + String baseName = getMetadataName(metadata, scheme); + String gaugeCountName = baseName + "_gcount"; + String gaugeSumName = baseName + "_gsum"; boolean metadataWritten = false; for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) { if (data.hasCount()) { @@ -264,8 +273,7 @@ private void writeGaugeCountSum( writeMetadata(writer, "_gcount", "gauge", metadata, scheme); metadataWritten = true; } - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_gcount", data.getLabels(), scheme); + writeNameAndLabels(writer, gaugeCountName, null, data.getLabels(), scheme); writeLong(writer, data.getCount()); writeScrapeTimestampAndNewline(writer, data); } @@ -277,8 +285,7 @@ private void writeGaugeCountSum( writeMetadata(writer, "_gsum", "gauge", metadata, scheme); metadataWritten = true; } - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_gsum", data.getLabels(), scheme); + writeNameAndLabels(writer, gaugeSumName, null, data.getLabels(), scheme); writeDouble(writer, data.getSum()); writeScrapeTimestampAndNewline(writer, data); } @@ -289,6 +296,9 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem throws IOException { boolean metadataWritten = false; MetricMetadata metadata = snapshot.getMetadata(); + String name = getMetadataName(metadata, scheme); + String countName = name + "_count"; + String sumName = name + "_sum"; for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { continue; @@ -299,25 +309,17 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem } for (Quantile quantile : data.getQuantiles()) { writeNameAndLabels( - writer, - getMetadataName(metadata, scheme), - null, - data.getLabels(), - scheme, - "quantile", - quantile.getQuantile()); + writer, name, null, data.getLabels(), scheme, "quantile", quantile.getQuantile()); writeDouble(writer, quantile.getValue()); writeScrapeTimestampAndNewline(writer, data); } if (data.hasCount()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_count", data.getLabels(), scheme); + writeNameAndLabels(writer, countName, null, data.getLabels(), scheme); writeLong(writer, data.getCount()); writeScrapeTimestampAndNewline(writer, data); } if (data.hasSum()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_sum", data.getLabels(), scheme); + writeNameAndLabels(writer, sumName, null, data.getLabels(), scheme); writeDouble(writer, data.getSum()); writeScrapeTimestampAndNewline(writer, data); } @@ -340,9 +342,10 @@ private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingSch throws IOException { MetricMetadata metadata = snapshot.getMetadata(); writeMetadata(writer, "", "gauge", metadata, scheme); + String name = getMetadataName(metadata, scheme); for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) { for (int i = 0; i < data.size(); i++) { - writer.write(getMetadataName(metadata, scheme)); + writer.write(name); writer.write('{'); for (int j = 0; j < data.getLabels().size(); j++) { if (j > 0) { @@ -356,7 +359,7 @@ private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingSch if (!data.getLabels().isEmpty()) { writer.write(","); } - writer.write(getMetadataName(metadata, scheme)); + writer.write(name); writer.write("=\""); writeEscapedString(writer, data.getName(i)); writer.write("\"} "); @@ -374,8 +377,9 @@ private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingSchem throws IOException { MetricMetadata metadata = snapshot.getMetadata(); writeMetadata(writer, "", "untyped", metadata, scheme); + String name = getMetadataName(metadata, scheme); for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme); + writeNameAndLabels(writer, name, null, data.getLabels(), scheme); writeDouble(writer, data.getValue()); writeScrapeTimestampAndNewline(writer, data); } @@ -424,7 +428,8 @@ private void writeMetadata( MetricMetadata metadata, EscapingScheme scheme) throws IOException { - String name = getMetadataName(metadata, scheme) + (suffix != null ? suffix : ""); + String baseName = getMetadataName(metadata, scheme); + String name = suffix != null ? baseName + suffix : baseName; if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { writer.write("# HELP "); writeName(writer, name, NameType.Metric); diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java index 5f5f05e8b..8cfed2b29 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java @@ -67,7 +67,22 @@ public static MetricSnapshots mergeDuplicates(MetricSnapshots metricSnapshots) { } static void writeLong(Writer writer, long value) throws IOException { - writer.append(Long.toString(value)); + if (value == Long.MIN_VALUE) { + writer.write("-9223372036854775808"); + return; + } + char[] buf = new char[20]; + int pos = 20; + boolean negative = value < 0; + long v = negative ? -value : value; + do { + buf[--pos] = (char) ('0' + (v % 10)); + v /= 10; + } while (v > 0); + if (negative) { + buf[--pos] = '-'; + } + writer.write(buf, pos, 20 - pos); } static void writeDouble(Writer writer, double d) throws IOException { @@ -77,7 +92,6 @@ static void writeDouble(Writer writer, double d) throws IOException { writer.write("-Inf"); } else { writer.write(Double.toString(d)); - // FloatingDecimal.getBinaryToASCIIConverter(d).appendTo(writer); } } @@ -86,7 +100,7 @@ static void writePrometheusTimestamp(Writer writer, long timestampMs, boolean ti if (timestampsInMs) { // correct for prometheus exposition format // https://prometheus.io/docs/instrumenting/exposition_formats/#text-format-details - writer.write(Long.toString(timestampMs)); + writeLong(writer, timestampMs); } else { // incorrect for prometheus exposition format - // but we need to support it for backwards compatibility @@ -95,7 +109,7 @@ static void writePrometheusTimestamp(Writer writer, long timestampMs, boolean ti } static void writeOpenMetricsTimestamp(Writer writer, long timestampMs) throws IOException { - writer.write(Long.toString(timestampMs / 1000L)); + writeLong(writer, timestampMs / 1000L); writer.write("."); long ms = timestampMs % 1000; if (ms < 100) { @@ -104,7 +118,7 @@ static void writeOpenMetricsTimestamp(Writer writer, long timestampMs) throws IO if (ms < 10) { writer.write("0"); } - writer.write(Long.toString(ms)); + writeLong(writer, ms); } static void writeEscapedString(Writer writer, String s) throws IOException { From 5fe88b16c00fd64c5d482fccbbd7926cb8575a58 Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Fri, 1 May 2026 16:37:04 -0400 Subject: [PATCH 4/6] fix lint Signed-off-by: Jay DeLuca --- .../metrics/benchmarks/HistogramTextFormatBenchmark.java | 3 +-- mise.toml | 1 + .../expositionformats/OpenMetricsTextFormatWriter.java | 8 +------- .../expositionformats/PrometheusTextFormatWriter.java | 8 +------- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java index 0ef1bbc2a..616e0c3e3 100644 --- a/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java +++ b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java @@ -20,8 +20,7 @@ import org.openjdk.jmh.annotations.State; /** - * Benchmarks for writing a classic histogram (10 label combinations × 12 buckets) to text - * formats. + * Benchmarks for writing a classic histogram (10 label combinations × 12 buckets) to text formats. * *

Two variants per format: * diff --git a/mise.toml b/mise.toml index cf5cb8657..803f0e09b 100644 --- a/mise.toml +++ b/mise.toml @@ -60,6 +60,7 @@ run = "./mvnw install -DskipTests -Dcoverage.skip=true" [tasks."lint"] description = "Run all lints" depends = ["lint:bom"] +raw_args = true run = "flint run" [tasks."lint:fix"] diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java index eeebad40c..83c98fde2 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java @@ -207,13 +207,7 @@ private void writeClassicHistogramBuckets( for (int i = 0; i < buckets.size(); i++) { cumulativeCount += buckets.getCount(i); writeNameAndLabels( - writer, - bucketName, - null, - data.getLabels(), - scheme, - "le", - buckets.getUpperBound(i)); + writer, bucketName, null, data.getLabels(), scheme, "le", buckets.getUpperBound(i)); writeLong(writer, cumulativeCount); Exemplar exemplar; if (i == 0) { diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java index 2b68919c9..165992f25 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java @@ -220,13 +220,7 @@ private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingS for (int i = 0; i < buckets.size(); i++) { cumulativeCount += buckets.getCount(i); writeNameAndLabels( - writer, - bucketName, - null, - data.getLabels(), - scheme, - "le", - buckets.getUpperBound(i)); + writer, bucketName, null, data.getLabels(), scheme, "le", buckets.getUpperBound(i)); writeLong(writer, cumulativeCount); writeScrapeTimestampAndNewline(writer, data); } From b29c0f14e0e2de64ad3f5e71a5ac4751e93dc562 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 2 May 2026 11:32:34 +0000 Subject: [PATCH 5/6] update benchmarks Signed-off-by: Ubuntu --- .../HistogramTextFormatBenchmark.java | 60 +++++++++++++++++-- mise.toml | 10 ++++ 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java index 616e0c3e3..1699bc827 100644 --- a/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java +++ b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java @@ -15,24 +15,43 @@ import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; /** * Benchmarks for writing a classic histogram (10 label combinations × 12 buckets) to text formats. * - *

Two variants per format: + *

Three variants per format: * *

    - *
  • {@code writeToByteArray} — OutputStream path, new BufferedWriter created per call. - *
  • {@code reusingWriter} — Writer path, BufferedWriter reused across calls. + *
  • {@code writeToByteArray} — OutputStream path, new BufferedWriter created per call (~41 + * KB/op; IO stack allocation dominates). + *
  • {@code writeToNull} — OutputStream path to /dev/null, new BufferedWriter still created per + * call (same allocation as writeToByteArray; shows ByteArrayOutputStream write cost is + * negligible). + *
  • {@code reusingWriter} — Writer path, BufferedWriter reused across calls (~18 KB/op; saves + * the ~25 KB BufferedWriter char[] buffer and OutputStreamWriter per call). + *
  • {@code reusingWriterToNull} — Writer path, BufferedWriter reused, output to /dev/null. + * Lowest-allocation floor; isolates pure formatting CPU cost with zero IO stack overhead. *
* - *

Baseline (before allocation optimizations): ~41 KB/op for both Prometheus and OpenMetrics - * formats, dominated by the per-call BufferedWriter buffer (~16 KB) and number-to-string - * conversions. + *

Key allocation sources: + * + *

    + *
  • BufferedWriter internal char[8192] buffer: 16,384 bytes per call (eliminated by reuse). + *
  • OutputStreamWriter + StreamEncoder state: ~1–2 KB per call (eliminated by reuse). + *
  • Remaining ~18 KB/op in reuse variants: ByteArrayOutputStream byte[] growth + any + * per-call String allocations remaining in the format path. + *
*/ +@Fork(3) +@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 10, time = 2, timeUnit = TimeUnit.SECONDS) public class HistogramTextFormatBenchmark { private static final MetricSnapshots SNAPSHOTS; @@ -95,6 +114,21 @@ public ReusableWriterState() { } } + @State(Scope.Benchmark) + public static class ReusableWriterToNullState { + + final BufferedWriter openMetricsWriter; + final BufferedWriter prometheusWriter; + + public ReusableWriterToNullState() { + OutputStream nullOutputStream = TextFormatUtilBenchmark.NullOutputStream.INSTANCE; + this.openMetricsWriter = + new BufferedWriter(new OutputStreamWriter(nullOutputStream, StandardCharsets.UTF_8)); + this.prometheusWriter = + new BufferedWriter(new OutputStreamWriter(nullOutputStream, StandardCharsets.UTF_8)); + } + } + @Benchmark public OutputStream openMetricsWriteToByteArray(WriterState writerState) throws IOException { ByteArrayOutputStream byteArrayOutputStream = writerState.byteArrayOutputStream; @@ -119,6 +153,13 @@ public Writer openMetricsReusingWriter(ReusableWriterState state) throws IOExcep return state.openMetricsWriter; } + @Benchmark + public Writer openMetricsReusingWriterToNull(ReusableWriterToNullState state) throws IOException { + OPEN_METRICS_TEXT_FORMAT_WRITER.write( + state.openMetricsWriter, SNAPSHOTS, EscapingScheme.ALLOW_UTF8); + return state.openMetricsWriter; + } + @Benchmark public OutputStream prometheusWriteToByteArray(WriterState writerState) throws IOException { ByteArrayOutputStream byteArrayOutputStream = writerState.byteArrayOutputStream; @@ -142,4 +183,11 @@ public Writer prometheusReusingWriter(ReusableWriterState state) throws IOExcept state.prometheusWriter, SNAPSHOTS, EscapingScheme.ALLOW_UTF8); return state.prometheusWriter; } + + @Benchmark + public Writer prometheusReusingWriterToNull(ReusableWriterToNullState state) throws IOException { + PROMETHEUS_TEXT_FORMAT_WRITER.write( + state.prometheusWriter, SNAPSHOTS, EscapingScheme.ALLOW_UTF8); + return state.prometheusWriter; + } } diff --git a/mise.toml b/mise.toml index 803f0e09b..dac8930c4 100644 --- a/mise.toml +++ b/mise.toml @@ -110,6 +110,16 @@ echo "Running benchmarks with args: $JMH_ARGS" java -jar ./benchmarks/target/benchmarks.jar -rf json -rff benchmark-results.json $JMH_ARGS """ +[tasks."benchmark:format"] +description = "Run format benchmarks only (HistogramTextFormat + TextFormatUtil), ~5 min" +run = """ +./mvnw -pl benchmarks -am -DskipTests clean package -q +java -jar ./benchmarks/target/benchmarks.jar \ + "HistogramTextFormatBenchmark|TextFormatUtilBenchmark" \ + -prof gc \ + -rf json -rff format-benchmark-results.json +""" + [tasks."benchmark:generate-summary"] description = "Generate summary from existing benchmark-results.json" run = "python3 ./.mise/tasks/generate_benchmark_summary.py" From b1f89baf39db0bedd2215c12f7535cf0f43dc75a Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Sat, 2 May 2026 13:18:14 -0400 Subject: [PATCH 6/6] format Signed-off-by: Jay DeLuca --- .../benchmarks/HistogramTextFormatBenchmark.java | 4 ++-- mise.toml | 10 ---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java index 1699bc827..559f38638 100644 --- a/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java +++ b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramTextFormatBenchmark.java @@ -45,8 +45,8 @@ *
    *
  • BufferedWriter internal char[8192] buffer: 16,384 bytes per call (eliminated by reuse). *
  • OutputStreamWriter + StreamEncoder state: ~1–2 KB per call (eliminated by reuse). - *
  • Remaining ~18 KB/op in reuse variants: ByteArrayOutputStream byte[] growth + any - * per-call String allocations remaining in the format path. + *
  • Remaining ~18 KB/op in reuse variants: ByteArrayOutputStream byte[] growth + any per-call + * String allocations remaining in the format path. *
*/ @Fork(3) diff --git a/mise.toml b/mise.toml index dac8930c4..803f0e09b 100644 --- a/mise.toml +++ b/mise.toml @@ -110,16 +110,6 @@ echo "Running benchmarks with args: $JMH_ARGS" java -jar ./benchmarks/target/benchmarks.jar -rf json -rff benchmark-results.json $JMH_ARGS """ -[tasks."benchmark:format"] -description = "Run format benchmarks only (HistogramTextFormat + TextFormatUtil), ~5 min" -run = """ -./mvnw -pl benchmarks -am -DskipTests clean package -q -java -jar ./benchmarks/target/benchmarks.jar \ - "HistogramTextFormatBenchmark|TextFormatUtilBenchmark" \ - -prof gc \ - -rf json -rff format-benchmark-results.json -""" - [tasks."benchmark:generate-summary"] description = "Generate summary from existing benchmark-results.json" run = "python3 ./.mise/tasks/generate_benchmark_summary.py"