Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package io.prometheus.metrics.benchmarks;

import io.prometheus.metrics.config.EscapingScheme;
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.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 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.
*
* <p>Three variants per format:
*
* <ul>
* <li>{@code writeToByteArray} — OutputStream path, new BufferedWriter created per call (~41
* KB/op; IO stack allocation dominates).
* <li>{@code writeToNull} — OutputStream path to /dev/null, new BufferedWriter still created per
* call (same allocation as writeToByteArray; shows ByteArrayOutputStream write cost is
* negligible).
* <li>{@code reusingWriter} — Writer path, BufferedWriter reused across calls (~18 KB/op; saves
* the ~25 KB BufferedWriter char[] buffer and OutputStreamWriter per call).
* <li>{@code reusingWriterToNull} — Writer path, BufferedWriter reused, output to /dev/null.
* Lowest-allocation floor; isolates pure formatting CPU cost with zero IO stack overhead.
* </ul>
*
* <p>Key allocation sources:
*
* <ul>
* <li>BufferedWriter internal char[8192] buffer: 16,384 bytes per call (eliminated by reuse).
* <li>OutputStreamWriter + StreamEncoder state: ~1–2 KB per call (eliminated by reuse).
* <li>Remaining ~18 KB/op in reuse variants: ByteArrayOutputStream byte[] growth + any
* per-call String allocations remaining in the format path.
* </ul>
*/
@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;

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 OpenMetricsTextFormatWriter OPEN_METRICS_TEXT_FORMAT_WRITER =
OpenMetricsTextFormatWriter.create();
private static final PrometheusTextFormatWriter PROMETHEUS_TEXT_FORMAT_WRITER =
PrometheusTextFormatWriter.create();

@State(Scope.Benchmark)
public static class WriterState {

final ByteArrayOutputStream byteArrayOutputStream;

public WriterState() {
this.byteArrayOutputStream = new ByteArrayOutputStream();
}
}

@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));
}
}

@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;
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 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 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;
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;
}

@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;
}

@Benchmark
public Writer prometheusReusingWriterToNull(ReusableWriterToNullState state) throws IOException {
PROMETHEUS_TEXT_FORMAT_WRITER.write(
state.prometheusWriter, SNAPSHOTS, EscapingScheme.ALLOW_UTF8);
return state.prometheusWriter;
}
}
16 changes: 13 additions & 3 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ run = "./mvnw install -DskipTests -Dcoverage.skip=true"

[tasks."lint"]
description = "Run all lints"
raw_args = true
depends = ["lint:bom"]
raw_args = true
run = "flint run"

[tasks."lint:fix"]
Expand Down Expand Up @@ -95,11 +95,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)"
Expand All @@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -258,14 +263,15 @@ 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();
long cumulativeCount = 0;
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) {
Expand Down Expand Up @@ -636,7 +642,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,
Expand Down
Loading
Loading