Skip to content

Commit 095ad56

Browse files
authored
perf: replace causal scan in find_last_delete_op with peer-by-peer range scan (#978)
iter_changes_causally_rev walks every change in the oplog in DAG order, which is O(total changes). Any delete op covering `id` must have observed it, so start_vv (the vv at `id`) is a valid lower bound per peer. Switching to iter_changes_peer_by_peer with that bound skips all changes that predate `id` without needing causal ordering. The match with the highest lamport timestamp is the latest delete.
1 parent 418d4ca commit 095ad56

1 file changed

Lines changed: 27 additions & 5 deletions

File tree

crates/loro-internal/src/loro.rs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2184,26 +2184,48 @@ impl LoroDoc {
21842184
}
21852185
}
21862186

2187-
// FIXME: PERF: This method is quite slow because it iterates all the changes
21882187
fn find_last_delete_op(oplog: &OpLog, id: ID, idx: ContainerIdx) -> Option<ID> {
2188+
// Any delete op that covers `id` must have observed it, so its peer's counter
2189+
// at delete time was > id.counter. start_vv (the vv at `id`) is therefore a
2190+
// valid lower bound: changes at or before start_vv[peer] predate `id` and can
2191+
// be skipped. We scan peer-by-peer rather than using the DAG-ordered
2192+
// iter_changes_causally_rev, which is O(total changes).
2193+
//
2194+
// We choose the matching delete op with the greatest (op_lamport, peer, counter)
2195+
// ordering. op_lamport is the Lamport of the specific op within the change
2196+
// (change.lamport + op offset), not just the change's starting Lamport, so
2197+
// concurrent deletes with equal change Lamports are broken deterministically.
21892198
let start_vv = oplog
21902199
.dag
21912200
.frontiers_to_vv(&id.into())
21922201
.unwrap_or_else(|| oplog.shallow_since_vv().to_vv());
2193-
for change in oplog.iter_changes_causally_rev(&start_vv, oplog.vv()) {
2194-
for op in change.ops.iter().rev() {
2202+
2203+
// (op_lamport, peer) gives a deterministic total order for concurrent deletes.
2204+
// A single peer cannot produce two ops with the same lamport, so peer suffices
2205+
// as a tie-breaker.
2206+
let mut best: Option<((loro_common::Lamport, loro_common::PeerID), ID)> = None;
2207+
2208+
for change in oplog.iter_changes_peer_by_peer(&start_vv, oplog.vv()) {
2209+
let peer = change.peer();
2210+
for op in change.ops.iter() {
21952211
if op.container != idx {
21962212
continue;
21972213
}
21982214
if let InnerContent::List(InnerListOp::Delete(d)) = &op.content {
21992215
if d.id_start.to_span(d.atom_len()).contains(id) {
2200-
return Some(ID::new(change.peer(), op.counter));
2216+
debug_assert!(op.counter >= change.id().counter);
2217+
let op_lamport = change.lamport
2218+
+ (op.counter - change.id().counter) as loro_common::Lamport;
2219+
let key = (op_lamport, peer);
2220+
if best.map_or(true, |(bk, _)| key > bk) {
2221+
best = Some((key, ID::new(peer, op.counter)));
2222+
}
22012223
}
22022224
}
22032225
}
22042226
}
22052227

2206-
None
2228+
best.map(|(_, op_id)| op_id)
22072229
}
22082230

22092231
#[derive(Debug)]

0 commit comments

Comments
 (0)