Shape a text once per width, not once per ask
A container measures a child by drawing it in a box it may not keep, so one layout asks a text for a dozen widths and comes back to widths it has already had -- the hottest text in the depth-8 tree draws 32 times. Each ask re-ran the shaper, because the two caches in front of it held one entry each and a trial width alternating with a final width evicts the answer about to be wanted again. `perf record` put 63% of a resize frame in text and 0.9% in `draw_inner`. So keep more than one: a bounded store of shapings on `TextData`, keyed by the text, the attrs and the width, holding the parley layout and the glyphs placed from it. Bounding the store rather than each buffer is what keeps it a fixed cost -- +4 MB on a tree of 4,000 texts, which is 19 MB less than the code before #16 holds after the same resizes. `TextBuffer` now holds the glyphs of the shaping it is drawn as, which is where `TextView::tex` was. That leaves one place to invalidate rather than two, so the `MutDetect` flags on a view's text and attrs have no reader and go, along with the `buf.changed = true` after every edit. On a 40-row tree of distinct random paragraphs, 500 resize frames: 124.2M instructions per frame before, 17.7M after, and 45.9M when the width never repeats. The five reference renders and the resize render are byte-identical, and the 100-seed sweep passes. `tests/revision_cost.rs` is that tree, written in the API subset `43ce8c7` shares so the same source measures the code this replaced. Report the worst frame and p99 beside the median, since a stutter is what somebody sees. Count glyph placements, and count a text render per ask rather than per shaping, so the store cannot hide how many times a layout drew the same text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f1a47e9b7b
commit
e5f8b6b244
7 files changed
+321
-43
No files matched your search
@@ -109,10 +109,15 @@ fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
|
||||
fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
|
||||
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let frames = elapsed.len();
|
||||
// The worst frame is the stutter somebody sees, so it goes beside the
|
||||
// median; p99 says whether it is the load or a single interruption.
|
||||
println!(
|
||||
"{label}: {frames} frame(s), min {:.3} ms, median {:.3} ms, total {:.1} ms",
|
||||
"{label}: {frames} frame(s), min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
|
||||
max {:.3} ms, total {:.1} ms",
|
||||
elapsed[0],
|
||||
elapsed[frames / 2],
|
||||
elapsed[frames * 99 / 100],
|
||||
elapsed[frames - 1],
|
||||
elapsed.iter().sum::<f64>(),
|
||||
);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
|
||||
Reference in new issue
Block a user