gpui: Improve performance of laying out long lines (#19215)

TL;DR: Another O(n^2) strikes.

In #19194 we received a report about a 7Mb JSON file that Zed struggles
with. Naturally this file showcased a O(n^2) in line layout; this file
has one long line.

During line layout for Mac we have to convert between UTF-16 and UTF-8
indices in the string, as CoreText works with UTF-16 and Rust strings
are UTF-8. The problem stemmed from the fact that we were re-seeking our
string converter on each glyph, which boils down to: we were reparsing
[0..curr_string_position] bytes up to full length of the string, which
is the O(n^2) in question. This PR changes this behaviour to reuse the
Index Converter if the position we're seeking to is not yet reached.
Basically, we're treating the converter as forward iterator and we try
to seek with the same iterator, if possible.

Where previously you could not even open the file in OP (within
reasonable time frame, I waited for 40 seconds before giving up), now
you can do it in.. slightly over a second. The best part is: the
experience is still not ideal. Typing in the buffer is sluggish. Still,
this is a start.


Release Notes:

- Mac: Improved performance with very long lines
This commit is contained in:
Piotr Osiewicz 2024-10-15 16:28:47 +02:00 committed by GitHub
parent 397e4bee0a
commit 4fa75a78b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 11 additions and 15 deletions

View file

@ -13087,18 +13087,11 @@ fn snippet_completions(
return vec![];
}
let snapshot = buffer.read(cx).text_snapshot();
let chunks = snapshot.reversed_chunks_in_range(text::Anchor::MIN..buffer_position);
let mut lines = chunks.lines();
let Some(line_at) = lines.next().filter(|line| !line.is_empty()) else {
return vec![];
};
let chars = snapshot.reversed_chars_for_range(text::Anchor::MIN..buffer_position);
let scope = language.map(|language| language.default_scope());
let classifier = CharClassifier::new(scope).for_completion(true);
let mut last_word = line_at
.chars()
.rev()
let mut last_word = chars
.take_while(|c| classifier.is_word(*c))
.collect::<String>();
last_word = last_word.chars().rev().collect();

View file

@ -470,9 +470,10 @@ impl MacTextSystemState {
// Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets.
let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef());
let mut runs = Vec::new();
for run in line.glyph_runs().into_iter() {
let glyph_runs = line.glyph_runs();
let mut runs = Vec::with_capacity(glyph_runs.len() as usize);
let mut ix_converter = StringIndexConverter::new(text);
for run in glyph_runs.into_iter() {
let attributes = run.attributes().unwrap();
let font = unsafe {
attributes
@ -482,7 +483,6 @@ impl MacTextSystemState {
};
let font_id = self.id_for_native_font(font);
let mut ix_converter = StringIndexConverter::new(text);
let mut glyphs = SmallVec::new();
for ((glyph_id, position), glyph_utf16_ix) in run
.glyphs()
@ -491,6 +491,10 @@ impl MacTextSystemState {
.zip(run.string_indices().iter())
{
let glyph_utf16_ix = usize::try_from(*glyph_utf16_ix).unwrap();
if ix_converter.utf16_ix > glyph_utf16_ix {
// We cannot reuse current index converter, as it can only seek forward. Restart the search.
ix_converter = StringIndexConverter::new(text);
}
ix_converter.advance_to_utf16_ix(glyph_utf16_ix);
glyphs.push(ShapedGlyph {
id: GlyphId(*glyph_id as u32),
@ -500,9 +504,8 @@ impl MacTextSystemState {
});
}
runs.push(ShapedRun { font_id, glyphs })
runs.push(ShapedRun { font_id, glyphs });
}
let typographic_bounds = line.get_typographic_bounds();
LineLayout {
runs,