diff --git a/iris/benches/message_list.rs b/iris/benches/message_list.rs index bf849b8..f923250 100644 --- a/iris/benches/message_list.rs +++ b/iris/benches/message_list.rs @@ -142,7 +142,7 @@ fn bench_first_frame(n: usize) { let start = Instant::now(); render.update(&root, &mut rsc); let elapsed = start.elapsed(); - let (draws, rewrites, moves) = render.take_counters(); + let (draws, rewrites, moves, _shapes) = render.take_counters(); report( &format!("(a) first frame, N={n}"), elapsed, @@ -177,7 +177,7 @@ fn bench_scroll(n: usize, ticks: usize) { let start = Instant::now(); render.update(&root, &mut rsc); total += start.elapsed(); - let (draws, rewrites, moves) = render.take_counters(); + let (draws, rewrites, moves, _shapes) = render.take_counters(); total_draws += draws; total_rewrites += rewrites; total_moves += moves; @@ -245,7 +245,7 @@ fn bench_input_grows(n: usize, lines: usize) { let start = Instant::now(); render.update(&root, &mut rsc); total += start.elapsed(); - let (draws, rewrites, moves) = render.take_counters(); + let (draws, rewrites, moves, _shapes) = render.take_counters(); total_draws += draws; total_rewrites += rewrites; total_moves += moves; @@ -302,7 +302,7 @@ fn bench_insert_above_anchor(n: usize, inserts: usize) { let start = Instant::now(); render.update(&root, &mut rsc); total += start.elapsed(); - let (draws, rewrites, moves) = render.take_counters(); + let (draws, rewrites, moves, _shapes) = render.take_counters(); total_draws += draws; total_rewrites += rewrites; total_moves += moves; @@ -384,7 +384,7 @@ fn bench_expand_holds_edge(n: usize, growths: usize) { let start = Instant::now(); render.update(&root, &mut rsc); total += start.elapsed(); - let (draws, rewrites, moves) = render.take_counters(); + let (draws, rewrites, moves, _shapes) = render.take_counters(); total_draws += draws; total_rewrites += rewrites; total_moves += moves; diff --git a/iris/core/src/ui/painter.rs b/iris/core/src/ui/painter.rs index 7a4a061..30316da 100644 --- a/iris/core/src/ui/painter.rs +++ b/iris/core/src/ui/painter.rs @@ -191,6 +191,10 @@ impl<'a> Painter<'a> { width: Option, ) -> RenderedText { 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(); ui.text .render(buffer, attrs, width, &mut ui.textures, density) diff --git a/iris/core/src/ui/render_state.rs b/iris/core/src/ui/render_state.rs index f7057f0..14f3cb5 100644 --- a/iris/core/src/ui/render_state.rs +++ b/iris/core/src/ui/render_state.rs @@ -55,6 +55,9 @@ pub struct UiRenderState { draw_count: u64, region_mut_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 @@ -76,17 +79,25 @@ impl UiRenderState { draw_count: 0, region_mut_count: 0, mov_count: 0, + shape_count: 0, } } /// Reads and zeroes the (draws, region_mut rewrites, move_offsets - /// writes) counters -- call once per frame before `update()` to - /// measure exactly that frame, per LAYOUT.md section 8. - pub fn take_counters(&mut self) -> (u64, u64, u64) { + /// writes, text shapes) counters -- call once per frame before + /// `update()` to measure exactly that frame, per LAYOUT.md section 8. + /// + /// 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.region_mut_count), std::mem::take(&mut self.mov_count), + std::mem::take(&mut self.shape_count), ) } diff --git a/iris/src/layout_tests.rs b/iris/src/layout_tests.rs index 4b5ea89..b56ce70 100644 --- a/iris/src/layout_tests.rs +++ b/iris/src/layout_tests.rs @@ -68,7 +68,7 @@ fn an_unchanged_frame_draws_and_rewrites_nothing() { render.take_counters(); // discard the first, real draw 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)); } @@ -101,7 +101,7 @@ fn scrolling_moves_in_o1_without_a_redraw() { // already clamped) rather than actually moving anything. rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-40.0); 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 // 1 move_offsets write, independent of how many rects are in the diff --git a/iris/src/widget/list.rs b/iris/src/widget/list.rs index 59af0b8..62d4dec 100644 --- a/iris/src/widget/list.rs +++ b/iris/src/widget/list.rs @@ -1104,7 +1104,7 @@ mod tests { .push_front(ListRow::new(key, w)); } 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 // 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); 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 // O(n) regression would show up as draws/moves scaling with diff --git a/iris/transcript-ui/src/lib.rs b/iris/transcript-ui/src/lib.rs index abfea0f..a80e282 100644 --- a/iris/transcript-ui/src/lib.rs +++ b/iris/transcript-ui/src/lib.rs @@ -615,7 +615,8 @@ mod apply_tests { screen.apply(&mut rsc, &old_items, &new_items); render.update(&tree, &mut rsc); 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 @@ -636,13 +637,25 @@ mod apply_tests { reply(100, "").len() > 3_000, "the long case must actually be a long message" ); - let (short_draws, _) = cost_of_one_delta(1); - let (long_draws, _) = cost_of_one_delta(100); + let (short_draws, short_shapes) = cost_of_one_delta(1); + let (long_draws, long_shapes) = cost_of_one_delta(100); assert_eq!( short_draws, long_draws, "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" ); + // 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]