12f206fb0a
# Conflicts: # benches/grid_projection.rs
249 lines
9.1 KiB
Rust
249 lines
9.1 KiB
Rust
//! 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;
|
|
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<String> = (0..regions).map(|i| format!("Region{i}")).collect();
|
|
let product_names: Vec<String> = (0..products).map(|i| format!("Product{i}")).collect();
|
|
let measure_names: Vec<String> = (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);
|
|
for g in 0..regions.div_ceil(10) {
|
|
v.toggle_group_collapse("Region", &format!("G{g}"));
|
|
}
|
|
}
|
|
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);
|
|
for g in 0..days.div_ceil(per_group) {
|
|
v.toggle_group_collapse("Day", &format!("G{g}"));
|
|
}
|
|
}
|
|
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?}"
|
|
);
|
|
}
|
|
|
|
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!("\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);
|
|
}
|