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,145 @@
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 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.
*
* <p>Two variants per format:
*
* <ul>
* <li>{@code writeToByteArray} — OutputStream path, new BufferedWriter created per call.
* <li>{@code reusingWriter} — Writer path, BufferedWriter reused across calls.
* </ul>
*
* <p>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 {

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

@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 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;
}
}
6 changes: 3 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 Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -190,20 +196,18 @@ private void writeClassicHistogramBuckets(
List<HistogramSnapshot.HistogramDataPointSnapshot> dataList,
EscapingScheme scheme)
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();
long cumulativeCount = 0;
for (int i = 0; i < buckets.size(); i++) {
cumulativeCount += buckets.getCount(i);
writeNameAndLabels(
writer,
getMetadataName(metadata, scheme),
"_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 All @@ -215,9 +219,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);
}
}

Expand All @@ -235,6 +239,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;
Expand All @@ -252,13 +259,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();
Expand All @@ -268,8 +269,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);
}
}

Expand All @@ -290,9 +291,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++) {
Expand All @@ -307,7 +309,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("\"} ");
Expand All @@ -325,8 +327,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);
Expand All @@ -338,16 +341,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);
Expand All @@ -356,19 +357,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 {
Expand Down Expand Up @@ -409,7 +403,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