Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy-zoomies

benches/xml_scan.rs

//! A/B benchmark: generic XML scanner (`xml`) vs OSM specialization (`vtd`).
//!
//! Corpus: deterministic synthetic OSM-ish XML (~100 MB) generated in-memory —
//! same byte stream on every run/machine, so numbers are comparable across the
//! refactor (baseline captured against the pre-extraction vtd, see
//! bench_history.json at the crate root if recorded).
//!
//! Run: `cargo bench -p znippy-zoomies --bench xml_scan`

use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};

// ── Deterministic corpus generator ───────────────────────────────────────────

/// xorshift64* — deterministic, no rand dependency in the hot loop.
struct Rng(u64);
impl Rng {
    fn next(&mut self) -> u64 {
        let mut x = self.0;
        x ^= x >> 12;
        x ^= x << 25;
        x ^= x >> 27;
        self.0 = x;
        x.wrapping_mul(0x2545_F491_4F6C_DD1D)
    }
    fn below(&mut self, n: u64) -> u64 {
        self.next() % n
    }
}

const TAG_KEYS: [&str; 10] = [
    "highway", "building", "natural", "landuse", "waterway", "railway", "amenity", "boundary",
    "name", "surface",
];

/// Generate ~`target_bytes` of OSM-shaped XML: ~85% nodes (mostly self-closing),
/// ways with nd refs + tags, occasional relations. Mirrors planet-file shape.
fn synth_osm_xml(target_bytes: usize) -> Vec<u8> {
    use std::fmt::Write as _;
    let mut out = String::with_capacity(target_bytes + 4096);
    out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
    out.push_str("<osm version=\"0.6\" generator=\"xml_scan bench\">\n");
    out.push_str(" <bound box=\"47.0,9.4,47.3,9.7\" origin=\"bench\"/>\n");

    let mut rng = Rng(0x5EED_CAFE_F00D_BEEF);
    let mut id: i64 = 1_000_000;
    while out.len() < target_bytes {
        id += 1;
        let lat = 47_000_0000i64 + rng.below(3_000_000) as i64;
        let lon = 9_400_0000i64 + rng.below(3_000_000) as i64;
        match rng.below(100) {
            // 80% bare self-closing nodes
            0..=79 => {
                let _ = writeln!(
                    out,
                    " <node id=\"{id}\" lat=\"{}.{:07}\" lon=\"{}.{:07}\" version=\"3\"/>",
                    lat / 10_000_000,
                    lat % 10_000_000,
                    lon / 10_000_000,
                    lon % 10_000_000,
                );
            }
            // 8% tagged nodes
            80..=87 => {
                let _ = writeln!(
                    out,
                    " <node id=\"{id}\" lat=\"{}.{:07}\" lon=\"{}.{:07}\" version=\"3\">",
                    lat / 10_000_000,
                    lat % 10_000_000,
                    lon / 10_000_000,
                    lon % 10_000_000,
                );
                for _ in 0..=rng.below(3) {
                    let k = TAG_KEYS[rng.below(10) as usize];
                    let _ = writeln!(out, "  <tag k=\"{k}\" v=\"value {}\"/>", rng.below(1000));
                }
                out.push_str(" </node>\n");
            }
            // 10% ways
            88..=97 => {
                let _ = writeln!(out, " <way id=\"{id}\" version=\"2\">");
                for _ in 0..(4 + rng.below(20)) {
                    let _ = writeln!(out, "  <nd ref=\"{}\"/>", 1_000_000 + rng.below(id as u64));
                }
                for _ in 0..=rng.below(4) {
                    let k = TAG_KEYS[rng.below(10) as usize];
                    let _ = writeln!(out, "  <tag k=\"{k}\" v=\"value {}\"/>", rng.below(1000));
                }
                out.push_str(" </way>\n");
            }
            // 2% relations
            _ => {
                let _ = writeln!(out, " <relation id=\"{id}\" version=\"1\">");
                for _ in 0..(2 + rng.below(8)) {
                    let _ = writeln!(
                        out,
                        "  <member type=\"way\" ref=\"{}\" role=\"outer\"/>",
                        1_000_000 + rng.below(id as u64),
                    );
                }
                let _ = writeln!(out, "  <tag k=\"boundary\" v=\"administrative\"/>");
                out.push_str(" </relation>\n");
            }
        }
    }
    out.push_str("</osm>\n");
    out.into_bytes()
}

const CORPUS_BYTES: usize = 100 * 1024 * 1024; // ~100 MB

// ── Benches ──────────────────────────────────────────────────────────────────

fn bench_xml_scan(c: &mut Criterion) {
    let corpus = synth_osm_xml(CORPUS_BYTES);
    let n_cores = std::thread::available_parallelism()
        .map(|n| n.get())
        .unwrap_or(4);

    let mut g = c.benchmark_group("xml_scan");
    g.throughput(Throughput::Bytes(corpus.len() as u64));
    g.sample_size(10);

    // ── OSM specialization (vtd) — the pre/post-refactor comparable ─────────
    g.bench_function("vtd/sequential", |b| {
        b.iter(|| {
            let mut count = 0u64;
            znippy_zoomies::vtd::build_elem_index(&corpus, |_| count += 1).unwrap();
            count
        })
    });

    for n in [4, n_cores] {
        g.bench_with_input(BenchmarkId::new("vtd/parallel", n), &n, |b, &n| {
            b.iter(|| {
                let mut count = 0u64;
                znippy_zoomies::vtd::build_elem_index_parallel(&corpus, n, |_| count += 1).unwrap();
                count
            })
        });
    }

    g.bench_function("vtd/count_elements", |b| {
        b.iter(|| znippy_zoomies::vtd::count_elements(&corpus))
    });

    g.bench_function("vtd/find_safe_slot_end", |b| {
        b.iter(|| znippy_zoomies::vtd::find_safe_slot_end(&corpus))
    });

    g.finish();
}

criterion_group!(benches, bench_xml_scan);
criterion_main!(benches);