Files
2026-07-13 12:08:36 +08:00

279 lines
9.0 KiB
Rust

use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
use criterion::{Criterion, black_box, criterion_group, criterion_main};
use floem::{
reactive::Scope,
text::{Attrs, AttrsList, FamilyOwned, TextLayout, Wrap},
views::editor::{
layout::TextLayoutLine,
phantom_text::PhantomTextLine,
visual_line::{
ConfigId, FontSizeCacheId, LineFontSizeProvider, Lines, ResolvedWrap,
TextLayoutProvider, VLine,
},
},
};
use lapce_core::{
buffer::rope_text::{RopeText, RopeTextRef},
cursor::CursorAffinity,
};
use lapce_xi_rope::Rope;
const FONT_SIZE: usize = 12;
// TODO: use the editor data view structures!
struct TLProv<'a> {
text: &'a Rope,
phantom: HashMap<usize, PhantomTextLine>,
font_family: Vec<FamilyOwned>,
has_multiline_phantom: bool,
}
impl TextLayoutProvider for TLProv<'_> {
fn text(&self) -> Rope {
self.text.clone()
}
// An implementation relatively close to the actual new text layout impl but simplified.
// TODO(minor): It would be nice to just use the same impl as view's
fn new_text_layout(
&self,
line: usize,
font_size: usize,
wrap: ResolvedWrap,
) -> Arc<TextLayoutLine> {
let rope_text = RopeTextRef::new(self.text);
let line_content_original = rope_text.line_content(line);
// Get the line content with newline characters replaced with spaces
// and the content without the newline characters
let (line_content, _line_content_original) =
if let Some(s) = line_content_original.strip_suffix("\r\n") {
(
format!("{s} "),
&line_content_original[..line_content_original.len() - 2],
)
} else if let Some(s) = line_content_original.strip_suffix('\n') {
(
format!("{s} ",),
&line_content_original[..line_content_original.len() - 1],
)
} else {
(
line_content_original.to_string(),
&line_content_original[..],
)
};
let phantom_text = self.phantom.get(&line).cloned().unwrap_or_default();
let line_content = phantom_text.combine_with_text(&line_content);
let attrs = Attrs::new()
.family(&self.font_family)
.font_size(font_size as f32);
let mut attrs_list = AttrsList::new(attrs.clone());
// We don't do line styles, since they aren't relevant
// Apply phantom text specific styling
for (offset, size, col, phantom) in phantom_text.offset_size_iter() {
let start = col + offset;
let end = start + size;
let mut attrs = attrs.clone();
if let Some(fg) = phantom.fg {
attrs = attrs.color(fg);
}
if let Some(phantom_font_size) = phantom.font_size {
attrs = attrs.font_size(phantom_font_size.min(font_size) as f32);
}
attrs_list.add_span(start..end, attrs);
// if let Some(font_family) = phantom.font_family.clone() {
// layout_builder = layout_builder.range_attribute(
// start..end,
// TextAttribute::FontFamily(font_family),
// );
// }
}
let mut text_layout = TextLayout::new();
text_layout.set_wrap(Wrap::Word);
match wrap {
// We do not have to set the wrap mode if we do not set the width
ResolvedWrap::None => {}
ResolvedWrap::Column(_col) => todo!(),
ResolvedWrap::Width(px) => {
text_layout.set_size(px, f32::MAX);
}
}
text_layout.set_text(&line_content, attrs_list, None);
// skip phantom text background styling because it doesn't shift positions
// skip severity styling
// skip diagnostic background styling
Arc::new(TextLayoutLine {
extra_style: Vec::new(),
text: text_layout,
whitespaces: None,
indent: 0.0,
phantom_text: PhantomTextLine::default(),
})
}
fn before_phantom_col(&self, line: usize, col: usize) -> usize {
if let Some(phantom) = self.phantom.get(&line) {
phantom.before_col(col)
} else {
col
}
}
fn has_multiline_phantom(&self) -> bool {
self.has_multiline_phantom
}
}
struct TestFontSize;
impl LineFontSizeProvider for TestFontSize {
fn font_size(&self, _line: usize) -> usize {
FONT_SIZE
}
fn cache_id(&self) -> FontSizeCacheId {
0
}
}
fn make_lines(text: &Rope, wrap: ResolvedWrap, init: bool) -> (TLProv<'_>, Lines) {
make_lines_ph(text, wrap, init, HashMap::new(), false)
}
fn make_lines_ph(
text: &Rope,
wrap: ResolvedWrap,
init: bool,
ph: HashMap<usize, PhantomTextLine>,
has_multiline_phantom: bool,
) -> (TLProv<'_>, Lines) {
// let wrap = Wrap::Word;
// let r_wrap = ResolvedWrap::Width(width);
let font_sizes = TestFontSize;
let text = TLProv {
text,
phantom: ph,
font_family: Vec::new(),
has_multiline_phantom,
};
let cx = Scope::new();
let lines = Lines::new(cx, RefCell::new(Rc::new(font_sizes)));
lines.set_wrap(wrap);
if init {
lines.init_all(0, ConfigId::new(0, 0), &text, true);
}
(text, lines)
}
fn medium_rope() -> Rope {
let mut text = String::new();
// TODO: use some actual file's content.
for i in 0..3000 {
let content = if i % 2 == 0 {
"This is a roughly typical line of text\n"
} else if i % 3 == 0 {
"\n"
} else {
"A short line\n"
};
text.push_str(content);
}
Rope::from(&text)
}
fn visual_line(c: &mut Criterion) {
let text = medium_rope();
// Should be very fast because it is trivially linear and there's no multiline phantom
c.bench_function("last vline (uninit)", |b| {
let (text_prov, lines) = make_lines(&text, ResolvedWrap::None, false);
b.iter(|| {
lines.clear_last_vline();
let last_vline = lines.last_vline(&text_prov);
black_box(last_vline);
})
});
// Unrealistic since the user will very rarely have all of the lines initialized
// Should still be fast because there's no wrapping or multiline phantom text
c.bench_function("last vline (all, no wrapping)", |b| {
let (text_prov, lines) = make_lines(&text, ResolvedWrap::None, true);
b.iter(|| {
lines.clear_last_vline();
let last_vline = lines.last_vline(&text_prov);
let _val = black_box(last_vline);
})
});
// TODO: we could precompute line count on the text layout?
// Unrealistic since the user will very rarely have all of the lines initialized
// Still decently fast, though. <1ms
c.bench_function("last vline (all, wrapping)", |b| {
let width = 100.0;
let (text_prov, lines) = make_lines(&text, ResolvedWrap::Width(width), true);
b.iter(|| {
// This should clear any other caching mechanisms that get added
lines.clear_last_vline();
let last_vline = lines.last_vline(&text_prov);
let _val = black_box(last_vline);
})
});
// Q: This seems like 1/5th the cost of last vline despite only being half the lines..
c.bench_function("vline of offset (all, wrapping)", |b| {
let width = 100.0;
let (text_prov, lines) = make_lines(&text, ResolvedWrap::Width(width), true);
// Not past the middle. If we were past the middle then it'd be just benching last vline
// calculation, which is admittedly relatively similar to this.
let offset = 1450;
b.iter(|| {
// This should clear any other caching mechanisms that get added
lines.clear_last_vline();
let vline =
lines.vline_of_offset(&text_prov, offset, CursorAffinity::Backward);
let _val = black_box(vline);
})
});
c.bench_function("offset of vline (all, wrapping)", |b| {
let width = 100.0;
let (text_prov, lines) = make_lines(&text, ResolvedWrap::Width(width), true);
let vline = VLine(3300);
b.iter(|| {
// This should clear any other caching mechanisms that get added
lines.clear_last_vline();
let offset = lines.offset_of_vline(&text_prov, vline);
let _val = black_box(offset);
})
});
// TODO: when we have the reverse search of vline for offset, we should have a separate instance where the last vline isn't cached, which would give us a range of 'worst case' (never reused, have to recopmute it everytime) and 'best case' (always reused)
// TODO: bench common operations, like a single line changing or the (equivalent of) cache rev
// updating.
}
criterion_group!(benches, visual_line);
criterion_main!(benches);