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:
Zixuan Chen 2024-02-29 23:00:02 +08:00 committed by GitHub
parent 06e3a5420d
commit 07c11f68b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 396 additions and 97 deletions

View file

@ -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,

View file

@ -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,

View file

@ -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!([])

View file

@ -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);
}
}

View file

@ -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,
);

View file

@ -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);

View file

@ -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)

View file

@ -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)

View file

@ -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,
});
}

View file

@ -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
}

View file

@ -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| {

View file

@ -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() {