Files
improvise/benches/reshape_observation.rs
T
Edward Langley 1dd6950ed3 A view-scoped ModelCommand names its view (improvise-lltt)
Nine ModelCommand variants configure a view and named it implicitly: the
effect read App::view_mut(), the HOST's slice. Since improvise-jytm made
ViewState::active_view authoritative per client, a client on another view
moved the host's view instead of its own -- its grid did not change and
every other client's did.

Each of the nine now carries `view: String`; model_effects maps it to
effect::ViewTarget::Named, and the nine effects carry a ViewTarget rather
than reading an ambient one. There is no operandless form left:
App::view_mut() is deleted and replaced by view_mut_of(&ViewTarget), so a
future geometry effect has to say which view it writes. The host's key
path passes ViewTarget::Active, which is right -- the TUI is the host and
has no session to ask. A Named view the document has not got is a no-op
rather than resolve_view's fallback onto the default.

Second half, which the ticket did not predict: naming the view made a
reframe of a view the host is NOT on reachable, and adopt_cursor_epoch
could not see one -- cursor_epoch is bumped only by the host's own
reset_cursor, so those clients were reset by nobody and told nothing
(gotcha 24's strictly-worse-than-nothing state). App::last_reframed_elsewhere
is the second trigger; its opening frames are taken lazily inside
view_mut_of, the one door such a write goes through, so the held-key path
still hashes one view and improvise-mcq4's declined cost is not paid.

Evidence and the three injected faults: context/test-evidence/command.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:23:27 -07:00

139 lines
5.1 KiB
Rust

//! What a model batch costs on the keypress path, which is where the reshape
//! observation sits (improvise-ni6k). Run with
//! `cargo bench --bench reshape_observation`.
use std::time::{Duration, 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::{CycleAxis, Effect, SetCell, ViewTarget};
/// The fixture `grid_projection` uses, so the two numbers are comparable.
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 regions: Vec<String> = (0..regions).map(|i| format!("Region{i}")).collect();
let products: Vec<String> = (0..products).map(|i| format!("Product{i}")).collect();
let measures: Vec<String> = (0..measures).map(|i| format!("Measure{i}")).collect();
for r in &regions {
wb.add_item("Region", r.clone()).unwrap();
}
for p in &products {
wb.add_item("Product", p.clone()).unwrap();
}
for m in &measures {
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 &regions {
for p in &products {
for m in &measures {
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));
}
}
}
// More than one view, because the observation runs over all of them.
wb.create_view("Second");
wb.switch_view("Default").unwrap();
let mut app = App::new(wb, None);
app.rebuild_layout();
app
}
/// Min-of-N for the reason `grid_projection` says: a mean over a few runs
/// measures the machine, not the code.
fn time_batch(iters: u32, app: &mut App, key: &CellKey) -> Duration {
let mut best = Duration::MAX;
for i in 0..iters.max(3) {
let effects: Vec<Box<dyn Effect>> = vec![Box::new(SetCell(
key.clone(),
CellValue::Number(i as f64 + 1.0),
))];
let start = Instant::now();
std::hint::black_box(app.apply_effects(effects));
best = best.min(start.elapsed());
}
best
}
fn case(label: &str, regions: usize, products: usize, measures: usize, iters: u32) {
let mut app = dense_app(regions, products, measures);
let (rows, cols) = (app.layout.row_count(), app.layout.col_count());
let key = CellKey::new(vec![
("Region".to_string(), "Region0".to_string()),
("Product".to_string(), "Product0".to_string()),
("_Measure".to_string(), "Measure0".to_string()),
]);
let batch = time_batch(iters, &mut app, &key);
println!(
"{label:<22} {rows:>5} x {cols:>3} = {:>7} cells one SetCell batch {batch:>11.3?}",
rows * cols
);
}
/// The **held-key** path improvise-748f exists for: a geometry batch writes no
/// cell, so `document_content` never runs and the frame fingerprint is alone.
fn geometry_case(label: &str, regions: usize, products: usize, measures: usize, iters: u32) {
// DO NOT set this false to "simplify". `prune_empty` is the ONLY thing that
// makes the fingerprint walk cells — see `context/code/command.md`.
geometry_at(label, regions, products, measures, iters, true);
}
fn geometry_at(
label: &str,
regions: usize,
products: usize,
measures: usize,
iters: u32,
prune: bool,
) {
let mut app = dense_app(regions, products, measures);
app.model_state.workbook.active_view_mut().prune_empty = prune;
app.rebuild_layout();
let (rows, cols) = (app.layout.row_count(), app.layout.col_count());
let mut best = Duration::MAX;
for i in 0..iters.max(3) {
let effects: Vec<Box<dyn Effect>> = vec![Box::new(CycleAxis(
ViewTarget::Active,
"Product".to_string(),
))];
let start = Instant::now();
std::hint::black_box(app.apply_effects(effects));
best = best.min(start.elapsed());
let _ = i;
}
println!(
"{label:<22} {rows:>5} x {cols:>3} = {:>7} cells one CycleAxis batch {best:>11.3?}",
rows * cols
);
}
fn main() {
println!("App::apply_effects, one model batch = one keypress that writes a cell");
println!("a 60fps frame budget is 16.7ms\n");
case("typical", 20, 5, 4, 200);
case("large", 60, 20, 8, 50);
case("18k cells (gotcha 41)", 150, 20, 6, 20);
case("pathological", 300, 30, 10, 5);
println!("\nthe held-key path (improvise-748f): a batch that writes no cell");
geometry_case("typical", 20, 5, 4, 200);
geometry_case("large", 60, 20, 8, 50);
geometry_case("18k cells (gotcha 41)", 150, 20, 6, 20);
geometry_case("pathological", 300, 30, 10, 5);
}