loro/crates/loro-internal/src/undo.rs
Leon Zhao cd04b27d65
fix: fuzz cache and counter feature (#373)
* fix: fuzz cache and counter feature

* fix: rev
2024-05-29 22:44:21 +08:00

703 lines
23 KiB
Rust

use std::{
collections::VecDeque,
sync::{Arc, Mutex},
};
use either::Either;
use fxhash::FxHashMap;
use loro_common::{
ContainerID, Counter, CounterSpan, HasCounterSpan, HasIdSpan, IdSpan, LoroError, LoroResult,
LoroValue, PeerID,
};
use tracing::{debug_span, info_span, instrument, trace};
use crate::{
change::get_sys_timestamp,
cursor::{AbsolutePosition, Cursor},
event::{Diff, EventTriggerKind},
version::Frontiers,
ContainerDiff, DocDiff, LoroDoc,
};
#[derive(Debug, Clone, Default)]
pub struct DiffBatch(pub(crate) FxHashMap<ContainerID, Diff>);
impl DiffBatch {
pub fn new(diff: Vec<DocDiff>) -> Self {
let mut map: FxHashMap<ContainerID, Diff> = Default::default();
for d in diff.into_iter() {
for item in d.diff.into_iter() {
let old = map.insert(item.id.clone(), item.diff);
assert!(old.is_none());
}
}
Self(map)
}
pub fn compose(&mut self, other: &Self) {
if other.0.is_empty() {
return;
}
for (idx, diff) in self.0.iter_mut() {
if let Some(b_diff) = other.0.get(idx) {
diff.compose_ref(b_diff);
}
}
}
pub fn transform(&mut self, other: &Self, left_priority: bool) {
if other.0.is_empty() || self.0.is_empty() {
return;
}
for (idx, diff) in self.0.iter_mut() {
if let Some(b_diff) = other.0.get(idx) {
diff.transform(b_diff, left_priority);
}
}
}
pub fn clear(&mut self) {
self.0.clear();
}
}
fn transform_cursor(
cursor_with_pos: &mut CursorWithPos,
remote_diff: &DiffBatch,
doc: &LoroDoc,
container_remap: &FxHashMap<ContainerID, ContainerID>,
) {
let mut cid = &cursor_with_pos.cursor.container;
while let Some(new_cid) = container_remap.get(&cid) {
cid = new_cid;
}
if let Some(diff) = remote_diff.0.get(cid) {
let new_pos = diff.transform_cursor(cursor_with_pos.pos.pos, false);
cursor_with_pos.pos.pos = new_pos;
};
let new_pos = cursor_with_pos.pos.pos;
match doc.get_handler(cid.clone()) {
crate::handler::Handler::Text(h) => {
let Some(new_cursor) = h.get_cursor_internal(new_pos, cursor_with_pos.pos.side, false)
else {
return;
};
cursor_with_pos.cursor = new_cursor;
}
crate::handler::Handler::List(h) => {
let Some(new_cursor) = h.get_cursor(new_pos, cursor_with_pos.pos.side) else {
return;
};
cursor_with_pos.cursor = new_cursor;
}
crate::handler::Handler::MovableList(h) => {
let Some(new_cursor) = h.get_cursor(new_pos, cursor_with_pos.pos.side) else {
return;
};
cursor_with_pos.cursor = new_cursor;
}
crate::handler::Handler::Map(_) => {}
crate::handler::Handler::Tree(_) => {}
crate::handler::Handler::Unknown(_) => {}
#[cfg(feature = "counter")]
crate::handler::Handler::Counter(_) => {}
}
}
/// UndoManager is responsible for managing undo/redo from the current peer's perspective.
///
/// Undo/local is local: it cannot be used to undone the changes made by other peers.
/// If you want to undo changes made by other peers, you may need to use the time travel feature.
///
/// PeerID cannot be changed during the lifetime of the UndoManager
#[derive(Debug)]
pub struct UndoManager {
peer: PeerID,
container_remap: FxHashMap<ContainerID, ContainerID>,
inner: Arc<Mutex<UndoManagerInner>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UndoOrRedo {
Undo,
Redo,
}
impl UndoOrRedo {
fn opposite(&self) -> UndoOrRedo {
match self {
Self::Undo => Self::Redo,
Self::Redo => Self::Undo,
}
}
}
pub type OnPush = Box<dyn Fn(UndoOrRedo, CounterSpan) -> UndoItemMeta + Send + Sync>;
pub type OnPop = Box<dyn Fn(UndoOrRedo, CounterSpan, UndoItemMeta) + Send + Sync>;
struct UndoManagerInner {
latest_counter: Counter,
undo_stack: Stack,
redo_stack: Stack,
processing_undo: bool,
last_undo_time: i64,
merge_interval: i64,
max_stack_size: usize,
exclude_origin_prefixes: Vec<Box<str>>,
on_push: Option<OnPush>,
on_pop: Option<OnPop>,
}
impl std::fmt::Debug for UndoManagerInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UndoManagerInner")
.field("latest_counter", &self.latest_counter)
.field("undo_stack", &self.undo_stack)
.field("redo_stack", &self.redo_stack)
.field("processing_undo", &self.processing_undo)
.field("last_undo_time", &self.last_undo_time)
.field("merge_interval", &self.merge_interval)
.field("max_stack_size", &self.max_stack_size)
.field("exclude_origin_prefixes", &self.exclude_origin_prefixes)
.finish()
}
}
#[derive(Debug)]
struct Stack {
stack: VecDeque<(VecDeque<StackItem>, Arc<Mutex<DiffBatch>>)>,
size: usize,
}
#[derive(Debug)]
struct StackItem {
span: CounterSpan,
meta: UndoItemMeta,
}
/// The metadata of an undo item.
///
/// The cursors inside the metadata will be transformed by remote operations as well.
/// So that when the item is popped, users can restore the cursors position correctly.
#[derive(Debug, Default)]
pub struct UndoItemMeta {
pub value: LoroValue,
pub cursors: Vec<CursorWithPos>,
}
#[derive(Debug)]
pub struct CursorWithPos {
pub cursor: Cursor,
pub pos: AbsolutePosition,
}
impl UndoItemMeta {
pub fn new() -> Self {
Self {
value: LoroValue::Null,
cursors: Default::default(),
}
}
/// It's assumed that the cursor is just acqured before the ops that
/// need to be undo/redo.
///
/// We need to rely on the validity of the original pos value
pub fn add_cursor(&mut self, cursor: &Cursor) {
self.cursors.push(CursorWithPos {
cursor: cursor.clone(),
pos: AbsolutePosition {
pos: cursor.origin_pos,
side: cursor.side,
},
});
}
pub fn set_value(&mut self, value: LoroValue) {
self.value = value;
}
}
impl Stack {
pub fn new() -> Self {
let mut stack = VecDeque::new();
stack.push_back((VecDeque::new(), Arc::new(Mutex::new(Default::default()))));
Stack { stack, size: 0 }
}
pub fn pop(&mut self) -> Option<(StackItem, Arc<Mutex<DiffBatch>>)> {
while self.stack.back().unwrap().0.is_empty() && self.stack.len() > 1 {
let (_, diff) = self.stack.pop_back().unwrap();
let diff = diff.try_lock().unwrap();
if !diff.0.is_empty() {
self.stack
.back_mut()
.unwrap()
.1
.try_lock()
.unwrap()
.compose(&diff);
}
}
if self.stack.len() == 1 && self.stack.back().unwrap().0.is_empty() {
// If the stack is empty, we need to clear the remote diff
self.stack.back_mut().unwrap().1.try_lock().unwrap().clear();
return None;
}
self.size -= 1;
let last = self.stack.back_mut().unwrap();
last.0.pop_back().map(|x| (x, last.1.clone()))
// If this row in stack is empty, we don't pop it right away
// Because we still need the remote diff to be available.
// Cursor position transformation relies on the remote diff in the same row.
}
pub fn push(&mut self, span: CounterSpan, meta: UndoItemMeta) {
self.push_with_merge(span, meta, false)
}
pub fn push_with_merge(&mut self, span: CounterSpan, meta: UndoItemMeta, can_merge: bool) {
let last = self.stack.back_mut().unwrap();
let mut last_remote_diff = last.1.try_lock().unwrap();
if !last_remote_diff.0.is_empty() {
// If the remote diff is not empty, we cannot merge
if last.0.is_empty() {
last.0.push_back(StackItem { span, meta });
last_remote_diff.clear();
} else {
drop(last_remote_diff);
let mut v = VecDeque::new();
v.push_back(StackItem { span, meta });
self.stack
.push_back((v, Arc::new(Mutex::new(DiffBatch::default()))));
}
self.size += 1;
} else {
if can_merge {
if let Some(last_span) = last.0.back_mut() {
if last_span.span.end == span.start {
// merge the span
last_span.span.end = span.end;
return;
}
}
}
self.size += 1;
last.0.push_back(StackItem { span, meta });
}
}
pub fn compose_remote_event(&mut self, diff: &[&ContainerDiff]) {
if self.is_empty() {
return;
}
let remote_diff = &mut self.stack.back_mut().unwrap().1;
let mut remote_diff = remote_diff.try_lock().unwrap();
for e in diff {
if let Some(d) = remote_diff.0.get_mut(&e.id) {
d.compose_ref(&e.diff);
} else {
remote_diff.0.insert(e.id.clone(), e.diff.clone());
}
}
}
pub fn transform_based_on_this_delta(&mut self, diff: &DiffBatch) {
if self.is_empty() {
return;
}
let remote_diff = &mut self.stack.back_mut().unwrap().1;
remote_diff.try_lock().unwrap().transform(diff, false);
}
pub fn clear(&mut self) {
self.stack = VecDeque::new();
self.stack.push_back((VecDeque::new(), Default::default()));
self.size = 0;
}
pub fn is_empty(&self) -> bool {
self.size == 0
}
pub fn len(&self) -> usize {
self.size
}
fn pop_front(&mut self) {
if self.is_empty() {
return;
}
self.size -= 1;
let first = self.stack.front_mut().unwrap();
let f = first.0.pop_front();
assert!(f.is_some());
if first.0.is_empty() {
self.stack.pop_front();
}
}
}
impl Default for Stack {
fn default() -> Self {
Stack::new()
}
}
impl UndoManagerInner {
fn new(last_counter: Counter) -> Self {
UndoManagerInner {
latest_counter: last_counter,
undo_stack: Default::default(),
redo_stack: Default::default(),
processing_undo: false,
merge_interval: 0,
last_undo_time: 0,
max_stack_size: usize::MAX,
exclude_origin_prefixes: vec![],
on_pop: None,
on_push: None,
}
}
fn record_checkpoint(&mut self, latest_counter: Counter) {
if latest_counter == self.latest_counter {
return;
}
assert!(self.latest_counter < latest_counter);
let now = get_sys_timestamp();
let span = CounterSpan::new(self.latest_counter, latest_counter);
let meta = self
.on_push
.as_ref()
.map(|x| x(UndoOrRedo::Undo, span))
.unwrap_or_default();
if !self.undo_stack.is_empty() && now - self.last_undo_time < self.merge_interval {
self.undo_stack.push_with_merge(span, meta, true);
} else {
self.last_undo_time = now;
self.undo_stack.push(span, meta);
}
self.latest_counter = latest_counter;
self.redo_stack.clear();
while self.undo_stack.len() > self.max_stack_size {
self.undo_stack.pop_front();
}
}
}
fn get_counter_end(doc: &LoroDoc, peer: PeerID) -> Counter {
doc.oplog()
.lock()
.unwrap()
.get_peer_changes(peer)
.and_then(|x| x.last().map(|x| x.ctr_end()))
.unwrap_or(0)
}
impl UndoManager {
pub fn new(doc: &LoroDoc) -> Self {
let peer = doc.peer_id();
let inner = Arc::new(Mutex::new(UndoManagerInner::new(get_counter_end(
doc, peer,
))));
let inner_clone = inner.clone();
doc.subscribe_root(Arc::new(move |event| match event.event_meta.by {
EventTriggerKind::Local => {
// TODO: PERF undo can be significantly faster if we can get
// the DiffBatch for undo here
let Ok(mut inner) = inner_clone.try_lock() else {
return;
};
if inner.processing_undo {
return;
}
if let Some(id) = event.event_meta.to.iter().find(|x| x.peer == peer) {
if inner
.exclude_origin_prefixes
.iter()
.any(|x| event.event_meta.origin.starts_with(&**x))
{
// If the event is from the excluded origin, we don't record it
// in the undo stack. But we need to record its effect like it's
// a remote event.
inner.undo_stack.compose_remote_event(event.events);
inner.redo_stack.compose_remote_event(event.events);
inner.latest_counter = id.counter + 1;
} else {
inner.record_checkpoint(id.counter + 1);
}
}
}
EventTriggerKind::Import => {
let mut inner = inner_clone.try_lock().unwrap();
inner.undo_stack.compose_remote_event(event.events);
inner.redo_stack.compose_remote_event(event.events);
}
EventTriggerKind::Checkout => {}
}));
UndoManager {
peer,
container_remap: Default::default(),
inner,
}
}
pub fn peer(&self) -> PeerID {
self.peer
}
pub fn set_merge_interval(&mut self, interval: i64) {
self.inner.try_lock().unwrap().merge_interval = interval;
}
pub fn set_max_undo_steps(&mut self, size: usize) {
self.inner.try_lock().unwrap().max_stack_size = size;
}
pub fn add_exclude_origin_prefix(&mut self, prefix: &str) {
self.inner
.try_lock()
.unwrap()
.exclude_origin_prefixes
.push(prefix.into());
}
pub fn record_new_checkpoint(&mut self, doc: &LoroDoc) -> LoroResult<()> {
if doc.peer_id() != self.peer {
return Err(LoroError::UndoWithDifferentPeerId {
expected: self.peer,
actual: doc.peer_id(),
});
}
doc.commit_then_renew();
let counter = get_counter_end(doc, self.peer);
self.inner.try_lock().unwrap().record_checkpoint(counter);
Ok(())
}
#[instrument(skip_all)]
pub fn undo(&mut self, doc: &LoroDoc) -> LoroResult<bool> {
self.perform(
doc,
|x| &mut x.undo_stack,
|x| &mut x.redo_stack,
UndoOrRedo::Undo,
)
}
#[instrument(skip_all)]
pub fn redo(&mut self, doc: &LoroDoc) -> LoroResult<bool> {
self.perform(
doc,
|x| &mut x.redo_stack,
|x| &mut x.undo_stack,
UndoOrRedo::Redo,
)
}
fn perform(
&mut self,
doc: &LoroDoc,
get_stack: impl Fn(&mut UndoManagerInner) -> &mut Stack,
get_opposite: impl Fn(&mut UndoManagerInner) -> &mut Stack,
kind: UndoOrRedo,
) -> LoroResult<bool> {
self.record_new_checkpoint(doc)?;
let end_counter = get_counter_end(doc, self.peer);
let mut top = {
let mut inner = self.inner.try_lock().unwrap();
inner.processing_undo = true;
get_stack(&mut inner).pop()
};
let mut executed = false;
while let Some((mut span, remote_diff)) = top {
{
let inner = self.inner.clone();
// We need to clone this because otherwise <transform_delta> will be applied to the same remote diff
let remote_change_clone = remote_diff.try_lock().unwrap().clone();
let commit = doc.undo_internal(
IdSpan {
peer: self.peer,
counter: span.span,
},
&mut self.container_remap,
Some(&remote_change_clone),
&mut |diff| {
info_span!("transform remote diff").in_scope(|| {
let mut inner = inner.try_lock().unwrap();
// <transform_delta>
get_stack(&mut inner).transform_based_on_this_delta(diff);
});
},
)?;
drop(commit);
if let Some(x) = self.inner.try_lock().unwrap().on_pop.as_ref() {
for cursor in span.meta.cursors.iter_mut() {
// <cursor_transform> We need to transform cursor here.
// Note that right now <transform_delta> is already done,
// remote_diff is also transformed by it now (that's what we need).
transform_cursor(
cursor,
&remote_diff.try_lock().unwrap(),
doc,
&self.container_remap,
);
}
x(kind, span.span, span.meta)
}
}
let new_counter = get_counter_end(doc, self.peer);
if end_counter != new_counter {
let mut inner = self.inner.try_lock().unwrap();
let meta = inner
.on_push
.as_ref()
.map(|x| x(kind.opposite(), CounterSpan::new(end_counter, new_counter)))
.unwrap_or_default();
get_opposite(&mut inner).push(CounterSpan::new(end_counter, new_counter), meta);
inner.latest_counter = new_counter;
executed = true;
break;
} else {
// continue to pop the undo item as this undo is a no-op
top = get_stack(&mut self.inner.try_lock().unwrap()).pop();
continue;
}
}
self.inner.try_lock().unwrap().processing_undo = false;
Ok(executed)
}
pub fn can_undo(&self) -> bool {
!self.inner.try_lock().unwrap().undo_stack.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.inner.try_lock().unwrap().redo_stack.is_empty()
}
pub fn set_on_push(&self, on_push: Option<OnPush>) {
self.inner.try_lock().unwrap().on_push = on_push;
}
pub fn set_on_pop(&self, on_pop: Option<OnPop>) {
self.inner.try_lock().unwrap().on_pop = on_pop;
}
}
/// Undo the given spans of operations.
///
/// # Parameters
///
/// - `spans`: A vector of tuples where each tuple contains an `IdSpan` and its associated `Frontiers`.
/// - `IdSpan`: Represents a span of operations identified by an ID.
/// - `Frontiers`: Represents the deps of the given id_span
/// - `latest_frontiers`: The latest frontiers of the document
/// - `calc_diff`: A closure that takes two `Frontiers` and calculates the difference between them, returning a `DiffBatch`.
///
/// # Returns
///
/// - `DiffBatch`: Applying this batch on the `latest_frontiers` will undo the ops in the given spans.
pub(crate) fn undo(
spans: Vec<(IdSpan, Frontiers)>,
last_frontiers_or_last_bi: Either<&Frontiers, &DiffBatch>,
calc_diff: impl Fn(&Frontiers, &Frontiers) -> DiffBatch,
on_last_event_a: &mut dyn FnMut(&DiffBatch),
) -> DiffBatch {
// The process of performing undo is:
//
// 0. Split the span into a series of continuous spans. There is no external dep within each continuous span.
//
// For each continuous span_i:
//
// 1. a. Calculate the event of checkout from id_span.last to id_span.deps, call it Ai. It undo the ops in the current span.
// b. Calculate A'i = Ai + T(Ci-1, Ai) if i > 0, otherwise A'i = Ai.
// NOTE: A'i can undo the ops in the current span and the previous spans, if it's applied on the id_span.last version.
// 2. Calculate the event of checkout from id_span.last to [the next span's last id] or [the latest version], call it Bi.
// 3. Transform event A'i based on Bi, call it Ci
// 4. If span_i is the last span, apply Ci to the current state.
// -------------------------------------------------------
// 0. Split the span into a series of continuous spans
// -------------------------------------------------------
let mut last_ci: Option<DiffBatch> = None;
for i in 0..spans.len() {
debug_span!("Undo", ?i, "Undo span {:?}", &spans[i]).in_scope(|| {
let (this_id_span, this_deps) = &spans[i];
// ---------------------------------------
// 1.a Calc event A_i
// ---------------------------------------
let mut event_a_i = debug_span!("1. Calc event A_i").in_scope(|| {
// Checkout to the last id of the id_span
calc_diff(&this_id_span.id_last().into(), this_deps)
});
// ---------------------------------------
// 2. Calc event B_i
// ---------------------------------------
let stack_diff_batch;
let event_b_i = 'block: {
let next = if i + 1 < spans.len() {
spans[i + 1].0.id_last().into()
} else {
match last_frontiers_or_last_bi.clone() {
Either::Left(last_frontiers) => last_frontiers.clone(),
Either::Right(right) => break 'block right,
}
};
stack_diff_batch = Some(calc_diff(&this_id_span.id_last().into(), &next));
stack_diff_batch.as_ref().unwrap()
};
// event_a_prime can undo the ops in the current span and the previous spans
let mut event_a_prime = if let Some(mut last_ci) = last_ci.take() {
// ------------------------------------------------------------------------------
// 1.b Transform and apply Ci-1 based on Ai, call it A'i
// ------------------------------------------------------------------------------
last_ci.transform(&event_a_i, true);
event_a_i.compose(&last_ci);
event_a_i
} else {
event_a_i
};
if i == spans.len() - 1 {
on_last_event_a(&event_a_prime);
}
// --------------------------------------------------
// 3. Transform event A'_i based on B_i, call it C_i
// --------------------------------------------------
event_a_prime.transform(event_b_i, true);
let c_i = event_a_prime;
last_ci = Some(c_i);
});
}
last_ci.unwrap()
}