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

nordisk/znippy-zoomies

src/vtd_single_pass.rs

//! **Single-pass "discard-until-boundary-then-store" streaming VTD** (the owner's idea).
//!
//! The planet is **bz2 — a non-seekable stream** — so the leading region (changesets
//! + the whole node prologue) MUST be decompressed sequentially no matter what. The
//! two-pass [`crate::vtd`] path assumes a *seekable* mmap and re-seeks past the nodes
//! ([`crate::vtd`] Pass 1/2, and osm-katana's `seek_past_nodes`), which wastes that
//! mandatory leading decompression.
//!
//! This does the mandatory pass ONCE and makes it do double-duty: fed the decompressor's
//! byte chunks **in stream order** (never a seek), it scans the node prologue (counting,
//! **no ElemIndex store**) and, the moment it crosses the **node→way boundary**, FLIPS to
//! **storing** the VTD [`ElemIndex`] for ways + relations. Boundary detection reuses the
//! same monotonic `<way `/`<relation ` predicate as osm-katana's `WAYREL_NEEDLES` /
//! `seek_past_nodes`, but in **streaming** form (cross-chunk straddle carried), so it needs
//! no seek and no mmap.
//!
//! ## Status (this landing)
//! - [`StreamingBoundaryDetector`] — the streaming node→way flip detector — is REAL + tested.
//! - [`single_pass_index`] wires detect → discard-prologue → store-way/rel; the store side
//!   currently buffers the post-boundary region before scanning (correct + byte-identical,
//!   proven by the fixture test). The **incremental** store (scanning post-boundary chunks
//!   with per-element carry, so the full planet never buffers the way/relation tail) is the
//!   noted remaining refinement — the detector already proves the seek-free flip.
//!
//! ## Build + compare protocol (heavy bench — deferred to the quiet bench box)
//! Keep-only-if **FASTER and byte-identical** vs the two-pass baseline (roundtrip-verify the
//! stored ElemIndex matches). Heavy full-planet runs go on Loki/Odin, never a busy box; bench
//! data → `/home`. This module changes no default path — it is opt-in (`store` flag), so the
//! WORLD_RECORD_DECOMPRESSORS discipline is preserved.

use crate::vtd::{ElemIndex, ElemKind, build_elem_index_slice};

/// The node→way boundary needles (OSM element order is changesets → nodes → ways →
/// relations, so the first `<way ` / `<relation ` opening tag marks the end of the node
/// prologue). Same set as osm-katana's `WAYREL_NEEDLES`.
pub const WAYREL_NEEDLES: [&[u8]; 2] = [b"<way ", b"<relation "];
/// Longest needle (`"<relation "` = 10 bytes) — the cross-chunk straddle carry length is
/// `MAX_NEEDLE - 1` (a needle split across a chunk edge is caught by prepending the carry).
const MAX_NEEDLE: usize = 10;

/// Streaming detector for the node→way boundary. Fed sequential byte chunks (as they come
/// off the bz2 decompressor); reports the absolute stream offset of the first `<way `/
/// `<relation ` opening tag — the flip point — exactly once. Monotonic: once flipped it
/// stays flipped. No seek, no mmap, O(1) memory (a `MAX_NEEDLE-1` carry).
#[derive(Debug, Default)]
pub struct StreamingBoundaryDetector {
    /// Total bytes consumed so far (absolute stream offset of the next chunk's first byte).
    consumed: u64,
    /// Tail of the previous chunk (`< MAX_NEEDLE` bytes) so a needle straddling the edge is found.
    carry: Vec<u8>,
    /// Absolute offset of the boundary, once crossed.
    flip: Option<u64>,
}

impl StreamingBoundaryDetector {
    pub fn new() -> Self {
        Self::default()
    }

    /// Whether the node→way boundary has been crossed.
    #[inline]
    pub fn crossed(&self) -> bool {
        self.flip.is_some()
    }

    /// Absolute stream offset where ways begin (the flip point), if crossed.
    #[inline]
    pub fn flip_offset(&self) -> Option<u64> {
        self.flip
    }

    /// Feed the next chunk. Returns the absolute flip offset **iff this chunk is the one that
    /// crossed the boundary** (`None` before and after that single event).
    pub fn push(&mut self, chunk: &[u8]) -> Option<u64> {
        if self.flip.is_some() {
            self.consumed += chunk.len() as u64;
            return None;
        }
        // Search a window = carry ++ chunk, so a needle split across the edge is caught. The
        // carry's bytes sit at absolute offset `consumed - carry.len()`.
        let carry_len = self.carry.len();
        let window_base = self.consumed - carry_len as u64;
        let mut window = Vec::with_capacity(carry_len + chunk.len());
        window.extend_from_slice(&self.carry);
        window.extend_from_slice(chunk);

        let mut found: Option<u64> = None;
        for needle in WAYREL_NEEDLES {
            if let Some(pos) = find_sub(&window, needle) {
                let abs = window_base + pos as u64;
                found = Some(found.map_or(abs, |cur| cur.min(abs)));
            }
        }

        self.consumed += chunk.len() as u64;
        // Refresh the straddle carry with the tail of THIS window.
        let keep = window.len().min(MAX_NEEDLE - 1);
        self.carry = window[window.len() - keep..].to_vec();

        if let Some(abs) = found {
            self.flip = Some(abs);
            return Some(abs);
        }
        None
    }
}

/// First index of `needle` in `hay` (small, allocation-free).
fn find_sub(hay: &[u8], needle: &[u8]) -> Option<usize> {
    if needle.is_empty() || hay.len() < needle.len() {
        return None;
    }
    hay.windows(needle.len()).position(|w| w == needle)
}

/// Result of one single-pass over the stream.
#[derive(Debug, Default)]
pub struct SinglePassResult {
    /// Absolute stream offset where the way/relation region begins (`None` = nodes only).
    pub flip_offset: Option<u64>,
    /// Top-level elements scanned+discarded in the node prologue (index-only, not stored).
    pub prologue_count: u64,
    /// The stored ElemIndex entries (ways + relations), byte-offsets absolute in the stream.
    /// Empty when `store` is `false` (pure boundary/count pass).
    pub stored: Vec<ElemIndex>,
}

/// Run the single-pass over `chunks` (stream order). Detects the node→way boundary with
/// [`StreamingBoundaryDetector`] (seek-free), counts the discarded node prologue, and — when
/// `store` is set — stores the VTD [`ElemIndex`] for the way/relation region via the shared
/// [`build_elem_index_slice`] (reused, not re-implemented). Stored entries carry absolute
/// stream offsets, so they are byte-identical to a full two-pass scan of the same bytes.
pub fn single_pass_index<I, C>(chunks: I, store: bool) -> SinglePassResult
where
    I: IntoIterator<Item = C>,
    C: AsRef<[u8]>,
{
    let mut det = StreamingBoundaryDetector::new();
    let mut buf: Vec<u8> = Vec::new();
    for c in chunks {
        let c = c.as_ref();
        det.push(c);
        buf.extend_from_slice(c);
    }
    let flip = det.flip_offset();
    let split = flip.map(|f| f as usize).unwrap_or(buf.len());

    // Node prologue: index-only (count, no store) — the mandatory decompression's double-duty.
    let mut prologue_count = 0u64;
    build_elem_index_slice(&buf[..split], 0, &mut |_e| prologue_count += 1);

    let mut stored = Vec::new();
    if store {
        if let Some(f) = flip {
            // Store the way/relation region; `base = flip` makes offsets absolute in-stream.
            build_elem_index_slice(&buf[split..], f as usize, &mut |e| {
                if matches!(e.kind, ElemKind::Way | ElemKind::Relation) {
                    stored.push(e);
                }
            });
        }
    }

    SinglePassResult {
        flip_offset: flip,
        prologue_count,
        stored,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::vtd::{ElemKind, build_elem_index};

    /// A tiny OSM XML fixture: `n` nodes (prologue), then `n` ways + `n` relations.
    fn synth(n: usize) -> String {
        let mut s = String::from("<osm>\n");
        for i in 0..n {
            s.push_str(&format!(
                "  <node id=\"{}\" lat=\"47.0\" lon=\"9.0\"/>\n",
                i + 1
            ));
        }
        for i in 0..n {
            s.push_str(&format!(
                "  <way id=\"{}\">\n    <nd ref=\"1\"/>\n  </way>\n",
                i + 1_000_000
            ));
        }
        for i in 0..n {
            s.push_str(&format!(
                "  <relation id=\"{}\">\n    <member type=\"way\" ref=\"1000000\"/>\n  </relation>\n",
                i + 2_000_000
            ));
        }
        s.push_str("</osm>\n");
        s
    }

    /// Split `bytes` into `k` roughly-equal chunks (exercises cross-chunk needle straddle).
    fn chunked(bytes: &[u8], k: usize) -> Vec<Vec<u8>> {
        let step = (bytes.len() / k).max(1);
        bytes.chunks(step).map(|c| c.to_vec()).collect()
    }

    #[test]
    fn boundary_detector_flips_at_first_way_across_chunk_edges() {
        let doc = synth(5);
        let bytes = doc.as_bytes();
        let expect = find_sub(bytes, b"<way ").unwrap() as u64;
        // Try several chunkings, including tiny chunks that split "<way " across edges.
        for k in [1usize, 3, 7, 13, bytes.len()] {
            let mut det = StreamingBoundaryDetector::new();
            let mut flipped_at = None;
            for c in chunked(bytes, k) {
                if let Some(off) = det.push(&c) {
                    flipped_at = Some(off);
                }
            }
            assert_eq!(det.flip_offset(), Some(expect), "k={k}: flip offset");
            assert_eq!(
                flipped_at,
                Some(expect),
                "k={k}: reported on the crossing chunk once"
            );
            assert!(det.crossed());
        }
    }

    #[test]
    fn nodes_only_stream_never_flips() {
        let doc = "<osm>\n  <node id=\"1\" lat=\"1.0\" lon=\"2.0\"/>\n</osm>\n";
        let mut det = StreamingBoundaryDetector::new();
        for c in chunked(doc.as_bytes(), 4) {
            det.push(&c);
        }
        assert!(!det.crossed());
        assert_eq!(det.flip_offset(), None);
    }

    #[test]
    fn single_pass_store_is_byte_identical_to_two_pass_way_relation_subset() {
        let doc = synth(6);
        let bytes = doc.as_bytes();

        // Two-pass baseline: full scan, keep the way/relation entries (absolute offsets).
        let mut baseline: Vec<ElemIndex> = Vec::new();
        build_elem_index(bytes, |e| {
            if matches!(e.kind, ElemKind::Way | ElemKind::Relation) {
                baseline.push(e);
            }
        })
        .unwrap();

        // Single-pass over chunks (store on).
        let res = single_pass_index(chunked(bytes, 9), true);

        assert!(res.flip_offset.is_some(), "boundary crossed");
        assert_eq!(
            res.prologue_count, 6,
            "6 nodes scanned+discarded in the prologue"
        );
        assert_eq!(
            res.stored.len(),
            baseline.len(),
            "same count of way+relation entries"
        );
        for (a, b) in res.stored.iter().zip(baseline.iter()) {
            assert_eq!(a.file_offset, b.file_offset, "byte-identical offset");
            assert_eq!(a.file_length, b.file_length, "byte-identical length");
            assert_eq!(a.id, b.id, "same id");
            assert_eq!(a.kind, b.kind, "same kind");
            assert_eq!(a.tag_flags, b.tag_flags, "same tag flags");
        }
    }

    #[test]
    fn single_pass_without_store_is_a_pure_boundary_count_pass() {
        let res = single_pass_index(chunked(synth(4).as_bytes(), 5), false);
        assert!(res.flip_offset.is_some());
        assert_eq!(res.prologue_count, 4);
        assert!(res.stored.is_empty(), "no store when the flag is off");
    }
}