Skip to content

Commit 7016bf7

Browse files
marschalldhoard
andauthored
Use bulk string writes for text formats (#1273)
Further optimize `TextFormatUtil#writeEscapedLabelValue`. We're seeing `TextFormatUtil#writeEscapedLabelValue` show up in our production traces due to the single `char` writes to `OutputStreamWriter`. We're using `OpenMetricsTextFormatWriter`. #1241 and #1248 should take care of most of these issues but there still remains some optimization potential left. `BufferedWriter#write(int)` has some minimal overhead in terms of locking. If we assume that rarely any characters need to be escaped and instead optimize to write as large of a part of the String as possible in one method call. Before --------- ``` Benchmark Mode Cnt Score Error Units TextFormatUtilBenchmark.openMetricsWriteToByteArray thrpt 25 438485.372 ± 4270.355 ops/s TextFormatUtilBenchmark.openMetricsWriteToNull thrpt 25 440105.281 ± 2891.572 ops/s TextFormatUtilBenchmark.prometheusWriteToByteArray thrpt 25 467213.001 ± 878.780 ops/s TextFormatUtilBenchmark.prometheusWriteToNull thrpt 25 472931.759 ± 976.028 ops/s ``` After ------- ``` Benchmark Mode Cnt Score Error Units TextFormatUtilBenchmark.openMetricsWriteToByteArray thrpt 25 462852.243 ± 5071.696 ops/s TextFormatUtilBenchmark.openMetricsWriteToNull thrpt 25 469910.681 ± 1670.430 ops/s TextFormatUtilBenchmark.prometheusWriteToByteArray thrpt 25 482362.506 ± 2051.684 ops/s TextFormatUtilBenchmark.prometheusWriteToNull thrpt 25 487707.557 ± 3344.881 ops/s ``` About a 5% to 6% gain for `OpenMetricsTextFormatWriter` and around 3% for `PrometheusTextFormatWriter`. Note that this benchmark is actually for `OpenMetricsTextFormatWriter` and `PrometheusTextFormatWriter` since `TextFormatUtil` is not public and can therefore not be benchmarked directly. The relative gains in `#writeEscapedLabelValue` are higher because the benchmark runs the full text format writers. I also added a test for `TextFormatUtil#writeEscapedLabelValue` since the code is now more complicated. --------- Signed-off-by: Philippe Marschall <[email protected]> Co-authored-by: Doug Hoard <[email protected]>
1 parent 39a83b9 commit 7016bf7

File tree

4 files changed

+189
-18
lines changed

4 files changed

+189
-18
lines changed

benchmarks/pom.xml

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,13 @@
4242
<version>${jmh.version}</version>
4343
</dependency>
4444
<dependency>
45-
<groupId>org.openjdk.jmh</groupId>
46-
<artifactId>jmh-generator-annprocess</artifactId>
47-
<version>${jmh.version}</version>
48-
</dependency>
49-
<!--
50-
<dependency>
51-
<groupId>javax.annotation</groupId>
52-
<artifactId>javax.annotation-api</artifactId>
53-
<version>1.3.2</version>
45+
<groupId>io.prometheus</groupId>
46+
<artifactId>prometheus-metrics-core</artifactId>
47+
<version>${project.version}</version>
5448
</dependency>
55-
-->
5649
<dependency>
5750
<groupId>io.prometheus</groupId>
58-
<artifactId>prometheus-metrics-core</artifactId>
51+
<artifactId>prometheus-metrics-exposition-textformats</artifactId>
5952
<version>${project.version}</version>
6053
</dependency>
6154
<dependency>
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package io.prometheus.metrics.benchmarks;
2+
3+
import io.prometheus.metrics.expositionformats.ExpositionFormatWriter;
4+
import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter;
5+
import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter;
6+
import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
7+
import io.prometheus.metrics.model.snapshots.GaugeSnapshot.GaugeDataPointSnapshot;
8+
import io.prometheus.metrics.model.snapshots.Labels;
9+
import io.prometheus.metrics.model.snapshots.MetricSnapshot;
10+
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
11+
import io.prometheus.metrics.model.snapshots.SummarySnapshot;
12+
import io.prometheus.metrics.model.snapshots.SummarySnapshot.SummaryDataPointSnapshot;
13+
import io.prometheus.metrics.model.snapshots.Unit;
14+
import java.io.ByteArrayOutputStream;
15+
import java.io.IOException;
16+
import java.io.OutputStream;
17+
import org.openjdk.jmh.annotations.Benchmark;
18+
import org.openjdk.jmh.annotations.Scope;
19+
import org.openjdk.jmh.annotations.State;
20+
21+
public class TextFormatUtilBenchmark {
22+
23+
private static final MetricSnapshots SNAPSHOTS;
24+
25+
static {
26+
MetricSnapshot gaugeSnapshot =
27+
GaugeSnapshot.builder()
28+
.name("gauge_snapshot_name")
29+
.dataPoint(
30+
GaugeDataPointSnapshot.builder()
31+
.labels(Labels.of("name", "value"))
32+
.scrapeTimestampMillis(1000L)
33+
.value(123.45d)
34+
.build())
35+
.build();
36+
37+
MetricSnapshot summaryDataPointSnapshot =
38+
SummarySnapshot.builder()
39+
.name("summary_snapshot_name_bytes")
40+
.dataPoint(
41+
SummaryDataPointSnapshot.builder()
42+
.count(5)
43+
.labels(Labels.of("name", "value"))
44+
.sum(123456d)
45+
.build())
46+
.unit(Unit.BYTES)
47+
.build();
48+
49+
SNAPSHOTS = MetricSnapshots.of(gaugeSnapshot, summaryDataPointSnapshot);
50+
}
51+
52+
private static final ExpositionFormatWriter OPEN_METRICS_TEXT_FORMAT_WRITER =
53+
new OpenMetricsTextFormatWriter(false, false);
54+
private static final ExpositionFormatWriter PROMETHEUS_TEXT_FORMAT_WRITER =
55+
new PrometheusTextFormatWriter(false);
56+
57+
@State(Scope.Benchmark)
58+
public static class WriterState {
59+
60+
final ByteArrayOutputStream byteArrayOutputStream;
61+
62+
public WriterState() {
63+
this.byteArrayOutputStream = new ByteArrayOutputStream();
64+
}
65+
}
66+
67+
@Benchmark
68+
public OutputStream openMetricsWriteToByteArray(WriterState writerState) throws IOException {
69+
// avoid growing the array
70+
ByteArrayOutputStream byteArrayOutputStream = writerState.byteArrayOutputStream;
71+
byteArrayOutputStream.reset();
72+
OPEN_METRICS_TEXT_FORMAT_WRITER.write(byteArrayOutputStream, SNAPSHOTS);
73+
return byteArrayOutputStream;
74+
}
75+
76+
@Benchmark
77+
public OutputStream openMetricsWriteToNull() throws IOException {
78+
OutputStream nullOutputStream = NullOutputStream.INSTANCE;
79+
OPEN_METRICS_TEXT_FORMAT_WRITER.write(nullOutputStream, SNAPSHOTS);
80+
return nullOutputStream;
81+
}
82+
83+
@Benchmark
84+
public OutputStream prometheusWriteToByteArray(WriterState writerState) throws IOException {
85+
// avoid growing the array
86+
ByteArrayOutputStream byteArrayOutputStream = writerState.byteArrayOutputStream;
87+
byteArrayOutputStream.reset();
88+
PROMETHEUS_TEXT_FORMAT_WRITER.write(byteArrayOutputStream, SNAPSHOTS);
89+
return byteArrayOutputStream;
90+
}
91+
92+
@Benchmark
93+
public OutputStream prometheusWriteToNull() throws IOException {
94+
OutputStream nullOutputStream = NullOutputStream.INSTANCE;
95+
PROMETHEUS_TEXT_FORMAT_WRITER.write(nullOutputStream, SNAPSHOTS);
96+
return nullOutputStream;
97+
}
98+
99+
static final class NullOutputStream extends OutputStream {
100+
101+
static final OutputStream INSTANCE = new NullOutputStream();
102+
103+
private NullOutputStream() {
104+
super();
105+
}
106+
107+
@Override
108+
public void write(int b) {}
109+
110+
@Override
111+
public void write(byte[] b) {}
112+
113+
@Override
114+
public void write(byte[] b, int off, int len) {}
115+
116+
@Override
117+
public void flush() {}
118+
119+
@Override
120+
public void close() {}
121+
}
122+
}

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

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,53 @@ static void writeTimestamp(Writer writer, long timestampMs) throws IOException {
3535
}
3636

3737
static void writeEscapedLabelValue(Writer writer, String s) throws IOException {
38-
for (int i = 0; i < s.length(); i++) {
39-
char c = s.charAt(i);
38+
// optimize for the common case where no escaping is needed
39+
int start = 0;
40+
// #indexOf is a vectorized intrinsic
41+
int backslashIndex = s.indexOf('\\', start);
42+
int quoteIndex = s.indexOf('\"', start);
43+
int newlineIndex = s.indexOf('\n', start);
44+
45+
int allEscapesIndex = backslashIndex & quoteIndex & newlineIndex;
46+
while (allEscapesIndex != -1) {
47+
int escapeStart = Integer.MAX_VALUE;
48+
if (backslashIndex != -1) {
49+
escapeStart = backslashIndex;
50+
}
51+
if (quoteIndex != -1) {
52+
escapeStart = Math.min(escapeStart, quoteIndex);
53+
}
54+
if (newlineIndex != -1) {
55+
escapeStart = Math.min(escapeStart, newlineIndex);
56+
}
57+
58+
// bulk write up to the first character that needs to be escaped
59+
if (escapeStart > start) {
60+
writer.write(s, start, escapeStart - start);
61+
}
62+
char c = s.charAt(escapeStart);
63+
start = escapeStart + 1;
4064
switch (c) {
4165
case '\\':
42-
writer.append("\\\\");
66+
writer.write("\\\\");
67+
backslashIndex = s.indexOf('\\', start);
4368
break;
4469
case '\"':
45-
writer.append("\\\"");
70+
writer.write("\\\"");
71+
quoteIndex = s.indexOf('\"', start);
4672
break;
4773
case '\n':
48-
writer.append("\\n");
74+
writer.write("\\n");
75+
newlineIndex = s.indexOf('\n', start);
4976
break;
50-
default:
51-
writer.append(c);
5277
}
78+
79+
allEscapesIndex = backslashIndex & quoteIndex & newlineIndex;
80+
}
81+
// up until the end nothing needs to be escaped anymore
82+
int remaining = s.length() - start;
83+
if (remaining > 0) {
84+
writer.write(s, start, remaining);
5385
}
5486
}
5587

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package io.prometheus.metrics.expositionformats;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.io.IOException;
6+
import java.io.StringWriter;
7+
import org.junit.jupiter.api.Test;
8+
9+
class TextFormatUtilTest {
10+
11+
@Test
12+
void writeEscapedLabelValue() throws IOException {
13+
assertEquals("aa\\\\bb\\\"cc\\ndd\\nee\\\\ff\\\"gg", escape("aa\\bb\"cc\ndd\nee\\ff\"gg"));
14+
assertEquals("\\\\", escape("\\"));
15+
assertEquals("\\\\\\\\", escape("\\\\"));
16+
assertEquals("text", escape("text"));
17+
}
18+
19+
private static String escape(String s) throws IOException {
20+
StringWriter writer = new StringWriter();
21+
TextFormatUtil.writeEscapedLabelValue(writer, s);
22+
return writer.toString();
23+
}
24+
}

0 commit comments

Comments
 (0)