Improve Tab Map performance (#32243)
## Context While looking into: #32051 and #16120 with instruments, I noticed that `TabSnapshot::to_tab_point` and `TabSnapshot::to_fold_point` are a common bottleneck between the two issues. This PR takes the first steps into closing the stated issues by improving the performance of both those functions. ### Method `to_tab_point` and `to_fold_point` iterate through each character in their rows to find tab characters and translate those characters into their respective transformations. This PR changes this iteration to take advantage of the tab character bitmap in the `Rope` data structure and goes directly to each tab character when iterating. The tab bitmap is now passed from each layer in-between the `Rope` to the `TabMap`. ### Testing I added several randomized tests to ensure that the new `to_tab_point` and `to_fold_point` functions have the same behavior as the old methods they're replacing. I also added `test_random_chunk_bitmap` on each layer the tab bitmap is passed up to the `TabMap` to make sure that the bitmap being passed is transformed correctly between the layers of `DisplayMap`. `test_random_chunk_bitmap` was added to these layers: - buffer - multi buffer - custom_highlights - inlay_map - fold_map ## Benchmarking I setup benchmarks with criterion that is runnable via `cargo bench -p editor --profile=release-fast`. When benchmarking I had my laptop plugged in and did so from the terminal with a minimal amount of processes running. I'm also on a m4 max ### Results #### To Tab Point Went from completing 6.8M iterations in 5s with an average time of `736.13 ns` to `683.38 ns` which is a `-7.1875%` improvement #### To Fold Point Went from completing 6.8M iterations in 5s with an average time of `736.55 ns` to `682.40 ns` which is a `-7.1659%` improvement #### Editor render Went from having an average render time of `62.561 µs` to `57.216 µs` which is a `-8.8248%` improvement #### Build Buffer with one long line Went from having an average buffer build time of `3.2549 ms` to `3.2635 ms` which is a `+0.2151%` regression within the margin of error #### Editor with 1000 multi cursor input Went from having an average edit time of `133.05 ms` to `122.96 ms` which is a `-7.5776%` improvement Release Notes: - N/A --------- Co-authored-by: Remco Smits <djsmits12@gmail.com> Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
This commit is contained in:
co-authored by
Remco Smits
Cole Miller
Piotr Osiewicz
parent
cb75c2aeb7
commit
b8c30f448f
@@ -7,6 +7,7 @@ use parking_lot::RwLock;
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
use std::env;
|
||||
use util::RandomCharIter;
|
||||
use util::test::sample_text;
|
||||
|
||||
#[ctor::ctor]
|
||||
@@ -3716,3 +3717,235 @@ fn test_new_empty_buffers_title_can_be_set(cx: &mut App) {
|
||||
});
|
||||
assert_eq!(multibuffer.read(cx).title(cx), "Hey");
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_chunk_bitmaps(cx: &mut App, mut rng: StdRng) {
|
||||
let multibuffer = if rng.random() {
|
||||
let len = rng.random_range(0..10000);
|
||||
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||||
cx.new(|cx| MultiBuffer::singleton(buffer, cx))
|
||||
} else {
|
||||
MultiBuffer::build_random(&mut rng, cx)
|
||||
};
|
||||
|
||||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||||
|
||||
let chunks = snapshot.chunks(0..snapshot.len(), false);
|
||||
|
||||
for chunk in chunks {
|
||||
let chunk_text = chunk.text;
|
||||
let chars_bitmap = chunk.chars;
|
||||
let tabs_bitmap = chunk.tabs;
|
||||
|
||||
if chunk_text.is_empty() {
|
||||
assert_eq!(
|
||||
chars_bitmap, 0,
|
||||
"Empty chunk should have empty chars bitmap"
|
||||
);
|
||||
assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
|
||||
continue;
|
||||
}
|
||||
|
||||
assert!(
|
||||
chunk_text.len() <= 128,
|
||||
"Chunk text length {} exceeds 128 bytes",
|
||||
chunk_text.len()
|
||||
);
|
||||
|
||||
// Verify chars bitmap
|
||||
let char_indices = chunk_text
|
||||
.char_indices()
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for byte_idx in 0..chunk_text.len() {
|
||||
let should_have_bit = char_indices.contains(&byte_idx);
|
||||
let has_bit = chars_bitmap & (1 << byte_idx) != 0;
|
||||
|
||||
if has_bit != should_have_bit {
|
||||
eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
|
||||
eprintln!("Char indices: {:?}", char_indices);
|
||||
eprintln!("Chars bitmap: {:#b}", chars_bitmap);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
has_bit, should_have_bit,
|
||||
"Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
|
||||
byte_idx, chunk_text, should_have_bit, has_bit
|
||||
);
|
||||
}
|
||||
|
||||
for (byte_idx, byte) in chunk_text.bytes().enumerate() {
|
||||
let is_tab = byte == b'\t';
|
||||
let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
|
||||
|
||||
if has_bit != is_tab {
|
||||
eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
|
||||
eprintln!("Tabs bitmap: {:#b}", tabs_bitmap);
|
||||
assert_eq!(
|
||||
has_bit, is_tab,
|
||||
"Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
|
||||
byte_idx, chunk_text, byte as char, is_tab, has_bit
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_chunk_bitmaps_with_diffs(cx: &mut App, mut rng: StdRng) {
|
||||
use buffer_diff::BufferDiff;
|
||||
use util::RandomCharIter;
|
||||
|
||||
let multibuffer = if rng.random() {
|
||||
let len = rng.random_range(100..10000);
|
||||
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||||
cx.new(|cx| MultiBuffer::singleton(buffer, cx))
|
||||
} else {
|
||||
MultiBuffer::build_random(&mut rng, cx)
|
||||
};
|
||||
|
||||
let _diff_count = rng.random_range(1..5);
|
||||
let mut diffs = Vec::new();
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
for buffer_id in multibuffer.excerpt_buffer_ids() {
|
||||
if rng.random_bool(0.7) {
|
||||
if let Some(buffer_handle) = multibuffer.buffer(buffer_id) {
|
||||
let buffer_text = buffer_handle.read(cx).text();
|
||||
let mut base_text = String::new();
|
||||
|
||||
for line in buffer_text.lines() {
|
||||
if rng.random_bool(0.3) {
|
||||
continue;
|
||||
} else if rng.random_bool(0.3) {
|
||||
let line_len = rng.random_range(0..50);
|
||||
let modified_line = RandomCharIter::new(&mut rng)
|
||||
.take(line_len)
|
||||
.collect::<String>();
|
||||
base_text.push_str(&modified_line);
|
||||
base_text.push('\n');
|
||||
} else {
|
||||
base_text.push_str(line);
|
||||
base_text.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if rng.random_bool(0.5) {
|
||||
let extra_lines = rng.random_range(1..5);
|
||||
for _ in 0..extra_lines {
|
||||
let line_len = rng.random_range(0..50);
|
||||
let extra_line = RandomCharIter::new(&mut rng)
|
||||
.take(line_len)
|
||||
.collect::<String>();
|
||||
base_text.push_str(&extra_line);
|
||||
base_text.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
let diff =
|
||||
cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer_handle, cx));
|
||||
diffs.push(diff.clone());
|
||||
multibuffer.add_diff(diff, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
if rng.random_bool(0.5) {
|
||||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||||
} else {
|
||||
let snapshot = multibuffer.snapshot(cx);
|
||||
let text = snapshot.text();
|
||||
|
||||
let mut ranges = Vec::new();
|
||||
for _ in 0..rng.random_range(1..5) {
|
||||
if snapshot.len() == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let diff_size = rng.random_range(5..1000);
|
||||
let mut start = rng.random_range(0..snapshot.len());
|
||||
|
||||
while !text.is_char_boundary(start) {
|
||||
start = start.saturating_sub(1);
|
||||
}
|
||||
|
||||
let mut end = rng.random_range(start..snapshot.len().min(start + diff_size));
|
||||
|
||||
while !text.is_char_boundary(end) {
|
||||
end = end.saturating_add(1);
|
||||
}
|
||||
let start_anchor = snapshot.anchor_after(start);
|
||||
let end_anchor = snapshot.anchor_before(end);
|
||||
ranges.push(start_anchor..end_anchor);
|
||||
}
|
||||
multibuffer.expand_diff_hunks(ranges, cx);
|
||||
}
|
||||
});
|
||||
|
||||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||||
|
||||
let chunks = snapshot.chunks(0..snapshot.len(), false);
|
||||
|
||||
for chunk in chunks {
|
||||
let chunk_text = chunk.text;
|
||||
let chars_bitmap = chunk.chars;
|
||||
let tabs_bitmap = chunk.tabs;
|
||||
|
||||
if chunk_text.is_empty() {
|
||||
assert_eq!(
|
||||
chars_bitmap, 0,
|
||||
"Empty chunk should have empty chars bitmap"
|
||||
);
|
||||
assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
|
||||
continue;
|
||||
}
|
||||
|
||||
assert!(
|
||||
chunk_text.len() <= 128,
|
||||
"Chunk text length {} exceeds 128 bytes",
|
||||
chunk_text.len()
|
||||
);
|
||||
|
||||
let char_indices = chunk_text
|
||||
.char_indices()
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for byte_idx in 0..chunk_text.len() {
|
||||
let should_have_bit = char_indices.contains(&byte_idx);
|
||||
let has_bit = chars_bitmap & (1 << byte_idx) != 0;
|
||||
|
||||
if has_bit != should_have_bit {
|
||||
eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
|
||||
eprintln!("Char indices: {:?}", char_indices);
|
||||
eprintln!("Chars bitmap: {:#b}", chars_bitmap);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
has_bit, should_have_bit,
|
||||
"Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
|
||||
byte_idx, chunk_text, should_have_bit, has_bit
|
||||
);
|
||||
}
|
||||
|
||||
for (byte_idx, byte) in chunk_text.bytes().enumerate() {
|
||||
let is_tab = byte == b'\t';
|
||||
let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
|
||||
|
||||
if has_bit != is_tab {
|
||||
eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
|
||||
eprintln!("Tabs bitmap: {:#b}", tabs_bitmap);
|
||||
assert_eq!(
|
||||
has_bit, is_tab,
|
||||
"Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
|
||||
byte_idx, chunk_text, byte as char, is_tab, has_bit
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user