M9: a refused batch's fixes in linear time (#1063) (#1522)

The server's fixes for a refused batch walked every deleted layer's whole subtree again and
compared each document-level fix with all the earlier ones, so a batch deleting a deep chain took
quadratic time on the hub thread. Each subtree walk now stops at a layer already restored, and
document-level fixes are looked up and sent once per target. The fixes are unchanged.

A refused batch deleting a 20,000-deep chain plus 40,000 comment puts: 27.2 s before, 0.14 s after
(debug build).

Fixes #1063
This commit is contained in:
BFlat
2026-10-29 18:44:03 +07:00
committed by GitHub
parent d4e6ad5d81
commit 08173cc51e
5 changed files with 63 additions and 18 deletions
+18 -13
View File
@@ -557,6 +557,7 @@ impl Server {
let doc = self.state.document();
let mut props: BTreeMap<NodeId, BTreeSet<Prop>> = BTreeMap::new();
let mut gone: BTreeSet<NodeId> = BTreeSet::new();
let (mut asked, mut sent) = (BTreeSet::new(), BTreeSet::new());
let mut out = Vec::new();
for c in changes {
match c {
@@ -569,9 +570,11 @@ impl Server {
Change::Delete { id } => {
gone.insert(*id);
}
// The same target has the same fix: look each one up once, and send each fix once.
other => {
if let Some(fix) = docprops::current(doc, other)?
&& !out.contains(&fix)
if Target::of(other).is_some_and(|t| asked.insert(t))
&& let Some(fix) = docprops::current(doc, other)?
&& Target::of(&fix).is_some_and(|t| sent.insert(t))
{
out.push(fix);
}
@@ -589,19 +592,21 @@ impl Server {
nodes.extend(t.write(self.state.value(&t)?.as_ref()));
}
}
let mut restored = std::collections::BTreeSet::new();
// Each deleted node's subtree, pre-order, every node once: a restored node's whole subtree
// went out with it, so the walk stops there (deleting a deep chain stays linear).
let mut restored = BTreeSet::new();
for id in gone {
match doc.node(id) {
Some(_) => {
for d in doc.subtree(id) {
if restored.insert(d)
&& let Some(n) = doc.node(d)
{
nodes.push(create_change(n, self.state.key(d))?);
}
}
if !doc.contains(id) {
nodes.push(Change::Delete { id });
continue;
}
let mut stack = vec![id];
while let Some(d) = stack.pop() {
let Some(n) = doc.node(d) else { continue };
if restored.insert(d) {
nodes.push(create_change(n, self.state.key(d))?);
stack.extend(n.children.iter().rev());
}
None => nodes.push(Change::Delete { id }),
}
}
nodes.extend(out);