{"date": "2026-08-05", "version": "0.1.7", "git_hash": "kvm-lane/osm-chain-kvm", "engine": "gatling", "machine": "oden — AMD Ryzen Threadripper PRO 3975WX", "cores": 32, "note": "FIRST ENTRY. The osm-tiles collector chain a–e measured INSIDE a QEMU/KVM guest (tests/osm_chain_kvm.rs), not on the host: PID 1 is the static `korp-chain-init`, the work directory is a real ext4 on virtio, and the input is the committed 5 492 803-byte Liechtenstein .osm.bz2 — real OSM XML, not a synthetic fixture. Guest: `-cpu host`, 4096 MiB, kernel 7.0.0-28-generic. WHY `-cpu host` IS LOAD-BEARING: on QEMU's default CPU model the first real run died at `[2.510718] traps: init[99] trap invalid opcode` inside the convert thread — the toolchain emits instructions that model does not implement — so every number here is from a guest given the host's feature set. COMPLETENESS IS A RECORDED FACT, NOT A FILESYSTEM GUESS: each step seals a redb completion record at its producer's own completion moment (fsync the artefact, THEN commit the row), and the cells judge on that record with the block-allocation check demoted to corroboration. Sealing costs one fsync + one redb commit per step and is inside every number below; it is not broken out because it did not rise above the run-to-run spread. THE CONVERT/TILE SPLIT IS A DIFFERENCE, NOT A TIMER: the same cell boots twice over one disk; boot 1 converts and tiles, boot 2 reuses the extract and only tiles, so convert = run1 − run2. HONESTY ON THE CORE SCALING: convert measures 13 414 ms at 1 vCPU and 1 117 ms at 4, a 12.0× ratio that is SUPERLINEAR and therefore not explained by parallelism alone. oden was building for five other lanes throughout; identical 4-vCPU work measured 2 210–4 285 ms across four boots (1.94× spread), so host contention is the leading suspect and the 1-vCPU row should be read as an upper bound, not a clean serial baseline. The osmium reference is a DIFFERENT OPERATION (geojsonseq export vs parquet+arrow convert) and ran on the HOST, not in a guest; it is a scale marker, not a like-for-like comparison. The pyramid is known not to be bit-reproducible, and this run saw it: ways.parquet came out 3 820 517 B on three boots and 3 820 536 B on a fourth (19 B, 0.0005 %) from identical input.", "results": [{"name": "chain_convert_bcd_kvm_guest", "converter": "korp_collectors::osm_tiles::cut_region_to_store (osm-katana convert + resolve_relations)", "input_format": "bz2", "input": "liechtenstein.osm.bz2 (real OSM XML, committed in-repo)", "input_mb": 5.49, "input_mb_decompressed": 61.0, "time_s": 1.117, "cores_used": 4, "single_core_mbs": 0.409, "multi_core_mbs": 4.92, "single_core_mbs_decompressed": 4.55, "multi_core_mbs_decompressed": 54.6, "single_core_time_s": 13.414, "reference_tool": "osmium export -f geojsonseq (host, not in a guest; 36–60 % CPU)", "reference_time_s": 7.6, "reference_mbs": 0.72, "reference_mbs_decompressed": 8.03, "speedup_vs_reference": "6.8× walltime at 4 vCPU — but a DIFFERENT operation (GeoJSONSeq export, 31 105 466 B out) and on the host, so read it as a scale marker only", "artefacts_bytes": {"nodes.parquet": 2913812, "ways.parquet": 3820517, "relations.parquet": 249642, "node_coords.arrow": 5651562}, "note": "Every artefact verified by APPLIED OUTPUT read back off the ext4 disk — PAR1 at the tail of each parquet, ARROW1 at BOTH ends of the node store — not by the run's own return value."}, {"name": "chain_tile_pyramid_e_kvm_guest", "converter": "osm_katana::tiles::tile_region via cut_region_to_store (gatling fork-join, NOT rayon)", "input_format": "parquet", "input": "the Liechtenstein extract produced by the row above", "input_mb": 6.98, "time_s": 0.364, "cores_used": 4, "single_core_mbs": 6.1, "multi_core_mbs": 19.18, "single_core_time_s": 1.144, "zooms": [6, 9], "tiles": 4, "reference_tool": "none — no second tiler exists for this pyramid format; the recipe token is stamped in each tile's parquet footer", "reference_mbs": null, "note": "Tiling ALONE, isolated by the reuse boot (the extract already on disk, so no convert in the number). The 3-zoom ladder [6,9,12] produces 56 tiles (z6 1 / z9 3 / z12 52) and cost 1 093 ms by the same subtraction on its best boot. 0 footerless tiles across every boot in this run."}, {"name": "chain_serve_readback_kvm_guest", "converter": "the SERVING read path — object GET + PAR1 footer check + TileRecipe::of_tile_bytes, per tile", "input_format": "parquet", "input": "every tile of the pyramid, read back out of the store", "input_mb": 5.89, "time_s": 0.001705, "cores_used": 1, "single_core_mbs": 3453.0, "multi_core_mbs": 3453.0, "reference_tool": "none — this is the read korp-server performs per map query; there is no second implementation to compare against", "reference_mbs": null, "observations": [{"tiles": 56, "bytes": 5889055, "micros": 1705, "mbs": 3453.0}, {"tiles": 4, "bytes": 1268721, "micros": 352, "mbs": 3604.3}, {"tiles": 11, "bytes": 2528229, "micros": 606, "mbs": 4172.0}], "note": "Serial, single-threaded, warm ext4 page cache inside the guest — an UPPER bound on what the serve path can do, and deliberately so: it is the ceiling the S3-backed path is measured against, not a prediction of it. Three independent pyramids agree within 21 %."}, {"name": "chain_recovery_after_power_cut_kvm_guest", "converter": "cut_region_to_store, second boot over the disk a SIGKILLed guest left behind", "input_format": "bz2", "input": "liechtenstein.osm.bz2", "input_mb": 5.49, "time_s": 1.489, "cores_used": 4, "single_core_mbs": null, "multi_core_mbs": 3.69, "reference_tool": "none — recovery has no throughput peer; the number that matters is that it happened at all", "reference_mbs": null, "note": "The guest is SIGKILLed as a process group 700 ms into the convert — no unwind, no flush, no cooperative stop. The next boot sees the extract INCOMPLETE, refuses to reuse it, reconverts and reaches a footered 4-tile pyramid. Recovery cost is within noise of a cold cut, i.e. the chain pays a full reconvert and does NOT try to salvage the wreckage — which is the intended behaviour, not a regression."}]}