585f25bf78
improvise-ni6k. Twelve effects called `app.reset_cursor()`; that table is
the shape gotcha 24 names as the disease, and it had a hole the day it
was written. The observation replaces it: `view_frames` fingerprints
every view's drawn frame, `apply_effects` diffs it per model effect, and
the cursor resets when the view it is in reframes. No effect declares
anything, and `adopt_cursor_epoch` reads the same set.
FOUR THINGS THE TICKET DID NOT PREDICT, each one a fixture or a design
question rather than a line:
1. The frame fingerprint and `layout_version` answer DIFFERENT questions,
so they are two observations and not one. `AddCategory` of an empty
category moves no row or column but does change the coordinates a
client's CellKey hangs off — layout_version must bump, the cursor need
not. `view_shape`/`last_reshaped_views` (config) is untouched and still
drives `layout_version`; `view_frame`/`last_reframed_views` (drawn) is
new and drives the cursor. Unifying them broke two tests in both
directions, which is how the distinction was found.
2. The observation must run INSIDE the effect loop, not after it. At
end-of-batch it clobbered deliberate cursor placement in the same batch
— `[AddItem(measure), <place cursor>]` sent the cursor home, failing
three records-mode tests. In-loop, a later effect wins, which is the
same ordering property gotcha 45 gives `document_config`.
3. `GridLayout::new` does NOT apply `prune_empty`; `with_frozen_records`
does. A frame hash built on `new` cannot see a pruned row, which is
exactly the class improvise-7kvw is in.
4. Two per-effect no-op guards were carrying the whole weight and are now
orphaned: `effective_items` and `item_is_drawn` are deleted, along with
six inline `moved`/`drawn`/`real`/`existed` predicates.
COST, measured as the ticket required, `benches/reshape_observation.rs`
(new), one SetCell batch, same fixtures as `grid_projection`:
typical 400 cells 172µs -> 206µs (+20%)
large 9.6k cells 3.70ms -> 4.17ms (+13%)
18k cells 7.09ms -> 8.41ms (+19%)
pathological 90k 35.2ms -> 39.8ms (+13%)
The batch is dominated by `rebuild_layout`/`recompute_formulas`, not by
the fingerprint.
WHAT CLOSES AS A CONSEQUENCE: improvise-7kvw, asserted by
`paging_across_a_prune_that_drops_a_row_invalidates_the_cursor`.
AddFormula/RemoveFormula join the shared `reshaping_commands` table (a
formula target is a `_Measure` item, so it is a column — gotcha 7), each
proved reshaping by the table's own `assert_ne!` on the grid shape.
WHAT DOES NOT CLOSE, and this is the honest part: `SortData`. A pure
record reorder changes no label and no count, and records-mode `row_items`
are positional, so a row_items fingerprint cannot see it. I wrote the
mechanism (hashing `layout.records`' coords) and then REMOVED it, because
I could not build a fixture where `SortData` actually reorders anything —
records came back in key order whatever order the cells went in. Shipping
a confident comment over a mechanism I could not demonstrate is this
repo's named defect, so it is reported instead. See the hand-back.
Negative arms unchanged and still passing, including the refused-command
arm and `SetNumberFormat`; disabling the observation fails the whole
reshaping table, which is the proof it is what does the work.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
97 lines
3.5 KiB
Rust
97 lines
3.5 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::{Effect, SetCell};
|
|
|
|
/// 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 ®ions {
|
|
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 ®ions {
|
|
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
|
|
);
|
|
}
|
|
|
|
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);
|
|
}
|