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);
}