Files
improvise/benches/scale_bisect.rs
T

348 lines
12 KiB
Rust

//! 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<String, String>, k: &str, d: &str) -> String {
args.get(k).cloned().unwrap_or_else(|| d.to_string())
}
fn document(c: &Cfg) -> (Workbook, Vec<CellKey>) {
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<usize> = (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::<f64>()
.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<T>(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<String, String> = 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());
}