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

nordisk/facett

facett-graphpan/tests/resident_lane.rs

//! **The RESIDENT cloud lane of the pan/zoom canvas at scale** — what it uploads, and
//! that the overlays are still right.
//!
//! The gate is opened without a device (`cloud-test-gate`): the paint callbacks land in
//! the frame's shapes and are never executed, so these run on a box with no GPU and
//! count what the lane SENDS. The pixels are proven on a real adapter by
//! `facett-demo`'s pages probe (`decograph` / `graphpan` at 100k, t14s).
//!
//! One test fn per scenario, serialised: the ONE resident model is process-global.
#![cfg(feature = "wgpu")]

use std::sync::Mutex;

use egui::{Color32, Pos2, Vec2};
use facett_core::render::gpu::graphcloud::cloud;
use facett_core::Facet;
use facett_graphpan::{
    paint_scene_opts, resident, Decorations, GraphEdge, GraphModel, GraphNode, GraphPanView, GraphView, Msg,
    NodeDecoration, PaintOpts, RESIDENT_ABOVE,
};

static ONE_AT_A_TIME: Mutex<()> = Mutex::new(());

const RING: Color32 = Color32::from_rgb(10, 250, 20);

/// `n` nodes on a grid, each wired to its right and lower neighbour (~2n edges).
fn grid(n: usize, salt: &str) -> GraphModel {
    let cols = (n as f32).sqrt().ceil() as usize;
    let nodes = (0..n)
        .map(|i| GraphNode {
            id: format!("{salt}{i}"),
            label: format!("node {i}"),
            fill: Color32::from_rgb(60, 90, 160),
            stroke: Color32::WHITE,
            pos: Pos2::new((i % cols) as f32 * 220.0, (i / cols) as f32 * 60.0),
        })
        .collect();
    let mut edges = Vec::new();
    for i in 0..n {
        for j in [i + 1, i + cols] {
            if j < n && (j != i + 1 || j % cols != 0) {
                edges.push(GraphEdge {
                    from: format!("{salt}{i}"),
                    to: format!("{salt}{j}"),
                    color: Color32::GRAY,
                    dashed: false,
                    label: None,
                });
            }
        }
    }
    GraphModel { nodes, edges }
}

fn ctx() -> egui::Context {
    let ctx = egui::Context::default();
    ctx.set_pixels_per_point(1.0);
    ctx
}

fn input() -> egui::RawInput {
    egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, Vec2::new(1200.0, 800.0))), ..Default::default() }
}

/// One frame of the pane; returns the frame's shapes.
fn frame(ctx: &egui::Context, v: &mut GraphPanView) -> Vec<egui::epaint::ClippedShape> {
    let mut out = ctx.run_ui(input(), |ui| {
        egui::CentralPanel::default().show(ui, |ui| v.ui(ui));
    });
    out.textures_delta.clear();
    out.shapes
}

fn callbacks(shapes: &[egui::epaint::ClippedShape]) -> usize {
    shapes.iter().filter(|s| matches!(s.shape, egui::Shape::Callback(_))).count()
}

/// Outlined rects of exactly `width` px and `color` — a ring.
fn rings(shapes: &[egui::epaint::ClippedShape], width: f32, color: Color32) -> usize {
    shapes
        .iter()
        .filter(|s| matches!(&s.shape, egui::Shape::Rect(r) if r.stroke.width == width && r.stroke.color == color))
        .count()
}

fn lane(v: &GraphPanView) -> (String, u64) {
    let l = v.legibility();
    (l.cloud_lane().to_string(), l.resident_uploads)
}

#[test]
fn a_model_uploads_once_and_camera_frames_upload_nothing() {
    let _one = ONE_AT_A_TIME.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
    cloud::assume_renderer_installed_for_tests();
    let n = RESIDENT_ABOVE + 1_000;
    let mut v = GraphPanView::new("t").with_model(grid(n, "a")).with_gpu(true);
    let _ = v.update(Msg::SetZoom(0.05));
    let ctx = ctx();

    let s = frame(&ctx, &mut v);
    assert_eq!(callbacks(&s), 1, "one cloud callback per frame");
    assert_eq!(lane(&v), ("resident".into(), 1), "the first frame uploads the model");
    assert_eq!(v.state_json()["legibility"]["cloud_lane"], "resident");

    // Camera-only frames: drag, zoom, a plain repaint — nothing is re-sent.
    for i in 0..12 {
        let _ = v.update(if i % 3 == 2 { Msg::ZoomBy(40.0) } else { Msg::PanBy(Vec2::new(7.0, -3.0)) });
        let s = frame(&ctx, &mut v);
        assert_eq!(callbacks(&s), 1);
        assert_eq!(lane(&v), ("resident".into(), 1), "camera frame {i} uploaded nothing");
    }
    // No per-node overlay at fit zoom: no ring for any of the 6 000 nodes.
    assert_eq!(rings(&s_last(&ctx, &mut v), 1.4, Color32::WHITE), 0, "plain nodes get no CPU ring on the resident lane");

    // A new model uploads exactly once more.
    v = v.with_model(grid(n, "b"));
    let _ = frame(&ctx, &mut v);
    assert_eq!(lane(&v), ("resident".into(), 2), "the model change re-uploads");
    for _ in 0..5 {
        let _ = v.update(Msg::PanBy(Vec2::new(-4.0, 2.0)));
        let _ = frame(&ctx, &mut v);
    }
    assert_eq!(lane(&v).1, 2, "and only once");
}

fn s_last(ctx: &egui::Context, v: &mut GraphPanView) -> Vec<egui::epaint::ClippedShape> {
    frame(ctx, v)
}

#[test]
fn selection_and_decorations_are_overlays_and_upload_nothing() {
    let _one = ONE_AT_A_TIME.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
    cloud::assume_renderer_installed_for_tests();
    let n = RESIDENT_ABOVE + 500;
    let mut deco = Decorations::default();
    deco.nodes.insert("c7".into(), NodeDecoration { ring: Some(RING), badge: Some("!".into()), badge_color: Some(RING) });
    deco.edges.push(GraphEdge { from: "c3".into(), to: "c9".into(), color: Color32::RED, dashed: true, label: Some("cut".into()) });
    let mut v = GraphPanView::new("t").with_model(grid(n, "c")).with_decorations(deco).with_gpu(true);
    let _ = v.update(Msg::SetZoom(0.05));
    let ctx = ctx();
    let s = frame(&ctx, &mut v);
    assert_eq!(lane(&v), ("resident".into(), 1));
    assert_eq!(rings(&s, 2.4, RING), 1, "the decoration ring is drawn over the cloud");

    // Select: the lit trace and the selection ring are overlays — no upload.
    let _ = v.update(Msg::Select("c0".into()));
    let s = frame(&ctx, &mut v);
    assert_eq!(lane(&v), ("resident".into(), 1), "a selection uploads nothing");
    let th = facett_core::Theme::default();
    assert_eq!(rings(&s, 3.0, th.accent), 1, "exactly one selection ring");
    let lit = v.lit_set();
    assert!(lit.len() > 100, "c0 reaches its grid quadrant downstream: {}", lit.len());
    let trace = s
        .iter()
        .filter(|c| matches!(&c.shape, egui::Shape::LineSegment { stroke, .. } if stroke.width == 1.6 && stroke.color == th.accent))
        .count();
    assert!(trace > 100 && trace <= resident::LIT_EDGE_CAP, "the lit trace is drawn over the cloud: {trace}");
    assert_eq!(v.selection_json()["node"], "c0");

    // Clear: the trace goes, the decoration ring stays.
    let _ = v.update(Msg::ClearSelection);
    let s = frame(&ctx, &mut v);
    assert_eq!(rings(&s, 3.0, th.accent), 0);
    assert_eq!(rings(&s, 2.4, RING), 1);
    assert_eq!(lane(&v).1, 1);
}

#[test]
fn small_graphs_keep_the_per_frame_lane_and_another_pane_forces_a_resend() {
    let _one = ONE_AT_A_TIME.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
    cloud::assume_renderer_installed_for_tests();
    let ctx = ctx();
    let mut small = GraphPanView::new("s").with_model(grid(200, "s")).with_gpu(true);
    let _ = frame(&ctx, &mut small);
    assert_eq!(lane(&small), ("per-frame".into(), 0), "at or below RESIDENT_ABOVE the instance lane draws");

    // Two scale panes take turns: the ONE resident model follows the pane that draws.
    let mut a = GraphPanView::new("a").with_model(grid(RESIDENT_ABOVE + 10, "x")).with_gpu(true);
    let mut b = GraphPanView::new("b").with_model(grid(RESIDENT_ABOVE + 10, "y")).with_gpu(true);
    let _ = frame(&ctx, &mut a);
    let _ = frame(&ctx, &mut a);
    assert_eq!(lane(&a).1, 1);
    let _ = frame(&ctx, &mut b);
    let _ = frame(&ctx, &mut a);
    assert_eq!(lane(&a).1, 2, "a re-sends after b replaced the resident model");
    assert_eq!(lane(&b).1, 1);
}

/// The free `paint_scene_opts` (dwarves' Universe calls it every frame with a borrowed
/// model) fingerprints the model: the same model is one upload, a moved node is one more.
#[test]
fn the_free_painter_uploads_once_per_model_content() {
    let _one = ONE_AT_A_TIME.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
    cloud::assume_renderer_installed_for_tests();
    let ctx = ctx();
    let mut model = grid(RESIDENT_ABOVE + 100, "f");
    let deco = Decorations::default();
    let mut view = GraphView { zoom: 0.05, ..GraphView::default() };
    let paint = |model: &GraphModel, view: &GraphView| {
        let mut rep = None;
        ctx.run_ui(input(), |ui| {
            egui::CentralPanel::default().show(ui, |ui| {
                let (resp, painter) = ui.allocate_painter(ui.available_size(), egui::Sense::click_and_drag());
                let origin = resp.rect.center() + view.pan;
                rep = Some(paint_scene_opts(
                    &painter,
                    model,
                    &deco,
                    view,
                    origin,
                    Color32::WHITE,
                    Color32::YELLOW,
                    Color32::GRAY,
                    PaintOpts::default().with_gpu(true),
                    Some((ui, resp.rect)),
                ));
            });
        }).textures_delta.clear();
        rep.expect("painted")
    };
    let before = resident::free_uploads();
    let r = paint(&model, &view);
    assert_eq!(r.cloud_lane(), "resident");
    assert_eq!(resident::free_uploads(), before + 1);
    for _ in 0..8 {
        view.pan += Vec2::new(5.0, 5.0);
        let _ = paint(&model, &view);
    }
    assert_eq!(resident::free_uploads(), before + 1, "pan frames re-send nothing");
    let fp = resident::fingerprint(&model);
    model.nodes[17].pos.x += 1.0;
    assert_ne!(resident::fingerprint(&model), fp, "a moved node is a different model");
    let _ = paint(&model, &view);
    let _ = paint(&model, &view);
    assert_eq!(resident::free_uploads(), before + 2, "one re-upload for the moved node");
}

/// The CPU cost of a camera-only frame at 100 000 nodes on the resident lane (native,
/// no tessellation, no GPU). `cargo test --release -p facett-graphpan --features wgpu
/// --test resident_lane -- --ignored --nocapture`.
#[test]
#[ignore = "a timing print, run by hand in --release"]
fn camera_frame_cost_at_100k() {
    let _one = ONE_AT_A_TIME.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
    cloud::assume_renderer_installed_for_tests();
    let ctx = ctx();
    let mut v = GraphPanView::new("t").with_model(grid(100_000, "m")).with_gpu(true);
    let _ = v.update(Msg::SetZoom(0.012));
    let t0 = std::time::Instant::now();
    let _ = frame(&ctx, &mut v);
    let first = t0.elapsed();
    let t1 = std::time::Instant::now();
    for _ in 0..60 {
        let _ = v.update(Msg::PanBy(Vec2::new(3.0, 1.0)));
        let _ = frame(&ctx, &mut v);
    }
    let per = t1.elapsed() / 60;
    eprintln!("graphpan 100k: first (upload) frame {first:?}, camera frame {per:?}, uploads {}", lane(&v).1);
    assert_eq!(lane(&v).1, 1);
}