//! One configuration per process: build a sparse document, time the real paths. //! `scale_bisect k=1000 n=100000 cats=3 prune=1 mode=grid`; see context/test-evidence/scale-bisect.md. use std::collections::{HashMap, HashSet}; use std::io::Write; use std::time::Instant; use improvise::model::cell::{CellKey, CellValue}; use improvise::view::Axis; use improvise::workbook::Workbook; use improvise_command::ui::app::App; use improvise_command::ui::effect::{self, ViewTarget}; use improvise_command::ui::host::Host; use improvise_command::ui::project::{self, Viewport}; use improvise_command::ui::reduce::caught_up; use improvise_io::persistence::{format_md, parse_md, save_to}; use improvise_io::storage::MemoryFs; use improvise_protocol::{ ClientFrame, KeyCode, KeyEvent, KeyModifiers, ModelCommand, RecordHash, render::RecordCell, }; fn lcg(s: &mut u64, n: usize) -> usize { *s = s .wrapping_mul(6_364_136_223_846_793_005) .wrapping_add(1_442_695_040_888_963_407); (*s >> 33) as usize % n } struct Cfg { k: usize, n: usize, cats: usize, prune: bool, mode: String, groups: bool, ops: String, } fn arg(args: &HashMap, k: &str, d: &str) -> String { args.get(k).cloned().unwrap_or_else(|| d.to_string()) } fn document(c: &Cfg) -> (Workbook, Vec) { let mut wb = Workbook::new("bench"); for ci in 0..c.cats { let cat = format!("C{ci}"); wb.add_category(cat.clone()).expect("under the limit"); for i in 0..c.k { if c.groups && ci == 0 { wb.add_item_in_group(&cat, format!("i{i}"), format!("G{}", i / 10)) .expect("added"); } else { wb.add_item(&cat, format!("i{i}")).expect("added"); } } } for m in ["Cost", "Rev"] { wb.add_item("_Measure", m).expect("virtual"); } let space = (c.k as f64).powi(c.cats as i32); let wanted = c.n.min((space / 2.0).max(1.0) as usize); let (mut s, mut seen, mut recs) = (0x9E37_79B9_7F4A_7C15u64, HashSet::new(), Vec::new()); while seen.len() < wanted { let d: Vec = (0..c.cats).map(|_| lcg(&mut s, c.k)).collect(); if !seen.insert(d.clone()) { continue; } let coords: Vec<(String, String)> = d .iter() .enumerate() .map(|(ci, x)| (format!("C{ci}"), format!("i{x}"))) .collect(); recs.push(CellKey::new(coords.clone())); for (m, v) in [("Cost", 3.0), ("Rev", 5.0)] { let mut key = coords.clone(); key.push(("_Measure".to_string(), m.to_string())); wb.model.set_cell(CellKey::new(key), CellValue::Number(v)); } } let view = wb.active_view_mut(); for ci in 0..c.cats { let axis = if c.mode == "records" || c.mode == "none" { Axis::None } else { Axis::Row }; view.set_axis(&format!("C{ci}"), axis); } if c.mode == "records" { view.set_axis("_Index", Axis::Row); view.set_axis("_Dim", Axis::Column); } else { view.set_axis("_Measure", Axis::Column); } view.prune_empty = c.prune; (wb, recs) } fn maxrss_mb() -> f64 { let mut u: libc::rusage = unsafe { std::mem::zeroed() }; unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut u) }; u.ru_maxrss as f64 / 1048576.0 } fn rss_mb() -> f64 { let out = std::process::Command::new("ps") .args(["-o", "rss=", "-p", &std::process::id().to_string()]) .output() .expect("ps"); String::from_utf8_lossy(&out.stdout) .trim() .parse::() .unwrap_or(0.0) / 1024.0 } /// Sample `f` up to `reps` times, stopping once `budget_ms` is spent (one sample minimum). fn sample(name: &str, reps: usize, budget_ms: f64, mut f: impl FnMut(usize)) { let mut v = Vec::new(); let start = Instant::now(); for r in 0..reps { let t = Instant::now(); f(r); v.push(t.elapsed().as_secs_f64() * 1000.0); if start.elapsed().as_secs_f64() * 1000.0 > budget_ms { break; } } v.sort_by(|a, b| a.partial_cmp(b).expect("finite")); println!( "{name} min={:.3} med={:.3} max={:.3} reps={}", v[0], v[v.len() / 2], v[v.len() - 1], v.len() ); std::io::stdout().flush().ok(); } fn once(name: &str, f: impl FnOnce() -> T) -> T { let t = Instant::now(); let v = f(); println!( "{name} min={0:.3} med={0:.3} max={0:.3} reps=1", t.elapsed().as_secs_f64() * 1000.0 ); std::io::stdout().flush().ok(); v } fn key(code: KeyCode) -> KeyEvent { KeyEvent::new(code, KeyModifiers::NONE) } fn main() { let args: HashMap = std::env::args() .skip(1) .filter_map(|a| { a.split_once('=') .map(|(k, v)| (k.to_string(), v.to_string())) }) .collect(); let c = Cfg { k: arg(&args, "k", "1000").parse().expect("k"), n: arg(&args, "n", "100000").parse().expect("n"), cats: arg(&args, "cats", "3").parse().expect("cats"), prune: arg(&args, "prune", "1") == "1", mode: arg(&args, "mode", "grid"), groups: arg(&args, "groups", "0") == "1", ops: arg( &args, "ops", "open,key,frame,window,setcell,host,reshape,io", ), }; let rows_cap: f64 = arg(&args, "rows_cap", "30000000").parse().expect("cap"); let product = (c.k as f64).powi(c.cats as i32); if c.mode == "grid" && product > rows_cap { println!( "REFUSED product_rows={product:.3e} (> {rows_cap:.0}: layout would materialize it)" ); return; } let want = |o: &str| c.ops.split(',').any(|x| x == o); let (wb, recs) = document(&c); println!( "cells={} records={} base_rss_mb={:.0}", wb.model.data.iter_cells().count(), recs.len(), rss_mb() ); let mut app = once("open", || App::new(wb, None)); println!( "rows={} cols={} rss_after_open_mb={:.0}", app.layout.row_count(), app.layout.col_count(), rss_mb() ); app.term_width = 200; app.term_height = 50; let name = app.view_name().to_string(); if want("key") { sample("keypress_down", 8, 6000.0, |_| { app.dispatch_key(key(KeyCode::Down)); }); } if want("frame") { let whole = |app: &App| Viewport { row_offset: 0, col_offset: 0, rows: app.layout.row_count(), cols: app.layout.col_count(), }; sample("tui_frame_grid_of_whole", 3, 8000.0, |_| { std::hint::black_box(project::grid_of(&app, whole(&app))); }); let cache = project::grid_of(&app, whole(&app)); let area = ratatui::layout::Rect::new(0, 0, 200, 50); sample("tui_render_gridwidget_buffer", 5, 4000.0, |_| { let mut buf = ratatui::buffer::Buffer::empty(area); let w = improvise::ui::grid::GridWidget::new( &cache, app.cursor(), &app.view_state.active_view, &app.layout, &app.view_state.mode, &app.view_state.search_query, &app.view_state.buffers, app.drill_state.as_ref(), ); ratatui::widgets::Widget::render(w, area, &mut buf); std::hint::black_box(buf); }); } if want("window") { let mid = app.layout.row_count() / 2; for (label, off) in [("top", 0), ("mid", mid)] { let vp = Viewport { row_offset: off, col_offset: 0, rows: 45, cols: 12, }; sample(&format!("browser_grid_of_{label}"), 5, 6000.0, |_| { std::hint::black_box(project::grid_of(&app, vp)); }); } } if want("setcell") && !recs.is_empty() { let existing = recs[recs.len() / 2].clone().with("_Measure", "Rev"); sample("setcell_existing", 5, 6000.0, |r| { app.apply_effects(vec![Box::new(effect::SetCell( existing.clone(), CellValue::Number(10.0 + r as f64), ))]); }); let mut s = 99u64; sample("setcell_new_record", 5, 6000.0, |_| { let mut coords: Vec<(String, String)> = (0..c.cats) .map(|ci| (format!("C{ci}"), format!("i{}", lcg(&mut s, c.k)))) .collect(); coords.push(("_Measure".into(), "Rev".into())); app.apply_effects(vec![Box::new(effect::SetCell( CellKey::new(coords), CellValue::Number(1.0), ))]); }); } if want("reshape") && c.mode == "grid" { let v = || ViewTarget::Active; sample("reshape_hide_item", 3, 6000.0, |r| { app.apply_effects(vec![Box::new(effect::HideItem { view: v(), category: "C1".into(), item: format!("i{r}"), })]); }); if c.groups { sample("reshape_toggle_group", 3, 6000.0, |_| { app.apply_effects(vec![Box::new(effect::ToggleGroup { view: v(), category: "C0".into(), group: "G0".into(), })]); }); } let last = format!("C{}", c.cats - 1); sample("reshape_set_axis", 4, 6000.0, |r| { let axis = if r % 2 == 0 { Axis::Page } else { Axis::Row }; app.apply_effects(vec![Box::new(effect::SetAxis { view: v(), category: last.clone(), axis, })]); }); sample("reshape_toggle_prune", 2, 6000.0, |_| { app.apply_effects(vec![Box::new(effect::TogglePruneEmpty(v()))]); }); } if want("io") { let wbr = &app.model_state.workbook; let text = once("format_md", || format_md(wbr)); println!("improv_mb={:.2}", text.len() as f64 / 1048576.0); once("parse_md", || parse_md(&text).expect("parses")); let fs = MemoryFs::new(); sample("persist_per_command_save_to", 3, 6000.0, |_| { save_to(&fs, wbr, std::path::Path::new("d.improv")).expect("write"); }); } if want("host") && !recs.is_empty() { let mut host = Host::new(app); let (id, snap) = once("host_connect_snapshot", || host.connect(45, 12)); sample("json_encode_snapshot", 3, 4000.0, |_| { std::hint::black_box(serde_json::to_vec(&snap.msg).expect("json")); }); println!( "snapshot_kb={:.1}", serde_json::to_vec(&snap.msg).expect("json").len() as f64 / 1024.0 ); let rec = recs[recs.len() / 2].clone(); let cell = RecordCell { record: RecordHash::of(&rec), measure: Some("Rev".into()), }; let mut enc: Vec<(f64, usize)> = Vec::new(); sample("host_handle_setcell_and_project", 4, 8000.0, |r| { let wire = caught_up( host.app(), ModelCommand::SetCell { view: name.clone(), cell: cell.clone(), value: CellValue::Number(50.0 + r as f64), }, ); let out = host.handle(id, ClientFrame::command(wire)); let t = Instant::now(); let bytes: usize = out .iter() .map(|a| serde_json::to_vec(&a.msg).expect("json").len()) .sum(); enc.push((t.elapsed().as_secs_f64() * 1000.0, bytes)); }); println!("json_encode_setcell_updates ms={:?}", enc); } println!("maxrss_mb={:.0}", maxrss_mb()); }