mirror of
https://github.com/loro-dev/loro.git
synced 2025-02-06 04:19:34 +00:00
Perf make revert faster (#279)
* refactor: include start_id in seq delete span * Add size benchmark example (#276) * test: add size bench example * chore: update lock file * refactor: optimize encoding * perf: make revert ops with the size of m O(m) * fix: delete span with id merge rule * fix: fix several bugs related to delete span id
This commit is contained in:
parent
06e3a5420d
commit
07c11f68b6
12 changed files with 396 additions and 97 deletions
|
@ -140,6 +140,9 @@ impl HasId for DeleteSpanWithId {
|
|||
}
|
||||
|
||||
impl Mergable for DeleteSpanWithId {
|
||||
/// If two spans are mergeable, their ids should be continuous.
|
||||
/// LHS's end id should be equal to RHS's start id.
|
||||
/// But their spans may be in a reversed order.
|
||||
fn is_mergable(&self, rhs: &Self, _conf: &()) -> bool
|
||||
where
|
||||
Self: Sized,
|
||||
|
|
|
@ -4,7 +4,7 @@ use generic_btree::rle::{HasLength, Mergeable, Sliceable};
|
|||
use loro_common::{CompactId, Counter, HasId, IdFull, IdSpan, Lamport, ID};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::AnchorType;
|
||||
use super::{tracker::UNKNOWN_PEER_ID, AnchorType};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Copy, Serialize, Deserialize)]
|
||||
pub(crate) struct RichtextChunk {
|
||||
|
@ -171,6 +171,11 @@ impl Sliceable for RichtextChunk {
|
|||
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
|
||||
pub(super) struct FugueSpan {
|
||||
pub id: IdFull,
|
||||
/// Sometimes, the id of the span is just a placeholder.
|
||||
/// This field is used to track the real id of the span.
|
||||
/// We know the real id when deletion happens because we
|
||||
/// have the start_id info for each deletion.
|
||||
pub real_id: Option<ID>,
|
||||
/// The status at the current version
|
||||
pub status: Status,
|
||||
/// The status at the `new` version.
|
||||
|
@ -218,6 +223,7 @@ impl Sliceable for FugueSpan {
|
|||
fn _slice(&self, range: Range<usize>) -> Self {
|
||||
Self {
|
||||
id: self.id.inc(range.start as Counter),
|
||||
real_id: self.real_id.map(|x| x.inc(range.start as Counter)),
|
||||
status: self.status,
|
||||
diff_status: self.diff_status,
|
||||
origin_left: if range.start == 0 {
|
||||
|
@ -257,6 +263,11 @@ impl Mergeable for FugueSpan {
|
|||
== self.id.counter + self.content.len() as Counter - 1
|
||||
&& self.origin_right == rhs.origin_right
|
||||
&& self.content.can_merge(&rhs.content)
|
||||
&& ((self.real_id.is_none() && rhs.real_id.is_none())
|
||||
|| (self.real_id.is_some()
|
||||
&& rhs.real_id.is_some()
|
||||
&& self.real_id.unwrap().inc(self.content.len() as i32)
|
||||
== rhs.real_id.unwrap()))
|
||||
}
|
||||
|
||||
fn merge_right(&mut self, rhs: &Self) {
|
||||
|
@ -265,6 +276,7 @@ impl Mergeable for FugueSpan {
|
|||
|
||||
fn merge_left(&mut self, left: &Self) {
|
||||
self.id = left.id;
|
||||
self.real_id = left.real_id;
|
||||
self.origin_left = left.origin_left;
|
||||
self.content.merge_left(&left.content);
|
||||
}
|
||||
|
@ -275,6 +287,11 @@ impl FugueSpan {
|
|||
pub fn new(id: IdFull, content: RichtextChunk) -> Self {
|
||||
Self {
|
||||
id,
|
||||
real_id: if id.peer == UNKNOWN_PEER_ID {
|
||||
None
|
||||
} else {
|
||||
Some(id.id())
|
||||
},
|
||||
status: Status::default(),
|
||||
diff_status: None,
|
||||
origin_left: None,
|
||||
|
|
|
@ -1670,14 +1670,18 @@ impl RichtextState {
|
|||
match span.elem {
|
||||
RichtextStateChunk::Text(s) => {
|
||||
let event_len = s.entity_range_to_event_range(start..end).len();
|
||||
let id = s.id().inc(start as i32);
|
||||
match ans.last_mut() {
|
||||
Some(last) if last.entity_end == entity_index => {
|
||||
Some(last)
|
||||
if last.entity_end == entity_index
|
||||
&& last.id_start.inc(last.event_len as i32) == id =>
|
||||
{
|
||||
last.entity_end += len;
|
||||
last.event_len += event_len;
|
||||
}
|
||||
_ => {
|
||||
ans.push(EntityRangeInfo {
|
||||
id_start: s.id(),
|
||||
id_start: id,
|
||||
entity_start: entity_index,
|
||||
entity_end: entity_index + len,
|
||||
event_len,
|
||||
|
@ -2317,7 +2321,7 @@ mod test {
|
|||
let ranges = self
|
||||
.state
|
||||
.get_text_entity_ranges(pos, len, PosType::Unicode);
|
||||
for range in ranges {
|
||||
for range in ranges.into_iter().rev() {
|
||||
self.state.drain_by_entity_index(
|
||||
range.entity_start,
|
||||
range.entity_end - range.entity_start,
|
||||
|
@ -2427,6 +2431,8 @@ mod test {
|
|||
fn delete_text() {
|
||||
let mut wrapper = SimpleWrapper::default();
|
||||
wrapper.insert(0, "Hello World!");
|
||||
assert_eq!(wrapper.state.len_unicode(), 12);
|
||||
assert_eq!(wrapper.state.len_entity(), 12);
|
||||
wrapper.delete(0, 5);
|
||||
assert_eq!(
|
||||
wrapper.state.get_richtext_value().to_json_value(),
|
||||
|
@ -2437,8 +2443,12 @@ mod test {
|
|||
])
|
||||
);
|
||||
|
||||
assert_eq!(wrapper.state.len_unicode(), 7);
|
||||
assert_eq!(wrapper.state.len_entity(), 7);
|
||||
wrapper.delete(1, 1);
|
||||
|
||||
assert_eq!(wrapper.state.len_unicode(), 6);
|
||||
assert_eq!(wrapper.state.len_entity(), 6);
|
||||
assert_eq!(
|
||||
wrapper.state.get_richtext_value().to_json_value(),
|
||||
json!([
|
||||
|
@ -2449,6 +2459,8 @@ mod test {
|
|||
);
|
||||
|
||||
wrapper.delete(5, 1);
|
||||
assert_eq!(wrapper.state.len_unicode(), 5);
|
||||
assert_eq!(wrapper.state.len_entity(), 5);
|
||||
assert_eq!(
|
||||
wrapper.state.get_richtext_value().to_json_value(),
|
||||
json!([
|
||||
|
@ -2459,6 +2471,7 @@ mod test {
|
|||
);
|
||||
|
||||
wrapper.delete(0, 5);
|
||||
assert_eq!(wrapper.state.len_unicode(), 0);
|
||||
assert_eq!(
|
||||
wrapper.state.get_richtext_value().to_json_value(),
|
||||
json!([])
|
||||
|
|
|
@ -29,7 +29,7 @@ impl Default for Tracker {
|
|||
}
|
||||
}
|
||||
|
||||
const UNKNOWN_PEER_ID: PeerID = u64::MAX;
|
||||
pub(super) const UNKNOWN_PEER_ID: PeerID = u64::MAX;
|
||||
impl Tracker {
|
||||
pub fn new_with_unknown() -> Self {
|
||||
let mut this = Self {
|
||||
|
@ -42,6 +42,7 @@ impl Tracker {
|
|||
let result = this.rope.tree.push(FugueSpan {
|
||||
content: RichtextChunk::new_unknown(u32::MAX / 4),
|
||||
id: IdFull::new(UNKNOWN_PEER_ID, 0, 0),
|
||||
real_id: None,
|
||||
status: Status::default(),
|
||||
diff_status: None,
|
||||
origin_left: None,
|
||||
|
@ -115,6 +116,11 @@ impl Tracker {
|
|||
FugueSpan {
|
||||
content,
|
||||
id: op_id,
|
||||
real_id: if op_id.peer == UNKNOWN_PEER_ID {
|
||||
None
|
||||
} else {
|
||||
Some(op_id.id())
|
||||
},
|
||||
status: Status::default(),
|
||||
diff_status: None,
|
||||
origin_left: None,
|
||||
|
@ -149,7 +155,14 @@ impl Tracker {
|
|||
/// If `reverse` is true, the deletion happens from the end of the range to the start.
|
||||
/// So the first op is the one that deletes element at `pos+len-1`, the last op
|
||||
/// is the one that deletes element at `pos`.
|
||||
pub(crate) fn delete(&mut self, mut op_id: ID, pos: usize, mut len: usize, reverse: bool) {
|
||||
pub(crate) fn delete(
|
||||
&mut self,
|
||||
mut op_id: ID,
|
||||
target_start_id: ID,
|
||||
pos: usize,
|
||||
mut len: usize,
|
||||
reverse: bool,
|
||||
) {
|
||||
// debug_log::group!("Tracker Delete");
|
||||
// debug_log::debug_dbg!(&op_id, pos, len, reverse);
|
||||
// debug_log::debug_dbg!(&self);
|
||||
|
@ -185,17 +198,15 @@ impl Tracker {
|
|||
// debug_log::debug_log!("after forwarding pos={} len={}", pos, len);
|
||||
// debug_log::debug_dbg!(&self);
|
||||
let mut ans = Vec::new();
|
||||
let split = self.rope.delete(pos, len, |span| {
|
||||
let mut id_span = span.id_span();
|
||||
if reverse {
|
||||
id_span.reverse();
|
||||
}
|
||||
ans.push(id_span);
|
||||
});
|
||||
|
||||
if reverse {
|
||||
ans.reverse();
|
||||
}
|
||||
let split = self
|
||||
.rope
|
||||
.delete(target_start_id, pos, len, reverse, &mut |span| {
|
||||
let mut id_span = span.id_span();
|
||||
if reverse {
|
||||
id_span.reverse();
|
||||
}
|
||||
ans.push(id_span);
|
||||
});
|
||||
|
||||
let mut cur_id = op_id;
|
||||
for id_span in ans {
|
||||
|
@ -206,7 +217,9 @@ impl Tracker {
|
|||
}
|
||||
|
||||
debug_assert_eq!(cur_id.counter - op_id.counter, len as Counter);
|
||||
self.update_insert_by_split(&split.arr);
|
||||
for s in split {
|
||||
self.update_insert_by_split(&s.arr);
|
||||
}
|
||||
|
||||
let end_id = op_id.inc(len as Counter);
|
||||
self.current_vv.extend_to_include_end_id(end_id);
|
||||
|
@ -397,7 +410,7 @@ mod test {
|
|||
fn test_retreat_and_forward_delete() {
|
||||
let mut t = Tracker::new();
|
||||
t.insert(IdFull::new(1, 0, 0), 0, RichtextChunk::new_text(0..10));
|
||||
t.delete(ID::new(2, 0), 0, 10, true);
|
||||
t.delete(ID::new(2, 0), ID::NONE_ID, 0, 10, true);
|
||||
t.checkout(&vv!(1 => 10, 2=>5));
|
||||
assert_eq!(t.rope.len(), 5);
|
||||
t.checkout(&vv!(1 => 10, 2=>0));
|
||||
|
@ -412,7 +425,7 @@ mod test {
|
|||
fn test_checkout_in_doc_with_del_span() {
|
||||
let mut t = Tracker::new();
|
||||
t.insert(IdFull::new(1, 0, 0), 0, RichtextChunk::new_text(0..10));
|
||||
t.delete(ID::new(2, 0), 0, 10, false);
|
||||
t.delete(ID::new(2, 0), ID::NONE_ID, 0, 10, false);
|
||||
t.checkout(&vv!(1 => 10, 2=>4));
|
||||
let v: Vec<FugueSpan> = t.rope.tree().iter().copied().collect();
|
||||
assert_eq!(v.len(), 2);
|
||||
|
@ -421,18 +434,4 @@ mod test {
|
|||
assert!(v[1].is_activated());
|
||||
assert_eq!(v[1].rle_len(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_checkout_in_doc_with_reversed_del_span() {
|
||||
let mut t = Tracker::new();
|
||||
t.insert(IdFull::new(1, 0, 0), 0, RichtextChunk::new_text(0..10));
|
||||
t.delete(ID::new(2, 0), 0, 10, true);
|
||||
t.checkout(&vv!(1 => 10, 2=>4));
|
||||
let v: Vec<FugueSpan> = t.rope.tree().iter().copied().collect();
|
||||
assert_eq!(v.len(), 2);
|
||||
assert!(v[0].is_activated());
|
||||
assert_eq!(v[0].rle_len(), 6);
|
||||
assert!(!v[1].is_activated());
|
||||
assert_eq!(v[1].rle_len(), 4);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,13 @@ use generic_btree::{
|
|||
BTree, BTreeTrait, Cursor, FindResult, LeafIndex, Query, SplittedLeaves,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use loro_common::{Counter, HasCounter, HasCounterSpan, HasIdSpan, IdFull, IdSpan, ID};
|
||||
use smallvec::SmallVec;
|
||||
use loro_common::{Counter, HasCounter, HasCounterSpan, HasIdSpan, IdSpan, Lamport, ID};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
use crate::container::richtext::{fugue_span::DiffStatus, FugueSpan, RichtextChunk, Status};
|
||||
|
||||
use super::UNKNOWN_PEER_ID;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(super) struct CrdtRope {
|
||||
pub(super) tree: BTree<CrdtRopeTrait>,
|
||||
|
@ -226,16 +228,39 @@ impl CrdtRope {
|
|||
}
|
||||
}
|
||||
|
||||
/// - If reversed is true, the deletion will be done in reversed order.
|
||||
/// But the start_id always refers to the first delete op's id.
|
||||
/// - If reversed is true, the returned `SplittedLeaves` will be in reversed order.
|
||||
pub(super) fn delete(
|
||||
&mut self,
|
||||
mut start_id: ID,
|
||||
pos: usize,
|
||||
len: usize,
|
||||
mut notify_deleted_span: impl FnMut(FugueSpan),
|
||||
) -> SplittedLeaves {
|
||||
reversed: bool,
|
||||
notify_deleted_span: &mut dyn FnMut(&FugueSpan),
|
||||
) -> SmallVec<[SplittedLeaves; 1]> {
|
||||
if len == 0 {
|
||||
return Default::default();
|
||||
}
|
||||
|
||||
if reversed && len > 1 {
|
||||
let mut ans = SmallVec::with_capacity(len);
|
||||
for i in (0..len).rev() {
|
||||
let a = self.delete(
|
||||
start_id.inc((len - i - 1) as i32),
|
||||
pos + i,
|
||||
1,
|
||||
false,
|
||||
notify_deleted_span,
|
||||
);
|
||||
|
||||
ans.extend(a);
|
||||
}
|
||||
|
||||
return ans;
|
||||
}
|
||||
|
||||
debug_log::debug_dbg!(&start_id);
|
||||
let start = self
|
||||
.tree
|
||||
.query::<ActiveLenQueryPreferRight>(&(pos as i32))
|
||||
|
@ -249,25 +274,35 @@ impl CrdtRope {
|
|||
let (a, b) = elem.update_with_split(start.offset..start.offset + len, |elem| {
|
||||
assert!(elem.is_activated());
|
||||
debug_assert_eq!(len, elem.rle_len());
|
||||
notify_deleted_span(*elem);
|
||||
notify_deleted_span(elem);
|
||||
elem.status.delete_times += 1;
|
||||
if elem.real_id.is_none() {
|
||||
elem.real_id = Some(start_id);
|
||||
}
|
||||
|
||||
start_id = start_id.inc(elem.rle_len() as i32);
|
||||
});
|
||||
|
||||
(true, a, b)
|
||||
});
|
||||
|
||||
// debug_log::debug_dbg!(&splitted);
|
||||
return splitted;
|
||||
return smallvec![splitted];
|
||||
}
|
||||
|
||||
let end = self
|
||||
.tree
|
||||
.query::<ActiveLenQueryPreferLeft>(&((pos + len) as i32))
|
||||
.unwrap();
|
||||
self.tree.update(start..end.cursor(), &mut |elem| {
|
||||
smallvec![self.tree.update(start..end.cursor(), &mut |elem| {
|
||||
if elem.is_activated() {
|
||||
notify_deleted_span(*elem);
|
||||
notify_deleted_span(elem);
|
||||
elem.status.delete_times += 1;
|
||||
if elem.real_id.is_none() {
|
||||
elem.real_id = Some(start_id);
|
||||
}
|
||||
|
||||
start_id = start_id.inc(elem.rle_len() as i32);
|
||||
Some(Cache {
|
||||
len: -(elem.rle_len() as i32),
|
||||
changed_num: 0,
|
||||
|
@ -275,7 +310,7 @@ impl CrdtRope {
|
|||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})]
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
|
@ -359,7 +394,12 @@ impl CrdtRope {
|
|||
DiffStatus::Created => {
|
||||
let rt = Some(CrdtRopeDelta::Insert {
|
||||
chunk: elem.content,
|
||||
id: elem.id,
|
||||
id: elem.real_id.unwrap(),
|
||||
lamport: if elem.id.peer == UNKNOWN_PEER_ID {
|
||||
None
|
||||
} else {
|
||||
Some(elem.id.lamport)
|
||||
},
|
||||
});
|
||||
if index > last_pos {
|
||||
next = rt;
|
||||
|
@ -410,7 +450,17 @@ impl CrdtRope {
|
|||
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
|
||||
pub(crate) enum CrdtRopeDelta {
|
||||
Retain(usize),
|
||||
Insert { chunk: RichtextChunk, id: IdFull },
|
||||
Insert {
|
||||
chunk: RichtextChunk,
|
||||
id: ID,
|
||||
/// This is a optional field, because we may not know the correct lamport
|
||||
/// for chunk id with UNKNOWN_PEER_ID.
|
||||
///
|
||||
/// This case happens when the chunk is created by default placeholder and
|
||||
/// the deletion happens that marks the chunk with its start_id. But it doesn't
|
||||
/// know the correct lamport for the chunk.
|
||||
lamport: Option<Lamport>,
|
||||
},
|
||||
Delete(usize),
|
||||
}
|
||||
|
||||
|
@ -621,7 +671,7 @@ impl LeafUpdate {
|
|||
mod test {
|
||||
use std::ops::Range;
|
||||
|
||||
use loro_common::{CompactId, Counter, PeerID, ID};
|
||||
use loro_common::{CompactId, Counter, IdFull, PeerID, ID};
|
||||
|
||||
use crate::container::richtext::RichtextChunk;
|
||||
|
||||
|
@ -735,7 +785,7 @@ mod test {
|
|||
let mut rope = CrdtRope::new();
|
||||
rope.insert(0, span(0, 0..10), |_| panic!());
|
||||
assert_eq!(rope.len(), 10);
|
||||
rope.delete(5, 2, |_| {});
|
||||
rope.delete(ID::NONE_ID, 5, 2, false, &mut |_| {});
|
||||
assert_eq!(rope.len(), 8);
|
||||
let fugue = rope.insert(6, span(1, 10..20), |_| panic!()).content;
|
||||
assert_eq!(fugue.origin_left, Some(CompactId::new(0, 7)));
|
||||
|
@ -827,7 +877,8 @@ mod test {
|
|||
CrdtRopeDelta::Retain(2),
|
||||
CrdtRopeDelta::Insert {
|
||||
chunk: RichtextChunk::new_text(10..13),
|
||||
id: IdFull::new(1, 0, 0)
|
||||
id: ID::new(1, 0),
|
||||
lamport: Some(0)
|
||||
}
|
||||
],
|
||||
vec,
|
||||
|
@ -851,7 +902,8 @@ mod test {
|
|||
assert_eq!(
|
||||
vec![CrdtRopeDelta::Insert {
|
||||
chunk: RichtextChunk::new_text(2..10),
|
||||
id: IdFull::new(0, 2, 2)
|
||||
id: ID::new(0, 2),
|
||||
lamport: Some(2)
|
||||
}],
|
||||
vec,
|
||||
);
|
||||
|
|
|
@ -5,7 +5,9 @@ use itertools::Itertools;
|
|||
|
||||
use enum_dispatch::enum_dispatch;
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use loro_common::{ContainerID, HasCounterSpan, HasIdSpan, LoroValue, PeerID, ID};
|
||||
use loro_common::{
|
||||
ContainerID, Counter, HasCounterSpan, HasIdSpan, IdFull, IdSpan, LoroValue, PeerID, ID,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
change::Lamport,
|
||||
|
@ -82,25 +84,9 @@ impl DiffCalculator {
|
|||
}
|
||||
let affected_set = if !self.has_all {
|
||||
// if we don't have all the ops, we need to calculate the diff by tracing back
|
||||
let mut after = after;
|
||||
let mut before = before;
|
||||
let mut merged = before.clone();
|
||||
let mut before_frontiers = before_frontiers;
|
||||
let mut after_frontiers = after_frontiers;
|
||||
merged.merge(after);
|
||||
let empty_vv: VersionVector = Default::default();
|
||||
if !after.includes_vv(before) {
|
||||
// If after is not after before, we need to calculate the diff from the beginning
|
||||
//
|
||||
// This is required because of [MapDiffCalculator]. It can be removed with
|
||||
// a better data structure. See #114.
|
||||
before = &empty_vv;
|
||||
after = &merged;
|
||||
before_frontiers = None;
|
||||
after_frontiers = None;
|
||||
self.has_all = true;
|
||||
self.last_vv = Default::default();
|
||||
} else if before.is_empty() {
|
||||
if before.is_empty() {
|
||||
self.has_all = true;
|
||||
self.last_vv = Default::default();
|
||||
}
|
||||
|
@ -361,7 +347,7 @@ impl DiffCalculatorTrait for MapDiffCalculator {
|
|||
op: crate::op::RichOp,
|
||||
_vv: Option<&crate::VersionVector>,
|
||||
) {
|
||||
let map = op.op().content.as_map().unwrap();
|
||||
let map = op.raw_op().content.as_map().unwrap();
|
||||
self.changed_key.insert(map.key.clone());
|
||||
}
|
||||
|
||||
|
@ -492,6 +478,7 @@ impl DiffCalculatorTrait for ListDiffCalculator {
|
|||
crate::container::list::list_op::InnerListOp::Delete(del) => {
|
||||
self.tracker.delete(
|
||||
op.id_start(),
|
||||
del.id_start,
|
||||
del.start() as usize,
|
||||
del.atom_len(),
|
||||
del.is_reversed(),
|
||||
|
@ -519,7 +506,11 @@ impl DiffCalculatorTrait for ListDiffCalculator {
|
|||
CrdtRopeDelta::Retain(len) => {
|
||||
delta = delta.retain(len);
|
||||
}
|
||||
CrdtRopeDelta::Insert { chunk: value, id } => match value.value() {
|
||||
CrdtRopeDelta::Insert {
|
||||
chunk: value,
|
||||
id,
|
||||
lamport,
|
||||
} => match value.value() {
|
||||
RichtextChunkValue::Text(range) => {
|
||||
for i in range.clone() {
|
||||
let v = oplog.arena.get_value(i as usize);
|
||||
|
@ -529,11 +520,39 @@ impl DiffCalculatorTrait for ListDiffCalculator {
|
|||
}
|
||||
delta = delta.insert(SliceRanges {
|
||||
ranges: smallvec::smallvec![SliceRange(range)],
|
||||
id,
|
||||
id: IdFull::new(id.peer, id.counter, lamport.unwrap()),
|
||||
});
|
||||
}
|
||||
RichtextChunkValue::StyleAnchor { .. } => unreachable!(),
|
||||
RichtextChunkValue::Unknown(_) => unreachable!(),
|
||||
RichtextChunkValue::Unknown(len) => {
|
||||
// assert not unknown id
|
||||
assert_ne!(id.peer, PeerID::MAX);
|
||||
let mut acc_len = 0;
|
||||
for rich_op in oplog.iter_ops(IdSpan::new(
|
||||
id.peer,
|
||||
id.counter,
|
||||
id.counter + len as Counter,
|
||||
)) {
|
||||
acc_len += rich_op.content_len();
|
||||
let op = rich_op.op();
|
||||
let lamport = rich_op.lamport();
|
||||
let content = op.content.as_list().unwrap().as_insert().unwrap();
|
||||
let range = content.0.clone();
|
||||
for i in content.0 .0.clone() {
|
||||
let v = oplog.arena.get_value(i as usize);
|
||||
if let Some(LoroValue::Container(c)) = &v {
|
||||
on_new_container(c);
|
||||
}
|
||||
}
|
||||
|
||||
delta = delta.insert(SliceRanges {
|
||||
ranges: smallvec::smallvec![range],
|
||||
id: IdFull::new(id.peer, op.counter, lamport),
|
||||
});
|
||||
}
|
||||
|
||||
debug_assert_eq!(acc_len, len as usize);
|
||||
}
|
||||
},
|
||||
CrdtRopeDelta::Delete(len) => {
|
||||
delta = delta.delete(len);
|
||||
|
@ -572,7 +591,7 @@ impl DiffCalculatorTrait for RichtextDiffCalculator {
|
|||
if let Some(vv) = vv {
|
||||
self.tracker.checkout(vv);
|
||||
}
|
||||
match &op.op().content {
|
||||
match &op.raw_op().content {
|
||||
crate::op::InnerContent::List(l) => match l {
|
||||
crate::container::list::list_op::InnerListOp::Insert { .. } => {
|
||||
unreachable!()
|
||||
|
@ -592,6 +611,7 @@ impl DiffCalculatorTrait for RichtextDiffCalculator {
|
|||
crate::container::list::list_op::InnerListOp::Delete(del) => {
|
||||
self.tracker.delete(
|
||||
op.id_start(),
|
||||
del.id_start,
|
||||
del.start() as usize,
|
||||
del.atom_len(),
|
||||
del.is_reversed(),
|
||||
|
@ -648,7 +668,11 @@ impl DiffCalculatorTrait for RichtextDiffCalculator {
|
|||
CrdtRopeDelta::Retain(len) => {
|
||||
delta = delta.retain(len);
|
||||
}
|
||||
CrdtRopeDelta::Insert { chunk: value, id } => match value.value() {
|
||||
CrdtRopeDelta::Insert {
|
||||
chunk: value,
|
||||
id,
|
||||
lamport,
|
||||
} => match value.value() {
|
||||
RichtextChunkValue::Text(text) => {
|
||||
delta = delta.insert(RichtextStateChunk::Text(
|
||||
// PERF: can be speedup by acquiring lock on arena
|
||||
|
@ -656,7 +680,7 @@ impl DiffCalculatorTrait for RichtextDiffCalculator {
|
|||
oplog
|
||||
.arena
|
||||
.slice_by_unicode(text.start as usize..text.end as usize),
|
||||
id,
|
||||
IdFull::new(id.peer, id.counter, lamport.unwrap()),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
@ -666,7 +690,36 @@ impl DiffCalculatorTrait for RichtextDiffCalculator {
|
|||
anchor_type,
|
||||
});
|
||||
}
|
||||
RichtextChunkValue::Unknown(_) => unreachable!(),
|
||||
RichtextChunkValue::Unknown(len) => {
|
||||
// assert not unknown id
|
||||
assert_ne!(id.peer, PeerID::MAX);
|
||||
debug_log::debug_dbg!(oplog.changes(), id, len);
|
||||
let mut acc_len = 0;
|
||||
for rich_op in oplog.iter_ops(IdSpan::new(
|
||||
id.peer,
|
||||
id.counter,
|
||||
id.counter + len as Counter,
|
||||
)) {
|
||||
acc_len += rich_op.content_len();
|
||||
let op = rich_op.op();
|
||||
let lamport = rich_op.lamport();
|
||||
let content = op.content.as_list().unwrap();
|
||||
match content {
|
||||
crate::container::list::list_op::InnerListOp::InsertText {
|
||||
slice,
|
||||
..
|
||||
} => {
|
||||
delta = delta.insert(RichtextStateChunk::Text(TextChunk::new(
|
||||
slice.clone(),
|
||||
IdFull::new(id.peer, op.counter, lamport),
|
||||
)));
|
||||
}
|
||||
_ => unreachable!("{:?}", content),
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert_eq!(acc_len, len as usize);
|
||||
}
|
||||
},
|
||||
CrdtRopeDelta::Delete(len) => {
|
||||
delta = delta.delete(len);
|
||||
|
|
|
@ -2132,6 +2132,7 @@ mod value {
|
|||
leb128::read::signed(&mut self.raw).map_err(|_| LoroError::DecodeDataCorruptionError)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn read_i32(&mut self) -> LoroResult<i32> {
|
||||
leb128::read::signed(&mut self.raw)
|
||||
.map(|x| x as i32)
|
||||
|
|
|
@ -4547,6 +4547,72 @@ mod failed_tests {
|
|||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzz_18() {
|
||||
test_multi_sites(
|
||||
5,
|
||||
&mut [
|
||||
List {
|
||||
site: 1,
|
||||
container_idx: 0,
|
||||
key: 0,
|
||||
value: I32(1),
|
||||
},
|
||||
List {
|
||||
site: 1,
|
||||
container_idx: 0,
|
||||
key: 0,
|
||||
value: Container(C::List),
|
||||
},
|
||||
List {
|
||||
site: 1,
|
||||
container_idx: 0,
|
||||
key: 0,
|
||||
value: Null,
|
||||
},
|
||||
List {
|
||||
site: 1,
|
||||
container_idx: 0,
|
||||
key: 0,
|
||||
value: Null,
|
||||
},
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzz_19() {
|
||||
test_multi_sites(
|
||||
5,
|
||||
&mut [
|
||||
List {
|
||||
site: 1,
|
||||
container_idx: 0,
|
||||
key: 0,
|
||||
value: I32(2),
|
||||
},
|
||||
List {
|
||||
site: 1,
|
||||
container_idx: 0,
|
||||
key: 0,
|
||||
value: I32(1),
|
||||
},
|
||||
List {
|
||||
site: 1,
|
||||
container_idx: 0,
|
||||
key: 1,
|
||||
value: Null,
|
||||
},
|
||||
List {
|
||||
site: 1,
|
||||
container_idx: 0,
|
||||
key: 0,
|
||||
value: Null,
|
||||
},
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_minify() {
|
||||
minify_error(5, vec![], test_multi_sites, normalize)
|
||||
|
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
use enum_as_inner::EnumAsInner;
|
||||
use enum_dispatch::enum_dispatch;
|
||||
use fxhash::FxHashMap;
|
||||
use loro_common::{Counter, HasId, InternalString, LoroValue, PeerID, ID};
|
||||
use loro_common::{Counter, HasId, HasLamport, InternalString, LoroValue, PeerID, ID};
|
||||
|
||||
use crate::{
|
||||
change::{Change, Lamport},
|
||||
|
@ -128,15 +128,15 @@ impl MapOpGroup {
|
|||
|
||||
impl OpGroupTrait for MapOpGroup {
|
||||
fn insert(&mut self, op: &RichOp) {
|
||||
let key = match &op.op.content {
|
||||
let key = match &op.raw_op().content {
|
||||
InnerContent::Map(map) => map.key.clone(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let entry = self.ops.entry(key).or_default();
|
||||
entry.insert(GroupedMapOpInfo {
|
||||
value: op.op.content.as_map().unwrap().value.clone(),
|
||||
counter: op.op.counter,
|
||||
lamport: op.lamport,
|
||||
value: op.raw_op().content.as_map().unwrap().value.clone(),
|
||||
counter: op.raw_op().counter,
|
||||
lamport: op.lamport(),
|
||||
peer: op.peer,
|
||||
});
|
||||
}
|
||||
|
@ -183,13 +183,13 @@ pub(crate) struct TreeOpGroup {
|
|||
|
||||
impl OpGroupTrait for TreeOpGroup {
|
||||
fn insert(&mut self, op: &RichOp) {
|
||||
let tree_op = op.op.content.as_tree().unwrap();
|
||||
let tree_op = op.raw_op().content.as_tree().unwrap();
|
||||
let target = tree_op.target;
|
||||
let parent = tree_op.parent;
|
||||
let entry = self.ops.entry(op.lamport).or_default();
|
||||
let entry = self.ops.entry(op.lamport()).or_default();
|
||||
entry.insert(GroupedTreeOpInfo {
|
||||
value: TreeOp { target, parent },
|
||||
counter: op.op.counter,
|
||||
counter: op.raw_op().counter,
|
||||
peer: op.peer,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
};
|
||||
use crate::{delta::DeltaValue, LoroValue};
|
||||
use enum_as_inner::EnumAsInner;
|
||||
use loro_common::{IdFull, IdSpan};
|
||||
use loro_common::{CounterSpan, IdFull, IdSpan};
|
||||
use rle::{HasIndex, HasLength, Mergable, Sliceable};
|
||||
use serde::{ser::SerializeSeq, Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
|
@ -84,9 +84,9 @@ impl RawOp<'_> {
|
|||
/// RichOp includes lamport and timestamp info, which is used for conflict resolution.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RichOp<'a> {
|
||||
pub op: &'a Op,
|
||||
op: &'a Op,
|
||||
pub peer: PeerID,
|
||||
pub lamport: Lamport,
|
||||
lamport: Lamport,
|
||||
pub timestamp: Timestamp,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
|
@ -264,11 +264,32 @@ impl<'a> RichOp<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_sliced(&self) -> Op {
|
||||
self.op.slice(self.start, self.end)
|
||||
pub fn new_by_cnt_range(change: &Change<Op>, span: CounterSpan, op: &'a Op) -> Option<Self> {
|
||||
let op_index_in_change = op.counter - change.id.counter;
|
||||
let op_slice_start = (span.start - op.counter).clamp(0, op.atom_len() as i32);
|
||||
let op_slice_end = (span.end - op.counter).clamp(0, op.atom_len() as i32);
|
||||
if op_slice_start == op_slice_end {
|
||||
return None;
|
||||
}
|
||||
Some(RichOp {
|
||||
op,
|
||||
peer: change.id.peer,
|
||||
lamport: change.lamport + op_index_in_change as Lamport,
|
||||
timestamp: change.timestamp,
|
||||
start: op_slice_start as usize,
|
||||
end: op_slice_end as usize,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn op(&self) -> &Op {
|
||||
pub fn op(&self) -> Cow<'_, Op> {
|
||||
if self.start == 0 && self.end == self.op.content_len() {
|
||||
Cow::Borrowed(self.op)
|
||||
} else {
|
||||
Cow::Owned(self.op.slice(self.start, self.end))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn raw_op(&self) -> &Op {
|
||||
self.op
|
||||
}
|
||||
|
||||
|
|
|
@ -7,12 +7,6 @@ use std::cmp::Ordering;
|
|||
use std::mem::take;
|
||||
use std::rc::Rc;
|
||||
|
||||
use fxhash::FxHashMap;
|
||||
use loro_common::{HasCounter, HasId};
|
||||
use rle::{HasLength, RleCollection, RlePush, RleVec, Sliceable};
|
||||
use smallvec::SmallVec;
|
||||
// use tabled::measurment::Percent;
|
||||
|
||||
use crate::change::{get_sys_timestamp, Change, Lamport, Timestamp};
|
||||
use crate::configure::Configure;
|
||||
use crate::container::list::list_op;
|
||||
|
@ -21,10 +15,14 @@ use crate::encoding::ParsedHeaderAndBody;
|
|||
use crate::encoding::{decode_oplog, encode_oplog, EncodeMode};
|
||||
use crate::group::OpGroups;
|
||||
use crate::id::{Counter, PeerID, ID};
|
||||
use crate::op::{ListSlice, RawOpContent, RemoteOp};
|
||||
use crate::op::{ListSlice, RawOpContent, RemoteOp, RichOp};
|
||||
use crate::span::{HasCounterSpan, HasIdSpan, HasLamportSpan};
|
||||
use crate::version::{Frontiers, ImVersionVector, VersionVector};
|
||||
use crate::LoroError;
|
||||
use fxhash::FxHashMap;
|
||||
use loro_common::{HasCounter, HasId, IdSpan};
|
||||
use rle::{HasLength, RleCollection, RlePush, RleVec, Sliceable};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
type ClientChanges = FxHashMap<PeerID, Vec<Change>>;
|
||||
pub use self::dag::FrontiersNotIncluded;
|
||||
|
@ -465,6 +463,34 @@ impl OpLog {
|
|||
.map(|c| c.lamport + (id.counter - c.id.counter) as Lamport)
|
||||
}
|
||||
|
||||
pub(crate) fn iter_ops(&self, id_span: IdSpan) -> impl Iterator<Item = RichOp> + '_ {
|
||||
self.changes
|
||||
.get(&id_span.client_id)
|
||||
.map(move |changes| {
|
||||
let len = changes.len();
|
||||
let start = changes
|
||||
.get_by_atom_index(id_span.counter.start)
|
||||
.map(|x| x.merged_index)
|
||||
.unwrap_or(len);
|
||||
let mut end = changes
|
||||
.get_by_atom_index(id_span.counter.end)
|
||||
.map(|x| x.merged_index)
|
||||
.unwrap_or(len);
|
||||
if end < changes.len() {
|
||||
end += 1;
|
||||
}
|
||||
|
||||
changes[start..end].iter().flat_map(move |c| {
|
||||
// TODO: PERF can be optimized
|
||||
c.ops()
|
||||
.iter()
|
||||
.filter_map(move |op| RichOp::new_by_cnt_range(c, id_span.counter, op))
|
||||
})
|
||||
})
|
||||
.into_iter()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub(crate) fn get_max_lamport_at(&self, id: ID) -> Lamport {
|
||||
self.get_change_at(id)
|
||||
.map(|c| {
|
||||
|
|
|
@ -1,7 +1,55 @@
|
|||
use std::{cmp::Ordering, sync::Arc};
|
||||
|
||||
use loro::{FrontiersNotIncluded, LoroDoc};
|
||||
use loro::{FrontiersNotIncluded, LoroDoc, LoroError, ToJson};
|
||||
use loro_internal::{delta::DeltaItem, handler::TextDelta, id::ID, DiffEvent, LoroResult};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn list_checkout() -> Result<(), LoroError> {
|
||||
let mut doc = LoroDoc::new();
|
||||
doc.get_list("list")
|
||||
.insert_container(0, loro::ContainerType::Map)?;
|
||||
doc.commit();
|
||||
let f0 = doc.state_frontiers();
|
||||
doc.get_list("list")
|
||||
.insert_container(0, loro::ContainerType::Text)?;
|
||||
doc.commit();
|
||||
let f1 = doc.state_frontiers();
|
||||
doc.get_list("list").delete(1, 1)?;
|
||||
doc.commit();
|
||||
let f2 = doc.state_frontiers();
|
||||
doc.get_list("list").delete(0, 1)?;
|
||||
doc.commit();
|
||||
doc.checkout(&f1)?;
|
||||
assert_eq!(
|
||||
doc.get_deep_value().to_json_value(),
|
||||
json!({
|
||||
"list": ["", {}]
|
||||
})
|
||||
);
|
||||
doc.checkout(&f2)?;
|
||||
assert_eq!(
|
||||
doc.get_deep_value().to_json_value(),
|
||||
json!({
|
||||
"list": [""]
|
||||
})
|
||||
);
|
||||
doc.checkout(&f0)?;
|
||||
assert_eq!(
|
||||
doc.get_deep_value().to_json_value(),
|
||||
json!({
|
||||
"list": [{}]
|
||||
})
|
||||
);
|
||||
doc.checkout(&f1)?;
|
||||
assert_eq!(
|
||||
doc.get_deep_value().to_json_value(),
|
||||
json!({
|
||||
"list": ["", {}]
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timestamp() {
|
||||
|
|
Loading…
Reference in a new issue