sum_tree: Replace rayon with futures (#41586)

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Co-authored by: Kate <kate@zed.dev>
This commit is contained in:
Lukas Wirth
2025-10-31 10:39:01 +00:00
committed by GitHub
parent 7c29c6d7a6
commit f2ce06c7b0
67 changed files with 1271 additions and 640 deletions
+1 -1
View File
@@ -14,10 +14,10 @@ path = "src/rope.rs"
[dependencies]
arrayvec = "0.7.1"
log.workspace = true
rayon.workspace = true
sum_tree.workspace = true
unicode-segmentation.workspace = true
util.workspace = true
gpui.workspace = true
[dev-dependencies]
ctor.workspace = true
+27 -10
View File
@@ -3,6 +3,7 @@ use std::ops::Range;
use criterion::{
BatchSize, BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main,
};
use gpui::{AsyncApp, TestAppContext};
use rand::prelude::*;
use rand::rngs::StdRng;
use rope::{Point, Rope};
@@ -26,10 +27,10 @@ fn generate_random_text(rng: &mut StdRng, len: usize) -> String {
str
}
fn generate_random_rope(rng: &mut StdRng, text_len: usize) -> Rope {
fn generate_random_rope(rng: &mut StdRng, text_len: usize, cx: &AsyncApp) -> Rope {
let text = generate_random_text(rng, text_len);
let mut rope = Rope::new();
rope.push(&text);
rope.push(&text, cx.background_executor());
rope
}
@@ -82,11 +83,13 @@ fn rope_benchmarks(c: &mut Criterion) {
group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| {
let mut rng = StdRng::seed_from_u64(SEED);
let text = generate_random_text(&mut rng, *size);
let cx = TestAppContext::single();
let cx = cx.to_async();
b.iter(|| {
let mut rope = Rope::new();
for _ in 0..10 {
rope.push(&text);
rope.push(&text, cx.background_executor());
}
});
});
@@ -99,8 +102,10 @@ fn rope_benchmarks(c: &mut Criterion) {
group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| {
let mut rng = StdRng::seed_from_u64(SEED);
let mut random_ropes = Vec::new();
let cx = TestAppContext::single();
let cx = cx.to_async();
for _ in 0..5 {
let rope = generate_random_rope(&mut rng, *size);
let rope = generate_random_rope(&mut rng, *size, &cx);
random_ropes.push(rope);
}
@@ -119,7 +124,9 @@ fn rope_benchmarks(c: &mut Criterion) {
group.throughput(Throughput::Bytes(*size as u64));
group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| {
let mut rng = StdRng::seed_from_u64(SEED);
let rope = generate_random_rope(&mut rng, *size);
let cx = TestAppContext::single();
let cx = cx.to_async();
let rope = generate_random_rope(&mut rng, *size, &cx);
b.iter_batched(
|| generate_random_rope_ranges(&mut rng, &rope),
@@ -139,7 +146,9 @@ fn rope_benchmarks(c: &mut Criterion) {
group.throughput(Throughput::Bytes(*size as u64));
group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| {
let mut rng = StdRng::seed_from_u64(SEED);
let rope = generate_random_rope(&mut rng, *size);
let cx = TestAppContext::single();
let cx = cx.to_async();
let rope = generate_random_rope(&mut rng, *size, &cx);
b.iter_batched(
|| generate_random_rope_ranges(&mut rng, &rope),
@@ -160,7 +169,9 @@ fn rope_benchmarks(c: &mut Criterion) {
group.throughput(Throughput::Bytes(*size as u64));
group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| {
let mut rng = StdRng::seed_from_u64(SEED);
let rope = generate_random_rope(&mut rng, *size);
let cx = TestAppContext::single();
let cx = cx.to_async();
let rope = generate_random_rope(&mut rng, *size, &cx);
b.iter(|| {
let chars = rope.chars().count();
@@ -175,7 +186,9 @@ fn rope_benchmarks(c: &mut Criterion) {
group.throughput(Throughput::Bytes(*size as u64));
group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| {
let mut rng = StdRng::seed_from_u64(SEED);
let rope = generate_random_rope(&mut rng, *size);
let cx = TestAppContext::single();
let cx = cx.to_async();
let rope = generate_random_rope(&mut rng, *size, &cx);
b.iter_batched(
|| generate_random_rope_points(&mut rng, &rope),
@@ -196,7 +209,9 @@ fn rope_benchmarks(c: &mut Criterion) {
group.throughput(Throughput::Bytes(*size as u64));
group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| {
let mut rng = StdRng::seed_from_u64(SEED);
let rope = generate_random_rope(&mut rng, *size);
let cx = TestAppContext::single();
let cx = cx.to_async();
let rope = generate_random_rope(&mut rng, *size, &cx);
b.iter_batched(
|| generate_random_rope_points(&mut rng, &rope),
@@ -216,7 +231,9 @@ fn rope_benchmarks(c: &mut Criterion) {
group.throughput(Throughput::Bytes(*size as u64));
group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| {
let mut rng = StdRng::seed_from_u64(SEED);
let rope = generate_random_rope(&mut rng, *size);
let cx = TestAppContext::single();
let cx = cx.to_async();
let rope = generate_random_rope(&mut rng, *size, &cx);
b.iter_batched(
|| {
+178 -98
View File
@@ -5,7 +5,7 @@ mod point_utf16;
mod unclipped;
use arrayvec::ArrayVec;
use rayon::iter::{IntoParallelIterator, ParallelIterator as _};
use gpui::BackgroundExecutor;
use std::{
cmp, fmt, io, mem,
ops::{self, AddAssign, Range},
@@ -31,6 +31,41 @@ impl Rope {
Self::default()
}
/// Create a new rope from a string without trying to parallelize the construction for large strings.
pub fn from_str_small(text: &str) -> Self {
let mut rope = Self::new();
rope.push_small(text);
rope
}
/// Create a new rope from a string.
pub fn from_str(text: &str, executor: &BackgroundExecutor) -> Self {
let mut rope = Self::new();
rope.push(text, executor);
rope
}
/// Create a new rope from a string without trying to parallelize the construction for large strings.
pub fn from_iter_small<'a, T: IntoIterator<Item = &'a str>>(iter: T) -> Self {
let mut rope = Rope::new();
for chunk in iter {
rope.push_small(chunk);
}
rope
}
/// Create a new rope from a string.
pub fn from_iter<'a, T: IntoIterator<Item = &'a str>>(
iter: T,
executor: &BackgroundExecutor,
) -> Self {
let mut rope = Rope::new();
for chunk in iter {
rope.push(chunk, executor);
}
rope
}
/// Checks that `index`-th byte is the first byte in a UTF-8 code point
/// sequence or the end of the string.
///
@@ -145,12 +180,12 @@ impl Rope {
self.check_invariants();
}
pub fn replace(&mut self, range: Range<usize>, text: &str) {
pub fn replace(&mut self, range: Range<usize>, text: &str, executor: &BackgroundExecutor) {
let mut new_rope = Rope::new();
let mut cursor = self.cursor(0);
new_rope.append(cursor.slice(range.start));
cursor.seek_forward(range.end);
new_rope.push(text);
new_rope.push(text, executor);
new_rope.append(cursor.suffix());
*self = new_rope;
}
@@ -168,28 +203,12 @@ impl Rope {
self.slice(start..end)
}
pub fn push(&mut self, mut text: &str) {
self.chunks.update_last(
|last_chunk| {
let split_ix = if last_chunk.text.len() + text.len() <= chunk::MAX_BASE {
text.len()
} else {
let mut split_ix = cmp::min(
chunk::MIN_BASE.saturating_sub(last_chunk.text.len()),
text.len(),
);
while !text.is_char_boundary(split_ix) {
split_ix += 1;
}
split_ix
};
pub fn push(&mut self, mut text: &str, executor: &BackgroundExecutor) {
self.fill_last_chunk(&mut text);
let (suffix, remainder) = text.split_at(split_ix);
last_chunk.push_str(suffix);
text = remainder;
},
(),
);
if text.is_empty() {
return;
}
#[cfg(all(test, not(rust_analyzer)))]
const NUM_CHUNKS: usize = 16;
@@ -200,7 +219,8 @@ impl Rope {
// but given the chunk boundary can land within a character
// we need to accommodate for the worst case where every chunk gets cut short by up to 4 bytes
if text.len() > NUM_CHUNKS * chunk::MAX_BASE - NUM_CHUNKS * 4 {
return self.push_large(text);
let future = self.push_large(text, executor.clone());
return executor.block(future);
}
// 16 is enough as otherwise we will hit the branch above
let mut new_chunks = ArrayVec::<_, NUM_CHUNKS>::new();
@@ -220,8 +240,57 @@ impl Rope {
self.check_invariants();
}
/// Pushes a string into the rope. Unlike [`push`], this method does not parallelize the construction on large strings.
pub fn push_small(&mut self, mut text: &str) {
self.fill_last_chunk(&mut text);
if text.is_empty() {
return;
}
// 16 is enough as otherwise we will hit the branch above
let mut new_chunks = Vec::new();
while !text.is_empty() {
let mut split_ix = cmp::min(chunk::MAX_BASE, text.len());
while !text.is_char_boundary(split_ix) {
split_ix -= 1;
}
let (chunk, remainder) = text.split_at(split_ix);
new_chunks.push(chunk);
text = remainder;
}
self.chunks
.extend(new_chunks.into_iter().map(Chunk::new), ());
self.check_invariants();
}
fn fill_last_chunk(&mut self, text: &mut &str) {
self.chunks.update_last(
|last_chunk| {
let split_ix = if last_chunk.text.len() + text.len() <= chunk::MAX_BASE {
text.len()
} else {
let mut split_ix = cmp::min(
chunk::MIN_BASE.saturating_sub(last_chunk.text.len()),
text.len(),
);
while !text.is_char_boundary(split_ix) {
split_ix += 1;
}
split_ix
};
let (suffix, remainder) = text.split_at(split_ix);
last_chunk.push_str(suffix);
*text = remainder;
},
(),
);
}
/// A copy of `push` specialized for working with large quantities of text.
fn push_large(&mut self, mut text: &str) {
async fn push_large(&mut self, mut text: &str, executor: BackgroundExecutor) {
// To avoid frequent reallocs when loading large swaths of file contents,
// we estimate worst-case `new_chunks` capacity;
// Chunk is a fixed-capacity buffer. If a character falls on
@@ -254,8 +323,22 @@ impl Rope {
const PARALLEL_THRESHOLD: usize = 4 * (2 * sum_tree::TREE_BASE);
if new_chunks.len() >= PARALLEL_THRESHOLD {
self.chunks
.par_extend(new_chunks.into_par_iter().map(Chunk::new), ());
let cx2 = executor.clone();
executor
.scoped(|scope| {
// SAFETY: transmuting to 'static is safe because the future is scoped
// and the underlying string data cannot go out of scope because dropping the scope
// will wait for the task to finish
let new_chunks =
unsafe { std::mem::transmute::<Vec<&str>, Vec<&'static str>>(new_chunks) };
let async_extend = self
.chunks
.async_extend(new_chunks.into_iter().map(Chunk::new), cx2);
scope.spawn(async_extend);
})
.await;
} else {
self.chunks
.extend(new_chunks.into_iter().map(Chunk::new), ());
@@ -292,8 +375,13 @@ impl Rope {
}
}
pub fn push_front(&mut self, text: &str) {
let suffix = mem::replace(self, Rope::from(text));
pub fn push_front(&mut self, text: &str, cx: &BackgroundExecutor) {
let suffix = mem::replace(self, Rope::from_str(text, cx));
self.append(suffix);
}
pub fn push_front_small(&mut self, text: &str) {
let suffix = mem::replace(self, Rope::from_str_small(text));
self.append(suffix);
}
@@ -577,37 +665,19 @@ impl Rope {
}
}
impl<'a> From<&'a str> for Rope {
fn from(text: &'a str) -> Self {
let mut rope = Self::new();
rope.push(text);
rope
}
}
// impl From<String> for Rope {
// #[inline(always)]
// fn from(text: String) -> Self {
// Rope::from(text.as_str())
// }
// }
impl<'a> FromIterator<&'a str> for Rope {
fn from_iter<T: IntoIterator<Item = &'a str>>(iter: T) -> Self {
let mut rope = Rope::new();
for chunk in iter {
rope.push(chunk);
}
rope
}
}
impl From<String> for Rope {
#[inline(always)]
fn from(text: String) -> Self {
Rope::from(text.as_str())
}
}
impl From<&String> for Rope {
#[inline(always)]
fn from(text: &String) -> Self {
Rope::from(text.as_str())
}
}
// impl From<&String> for Rope {
// #[inline(always)]
// fn from(text: &String) -> Self {
// Rope::from(text.as_str())
// }
// }
impl fmt::Display for Rope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -1639,6 +1709,7 @@ where
mod tests {
use super::*;
use Bias::{Left, Right};
use gpui::TestAppContext;
use rand::prelude::*;
use std::{cmp::Ordering, env, io::Read};
use util::RandomCharIter;
@@ -1648,17 +1719,17 @@ mod tests {
zlog::init_test();
}
#[test]
fn test_all_4_byte_chars() {
#[gpui::test]
async fn test_all_4_byte_chars(cx: &mut TestAppContext) {
let mut rope = Rope::new();
let text = "🏀".repeat(256);
rope.push(&text);
rope.push(&text, cx.background_executor());
assert_eq!(rope.text(), text);
}
#[test]
fn test_clip() {
let rope = Rope::from("🧘");
#[gpui::test]
fn test_clip(cx: &mut TestAppContext) {
let rope = Rope::from_str("🧘", cx.background_executor());
assert_eq!(rope.clip_offset(1, Bias::Left), 0);
assert_eq!(rope.clip_offset(1, Bias::Right), 4);
@@ -1704,9 +1775,9 @@ mod tests {
);
}
#[test]
fn test_prev_next_line() {
let rope = Rope::from("abc\ndef\nghi\njkl");
#[gpui::test]
fn test_prev_next_line(cx: &mut TestAppContext) {
let rope = Rope::from_str("abc\ndef\nghi\njkl", cx.background_executor());
let mut chunks = rope.chunks();
assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'a');
@@ -1748,16 +1819,16 @@ mod tests {
assert_eq!(chunks.peek(), None);
}
#[test]
fn test_lines() {
let rope = Rope::from("abc\ndefg\nhi");
#[gpui::test]
fn test_lines(cx: &mut TestAppContext) {
let rope = Rope::from_str("abc\ndefg\nhi", cx.background_executor());
let mut lines = rope.chunks().lines();
assert_eq!(lines.next(), Some("abc"));
assert_eq!(lines.next(), Some("defg"));
assert_eq!(lines.next(), Some("hi"));
assert_eq!(lines.next(), None);
let rope = Rope::from("abc\ndefg\nhi\n");
let rope = Rope::from_str("abc\ndefg\nhi\n", cx.background_executor());
let mut lines = rope.chunks().lines();
assert_eq!(lines.next(), Some("abc"));
assert_eq!(lines.next(), Some("defg"));
@@ -1765,14 +1836,14 @@ mod tests {
assert_eq!(lines.next(), Some(""));
assert_eq!(lines.next(), None);
let rope = Rope::from("abc\ndefg\nhi");
let rope = Rope::from_str("abc\ndefg\nhi", cx.background_executor());
let mut lines = rope.reversed_chunks_in_range(0..rope.len()).lines();
assert_eq!(lines.next(), Some("hi"));
assert_eq!(lines.next(), Some("defg"));
assert_eq!(lines.next(), Some("abc"));
assert_eq!(lines.next(), None);
let rope = Rope::from("abc\ndefg\nhi\n");
let rope = Rope::from_str("abc\ndefg\nhi\n", cx.background_executor());
let mut lines = rope.reversed_chunks_in_range(0..rope.len()).lines();
assert_eq!(lines.next(), Some(""));
assert_eq!(lines.next(), Some("hi"));
@@ -1780,14 +1851,14 @@ mod tests {
assert_eq!(lines.next(), Some("abc"));
assert_eq!(lines.next(), None);
let rope = Rope::from("abc\nlonger line test\nhi");
let rope = Rope::from_str("abc\nlonger line test\nhi", cx.background_executor());
let mut lines = rope.chunks().lines();
assert_eq!(lines.next(), Some("abc"));
assert_eq!(lines.next(), Some("longer line test"));
assert_eq!(lines.next(), Some("hi"));
assert_eq!(lines.next(), None);
let rope = Rope::from("abc\nlonger line test\nhi");
let rope = Rope::from_str("abc\nlonger line test\nhi", cx.background_executor());
let mut lines = rope.reversed_chunks_in_range(0..rope.len()).lines();
assert_eq!(lines.next(), Some("hi"));
assert_eq!(lines.next(), Some("longer line test"));
@@ -1796,7 +1867,7 @@ mod tests {
}
#[gpui::test(iterations = 100)]
fn test_random_rope(mut rng: StdRng) {
async fn test_random_rope(cx: &mut TestAppContext, mut rng: StdRng) {
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
@@ -1812,7 +1883,7 @@ mod tests {
let mut new_actual = Rope::new();
let mut cursor = actual.cursor(0);
new_actual.append(cursor.slice(start_ix));
new_actual.push(&new_text);
new_actual.push(&new_text, cx.background_executor());
cursor.seek_forward(end_ix);
new_actual.append(cursor.suffix());
actual = new_actual;
@@ -2112,10 +2183,10 @@ mod tests {
}
}
#[test]
fn test_chunks_equals_str() {
#[gpui::test]
fn test_chunks_equals_str(cx: &mut TestAppContext) {
let text = "This is a multi-chunk\n& multi-line test string!";
let rope = Rope::from(text);
let rope = Rope::from_str(text, cx.background_executor());
for start in 0..text.len() {
for end in start..text.len() {
let range = start..end;
@@ -2158,34 +2229,37 @@ mod tests {
}
}
let rope = Rope::from("");
let rope = Rope::from_str("", cx.background_executor());
assert!(rope.chunks_in_range(0..0).equals_str(""));
assert!(rope.reversed_chunks_in_range(0..0).equals_str(""));
assert!(!rope.chunks_in_range(0..0).equals_str("foo"));
assert!(!rope.reversed_chunks_in_range(0..0).equals_str("foo"));
}
#[test]
fn test_is_char_boundary() {
#[gpui::test]
fn test_is_char_boundary(cx: &mut TestAppContext) {
let fixture = "";
let rope = Rope::from("");
let rope = Rope::from_str("", cx.background_executor());
for b in 0..=fixture.len() {
assert_eq!(rope.is_char_boundary(b), fixture.is_char_boundary(b));
}
let fixture = "";
let rope = Rope::from("");
let rope = Rope::from_str("", cx.background_executor());
for b in 0..=fixture.len() {
assert_eq!(rope.is_char_boundary(b), fixture.is_char_boundary(b));
}
let fixture = "🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩";
let rope = Rope::from("🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩");
let rope = Rope::from_str(
"🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩",
cx.background_executor(),
);
for b in 0..=fixture.len() {
assert_eq!(rope.is_char_boundary(b), fixture.is_char_boundary(b));
}
}
#[test]
fn test_floor_char_boundary() {
#[gpui::test]
fn test_floor_char_boundary(cx: &mut TestAppContext) {
// polyfill of str::floor_char_boundary
fn floor_char_boundary(str: &str, index: usize) -> usize {
if index >= str.len() {
@@ -2201,7 +2275,7 @@ mod tests {
}
let fixture = "";
let rope = Rope::from("");
let rope = Rope::from_str("", cx.background_executor());
for b in 0..=fixture.len() {
assert_eq!(
rope.floor_char_boundary(b),
@@ -2210,7 +2284,7 @@ mod tests {
}
let fixture = "";
let rope = Rope::from("");
let rope = Rope::from_str("", cx.background_executor());
for b in 0..=fixture.len() {
assert_eq!(
rope.floor_char_boundary(b),
@@ -2219,7 +2293,10 @@ mod tests {
}
let fixture = "🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩";
let rope = Rope::from("🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩");
let rope = Rope::from_str(
"🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩",
cx.background_executor(),
);
for b in 0..=fixture.len() {
assert_eq!(
rope.floor_char_boundary(b),
@@ -2228,8 +2305,8 @@ mod tests {
}
}
#[test]
fn test_ceil_char_boundary() {
#[gpui::test]
fn test_ceil_char_boundary(cx: &mut TestAppContext) {
// polyfill of str::ceil_char_boundary
fn ceil_char_boundary(str: &str, index: usize) -> usize {
if index > str.len() {
@@ -2244,19 +2321,22 @@ mod tests {
}
let fixture = "";
let rope = Rope::from("");
let rope = Rope::from_str("", cx.background_executor());
for b in 0..=fixture.len() {
assert_eq!(rope.ceil_char_boundary(b), ceil_char_boundary(&fixture, b));
}
let fixture = "";
let rope = Rope::from("");
let rope = Rope::from_str("", cx.background_executor());
for b in 0..=fixture.len() {
assert_eq!(rope.ceil_char_boundary(b), ceil_char_boundary(&fixture, b));
}
let fixture = "🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩";
let rope = Rope::from("🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩");
let rope = Rope::from_str(
"🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩",
cx.background_executor(),
);
for b in 0..=fixture.len() {
assert_eq!(rope.ceil_char_boundary(b), ceil_char_boundary(&fixture, b));
}