docs planning RUST_STATION_INGESTION_RST07 - AlexandreZanata/Abastevo GitHub Wiki
RST-07 — Benchmark and index experiment
Status: MEASURED — no production tuning beyond one adopted index, no
partitioning, no national fetch. Date: 2026-10-09. Scope:
RUST_STATION_INGESTION_PLAN.md RST-07. Language: English document; user
communication in Portuguese.
Hardware: Intel i7-13620H workstation, disposable PostGIS 18.6/3.6.4, Go 1.27 toolchain, Rust 1.99 release builds. All inputs synthetic and deterministic (seed 7); nothing below describes the real ANP census, whose size was not measured (national fetch out of scope).
1. Parse: Rust vs Go on the shared 100k snapshot
Generator tools/station-prep/src/bin/bench_generate.rs emits raw and
normalized layouts from one RNG sequence, so both implementations must
accept the same CNPJ multiset. Driver bench_parse.rs prints counts,
rows/sec and the multiset checksum; BenchmarkStageCSV100k asserts the
same accounting and logs its own checksum.
- Rust: 100k rows in 310 ms (322k rows/s), RSS 214 MiB; 1M rows in 3.9 s parse (259k rows/s), RSS 2.19 GiB with raised caps (defaults refuse past 500k rows by design).
- Go
StageCSV: 100k rows in ~0.7–1.0 s (124–152k rows/s), heap 8.2 MiB, accounting 98004/1996/0 identical to Rust. - Multiset checksum equal on both sides:
8e493e7d83a46e6addace8e1dc8c86f14254ad8fa0636e96aefdc2c1e048128a. The checksum recipe itself is pinned by a unit golden test on both sides (SIMP-0001 →6f15da0b…4e75fe23).
Finding: the in-memory accepted-row model costs ~2.2 KiB/row, so the provisional 256 MiB target cannot hold national snapshots without streaming emission (no retained batch). Interim bound stands on the default caps (~1.1 GiB at 500k rows, inside the 4 GiB worker); the streaming follow-up is recorded, not implemented here.
2. Reads: 100k and 1M census-shaped stations
BenchmarkCityReads100k/1M seed skewed reviewed stations (70% in 5
dense cities, cluster near the dense query center) and drive the exact
app path (Reader.Search/Nearby, limit 20) for dense/sparse city and
dense/sparse nearby workloads, baseline indexes versus candidate
(state, municipality_code, status, id).
100k (seed 1.6 s, table 26.4 MiB, indexes 11.4 MiB):
- Baseline p95: city-dense 8.56 ms, city-sparse 1.40 ms, nearby-dense 5.05 ms, nearby-sparse 0.12 ms.
- Candidate: no benefit (planner keeps the existing index; +5.6 MiB).
1M (seed 16.8 s, table 259.2 MiB, indexes 108.9 MiB):
- Baseline p95: city-dense 83.71 ms, city-sparse 3.56 ms, nearby-dense 44.23 ms, nearby-sparse 0.34 ms.
- Candidate p95: city-dense 49.20 ms (−41%, planner picks a parallel index-only scan), others within noise; index build 1.7 s, +56 MiB.
Decision: adopt the composite covering index (migration 000051) and reject table partitioning — reads are single-digit ms at 100k and inside targets at 1M, so partitions would buy complexity without material benefit while threatening global CNPJ uniqueness and stable foreign keys. Sparse workloads stay sub-2 ms at both scales.
3. Revised laboratory targets (synthetic + VPS headroom, db 8 GiB)
- Parser: streaming emission required before national snapshots; interim cap-bound RSS ~1.1 GiB at 500k rows (worker fits, lab ideal does not — follow-up owed, no new claim).
- City first page p95 ≤ 100 ms: holds at 1M baseline (83.7 ms), 49 ms indexed. Nearby p95 ≤ 150 ms: holds (44 ms at 1M). Sparse reads stay trivially inside both.
- Reproduce:
bench_generate 100000 7{,+normalized}thenbench_parse,go test -tags=integration -bench 'BenchmarkStageCSV100k|BenchmarkCityReads(100k|1M)' -benchtime 1x -v.
4. Limits and next task
- Numbers cover synthetic shapes on one workstation; production needs its own bounded smoke before any SLO language.
- Next (smallest): RST-08 incremental operations — bounded discovery schedule, conditional downloads, backoff/circuit stop and crash recovery with every failure tested now.