fix(content): splice copies only the pieces a range touches

Pieces::splice copied the unedited bytes between edits by visiting
every piece for every edit, so a page of many content pieces and many
replaced operators cost pieces x edits steps. The copy starts at the
piece holding its first byte and stops past its last one.
This commit is contained in:
aiongg
2026-10-22 17:43:03 +00:00
parent 3c64f9314e
commit 07aea983d0
2 changed files with 36 additions and 1 deletions
+12 -1
View File
@@ -504,9 +504,20 @@ impl Pieces {
/// for byte. Returns every piece's new bytes (unchanged pieces come back equal).
pub fn splice(&self, edits: impl IntoIterator<Item = (std::ops::Range<usize>, Vec<u8>)>) -> Vec<Vec<u8>> {
let mut out: Vec<Vec<u8>> = (0..self.len()).map(|i| Vec::with_capacity(self.range(i).len())).collect();
// Only the pieces from the one holding `from` up to `to`: one call per edit must not
// visit every piece.
let copy = |out: &mut Vec<Vec<u8>>, from: usize, to: usize| {
for (i, piece) in out.iter_mut().enumerate() {
if from >= to {
return;
}
let first = self.piece_of(from);
for (i, piece) in out.iter_mut().enumerate().skip(first) {
#[cfg(test)]
tests::PIECE_VISITS.with(|c| c.set(c.get() + 1));
let r = self.range(i);
if r.start >= to {
break;
}
let (a, b) = (from.max(r.start), to.min(r.end));
if a < b {
piece.extend_from_slice(self.data.get(a..b).unwrap_or_default());
+24
View File
@@ -5,6 +5,30 @@ thread_local! {
pub(crate) static RESCANNED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
thread_local! {
/// Pieces [`Pieces::splice`] looked at while copying, on this thread.
pub(crate) static PIECE_VISITS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
#[test]
fn splicing_visits_each_piece_a_few_times() {
let n = 5_000;
let pieces: Vec<String> = (0..n).map(|i| format!("({i}) Tj")).collect();
let joined = Pieces::join(&pieces);
let parsed = joined.parse();
let edits: Vec<_> = parsed.ops.iter().map(|op| (op.span.clone(), b"(x) Tj".to_vec())).collect();
assert_eq!(edits.len(), n);
PIECE_VISITS.with(|c| c.set(0));
let out = joined.splice(edits);
let visits = PIECE_VISITS.with(|c| c.get());
assert!(visits <= 4 * n, "{visits} piece visits for {n} pieces");
assert!(out.iter().all(|p| p == b"(x) Tj"));
// A replacement spanning two pieces stays whole in the first and leaves the rest of the second.
let two = Pieces::join(&["q (a)", "Tj Q"]);
let tj = two.parse().ops.into_iter().find(|o| o.is("Tj")).map(|o| o.span).unwrap_or_default();
assert_eq!(two.splice([(tj, b"(b) Tj".to_vec())]), [b"q (b) Tj".to_vec(), b" Q".to_vec()]);
}
#[test]
fn a_long_unreadable_number_is_read_once() {
for junk in ["+", "5"] {