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:
iris-aiandClaude Opus 5 committed 2026-09-14 18:49:45 -04:00
1 parent f1a47e9b7b
commit e5f8b6b244
7 files changed
+321 -43

No files matched your search

-4
View File
@@ -130,7 +130,6 @@ impl<'a> TextEditCtx<'a> {
pub fn set(&mut self, text: &str) {
let text = self.string(text);
self.text.view.buf.set_text(text);
self.text.view.buf.changed = true;
self.text.selection = None;
}
@@ -177,7 +176,6 @@ impl<'a> TextEditCtx<'a> {
};
let at = at.min(self.text.view.buf.text().len());
self.text.view.buf.edit().insert_str(at, text);
self.text.view.buf.changed = true;
self.set_caret(at + text.len());
}
@@ -190,7 +188,6 @@ impl<'a> TextEditCtx<'a> {
}
let range = sel.text_range();
self.text.view.buf.edit().replace_range(range.clone(), "");
self.text.view.buf.changed = true;
self.set_caret(range.start);
true
}
@@ -268,7 +265,6 @@ impl<'a> TextEditCtx<'a> {
fn delete_range(&mut self, start: usize, end: usize) {
self.text.view.buf.edit().replace_range(start..end, "");
self.text.view.buf.changed = true;
self.set_caret(start);
}