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

nordisk/facett

facett-map/tests/bake_alloc_budget.rs

#![cfg(feature = "wgpu")]
//! **The GPU bake's heap budget** (stinky `mapdraw-5`) — one allocation per way, not
//! a throwaway `Vec` per way.
//!
//! `build_gpu_geometry` expanded every way's quad geometry with
//!
//! ```ignore
//! line_vertices.extend_from_slice(&build_line_geometry(std::slice::from_ref(poly)));
//! ```
//!
//! — a fresh `Vec<LineVertex>` per way, grown by repeated `extend_from_slice` and then
//! memcpy'd into the shared buffer and dropped, plus a fresh `Vec<Join>` per way inside
//! the expander. MEASURED on the real Sweden payload (20 000 ways / 452 697 vertices):
//! **88 807 allocations and 255.2 MB of transient heap to produce 178 MB of buffers**,
//! 70.55 ms wall.
//!
//! # Why a byte budget alone could not see it
//!
//! The throwaway `Vec` allocates roughly what the shared buffer's own doubling
//! allocates — so BYTES barely separate the two shapes. What separates them is the
//! allocation **COUNT**: the temporary costs `O(log(way vertices))` events *per way*,
//! the shared buffer costs `O(log(total))` events *in total*. This file therefore
//! asserts the count, and asserts it as a RATIO to the way count so the number cannot
//! be re-tuned into a tautology when the fixture changes size.
//!
//! # Why it is not a wall-clock test
//!
//! A ms ceiling in the suite fails on machine load, not on code (stinky `mapgpu-2`
//! says so about `facett-core`'s force-layout test, and oden runs eight agents). An
//! allocation count is contended-machine-proof: it is the same number on an idle box
//! and on a box at load 120.
//!
//! # RED PROVEN
//!
//! With the per-way temporary put back (`extend_from_slice(&build_line_geometry(..))`
//! plus the per-way `joins` `Vec`), `the_bake_does_not_allocate_per_way_temporaries`
//! fails — the figures are in the assertion message, which prints what it measured.
//!
//! The refactor's OUTPUT equality is not re-asserted here: `cull_indirect.rs`'s
//! `per_way_quad_expansion_concatenates_to_the_whole_tier_expansion` already pins the
//! per-way expansion byte-for-byte against the whole-slice call, and a second copy of
//! that assertion is exactly the twin LAW 5 rejects.

use facett_map::Way;
use facett_map::gpu::{build_gpu_geometry, ways_origin};
use facett_map::style::MapPalette;

#[path = "common/heap.rs"]
mod heap;

#[global_allocator]
static A: heap::Counting = heap::Counting;

/// `n` unfilled roads of `v` vertices each — the shape and density of the served korp
/// basemap (the real Sweden payload is 452 697 vertices over 12 625 GPU-lane ways,
/// i.e. ~36 vertices a way, which is what `v` defaults to at the call sites below).
fn roads(n: usize, v: usize) -> Vec<Way> {
    let pal = MapPalette::cartographic(true);
    let tags = vec![("highway".to_string(), "primary".to_string())];
    (0..n)
        .filter_map(|i| {
            let (lon0, lat0) = (15.0 + (i % 100) as f64 * 1e-3, 59.0 + (i / 100) as f64 * 1e-3);
            let pts: Vec<(f64, f64)> = (0..v).map(|k| (lon0 + k as f64 * 2e-5, lat0 + k as f64 * 1e-5)).collect();
            Way::from_lonlat_with(i as i64, &pts, &tags, &pal)
        })
        .collect()
}

/// Bake `ways` and report `(allocations, bytes, produced LineVertex count)`.
fn bake_cost(ways: &[Way]) -> (usize, usize, usize) {
    let origin = ways_origin(ways.iter()).unwrap_or([0.0, 0.0]);
    let (geo, allocs, bytes) = heap::measure(|| build_gpu_geometry(ways.iter().map(|w| (w, 0u32)), origin));
    (allocs, bytes, geo.line_vertices.len())
}

/// **THE GUARD.** The bake's allocation count must not scale with the way count
/// beyond the one per-way vector the geometry genuinely needs (the origin-local
/// Mercator centreline, which is MOVED into the polyline and is not a temporary).
///
/// Stated as a ratio, and pinned at BOTH ends:
///
/// * an upper bound of **2.0 allocations a way**, which the per-way-temporary shape
///   cannot meet (it pays one for the throwaway result `Vec`, one for `joins`, and
///   one per doubling of the throwaway as it grows to the way's quad count);
/// * a lower bound asserting the fixture is not trivially small, because "0.4
///   allocations a way" over 10 ways proves nothing.
#[test]
fn the_bake_does_not_allocate_per_way_temporaries() {
    const WAYS: usize = 12_000;
    const VERTS: usize = 36;

    let ways = roads(WAYS, VERTS);
    assert_eq!(ways.len(), WAYS, "fixture must build every way, else the ratio is measured over the wrong n");

    let (allocs, bytes, quad_verts) = bake_cost(&ways);

    // The premise: this really is a big bake, so a per-way cost has room to show.
    assert!(
        quad_verts >= WAYS * (VERTS - 1) * 6,
        "fixture premise: {WAYS} ways x {VERTS} verts must expand to >= {} quad vertices, got {quad_verts}",
        WAYS * (VERTS - 1) * 6
    );

    let per_way = allocs as f64 / WAYS as f64;
    eprintln!(
        "[bake] {WAYS} ways x {VERTS} verts -> {quad_verts} quad vertices: {allocs} allocations \
         ({per_way:.2}/way), {bytes} B"
    );
    assert!(
        per_way <= 2.0,
        "the bake allocated {allocs} times for {WAYS} ways = {per_way:.2}/way ({bytes} B). \
         Budget is 2.0/way: ONE origin-local Mercator vector per way (moved into the \
         polyline, not a temporary) plus the shared buffers' O(log n) growth. A count \
         above that means a per-way temporary is back — see the module note."
    );
}

/// **The shape assertion, and the reason the constant above cannot simply be
/// re-tuned.** Per-way allocation count must not depend on how LONG the way is.
///
/// A ceiling on its own is blind twice over: it can be raised, and "2x the ways cost
/// 2x the allocations" is equally true of both shapes, so a growth ratio over the way
/// COUNT proves nothing. What separates them is the way's VERTEX count. A throwaway
/// result `Vec` grown by `extend_from_slice` costs one allocation per doubling, and
/// the number of doublings is `log2(quad vertices)` — so an 8x longer way costs the
/// old shape ~3 more allocations EACH, while the fixed shape still pays exactly one
/// (the origin-local Mercator vector, which is moved into the polyline).
///
/// Same total vertex budget on both sides, so the shared buffers' own growth is held
/// constant and only the per-way term can move the number.
#[test]
fn per_way_allocation_does_not_grow_with_the_way_length() {
    // 8 000 x 36 and 1 000 x 288 — the same 288 000 source vertices either way.
    let short = roads(8_000, 36);
    let long = roads(1_000, 288);

    let (a_short, _, v_short) = bake_cost(&short);
    let (a_long, _, v_long) = bake_cost(&long);

    // Within 5 % — `n` vertices give `n-1` segments, so the two fixtures differ by the
    // per-way endpoint and nothing else. The point of holding the total is that the
    // SHARED buffers' own doubling count is then the same on both sides, leaving the
    // per-way term as the only thing that can move the number.
    let skew = (v_short as f64 - v_long as f64).abs() / v_short as f64;
    assert!(skew < 0.05, "premise: both fixtures must expand to ~the same quad-vertex total ({v_short} vs {v_long})");

    let per_short = a_short as f64 / 8_000.0;
    let per_long = a_long as f64 / 1_000.0;
    eprintln!("[bake] per-way allocations: 36-vertex ways {per_short:.2}, 288-vertex ways {per_long:.2}");
    assert!(
        per_long <= per_short + 0.5,
        "8x longer ways cost {per_long:.2} allocations each against {per_short:.2} for the short \
         ones ({a_long} over 1 000 vs {a_short} over 8 000). A per-way cost that GROWS with the \
         way's length is a temporary being doubled into existence per way — the fixed shape pays \
         exactly one allocation a way whatever the length."
    );
}