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