//! Does the TUI's whole-grid `GridCache` dominate a frame? See improvise-i5tk, //! improvise-jmjz and `context/code/command.md`. `cargo bench --bench grid_projection`. use std::time::{Duration, Instant}; use improvise::format::FormatPlan; use improvise::model::cell::{CellKey, CellValue}; use improvise::view::{Axis, GridLayout}; use improvise::workbook::Workbook; use improvise_command::ui::app::App; use improvise_command::ui::project::{self, Viewport}; use improvise_command::ui::viewport::compute_col_widths; /// A dense pivot grid: `regions * products` rows by `measures` columns, every /// cell filled, which is the worst case for a per-cell evaluation. fn dense_app(regions: usize, products: usize, measures: usize) -> App { let mut wb = Workbook::new("Bench"); wb.add_category("Region").unwrap(); wb.add_category("Product").unwrap(); let region_names: Vec = (0..regions).map(|i| format!("Region{i}")).collect(); let product_names: Vec = (0..products).map(|i| format!("Product{i}")).collect(); let measure_names: Vec = (0..measures).map(|i| format!("Measure{i}")).collect(); for r in ®ion_names { wb.add_item("Region", r.clone()).unwrap(); } for p in &product_names { wb.add_item("Product", p.clone()).unwrap(); } for m in &measure_names { wb.add_item("_Measure", m.clone()).unwrap(); } { let v = wb.active_view_mut(); v.set_axis("Region", Axis::Row); v.set_axis("Product", Axis::Row); v.set_axis("_Measure", Axis::Column); } let mut n = 0.0f64; for r in ®ion_names { for p in &product_names { for m in &measure_names { n += 1.0; let key = CellKey::new(vec![ ("Region".to_string(), r.clone()), ("Product".to_string(), p.clone()), ("_Measure".to_string(), m.clone()), ]); wb.model.set_cell(key, CellValue::Number(n)); } } } let mut app = App::new(wb, None); app.rebuild_layout(); app } /// Collapsed groups on the row axis and a formula chain on the column axis, so /// every fold cell and every total re-derives the chain (improvise-h6z1). fn chain_app(regions: usize, products: usize) -> App { let mut wb = Workbook::new("Chain"); wb.add_category("Region").unwrap(); wb.add_category("Product").unwrap(); for r in 0..regions { wb.add_item_in_group("Region", format!("Region{r}"), format!("G{}", r / 10)) .unwrap(); } for p in 0..products { wb.add_item("Product", format!("Product{p}")).unwrap(); } wb.add_item("_Measure", "Revenue").unwrap(); let chain = [ "F1 = Revenue * 2", "F2 = F1 + Revenue", "F3 = F2 + F1", "F4 = F3 + F2", "F5 = F4 / F1", ]; for text in chain { let formula = improvise::formula::parse_formula(text, "_Measure").unwrap(); wb.model.add_formula(formula); } { let v = wb.active_view_mut(); v.set_axis("Region", Axis::Row); v.set_axis("Product", Axis::Row); v.set_axis("_Measure", Axis::Column); } let mut n = 0.0f64; for r in 0..regions { for p in 0..products { n += 1.0; let key = CellKey::new(vec![ ("Region".to_string(), format!("Region{r}")), ("Product".to_string(), format!("Product{p}")), ("_Measure".to_string(), "Revenue".to_string()), ]); wb.model.set_cell(key, CellValue::Number(n)); } } let mut app = App::new(wb, None); app.rebuild_layout(); app } /// Days in collapsed groups of `per_group` on Row, with Product: every row is a /// fold over a few days, never the whole category (improvise-jq0m). fn grouped_app(days: usize, per_group: usize, products: usize, measures: usize) -> App { let mut wb = Workbook::new("Grouped"); wb.add_category("Day").unwrap(); wb.add_category("Product").unwrap(); for d in 0..days { let group = format!("G{}", d / per_group); wb.add_item_in_group("Day", format!("Day{d}"), group) .unwrap(); } for p in 0..products { wb.add_item("Product", format!("Product{p}")).unwrap(); } for m in 0..measures { wb.add_item("_Measure", format!("Measure{m}")).unwrap(); } { let v = wb.active_view_mut(); v.set_axis("Day", Axis::Row); v.set_axis("Product", Axis::Row); v.set_axis("_Measure", Axis::Column); } let mut n = 0.0f64; for d in 0..days { for p in 0..products { for m in 0..measures { n += 1.0; let key = CellKey::new(vec![ ("Day".to_string(), format!("Day{d}")), ("Product".to_string(), format!("Product{p}")), ("_Measure".to_string(), format!("Measure{m}")), ]); wb.model.set_cell(key, CellValue::Number(n)); } } } let mut app = App::new(wb, None); app.rebuild_layout(); app } fn whole_grid(app: &App) -> Viewport { Viewport { row_offset: 0, col_offset: 0, rows: app.layout.row_count(), cols: app.layout.col_count(), } } /// A terminal window: what the TUI would ask for if a cache position were /// relative, which is the browser client's arrangement. fn terminal_window() -> Viewport { Viewport { row_offset: 0, col_offset: 0, rows: 45, cols: 12, } } /// Min-of-N, not the mean: a mean over a handful of iterations is dominated by /// whatever else the machine was doing, which is how a share above 100% happens. fn time(iters: u32, app: &App, viewport: Viewport) -> Duration { let mut best = Duration::MAX; for _ in 0..iters.max(3) { let start = Instant::now(); std::hint::black_box(project::grid_of(app, viewport)); best = best.min(start.elapsed()); } best } /// `compute_col_widths` is whole-grid whatever the viewport says, so it is the part /// narrowing the cells loop cannot reach. It must be timed with the arguments /// `grid_of` passes it, or the share is of a call that never happens. // Since improvise-jmjz it IS the fused walk, with an empty window, so its share // rose while the frame fell: it is now the work, not the redundancy. fn time_widths(iters: u32, app: &App) -> Duration { let layout = &app.layout; let model = &app.model_state.workbook.model; let plan = FormatPlan::of(model, app.model_state.workbook.active_view()); let mut best = Duration::MAX; for _ in 0..iters.max(3) { let start = Instant::now(); std::hint::black_box(compute_col_widths(model, layout, &plan)); best = best.min(start.elapsed()); } best } fn case(label: &str, regions: usize, products: usize, measures: usize, iters: u32) { report(label, dense_app(regions, products, measures), iters); } fn report(label: &str, app: App, iters: u32) { let rows = app.layout.row_count(); let cols = app.layout.col_count(); let cells = rows * cols; let full = time(iters, &app, whole_grid(&app)); let windowed = time(iters, &app, terminal_window()); let widths = time_widths(iters, &app); let ratio = full.as_secs_f64() / windowed.as_secs_f64().max(f64::EPSILON); let share = 100.0 * widths.as_secs_f64() / full.as_secs_f64().max(f64::EPSILON); // A part cannot take longer than the whole that contains it. Say so rather // than printing a number that reads like a measurement. let share = match share > 100.0 { true => "UNSTABLE".to_string(), false => format!("{share:.0}%"), }; // The TUI pays both on every keypress: widths in `cmd_context`, the grid in draw. let keypress = full + widths; println!( "{label:<22} {rows:>5} x {cols:>3} = {cells:>7} cells whole-grid {full:>11.3?} \ windowed {windowed:>11.3?} ratio {ratio:>4.1}x col_widths {widths:>11.3?} ({share} of the frame) \ keypress {keypress:>11.3?}" ); } /// The layout build with `prune_empty` on: every cell evaluated to find the empty /// rows and columns, on the same keypress path as `grid_of` (improvise-metf). fn report_prune(label: &str, mut app: App, iters: u32) { app.model_state.workbook.active_view_mut().prune_empty = true; let model = &app.model_state.workbook.model; let view = app.model_state.workbook.active_view(); let mut best = Duration::MAX; for _ in 0..iters.max(3) { let start = Instant::now(); std::hint::black_box(GridLayout::with_frozen_records(model, view, None)); best = best.min(start.elapsed()); } println!("{label:<22} layout build with prune_empty {best:>11.3?}"); } fn main() { println!("project::grid_of, one call = one TUI frame's grid cache"); println!("a 60fps frame budget is 16.7ms; the TUI redraws on each keypress\n"); case("typical", 20, 5, 4, 200); case("1.2k cells", 60, 5, 4, 100); case("2.4k cells", 60, 10, 4, 100); case("4.8k cells", 60, 20, 4, 50); case("large", 60, 20, 8, 50); case("18k cells (gotcha 41)", 150, 20, 6, 20); case("pathological", 300, 30, 10, 5); println!("\nfolded rows and a five-formula chain (improvise-h6z1)\n"); report("folded chain ~18k", chain_app(1000, 30), 5); report("folded chain ~90k", chain_app(5000, 30), 3); println!("\nthe same, pruned (improvise-metf)\n"); report_prune("dense 18k", dense_app(150, 20, 6), 5); report_prune("folded chain ~18k", chain_app(1000, 30), 5); report_prune("folded chain ~90k", chain_app(5000, 30), 3); println!("\ncollapsed small groups on the row axis (improvise-jq0m)\n"); report("364 days in weeks", grouped_app(364, 7, 20, 6), 5); report("364 days in pairs", grouped_app(364, 2, 20, 6), 5); report("1000 days in pairs", grouped_app(1000, 2, 10, 4), 3); }