Skip to content

Commit 5d36228

Browse files
perf: scan metric name validity once per metric family (#2443)
## Summary The Prometheus, OpenMetrics, and OpenMetrics 2 text writers validate the metric name with `PrometheusNaming.isValidLegacyMetricName(name)` on **every data point line** — once to choose bare-vs-braced metric syntax, and again inside `writeName()`. The metric name is constant across all data points of a family, so for a family with N series this re-scans the same string ~2N times per scrape. This change scans the name **once per family** and threads the boolean result down through `writeNameAndLabels()` / `writeName()` (a new `writeName(writer, name, nameType, boolean validLegacyName)` overload trusts the precomputed value). A suffix built from legacy characters (`_total`, `_bucket`, `_count`, `_created`, …) never changes legacy validity, so the base-name result applies to the suffixed names too. ## No behavior change Output is byte-identical — the full exposition-format test suite passes unchanged, including the golden-output `ExpositionFormatsTest` across every format and escaping scheme, `OpenMetrics2TextFormatWriterTest`, and `PrometheusNamingTest`. This is a CPU-only optimization; per-scrape allocation is unchanged. ## Benchmark A local high-cardinality scrape+serialize benchmark (one counter family, 100,000 series; JMH `-f 1 -wi 4 -i 5`, GC profiler): | | ops/s | gc.alloc.rate.norm | |---|---|---| | before | 6.876 ± 0.176 | 50,921,114 B/op | | after | 7.346 ± 0.327 | 50,921,081 B/op | ~+7% throughput on this path, allocation unchanged. The effect scales with the number of series and the metric-name length. ## Notes The per-label-name validity check is a similar (and larger, O(N × labels)) per-scrape rescan and is a natural follow-up; this PR intentionally scopes to the metric name only. This pull request and its description were written by Isaac. Signed-off-by: David Mollitor <david.mollitor@databricks.com> Co-authored-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
1 parent b26fbf2 commit 5d36228

4 files changed

Lines changed: 194 additions & 90 deletions

File tree

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

Lines changed: 69 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,10 @@ private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingSchem
180180
if (openMetrics2Properties.getSuffixes()) {
181181
counterName = ensureSuffix(counterName, "_total");
182182
}
183+
boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(counterName);
183184
writeMetadataWithName(writer, counterName, "counter", metadata);
184185
for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) {
185-
writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme);
186+
writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme, nameValidLegacy);
186187
writeDouble(writer, data.getValue());
187188
if (data.hasScrapeTimestamp()) {
188189
writer.write(' ');
@@ -201,9 +202,10 @@ private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme sc
201202
throws IOException {
202203
MetricMetadata metadata = snapshot.getMetadata();
203204
String name = getMetricName(metadata, scheme);
205+
boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name);
204206
writeMetadataWithName(writer, name, "gauge", metadata);
205207
for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) {
206-
writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
208+
writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy);
207209
writeDouble(writer, data.getValue());
208210
if (exemplarsOnAllMetricTypesEnabled) {
209211
writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
@@ -219,40 +221,51 @@ private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingS
219221
openMetrics2Properties.getCompositeValues() || openMetrics2Properties.getNativeHistograms();
220222
MetricMetadata metadata = snapshot.getMetadata();
221223
String name = getMetricName(metadata, scheme);
224+
boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name);
222225
if (!compositeHistogram && !openMetrics2Properties.getExemplarCompliance()) {
223-
writeClassicHistogram(writer, name, snapshot, scheme);
226+
writeClassicHistogram(writer, name, snapshot, scheme, nameValidLegacy);
224227
return;
225228
}
226229
if (snapshot.isGaugeHistogram()) {
227230
writeMetadataWithName(writer, name, "gaugehistogram", metadata);
228231
for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
229232
if (openMetrics2Properties.getNativeHistograms() && data.hasNativeHistogramData()) {
230-
writeNativeHistogramDataPoint(writer, name, "gcount", "gsum", data, scheme, false);
233+
writeNativeHistogramDataPoint(
234+
writer, name, "gcount", "gsum", data, scheme, false, nameValidLegacy);
231235
} else {
232-
writeCompositeHistogramDataPoint(writer, name, "gcount", "gsum", data, scheme, false);
236+
writeCompositeHistogramDataPoint(
237+
writer, name, "gcount", "gsum", data, scheme, false, nameValidLegacy);
233238
}
234239
}
235240
} else {
236241
writeMetadataWithName(writer, name, "histogram", metadata);
237242
for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
238243
if (openMetrics2Properties.getNativeHistograms() && data.hasNativeHistogramData()) {
239-
writeNativeHistogramDataPoint(writer, name, "count", "sum", data, scheme, true);
244+
writeNativeHistogramDataPoint(
245+
writer, name, "count", "sum", data, scheme, true, nameValidLegacy);
240246
} else {
241-
writeCompositeHistogramDataPoint(writer, name, "count", "sum", data, scheme, true);
247+
writeCompositeHistogramDataPoint(
248+
writer, name, "count", "sum", data, scheme, true, nameValidLegacy);
242249
}
243250
}
244251
}
245252
}
246253

247254
private void writeClassicHistogram(
248-
Writer writer, String name, HistogramSnapshot snapshot, EscapingScheme scheme)
255+
Writer writer,
256+
String name,
257+
HistogramSnapshot snapshot,
258+
EscapingScheme scheme,
259+
boolean nameValidLegacy)
249260
throws IOException {
250261
if (snapshot.isGaugeHistogram()) {
251262
writeMetadataWithName(writer, name, "gaugehistogram", snapshot.getMetadata());
252-
writeClassicHistogramDataPoints(writer, name, "_gcount", "_gsum", snapshot, scheme);
263+
writeClassicHistogramDataPoints(
264+
writer, name, "_gcount", "_gsum", snapshot, scheme, nameValidLegacy);
253265
} else {
254266
writeMetadataWithName(writer, name, "histogram", snapshot.getMetadata());
255-
writeClassicHistogramDataPoints(writer, name, "_count", "_sum", snapshot, scheme);
267+
writeClassicHistogramDataPoints(
268+
writer, name, "_count", "_sum", snapshot, scheme, nameValidLegacy);
256269
}
257270
}
258271

@@ -262,7 +275,8 @@ private void writeClassicHistogramDataPoints(
262275
String countSuffix,
263276
String sumSuffix,
264277
HistogramSnapshot snapshot,
265-
EscapingScheme scheme)
278+
EscapingScheme scheme,
279+
boolean nameValidLegacy)
266280
throws IOException {
267281
String bucketName = name + "_bucket";
268282
for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
@@ -272,7 +286,14 @@ private void writeClassicHistogramDataPoints(
272286
for (int i = 0; i < buckets.size(); i++) {
273287
cumulativeCount += buckets.getCount(i);
274288
writeNameAndLabels(
275-
writer, bucketName, null, data.getLabels(), scheme, "le", buckets.getUpperBound(i));
289+
writer,
290+
bucketName,
291+
null,
292+
data.getLabels(),
293+
scheme,
294+
"le",
295+
buckets.getUpperBound(i),
296+
nameValidLegacy);
276297
writeLong(writer, cumulativeCount);
277298
Exemplar exemplar;
278299
if (i == 0) {
@@ -283,9 +304,10 @@ private void writeClassicHistogramDataPoints(
283304
writeScrapeTimestampAndExemplar(writer, data, exemplar, scheme);
284305
}
285306
if (data.hasCount() && data.hasSum()) {
286-
writeClassicCountAndSum(writer, name, data, countSuffix, sumSuffix, exemplars, scheme);
307+
writeClassicCountAndSum(
308+
writer, name, data, countSuffix, sumSuffix, exemplars, scheme, nameValidLegacy);
287309
}
288-
writeClassicCreated(writer, name, data, scheme);
310+
writeClassicCreated(writer, name, data, scheme, nameValidLegacy);
289311
}
290312
}
291313

@@ -296,16 +318,17 @@ private void writeClassicCountAndSum(
296318
String countSuffix,
297319
String sumSuffix,
298320
Exemplars exemplars,
299-
EscapingScheme scheme)
321+
EscapingScheme scheme,
322+
boolean nameValidLegacy)
300323
throws IOException {
301-
writeNameAndLabels(writer, name, countSuffix, data.getLabels(), scheme);
324+
writeNameAndLabels(writer, name, countSuffix, data.getLabels(), scheme, nameValidLegacy);
302325
writeLong(writer, data.getCount());
303326
if (exemplarsOnAllMetricTypesEnabled) {
304327
writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme);
305328
} else {
306329
writeScrapeTimestampAndExemplar(writer, data, null, scheme);
307330
}
308-
writeNameAndLabels(writer, name, sumSuffix, data.getLabels(), scheme);
331+
writeNameAndLabels(writer, name, sumSuffix, data.getLabels(), scheme, nameValidLegacy);
309332
writeDouble(writer, data.getSum());
310333
writeScrapeTimestampAndExemplar(writer, data, null, scheme);
311334
}
@@ -314,10 +337,11 @@ private void writeClassicCreated(
314337
Writer writer,
315338
String name,
316339
HistogramSnapshot.HistogramDataPointSnapshot data,
317-
EscapingScheme scheme)
340+
EscapingScheme scheme,
341+
boolean nameValidLegacy)
318342
throws IOException {
319343
if (createdTimestampsEnabled && data.hasCreatedTimestamp()) {
320-
writeNameAndLabels(writer, name, "_created", data.getLabels(), scheme);
344+
writeNameAndLabels(writer, name, "_created", data.getLabels(), scheme, nameValidLegacy);
321345
writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis());
322346
if (data.hasScrapeTimestamp()) {
323347
writer.write(' ');
@@ -334,9 +358,10 @@ private void writeCompositeHistogramDataPoint(
334358
String sumKey,
335359
HistogramSnapshot.HistogramDataPointSnapshot data,
336360
EscapingScheme scheme,
337-
boolean includeStartTimestamp)
361+
boolean includeStartTimestamp,
362+
boolean nameValidLegacy)
338363
throws IOException {
339-
writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
364+
writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy);
340365
writer.write('{');
341366
writer.write(countKey);
342367
writer.write(':');
@@ -366,9 +391,10 @@ private void writeNativeHistogramDataPoint(
366391
String sumKey,
367392
HistogramSnapshot.HistogramDataPointSnapshot data,
368393
EscapingScheme scheme,
369-
boolean includeStartTimestamp)
394+
boolean includeStartTimestamp,
395+
boolean nameValidLegacy)
370396
throws IOException {
371-
writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
397+
writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy);
372398
writer.write('{');
373399
writer.write(countKey);
374400
writer.write(':');
@@ -489,6 +515,7 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem
489515
om1Writer.writeSummary(writer, snapshot, scheme, name);
490516
return;
491517
}
518+
boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name);
492519
boolean metadataWritten = false;
493520
for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) {
494521
if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) {
@@ -498,17 +525,18 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem
498525
writeMetadataWithName(writer, name, "summary", metadata);
499526
metadataWritten = true;
500527
}
501-
writeCompositeSummaryDataPoint(writer, name, data, scheme);
528+
writeCompositeSummaryDataPoint(writer, name, data, scheme, nameValidLegacy);
502529
}
503530
}
504531

505532
private void writeCompositeSummaryDataPoint(
506533
Writer writer,
507534
String name,
508535
SummarySnapshot.SummaryDataPointSnapshot data,
509-
EscapingScheme scheme)
536+
EscapingScheme scheme,
537+
boolean nameValidLegacy)
510538
throws IOException {
511-
writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
539+
writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy);
512540
writer.write('{');
513541
boolean first = true;
514542
if (data.hasCount()) {
@@ -557,9 +585,10 @@ private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme sche
557585
// OM2 spec: Info MetricFamily name MUST end in _info.
558586
// In OM2, TYPE/HELP use the same name as the data lines.
559587
String infoName = ensureSuffix(getMetricName(metadata, scheme), "_info");
588+
boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(infoName);
560589
writeMetadataWithName(writer, infoName, "info", metadata);
561590
for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) {
562-
writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme);
591+
writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme, nameValidLegacy);
563592
writer.write("1");
564593
writeScrapeTimestampAndExemplar(writer, data, null, scheme);
565594
}
@@ -605,9 +634,10 @@ private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingSchem
605634
throws IOException {
606635
MetricMetadata metadata = snapshot.getMetadata();
607636
String name = getMetricName(metadata, scheme);
637+
boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name);
608638
writeMetadataWithName(writer, name, "unknown", metadata);
609639
for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) {
610-
writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
640+
writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy);
611641
writeDouble(writer, data.getValue());
612642
if (exemplarsOnAllMetricTypesEnabled) {
613643
writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
@@ -622,9 +652,10 @@ private void writeNameAndLabels(
622652
String name,
623653
@Nullable String suffix,
624654
Labels labels,
625-
EscapingScheme escapingScheme)
655+
EscapingScheme escapingScheme,
656+
boolean nameValidLegacy)
626657
throws IOException {
627-
writeNameAndLabels(writer, name, suffix, labels, escapingScheme, null, 0.0);
658+
writeNameAndLabels(writer, name, suffix, labels, escapingScheme, null, 0.0, nameValidLegacy);
628659
}
629660

630661
private void writeNameAndLabels(
@@ -634,16 +665,19 @@ private void writeNameAndLabels(
634665
Labels labels,
635666
EscapingScheme escapingScheme,
636667
@Nullable String additionalLabelName,
637-
double additionalLabelValue)
668+
double additionalLabelValue,
669+
boolean nameValidLegacy)
638670
throws IOException {
639671
boolean metricInsideBraces = false;
640-
// If the name does not pass the legacy validity check, we must put the
641-
// metric name inside the braces.
642-
if (!PrometheusNaming.isValidLegacyMetricName(name)) {
672+
// If the name does not pass the legacy validity check, we must put the metric name inside the
673+
// braces. Validity is scanned once per family by the caller (the metric name is constant across
674+
// all data points), not re-scanned for every series. A suffix built from legacy characters
675+
// (_total, _bucket, ...) does not change legacy validity, so the base-name check applies here.
676+
if (!nameValidLegacy) {
643677
metricInsideBraces = true;
644678
writer.write('{');
645679
}
646-
writeName(writer, suffix != null ? name + suffix : name, NameType.Metric);
680+
writeName(writer, suffix != null ? name + suffix : name, NameType.Metric, nameValidLegacy);
647681
if (!labels.isEmpty() || additionalLabelName != null) {
648682
writeLabels(
649683
writer,

0 commit comments

Comments
 (0)