Skip to content

Commit 4292b07

Browse files
authored
Merge pull request #374 from ProjectASAP/feat/merger-cpu-io-bench
feat(merger): CPU/IO benchmark for ingest/blockbuild/compact/ship — fills Fig 11 gap
2 parents fd67f1c + 198bd70 commit 4292b07

3 files changed

Lines changed: 587 additions & 0 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package merger
2+
3+
// compare_bench_test.go — SAME-MACHINE, SAME-WORKLOAD throughput comparison of
4+
// the merger's decode-free central ingest against the path Thanos Receive uses:
5+
// the Prometheus TSDB head Appender (parse-done -> Append encodes into head XOR
6+
// chunks + indexes each series -> Commit writes the head WAL). This is the
7+
// per-sample CPU the merger OFFLOADS to the edge (the edge pre-encodes the XOR
8+
// chunk; the merger ingests it decode-free and only pays the per-series index at
9+
// block-build time).
10+
//
11+
// The Prometheus path decodes the SAME XOR frags the merger ingests, so both
12+
// systems ingest byte-identical samples. Run all on tmpfs for a CPU-bound,
13+
// apples-to-apples number (the fsync-bearing stages are IO-bound on spinning
14+
// disk and would otherwise measure the disk, not the engine):
15+
//
16+
// TMPDIR=/dev/shm GOPRIVATE='github.com/ProjectASAP/*' go test ./internal/merger/ \
17+
// -run '^$' -bench 'IngestFragmentBatch|WindowToPendingBlock|PrometheusTSDBAppendCommit' \
18+
// -benchmem -benchtime=10x -count=3
19+
//
20+
// Fair comparison metric = "central CPU per sample to make data queryable+indexed":
21+
// - Prometheus/Thanos: BenchmarkPrometheusTSDBAppendCommit (append+commit; head
22+
// is indexed & queryable immediately).
23+
// - Merger: BenchmarkIngestFragmentBatch + BenchmarkWindowToPendingBlock
24+
// (ingest is decode-free and NOT yet queryable; block-build is where the
25+
// merger pays the per-series index — that sum is the honest queryable gate,
26+
// NOT the raw ingest figure alone).
27+
//
28+
// === MEASURED, SAME MACHINE (Xeon Gold 5512U), SAME WORKLOAD (1000 series x 120
29+
// samples = 120k samples/window), SINGLE CORE, tmpfs (/dev/shm) ===
30+
//
31+
// path (per core) per 120k samples/s/core basis
32+
// ASAP merger — decode-free ingest 23.8 ms 5.04 M buffer pre-encoded XOR chunks + WAL (no per-sample parse/encode)
33+
// VictoriaMetrics v1.145 remote_write — 3.20 M snappy/proto parse + AddRows (bg part-merge deferred); SERVER-CONFIRMED 6.0M rows, 1000 series queryable
34+
// Prometheus TSDB v0.308 (Thanos core) 87.0 ms 1.38 M per-sample encode + index + WAL commit (head queryable; input pre-decoded, so this is the pure TSDB-core cost)
35+
// ASAP merger — window->pending block ~225 ms ~0.53 M full on-disk TSDB block per 2-min window (deferred/background; the merger's heaviest per-sample stage)
36+
//
37+
// Read (honest):
38+
// - Single-core ingest-to-queryable-buffer: merger 5.0M > VM 3.2M > Prometheus
39+
// 1.4M. The merger leads because the EDGE pre-encoded the XOR chunk (offloaded
40+
// ~1.38 us/sample edge cost, Fig 11), so central ingest is decode-free. VM
41+
// beats Prometheus-TSDB because its ingest path is more optimised, not because
42+
// of any ASAP work.
43+
// - Persisting on-disk blocks is a DEFERRED/background cost for all three (VM
44+
// part-merge, Prometheus 2h head->block, merger per-window block-build). The
45+
// merger's block-build (~0.53M/s/core here) is where its deferred per-series
46+
// index lands and is its slowest stage — a real perf target, not a win.
47+
// - VM CAVEAT: remote_write timestamps MUST be within -retentionPeriod or VM
48+
// parses-then-DROPS them (an early run timestamped at the corpus's 2023 base
49+
// showed a bogus 11.5 M/s "accept" rate with 0 series stored). The 3.20 M/s
50+
// above is with near-now timestamps and is server-confirmed queryable.
51+
// - allocs/window: merger ingest 14.1k, Prometheus append 22.3k.
52+
53+
import (
54+
"context"
55+
"testing"
56+
57+
"github.com/prometheus/prometheus/model/labels"
58+
"github.com/prometheus/prometheus/storage"
59+
"github.com/prometheus/prometheus/tsdb"
60+
"github.com/prometheus/prometheus/tsdb/chunkenc"
61+
)
62+
63+
// decodedSeries is the corpus expressed as raw (labels, samples) — the form a
64+
// remote_write/scrape ingester (the Prometheus TSDB head that Thanos Receive
65+
// embeds) consumes. Decoded ONCE, outside the timed loop, from the same XOR
66+
// frags the merger ingests.
67+
type decodedSeries struct {
68+
ls labels.Labels
69+
ts []int64
70+
val []float64
71+
}
72+
73+
func decodeCorpus(tb testing.TB, c benchCorpus) []decodedSeries {
74+
tb.Helper()
75+
out := make([]decodedSeries, 0, len(c.frags))
76+
for fi := range c.frags {
77+
f := &c.frags[fi]
78+
chk, err := chunkenc.FromData(chunkenc.EncXOR, f.Data)
79+
if err != nil {
80+
tb.Fatal(err)
81+
}
82+
ds := decodedSeries{ls: labelsFor(f.MetricName, f.Attributes)}
83+
it := chk.Iterator(nil)
84+
for it.Next() == chunkenc.ValFloat {
85+
t, v := it.At()
86+
ds.ts = append(ds.ts, t)
87+
ds.val = append(ds.val, v)
88+
}
89+
if err := it.Err(); err != nil {
90+
tb.Fatal(err)
91+
}
92+
out = append(out, ds)
93+
}
94+
return out
95+
}
96+
97+
// BenchmarkPrometheusTSDBAppendCommit — the Thanos Receive central ingest path
98+
// (Prometheus TSDB head Appender). Per 120k-sample window: Append every
99+
// (series,t,v) (encodes into head XOR chunks + indexes each series) then Commit
100+
// (head WAL). Same workload/machine/disk as the merger benches.
101+
func BenchmarkPrometheusTSDBAppendCommit(b *testing.B) {
102+
corpus := buildBenchCorpus(benchNumSeries, 1, benchPerChunk)
103+
series := decodeCorpus(b, corpus)
104+
ctx := context.Background()
105+
b.SetBytes(int64(corpus.numSamples) * 16) // 16 B/sample (8B ts + 8B f64) raw
106+
b.ReportAllocs()
107+
b.ResetTimer()
108+
for i := 0; i < b.N; i++ {
109+
b.StopTimer()
110+
dir := b.TempDir()
111+
db, err := tsdb.Open(dir, nil, nil, tsdb.DefaultOptions(), nil)
112+
if err != nil {
113+
b.Fatal(err)
114+
}
115+
// keep each iteration's window strictly forward of the last so the head
116+
// never rejects out-of-order (fresh DB per iter also guarantees this).
117+
off := int64(i+1) * int64(benchPerChunk) * 1000 * 2
118+
b.StartTimer()
119+
120+
app := db.Appender(ctx)
121+
for si := range series {
122+
s := &series[si]
123+
var ref storage.SeriesRef
124+
for k := range s.ts {
125+
var aerr error
126+
ref, aerr = app.Append(ref, s.ls, s.ts[k]+off, s.val[k])
127+
if aerr != nil {
128+
b.Fatal(aerr)
129+
}
130+
}
131+
}
132+
if err := app.Commit(); err != nil {
133+
b.Fatal(err)
134+
}
135+
136+
b.StopTimer()
137+
_ = db.Close()
138+
b.StartTimer()
139+
}
140+
}

0 commit comments

Comments
 (0)