Skip to content

Count pump records in a flat rmgr by route array - #263

Merged
serprex merged 1 commit into
mainfrom
split/record-counters
Oct 23, 2026
Merged

serprex merged 1 commit into
mainfrom
split/record-counters

Conversation

@serprex

@serprex serprex commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Pump metrics update a BTreeMap for every WAL record. Index counters by rmgr and route in a fixed array, preserving snapshot order and existing metric values.

Extracted from original #228 and based directly on main. Contains no changes from #226 or other extracted PRs.

Validation: cargo fmt --all -- --check, cargo clippy --workspace --all-targets --locked -- -D warnings, and cargo test --locked --lib source::wal_stream (29 passed). Rebuild crate for each isolated diff.

Measured impact (2026-10-22)

Compared c96afaaa5298862d99193fe8c97c3d7d51bf03a1 against shared parent 151fd47cbd75ec7b9bc63ffe7040f049e702ecd0, with each PR built separately.

Workload Base (ns/record) PR (ns/record) Median change
Counter observation, 24 rmgr/route pairs 6.80 [6.67, 7.01] 0.75 [0.68, 0.80] -89.0%

Workload: 10,240,000 calls to production MetricsRecordSink::observe, cycling 4,096 prepared records across eight rmgr IDs and three routes. Check exact total; consume summary after timing. Measures counter updates only; array initialization and snapshot enumeration remain outside timing. No end-to-end replication speedup established.

AMD Ryzen 7 7840U; rustc 1.99.1; locked dependencies; optimized release builds; mimalloc 0.1.52. Seven trials per revision, alternating base/PR order, pinned to CPUs 2,4,6 (three physical cores). Values show median [min, max]; negative time changes mean less work or waiting. Local component measurements, without PostgreSQL or ClickHouse services.

@serprex

serprex commented Oct 22, 2026

Copy link
Copy Markdown
Member Author

Compared 16-rmgr chunks with flat counters from this PR. Chunking saves memory when observed rmgr IDs cluster, with a small recurring update cost.

Prototype: [Option<Box<[[u64; 3]; 16]>>; 16], allocating a chunk on first observation and skipping absent chunks in by_rm_route(). Keeps full u8 rmgr domain and sorted (rmgr, route) enumeration.

Archived workload: 4,096 prepared records cycling rmgr IDs [10, 1, 11, 0, 9, 2, 8, 12] and three routes, giving 24 populated pairs in one chunk. Seven trials per variant with rotated/reversed execution order; 40,960,000 observations per trial. Values below show update median [min, max], plus enumeration and metric-map medians.

Representation Storage, bytes Allocations Update, ns/record Enumerate, ns Build metric map, ns
BTreeMap 640 4 11.39 [11.21, 11.51] 24 1,284
Boxed flat array, PR head 6,160 1 0.84 [0.80, 0.89] 516 1,431
Inline flat array 6,152 0 0.81 [0.77, 0.82] 519 1,439
16-rmgr chunks 520 1 0.93 [0.90, 0.96] 30 1,429

Against inline array, chunking saves 5,632 bytes (91.5%), costs 15.6% per increment (+0.13 ns), and enumerates 17.1× faster. Despite faster checksum enumeration, metric-map construction remains roughly unchanged in this harness. Two populated chunks would use 904 bytes.

Additional distributions, same prepared-record count and observation count:

Distribution Flat inline update, ns/record Chunked update, ns/record Flat inline storage, bytes Chunked storage, bytes
8 IDs spread across 8 chunks, 24 pairs 0.80 0.93 6,152 3,208
All 256 IDs, 768 pairs 0.83 1.02 6,152 6,280

Storage includes total, inline struct size, and requested live heap bytes. Excludes allocator rounding/metadata, input records, and reporting output. Allocation counts cover sink construction and first observations. Flat-array initialization stays outside update timing; lazy chunk/map allocations happen inside. Memory uses a separately instrumented executable; timing uses uninstrumented mimalloc.

Method: extracted counter implementations from base 151fd47 and PR head c96afaa into one standalone optimized Rust harness, with production Record, Route, and labeling source. Inline variant removes boxing; chunked variant changes storage, increment indexing, and enumeration. Validate exact totals, nonzero output, and sorted counts across all 256 IDs and three routes. Enumeration consumes a checksum; metric-map timing builds production-shaped labels and BTreeMap output. AMD Ryzen 7 7840U, rustc 1.99.1, -C opt-level=3, mimalloc 0.1.52, affinity CPUs 2,4,6. Repeat runs at original 10,240,000-observation count are also retained.

Control caveat: archived original binaries rerun at their original 10,240,000 observations give BTreeMap 6.84 [6.64, 7.12] and PR array 0.75 [0.69, 0.79] ns/record, consistent with existing PR measurements. Standalone BTreeMap timing differs materially, so treat absolute timings as harness-specific and do not use its ratios to revise published PR speedup. Flat/chunk comparisons above use identical harness. No end-to-end replication claim.

Choose chunking for lower per-session storage, especially with one or two populated chunks. Choose inline array for lowest update cost and no counter allocations.

@serprex

serprex commented Oct 22, 2026

Copy link
Copy Markdown
Member Author

Follow-up using actual WAL rather than synthetic rmgr selection: ordinary OLTP stayed in one 16-ID chunk; second chunk depended on features producing IDs 16–21. No evidence here supports assuming two chunks for ordinary workloads.

Inspected retained WAL on running PostgreSQL 17.11, then generated workloads in an isolated database with pg_walinspect. Recorded WAL boundaries before/after each phase and grouped records by resource manager. Both 4,000-transaction pgbench runs completed without failures.

WAL sample Records IDs 16–21 emitted Chunks needed by interval
Retained system WAL, before experiment 4,192 None 1
Initialize pgbench, 100,000 accounts 100,418 None 1
Standard pgbench, 4,000 transactions 29,392 None 1
VACUUM FREEZE / ANALYZE / REINDEX / DDL / DML 9,273 None 1
10,000 inserts with Btree / Hash / GIN / GiST / sequence 60,819 None 1
Create BRIN index 132 BRIN=107 2
Standard pgbench with BRIN, 4,000 transactions 29,202 BRIN=107 2
Append 10,000 accounts and summarize BRIN 20,778 BRIN=749 2
Build SPGiST index 27 None 1
10,000 inserts with SPGiST 73,413 SPGist=10,000 2
Create bloom index 392 Generic=215 2
Emit one transactional logical message 2 LogicalMessage=1 2

Existing schema survey across 28 databases found 12,933 Btree indexes, 617 GIN, 15 GiST, and one BRIN; no SPGiST or bloom indexes. Existing BRIN table had zero recorded inserts/updates/deletes, consistent with its absence from retained WAL sample. This development cluster is not a production prevalence sample.

Intervals above are counted independently. One sink observing full experiment would retain second chunk from BRIN creation onward. One high-ID record suffices to allocate second chunk for remaining sink lifetime. Physical WAL spans all databases, so BRIN/SPGiST or another high-ID producer in any database matters. wal_level=logical alone did not emit LogicalMessage records; explicit pg_logical_emit_message did. track_commit_timestamp was off, and replication-origin/commit-timestamp workloads were not tested. SPGiST bulk build used XLOG/FPI, so index existence alone does not guarantee ID 16; subsequent inserts emitted 10,000 SPGiST records.

For reference, existing fixtures also split: filter (42,091 records), xlog_switch (106), and vacuum_full_pg_depend (874) use one chunk; mixed-index classify (29,738) uses two, with 401 SPGiST and 403 BRIN records.

Implication for chunk size: 16 IDs uses 520 bytes for ordinary low-ID traffic; 32 IDs uses 840 bytes. When any built-in ID 16–21 appears, 16 IDs uses 904 bytes versus 840 for 32 IDs. All sizes include total and requested heap storage, excluding allocator overhead. Thus 32 IDs spends 320 extra bytes in one-chunk workloads to save 64 bytes and one allocation in two-chunk workloads. These WAL observations establish occupancy, not a new timing comparison.

Experiment database and its extensions were removed after recording results. Existing application tables and server settings were unchanged.

@serprex
serprex force-pushed the split/record-counters branch from c96afaa to a7f6b51 Compare October 22, 2026 19:00
MetricsRecordSink bumped a BTreeMap entry for every WAL record on the
pump thread, 2.7% of its cycles on the lifecycle benchmark. Rmgr ids
fit in a byte and routes in three slots, so a 6 KiB array indexes the
counter directly; readers iterate the nonzero cells in the same order
the map gave.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@serprex
serprex force-pushed the split/record-counters branch from a7f6b51 to 6e4955e Compare October 22, 2026 19:05
@serprex
serprex requested a review from harshil-goel October 22, 2026 19:05
@serprex
serprex merged commit b84af9f into main Oct 23, 2026
6 checks passed
@serprex
serprex deleted the split/record-counters branch October 23, 2026 01:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants