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

nordisk/facett

facett-simview/src/lib.rs

//! **facett-simview** — the window onto the bench's *Simulate*: the population in 3D, the
//! ego network of whoever you pick, and the two buttons that change the population.
//!
//! ```text
//! ┌ Generate │ Permutate 10% │ id [_____] Show │ status ─────────────────────────────┐
//! ├───────────────────────────────────────┬──────────────────────────────────────────┤
//! │ facett-graph3d: every Person, placed  │ V.G (facett-graphnav): the selected      │
//! │ in a cluster per kommun, the latest   │ node and its 1-hop neighbours, fetched   │
//! │ round lit                             │ when the selection changes               │
//! └───────────────────────────────────────┴──────────────────────────────────────────┘
//! ```
//!
//! The page is a facet like any other: it ships as the `sim` bundle group of
//! facet-wrapped (`facet-wrapped-group --features sim`), so the browser GPU start, the
//! adapter retry, the "no GPU" text and the GPU lanes (the shared instanced cloud this
//! viewer's 3D draws on) are facet-wrapped's, not this crate's.
//!
//! Everything the app learns comes through the HTTP contract in [`contract`], over a
//! [`Backend`]: the browser's `fetch` on wasm, [`mock::MockBackend`] in the tests and the
//! native window. The app never waits on a request — [`SimView::step`] drains the answers
//! that have arrived and sends what is due (the 2 s `/power` poll while a job runs), and
//! drawing reads only the state that leaves behind.

pub mod backend;
pub mod contract;
pub mod mock;
#[cfg(not(target_arch = "wasm32"))]
pub mod devserver;

use std::collections::{BTreeSet, HashMap};

use egui::Color32;
use facett_core::Facet;
use facett_graph3d::{Graph3D, Layout3D, Msg as G3Msg, CPU_PAINT_MAX_NODES};
use facett_graphnav::{Edge, GraphNav, Node, PropGraph};

pub use backend::Backend;
use contract::{EdgeRow, Job, Meta, NodeRow, Req, Resp, VERB_GENERATE, VERB_PERMUTATE};

/// Seconds between `/power` polls while a job runs (the contract's 2 s).
pub const POLL_S: f64 = 2.0;

/// The colour of a node of the latest round.
pub const LIT: Color32 = Color32::from_rgb(255, 196, 0);

/// The colour of a label (persons are one label today; the palette is by label so a
/// wider node set colours itself).
#[must_use]
pub fn label_color(label: &str) -> Color32 {
    const PALETTE: [Color32; 6] = [
        Color32::from_rgb(70, 130, 200),
        Color32::from_rgb(90, 180, 120),
        Color32::from_rgb(190, 100, 170),
        Color32::from_rgb(200, 120, 70),
        Color32::from_rgb(120, 120, 210),
        Color32::from_rgb(150, 160, 90),
    ];
    match label {
        "Person" => PALETTE[0],
        other => PALETTE[1 + (fnv(other.as_bytes()) % (PALETTE.len() as u64 - 1)) as usize],
    }
}

fn fnv(b: &[u8]) -> u64 {
    b.iter().fold(0xcbf2_9ce4_8422_2325_u64, |h, &x| (h ^ u64::from(x)).wrapping_mul(0x0100_0000_01b3))
}

fn mix(mut z: u64) -> u64 {
    z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
    z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
    z ^ (z >> 31)
}

/// **Where each person sits:** one cluster per `kommun` (centres on a Fibonacci sphere,
/// in name order so the picture is stable across fetches), each person jittered inside
/// its cluster by a hash of its id. O(n), so 110 000 persons never meet a force layout;
/// a permutation that moves a person moves its dot.
#[must_use]
pub fn positions(persons: &[NodeRow]) -> Vec<[f32; 3]> {
    let kommuner: BTreeSet<&str> = persons.iter().filter_map(|p| p.kommun.as_deref()).collect();
    let k = kommuner.len().max(1);
    let golden = std::f32::consts::PI * (3.0 - 5.0_f32.sqrt());
    let centre: HashMap<&str, [f32; 3]> = kommuner
        .iter()
        .enumerate()
        .map(|(i, name)| {
            let y = if k > 1 { 1.0 - (i as f32 / (k - 1) as f32) * 2.0 } else { 0.0 };
            let r = (1.0 - y * y).max(0.0).sqrt();
            let th = golden * i as f32;
            (*name, [th.cos() * r * 0.75, y * 0.75, th.sin() * r * 0.75])
        })
        .collect();
    let spread = (0.9 / (k as f32).sqrt()).clamp(0.06, 0.35);
    persons
        .iter()
        .map(|p| {
            let c = p.kommun.as_deref().and_then(|n| centre.get(n)).copied().unwrap_or([0.0; 3]);
            let h = mix(p.id as u64);
            let u = |s: u32| ((h >> s) & 0xffff) as f32 / 65_535.0;
            // A point in the unit ball (direction from two hashes, radius cube-rooted).
            let (th, z, r) = (u(0) * std::f32::consts::TAU, u(16) * 2.0 - 1.0, u(32).cbrt() * spread);
            let s = (1.0 - z * z).max(0.0).sqrt();
            [c[0] + th.cos() * s * r, c[1] + z * r, c[2] + th.sin() * s * r]
        })
        .collect()
}

/// The ego network V.G shows: who, and what came back.
#[derive(Debug, Clone, Default)]
struct Ego {
    /// The node it is the ego network of.
    of: i64,
    edges: Vec<EdgeRow>,
    nodes: Vec<NodeRow>,
}

/// The Simulate viewer.
pub struct SimView {
    backend: Box<dyn Backend>,
    /// The persons, in 3D index order.
    persons: Vec<NodeRow>,
    index_of: HashMap<i64, usize>,
    meta: Option<Meta>,
    latest_round: i64,
    highlighted: usize,
    graph: Graph3D,
    /// The 3D selection last seen, to notice a click.
    seen_3d: Option<usize>,
    /// V.G — `None` until an ego network has arrived.
    nav: Option<GraphNav>,
    ego: Option<Ego>,
    /// The node whose ego network is asked for (the selection).
    selected: Option<i64>,
    /// Edges of the ego asked for, waiting on the node rows.
    ego_edges: Option<(i64, Vec<EdgeRow>)>,
    job: Job,
    /// A job is running (a 202, a 409, or `/power` said busy): poll.
    busy: bool,
    next_poll: f64,
    poll_in_flight: bool,
    in_flight: usize,
    /// Requests sent, ever (a test reads it).
    sent: u64,
    status: String,
    id_text: String,
    now: f64,
    gpu: bool,
}

impl SimView {
    /// A viewer over `backend`; asks for the meta, the persons and the job state at once.
    pub fn new(backend: Box<dyn Backend>) -> Self {
        let mut s = SimView {
            backend,
            persons: Vec::new(),
            index_of: HashMap::new(),
            meta: None,
            latest_round: 0,
            highlighted: 0,
            graph: Self::graph3d(Vec::new(), Vec::new()),
            seen_3d: None,
            nav: None,
            ego: None,
            selected: None,
            ego_edges: None,
            job: Job::default(),
            busy: false,
            next_poll: 0.0,
            poll_in_flight: false,
            in_flight: 0,
            sent: 0,
            status: "loading the population…".into(),
            id_text: String::new(),
            now: 0.0,
            gpu: cfg!(feature = "gpu"),
        };
        s.send(Req::Meta);
        s.send(Req::Persons);
        s.send(Req::Power);
        s.poll_in_flight = true;
        s
    }

    /// The live viewer: the browser's `fetch` on wasm; natively the in-process mock (the
    /// registry builds every entry on the host, and there is no server there).
    #[must_use]
    pub fn live() -> Self {
        #[cfg(target_arch = "wasm32")]
        {
            Self::new(Box::new(backend::fetch::FetchBackend::new()))
        }
        #[cfg(not(target_arch = "wasm32"))]
        {
            Self::new(Box::new(mock::MockBackend::new(mock::MockServer::new(2_000))))
        }
    }

    fn graph3d(nodes: Vec<(String, Color32)>, pos: Vec<[f32; 3]>) -> Graph3D {
        // Sphere is O(n); the real placement is Given, set right after.
        #[allow(unused_mut)]
        let mut g = Graph3D::new(nodes, Vec::new(), Layout3D::Sphere).with_title("population").with_controls(true).with_positions(pos);
        #[cfg(feature = "gpu")]
        {
            g = g.with_gpu(true);
        }
        let _ = g.update(G3Msg::SetLayout(Layout3D::Given));
        let _ = g.update(G3Msg::SetSpin(false));
        g
    }

    fn send(&mut self, req: Req) {
        self.sent += 1;
        self.in_flight += 1;
        self.backend.send(req);
    }

    /// Press a button: `simulate` (Generate) or `permutate`.
    pub fn press(&mut self, verb: &'static str) {
        self.status = format!("{verb}: asking…");
        self.send(Req::Press(verb));
    }

    /// Show the ego network of `id` in V.G (and light it in 3D when it is a person).
    pub fn select(&mut self, id: i64) {
        self.selected = Some(id);
        self.id_text = id.to_string();
        self.ego_edges = None;
        match self.index_of.get(&id) {
            Some(&i) => {
                let _ = self.graph.update(G3Msg::SelectNode(i));
                self.seen_3d = Some(i);
            }
            None => {
                let _ = self.graph.update(G3Msg::ClearSelection);
                self.seen_3d = None;
            }
        }
        self.send(Req::Ego(id));
    }

    /// **One tick of the app, apart from drawing:** take what came back, send what is due.
    pub fn step(&mut self, now: f64) {
        self.now = now;
        for r in self.backend.drain() {
            self.in_flight = self.in_flight.saturating_sub(1);
            self.on_resp(r);
        }
        if self.busy && !self.poll_in_flight && now >= self.next_poll {
            self.poll_in_flight = true;
            self.next_poll = now + POLL_S;
            self.send(Req::Power);
        }
    }

    fn body(r: &Resp) -> String {
        let s = String::from_utf8_lossy(&r.body);
        let s = s.trim();
        if s.is_empty() { "(empty body)".into() } else { s.chars().take(300).collect() }
    }

    fn on_resp(&mut self, r: Resp) {
        match &r.req {
            Req::Press(verb) => match r.status {
                202 => {
                    self.status = format!("{verb}: accepted, running…");
                    self.start_polling();
                }
                409 => {
                    self.status = format!("{verb}: refused — a job is already running (409)");
                    self.start_polling();
                }
                s => self.status = format!("{verb}: HTTP {s}: {}", Self::body(&r)),
            },
            Req::Power => {
                self.poll_in_flight = false;
                if r.status != 200 {
                    self.status = format!("/power: HTTP {}: {}", r.status, Self::body(&r));
                    return;
                }
                let Some(job) = Job::parse(&r.body) else {
                    self.status = format!("/power: no `falkordb` bench in {}", Self::body(&r));
                    return;
                };
                let was_busy = self.busy;
                self.busy = job.busy;
                if job.busy {
                    let verb = job.verb.clone().unwrap_or_else(|| "job".into());
                    self.status = format!("{verb}: running for {} s…", job.for_s.unwrap_or(0));
                } else if let Some((verb, ok, detail)) = job.last.clone() {
                    if was_busy {
                        if ok {
                            self.status = format!("{verb}: done — {detail}; refetching");
                            self.send(Req::Meta);
                            self.send(Req::Persons);
                        } else {
                            self.status = format!("{verb}: FAILED — {detail}");
                        }
                    } else if self.status.starts_with("loading") || self.persons.is_empty() {
                        // First look: say what the last job was, the load says the rest.
                    } else if !self.status.contains(&verb) {
                        self.status = format!("last: {verb} {} — {detail}", if ok { "ok" } else { "FAILED" });
                    }
                }
                self.job = job;
            }
            Req::Meta => match (r.status, Meta::parse(&r.body)) {
                (200, Some(m)) => {
                    self.meta = Some(m);
                    self.recolor();
                }
                (s, _) => self.status = format!("meta.json: HTTP {s}: {}", Self::body(&r)),
            },
            Req::Persons => match (r.status, contract::decode_nodes(&r.body)) {
                (200, Ok(rows)) => self.set_persons(rows),
                (200, Err(e)) => self.status = format!("nodes.arrow: {e}"),
                (s, _) => self.status = format!("nodes.arrow: HTTP {s}: {}", Self::body(&r)),
            },
            Req::Ego(id) => {
                if self.selected != Some(*id) {
                    return; // a newer selection superseded it
                }
                match (r.status, contract::decode_edges(&r.body)) {
                    (200, Ok(edges)) => {
                        let ids: BTreeSet<i64> = edges.iter().flat_map(|e| [e.src, e.dst]).chain([*id]).collect();
                        self.ego_edges = Some((*id, edges));
                        self.send(Req::NodesByIds(ids.into_iter().collect()));
                    }
                    (200, Err(e)) => self.status = format!("ego.arrow: {e}"),
                    (s, _) => self.status = format!("ego.arrow: HTTP {s}: {}", Self::body(&r)),
                }
            }
            Req::NodesByIds(_) => {
                let Some((of, _)) = &self.ego_edges else { return };
                if self.selected != Some(*of) {
                    return;
                }
                match (r.status, contract::decode_nodes(&r.body)) {
                    (200, Ok(nodes)) => {
                        let (of, edges) = self.ego_edges.take().unwrap_or_default();
                        self.set_ego(Ego { of, edges, nodes });
                    }
                    (200, Err(e)) => self.status = format!("nodes.arrow?ids: {e}"),
                    (s, _) => self.status = format!("nodes.arrow?ids: HTTP {s}: {}", Self::body(&r)),
                }
            }
        }
    }

    fn start_polling(&mut self) {
        self.busy = true;
        self.next_poll = self.now + POLL_S;
    }

    fn set_persons(&mut self, rows: Vec<NodeRow>) {
        let pos = positions(&rows);
        let nodes = rows.iter().map(|p| (p.name.clone(), label_color(&p.label))).collect();
        self.index_of = rows.iter().enumerate().map(|(i, p)| (p.id, i)).collect();
        self.persons = rows;
        self.graph = Self::graph3d(nodes, pos);
        self.seen_3d = None;
        if let Some(i) = self.selected.and_then(|id| self.index_of.get(&id).copied()) {
            let _ = self.graph.update(G3Msg::SelectNode(i));
            self.seen_3d = Some(i);
        }
        self.recolor();
        if self.status.starts_with("loading") {
            self.status = format!("{} persons, round {}", self.persons.len(), self.latest_round);
        }
    }

    /// Light the latest round: `meta.round` when the meta has arrived, else the highest
    /// round among the persons.
    fn recolor(&mut self) {
        let max = self.persons.iter().map(|p| p.round).max().unwrap_or(0);
        self.latest_round = self.meta.map_or(max, |m| m.round);
        let mut lit = 0;
        for (node, p) in self.graph.nodes.iter_mut().zip(&self.persons) {
            node.color = if p.round == self.latest_round {
                lit += 1;
                LIT
            } else {
                label_color(&p.label).gamma_multiply(0.55)
            };
        }
        self.highlighted = lit;
        self.graph.touch();
    }

    fn set_ego(&mut self, ego: Ego) {
        let mut g = PropGraph::new();
        for n in &ego.nodes {
            let mut node = Node::new(n.id.to_string(), n.label.clone()).with("name", n.name.clone()).with("round", n.round.to_string());
            if let Some(k) = &n.kommun {
                node = node.with("kommun", k.clone());
            }
            g.add_node(node);
        }
        let have: BTreeSet<i64> = ego.nodes.iter().map(|n| n.id).collect();
        for e in ego.edges.iter().filter(|e| have.contains(&e.src) && have.contains(&e.dst)) {
            g.add_edge(Edge::new(e.src.to_string(), e.dst.to_string(), e.etype.clone()));
        }
        let mut nav = GraphNav::new(format!("V.G — ego of {}", ego.of), g);
        nav.update(facett_graphnav::Msg::SelectNode(ego.of.to_string()));
        self.nav = Some(nav);
        self.ego = Some(ego);
    }

    /// The render lane the 3D view is on, said as what it is.
    #[must_use]
    pub fn render_lane(&self) -> &'static str {
        #[cfg(feature = "gpu")]
        {
            if facett_core::render::gpu::graphcloud::cloud::cloud_renderer_installed() {
                return "gpu-cloud";
            }
        }
        if self.persons.len() > CPU_PAINT_MAX_NODES { "cpu-strided" } else { "cpu" }
    }

    /// The 3D viewer (a test reads its nodes and selection).
    #[must_use]
    pub fn graph(&self) -> &Graph3D {
        &self.graph
    }

    /// The 3D viewer, to drive it the way a click does (`Msg::SelectNode`).
    pub fn graph_mut(&mut self) -> &mut Graph3D {
        &mut self.graph
    }

    /// V.G, once an ego network has arrived.
    #[must_use]
    pub fn nav(&self) -> Option<&GraphNav> {
        self.nav.as_ref()
    }

    /// Requests that have not been answered yet.
    #[must_use]
    pub fn in_flight(&self) -> usize {
        self.in_flight
    }

    fn top_bar(&mut self, ui: &mut egui::Ui) {
        ui.horizontal(|ui| {
            if ui.button("Generate").on_hover_text("POST /power/falkordb/simulate").clicked() {
                self.press(VERB_GENERATE);
            }
            if ui.button("Permutate 10%").on_hover_text("POST /power/falkordb/permutate").clicked() {
                self.press(VERB_PERMUTATE);
            }
            ui.separator();
            ui.label("id");
            let field = ui.add(egui::TextEdit::singleline(&mut self.id_text).desired_width(90.0));
            let enter = field.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
            if (ui.button("Show").clicked() || enter) && let Ok(id) = self.id_text.trim().parse::<i64>() {
                self.select(id);
            }
            ui.separator();
            if self.busy {
                ui.spinner();
            }
            ui.label(&self.status);
            ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                ui.weak(format!(
                    "{} persons · round {} lit: {} · {}",
                    self.persons.len(),
                    self.latest_round,
                    self.highlighted,
                    self.render_lane()
                ));
            });
        });
    }
}

impl Facet for SimView {
    fn title(&self) -> &str {
        "simview"
    }

    fn ui(&mut self, ui: &mut egui::Ui) {
        let now = ui.input(|i| i.time);
        self.step(now);
        self.top_bar(ui);
        ui.separator();
        let h = ui.available_height();
        ui.columns(2, |cols| {
            cols[0].set_min_height(h);
            self.graph.ui(&mut cols[0]);
            match self.nav.as_mut() {
                Some(nav) => nav.ui(&mut cols[1]),
                None => {
                    cols[1].centered_and_justified(|ui| {
                        ui.weak(match self.selected {
                            Some(id) => format!("fetching the ego network of {id}…"),
                            None => "click a person in 3D, or type an id, for its ego network".into(),
                        });
                    });
                }
            }
        });
        // A click in 3D is a selection.
        let sel = self.graph.state().selected;
        if sel != self.seen_3d {
            self.seen_3d = sel;
            if let Some(id) = sel.and_then(|i| self.persons.get(i)).map(|p| p.id) {
                self.select(id);
            }
        }
        // Answers land between frames with no input event; keep looking while waiting.
        if self.in_flight > 0 || self.busy {
            ui.ctx().request_repaint_after(std::time::Duration::from_millis(200));
        }
    }

    fn state_json(&self) -> serde_json::Value {
        let g = self.graph.state_json();
        serde_json::json!({
            "nodes": self.persons.len(),
            "graph3d_nodes": self.graph.nodes.len(),
            "highlighted": self.highlighted,
            "latest_round": self.latest_round,
            "meta": self.meta.map(|m| serde_json::json!({"round": m.round, "persons": m.persons, "nodes": m.nodes, "edges": m.edges})),
            "selected": self.selected,
            "selected_3d": g["selected_node"],
            "ego": self.ego.as_ref().map(|e| serde_json::json!({"of": e.of, "nodes": e.nodes.len(), "edges": e.edges.len()})),
            "vg": self.nav.as_ref().map(|n| serde_json::json!({"nodes": n.graph.nodes.len(), "edges": n.graph.edges.len(), "selected": n.selected()})),
            "status": self.status,
            "busy": self.busy,
            "job": {"busy": self.job.busy, "verb": self.job.verb, "last": self.job.last.as_ref().map(|(v, ok, d)| serde_json::json!({"verb": v, "ok": ok, "detail": d}))},
            "in_flight": self.in_flight,
            "requests": self.sent,
            "render_lane": self.render_lane(),
            "gpu_feature": self.gpu,
            "cpu_paint_stride": g["cpu_paint_stride"],
            "layout": g["layout"],
        })
    }

    /// Robot driving: `{"press":"simulate"|"permutate"}`, `{"select": id}`.
    fn update_json(&mut self, msg_json: &str) {
        let Ok(v) = serde_json::from_str::<serde_json::Value>(msg_json) else { return };
        match v.get("press").and_then(serde_json::Value::as_str) {
            Some(VERB_GENERATE) => self.press(VERB_GENERATE),
            Some(VERB_PERMUTATE) => self.press(VERB_PERMUTATE),
            _ => {}
        }
        if let Some(id) = v.get("select").and_then(serde_json::Value::as_i64) {
            self.select(id);
        }
    }
}