iris: count text layouts, so "a delta shapes one block" is measured rather than argued
take_counters gains a fourth counter, text shapes, bumped in Painter::render_text -- which TextView::render only reaches on a cache miss, so it counts shapes and not requests. A draw counter cannot stand in for it in either direction: a widget can be redrawn without re-shaping (the layout is memoized by width) and re-shaped without any extra draw, and re-shaping is the whole thing the per-block transcript row exists to avoid. With it, a_delta_into_a_long_reply_redraws_the_same_widgets_as_a_short_one asserts the number docs/DECISIONS.md's 2026-09-06 entry actually claims: one delta into a 100-paragraph reply shapes exactly one text layout, the same as into a one-paragraph one. Before the split that was necessarily O(message), since the reply was one buffer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
155d899e55
commit
c3cfc67bb3
6 files changed
+43
-15
No files matched your search
@@ -142,7 +142,7 @@ fn bench_first_frame(n: usize) {
|
|||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
let (draws, rewrites, moves) = render.take_counters();
|
let (draws, rewrites, moves, _shapes) = render.take_counters();
|
||||||
report(
|
report(
|
||||||
&format!("(a) first frame, N={n}"),
|
&format!("(a) first frame, N={n}"),
|
||||||
elapsed,
|
elapsed,
|
||||||
@@ -177,7 +177,7 @@ fn bench_scroll(n: usize, ticks: usize) {
|
|||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
total += start.elapsed();
|
total += start.elapsed();
|
||||||
let (draws, rewrites, moves) = render.take_counters();
|
let (draws, rewrites, moves, _shapes) = render.take_counters();
|
||||||
total_draws += draws;
|
total_draws += draws;
|
||||||
total_rewrites += rewrites;
|
total_rewrites += rewrites;
|
||||||
total_moves += moves;
|
total_moves += moves;
|
||||||
@@ -245,7 +245,7 @@ fn bench_input_grows(n: usize, lines: usize) {
|
|||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
total += start.elapsed();
|
total += start.elapsed();
|
||||||
let (draws, rewrites, moves) = render.take_counters();
|
let (draws, rewrites, moves, _shapes) = render.take_counters();
|
||||||
total_draws += draws;
|
total_draws += draws;
|
||||||
total_rewrites += rewrites;
|
total_rewrites += rewrites;
|
||||||
total_moves += moves;
|
total_moves += moves;
|
||||||
@@ -302,7 +302,7 @@ fn bench_insert_above_anchor(n: usize, inserts: usize) {
|
|||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
total += start.elapsed();
|
total += start.elapsed();
|
||||||
let (draws, rewrites, moves) = render.take_counters();
|
let (draws, rewrites, moves, _shapes) = render.take_counters();
|
||||||
total_draws += draws;
|
total_draws += draws;
|
||||||
total_rewrites += rewrites;
|
total_rewrites += rewrites;
|
||||||
total_moves += moves;
|
total_moves += moves;
|
||||||
@@ -384,7 +384,7 @@ fn bench_expand_holds_edge(n: usize, growths: usize) {
|
|||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
total += start.elapsed();
|
total += start.elapsed();
|
||||||
let (draws, rewrites, moves) = render.take_counters();
|
let (draws, rewrites, moves, _shapes) = render.take_counters();
|
||||||
total_draws += draws;
|
total_draws += draws;
|
||||||
total_rewrites += rewrites;
|
total_rewrites += rewrites;
|
||||||
total_moves += moves;
|
total_moves += moves;
|
||||||
|
|||||||
@@ -191,6 +191,10 @@ impl<'a> Painter<'a> {
|
|||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
) -> RenderedText {
|
) -> RenderedText {
|
||||||
let density = self.state.density;
|
let density = self.state.density;
|
||||||
|
// Counted here rather than in `TextView::render`, which returns
|
||||||
|
// its memoized layout without reaching this -- so this counts
|
||||||
|
// shapes, not requests. `UiRenderState::take_counters`.
|
||||||
|
self.state.shape_count += 1;
|
||||||
let ui = self.rsc.ui_mut();
|
let ui = self.rsc.ui_mut();
|
||||||
ui.text
|
ui.text
|
||||||
.render(buffer, attrs, width, &mut ui.textures, density)
|
.render(buffer, attrs, width, &mut ui.textures, density)
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ pub struct UiRenderState {
|
|||||||
draw_count: u64,
|
draw_count: u64,
|
||||||
region_mut_count: u64,
|
region_mut_count: u64,
|
||||||
mov_count: u64,
|
mov_count: u64,
|
||||||
|
/// Text layouts actually computed -- bumped by `Painter::render_text`,
|
||||||
|
/// which `TextView::render` only reaches on a cache miss.
|
||||||
|
pub(super) shape_count: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A move chain more than this deep would mean something else is wrong
|
/// A move chain more than this deep would mean something else is wrong
|
||||||
@@ -76,17 +79,25 @@ impl UiRenderState {
|
|||||||
draw_count: 0,
|
draw_count: 0,
|
||||||
region_mut_count: 0,
|
region_mut_count: 0,
|
||||||
mov_count: 0,
|
mov_count: 0,
|
||||||
|
shape_count: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads and zeroes the (draws, region_mut rewrites, move_offsets
|
/// Reads and zeroes the (draws, region_mut rewrites, move_offsets
|
||||||
/// writes) counters -- call once per frame before `update()` to
|
/// writes, text shapes) counters -- call once per frame before
|
||||||
/// measure exactly that frame, per LAYOUT.md section 8.
|
/// `update()` to measure exactly that frame, per LAYOUT.md section 8.
|
||||||
pub fn take_counters(&mut self) -> (u64, u64, u64) {
|
///
|
||||||
|
/// The fourth is the one a draw count cannot stand in for: a widget
|
||||||
|
/// can be redrawn without re-shaping (`TextView::render` memoizes by
|
||||||
|
/// width) and re-shaped without any extra draw, and it is re-shaping
|
||||||
|
/// that the per-block transcript row exists to avoid -- see
|
||||||
|
/// `transcript_ui`'s `a_delta_into_a_long_reply_shapes_one_block`.
|
||||||
|
pub fn take_counters(&mut self) -> (u64, u64, u64, u64) {
|
||||||
(
|
(
|
||||||
std::mem::take(&mut self.draw_count),
|
std::mem::take(&mut self.draw_count),
|
||||||
std::mem::take(&mut self.region_mut_count),
|
std::mem::take(&mut self.region_mut_count),
|
||||||
std::mem::take(&mut self.mov_count),
|
std::mem::take(&mut self.mov_count),
|
||||||
|
std::mem::take(&mut self.shape_count),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ fn an_unchanged_frame_draws_and_rewrites_nothing() {
|
|||||||
render.take_counters(); // discard the first, real draw
|
render.take_counters(); // discard the first, real draw
|
||||||
|
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
let (draws, rewrites, moves) = render.take_counters();
|
let (draws, rewrites, moves, _shapes) = render.take_counters();
|
||||||
assert_eq!((draws, rewrites, moves), (0, 0, 0));
|
assert_eq!((draws, rewrites, moves), (0, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ fn scrolling_moves_in_o1_without_a_redraw() {
|
|||||||
// already clamped) rather than actually moving anything.
|
// already clamped) rather than actually moving anything.
|
||||||
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-40.0);
|
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-40.0);
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
let (draws, _rewrites, moves) = render.take_counters();
|
let (draws, _rewrites, moves, _shapes) = render.take_counters();
|
||||||
|
|
||||||
// The pass condition (LAYOUT.md section 8, condition 3) is 0 draws and
|
// The pass condition (LAYOUT.md section 8, condition 3) is 0 draws and
|
||||||
// 1 move_offsets write, independent of how many rects are in the
|
// 1 move_offsets write, independent of how many rects are in the
|
||||||
|
|||||||
@@ -1104,7 +1104,7 @@ mod tests {
|
|||||||
.push_front(ListRow::new(key, w));
|
.push_front(ListRow::new(key, w));
|
||||||
}
|
}
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
let (draws, _rewrites, _moves) = render.take_counters();
|
let (draws, _rewrites, _moves, _shapes) = render.take_counters();
|
||||||
|
|
||||||
// None of the already-visible rows (11, 12) were touched: the
|
// None of the already-visible rows (11, 12) were touched: the
|
||||||
// extents for those keys are numerically unchanged, and the only
|
// extents for those keys are numerically unchanged, and the only
|
||||||
@@ -1242,7 +1242,7 @@ mod tests {
|
|||||||
|
|
||||||
rsc.ui.widgets.get_mut(&list_weak).unwrap().scroll(5.0);
|
rsc.ui.widgets.get_mut(&list_weak).unwrap().scroll(5.0);
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
let (draws, _rewrites, moves) = render.take_counters();
|
let (draws, _rewrites, moves, _shapes) = render.take_counters();
|
||||||
|
|
||||||
// The visible window is a fixed ~10 rows regardless of n; an
|
// The visible window is a fixed ~10 rows regardless of n; an
|
||||||
// O(n) regression would show up as draws/moves scaling with
|
// O(n) regression would show up as draws/moves scaling with
|
||||||
|
|||||||
@@ -615,7 +615,8 @@ mod apply_tests {
|
|||||||
screen.apply(&mut rsc, &old_items, &new_items);
|
screen.apply(&mut rsc, &old_items, &new_items);
|
||||||
render.update(&tree, &mut rsc);
|
render.update(&tree, &mut rsc);
|
||||||
assert_eq!(screen.take_rebuilds(), 0, "the delta path must be taken");
|
assert_eq!(screen.take_rebuilds(), 0, "the delta path must be taken");
|
||||||
(render.take_counters().0, 0)
|
let (draws, _, _, shapes) = render.take_counters();
|
||||||
|
(draws, shapes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The pass condition for docs/DECISIONS.md's per-block row: a delta
|
/// The pass condition for docs/DECISIONS.md's per-block row: a delta
|
||||||
@@ -636,13 +637,25 @@ mod apply_tests {
|
|||||||
reply(100, "").len() > 3_000,
|
reply(100, "").len() > 3_000,
|
||||||
"the long case must actually be a long message"
|
"the long case must actually be a long message"
|
||||||
);
|
);
|
||||||
let (short_draws, _) = cost_of_one_delta(1);
|
let (short_draws, short_shapes) = cost_of_one_delta(1);
|
||||||
let (long_draws, _) = cost_of_one_delta(100);
|
let (long_draws, long_shapes) = cost_of_one_delta(100);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
short_draws, long_draws,
|
short_draws, long_draws,
|
||||||
"a delta into a 100-paragraph reply redrew {long_draws} widgets against \
|
"a delta into a 100-paragraph reply redrew {long_draws} widgets against \
|
||||||
{short_draws} for a one-paragraph reply -- the earlier blocks are not being kept"
|
{short_draws} for a one-paragraph reply -- the earlier blocks are not being kept"
|
||||||
);
|
);
|
||||||
|
// The half a draw counter cannot see, and the one the per-block
|
||||||
|
// row actually exists for: a redraw is free if the text engine
|
||||||
|
// hits its memo, and a re-shape is the expensive thing. One
|
||||||
|
// shape, whatever the message is worth -- the block the delta
|
||||||
|
// landed in. Before the split this was necessarily O(message),
|
||||||
|
// since the whole reply was one buffer.
|
||||||
|
assert_eq!(
|
||||||
|
(short_shapes, long_shapes),
|
||||||
|
(1, 1),
|
||||||
|
"a delta shaped {long_shapes} text layouts in a 100-paragraph reply and \
|
||||||
|
{short_shapes} in a one-paragraph one; it must be the last block and nothing else"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user