Skip to content

Commit e4937eb

Browse files
authored
rope: Improve prepend performance for small inputs on small ropes (zed-industries#50389)
Release Notes: - N/A *or* Added/Fixed/Improved ...
1 parent 746ecb0 commit e4937eb

File tree

3 files changed

+225
-0
lines changed

3 files changed

+225
-0
lines changed

crates/rope/src/chunk.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ impl Chunk {
102102
self.append(Chunk::new(text).as_slice());
103103
}
104104

105+
#[inline(always)]
106+
pub fn prepend_str(&mut self, text: &str) {
107+
self.prepend(Chunk::new(text).as_slice());
108+
}
109+
105110
#[inline(always)]
106111
pub fn append(&mut self, slice: ChunkSlice) {
107112
if slice.is_empty() {
@@ -116,6 +121,28 @@ impl Chunk {
116121
self.text.push_str(slice.text);
117122
}
118123

124+
#[inline(always)]
125+
pub fn prepend(&mut self, slice: ChunkSlice) {
126+
if slice.is_empty() {
127+
return;
128+
}
129+
if self.text.is_empty() {
130+
*self = Chunk::new(slice.text);
131+
return;
132+
}
133+
134+
let shift = slice.text.len();
135+
self.chars = slice.chars | (self.chars << shift);
136+
self.chars_utf16 = slice.chars_utf16 | (self.chars_utf16 << shift);
137+
self.newlines = slice.newlines | (self.newlines << shift);
138+
self.tabs = slice.tabs | (self.tabs << shift);
139+
140+
let mut new_text = ArrayString::<MAX_BASE>::new();
141+
new_text.push_str(slice.text);
142+
new_text.push_str(&self.text);
143+
self.text = new_text;
144+
}
145+
119146
#[inline(always)]
120147
pub fn as_slice(&self) -> ChunkSlice<'_> {
121148
ChunkSlice {
@@ -890,6 +917,24 @@ mod tests {
890917
verify_chunk(chunk1.as_slice(), &(str1 + &str2[start_offset..end_offset]));
891918
}
892919

920+
#[gpui::test(iterations = 1000)]
921+
fn test_prepend_random_strings(mut rng: StdRng) {
922+
let len1 = rng.random_range(0..=MAX_BASE);
923+
let len2 = rng.random_range(0..=MAX_BASE).saturating_sub(len1);
924+
let str1 = random_string_with_utf8_len(&mut rng, len1);
925+
let str2 = random_string_with_utf8_len(&mut rng, len2);
926+
let mut chunk1 = Chunk::new(&str1);
927+
let chunk2 = Chunk::new(&str2);
928+
let char_offsets = char_offsets_with_end(&str2);
929+
let start_index = rng.random_range(0..char_offsets.len());
930+
let start_offset = char_offsets[start_index];
931+
let end_offset = char_offsets[rng.random_range(start_index..char_offsets.len())];
932+
let slice = chunk2.slice(start_offset..end_offset);
933+
let prefix_text = &str2[start_offset..end_offset];
934+
chunk1.prepend(slice);
935+
verify_chunk(chunk1.as_slice(), &(prefix_text.to_owned() + &str1));
936+
}
937+
893938
/// Return the byte offsets for each character in a string.
894939
///
895940
/// These are valid offsets to split the string.

crates/rope/src/rope.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ impl Rope {
167167
(),
168168
);
169169

170+
if text.is_empty() {
171+
self.check_invariants();
172+
return;
173+
}
174+
170175
#[cfg(all(test, not(rust_analyzer)))]
171176
const NUM_CHUNKS: usize = 16;
172177
#[cfg(not(all(test, not(rust_analyzer))))]
@@ -269,6 +274,23 @@ impl Rope {
269274
}
270275

271276
pub fn push_front(&mut self, text: &str) {
277+
if text.is_empty() {
278+
return;
279+
}
280+
if self.is_empty() {
281+
self.push(text);
282+
return;
283+
}
284+
if self
285+
.chunks
286+
.first()
287+
.is_some_and(|c| c.text.len() + text.len() <= chunk::MAX_BASE)
288+
{
289+
self.chunks
290+
.update_first(|first_chunk| first_chunk.prepend_str(text), ());
291+
self.check_invariants();
292+
return;
293+
}
272294
let suffix = mem::replace(self, Rope::from(text));
273295
self.append(suffix);
274296
}
@@ -2339,6 +2361,119 @@ mod tests {
23392361
}
23402362
}
23412363

2364+
#[test]
2365+
fn test_push_front_empty_text_on_empty_rope() {
2366+
let mut rope = Rope::new();
2367+
rope.push_front("");
2368+
assert_eq!(rope.text(), "");
2369+
assert_eq!(rope.len(), 0);
2370+
}
2371+
2372+
#[test]
2373+
fn test_push_front_empty_text_on_nonempty_rope() {
2374+
let mut rope = Rope::from("hello");
2375+
rope.push_front("");
2376+
assert_eq!(rope.text(), "hello");
2377+
}
2378+
2379+
#[test]
2380+
fn test_push_front_on_empty_rope() {
2381+
let mut rope = Rope::new();
2382+
rope.push_front("hello");
2383+
assert_eq!(rope.text(), "hello");
2384+
assert_eq!(rope.len(), 5);
2385+
assert_eq!(rope.max_point(), Point::new(0, 5));
2386+
}
2387+
2388+
#[test]
2389+
fn test_push_front_single_space() {
2390+
let mut rope = Rope::from("hint");
2391+
rope.push_front(" ");
2392+
assert_eq!(rope.text(), " hint");
2393+
assert_eq!(rope.len(), 5);
2394+
}
2395+
2396+
#[gpui::test(iterations = 50)]
2397+
fn test_push_front_random(mut rng: StdRng) {
2398+
let initial_len = rng.random_range(0..=64);
2399+
let initial_text: String = RandomCharIter::new(&mut rng).take(initial_len).collect();
2400+
let mut rope = Rope::from(initial_text.as_str());
2401+
2402+
let mut expected = initial_text;
2403+
2404+
for _ in 0..rng.random_range(1..=10) {
2405+
let prefix_len = rng.random_range(0..=32);
2406+
let prefix: String = RandomCharIter::new(&mut rng).take(prefix_len).collect();
2407+
2408+
rope.push_front(&prefix);
2409+
expected.insert_str(0, &prefix);
2410+
2411+
assert_eq!(
2412+
rope.text(),
2413+
expected,
2414+
"text mismatch after push_front({:?})",
2415+
prefix
2416+
);
2417+
assert_eq!(rope.len(), expected.len());
2418+
2419+
let actual_summary = rope.summary();
2420+
let expected_summary = TextSummary::from(expected.as_str());
2421+
assert_eq!(
2422+
actual_summary.len, expected_summary.len,
2423+
"len mismatch for {:?}",
2424+
expected
2425+
);
2426+
assert_eq!(
2427+
actual_summary.lines, expected_summary.lines,
2428+
"lines mismatch for {:?}",
2429+
expected
2430+
);
2431+
assert_eq!(
2432+
actual_summary.chars, expected_summary.chars,
2433+
"chars mismatch for {:?}",
2434+
expected
2435+
);
2436+
assert_eq!(
2437+
actual_summary.longest_row, expected_summary.longest_row,
2438+
"longest_row mismatch for {:?}",
2439+
expected
2440+
);
2441+
2442+
// Verify offset-to-point and point-to-offset round-trip at boundaries.
2443+
for (ix, _) in expected.char_indices().chain(Some((expected.len(), '\0'))) {
2444+
assert_eq!(
2445+
rope.point_to_offset(rope.offset_to_point(ix)),
2446+
ix,
2447+
"offset round-trip failed at {} for {:?}",
2448+
ix,
2449+
expected
2450+
);
2451+
}
2452+
}
2453+
}
2454+
2455+
#[gpui::test(iterations = 50)]
2456+
fn test_push_front_large_prefix(mut rng: StdRng) {
2457+
let initial_len = rng.random_range(0..=32);
2458+
let initial_text: String = RandomCharIter::new(&mut rng).take(initial_len).collect();
2459+
let mut rope = Rope::from(initial_text.as_str());
2460+
2461+
let prefix_len = rng.random_range(64..=256);
2462+
let prefix: String = RandomCharIter::new(&mut rng).take(prefix_len).collect();
2463+
2464+
rope.push_front(&prefix);
2465+
let expected = format!("{}{}", prefix, initial_text);
2466+
2467+
assert_eq!(rope.text(), expected);
2468+
assert_eq!(rope.len(), expected.len());
2469+
2470+
let actual_summary = rope.summary();
2471+
let expected_summary = TextSummary::from(expected.as_str());
2472+
assert_eq!(actual_summary.len, expected_summary.len);
2473+
assert_eq!(actual_summary.lines, expected_summary.lines);
2474+
assert_eq!(actual_summary.chars, expected_summary.chars);
2475+
}
2476+
23422477
fn clip_offset(text: &str, mut offset: usize, bias: Bias) -> usize {
23432478
while !text.is_char_boundary(offset) {
23442479
match bias {

crates/sum_tree/src/sum_tree.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,51 @@ impl<T: Item> SumTree<T> {
661661
}
662662
}
663663

664+
pub fn update_first(
665+
&mut self,
666+
f: impl FnOnce(&mut T),
667+
cx: <T::Summary as Summary>::Context<'_>,
668+
) {
669+
self.update_first_recursive(f, cx);
670+
}
671+
672+
fn update_first_recursive(
673+
&mut self,
674+
f: impl FnOnce(&mut T),
675+
cx: <T::Summary as Summary>::Context<'_>,
676+
) -> Option<T::Summary> {
677+
match Arc::make_mut(&mut self.0) {
678+
Node::Internal {
679+
summary,
680+
child_summaries,
681+
child_trees,
682+
..
683+
} => {
684+
let first_summary = child_summaries.first_mut().unwrap();
685+
let first_child = child_trees.first_mut().unwrap();
686+
*first_summary = first_child.update_first_recursive(f, cx).unwrap();
687+
*summary = sum(child_summaries.iter(), cx);
688+
Some(summary.clone())
689+
}
690+
Node::Leaf {
691+
summary,
692+
items,
693+
item_summaries,
694+
} => {
695+
if let Some((item, item_summary)) =
696+
items.first_mut().zip(item_summaries.first_mut())
697+
{
698+
(f)(item);
699+
*item_summary = item.summary(cx);
700+
*summary = sum(item_summaries.iter(), cx);
701+
Some(summary.clone())
702+
} else {
703+
None
704+
}
705+
}
706+
}
707+
}
708+
664709
pub fn extent<'a, D: Dimension<'a, T::Summary>>(
665710
&'a self,
666711
cx: <T::Summary as Summary>::Context<'_>,

0 commit comments

Comments
 (0)