Skip to content

Commit

Permalink
feat: movable tree support (#120)
Browse files Browse the repository at this point in the history
* feat: tree state

* feat: tree value

* feat: tree handler

* fix: tree diff

* test: fuzz tree

* feat: tree snapshot

* fix: tree default value

* fix: test new node

* fix: tree diff

* fix: tree unresolved value

* fix: tree fuzz

* fix: tree fuzz move

* fix: sort by tree id

* fix: tree diff sorted by lamport

* fix: sort roots before tree converted to string

* fix: rebase main

* fix: tree fuzz

* fix: delete undo

* fix: tree to json children sorted

* fix: diff calculate

* fix: diff cycle move

* fix: tree old parent cache

* feat: cache

* fix: local op add tree cache

* fix: don't add same tree move to cache

* fix: need update cache

* feat: new cache

* bench: add checkout bench

* chore: clean

* fix: apply node uncheck

* perf: lamport bound

* fix: calc old parent

* feat: tree wasm

* fix: change tree diff

* fix: tree diff retreat

* fix: tree diff should not apply when add node

* feat: new tree loro value

* chore: typo

* fix: tree deep value

* fix: snapshot tree index -1

* fix: decode tree snapshot use state

* fix: release state lock when emit event

* fix: tree node meta container

* fix: need set map container when covert to local tree op

* fix: tree value add deleted

* fix: more then one op in a change

* fix: tree fuzz deleted equal

* fix: tree calc min lamport

* feat: tree encoding v2

* doc: movable tree

* fix: test tree meta

* test: remove import bytes check

* refactor: diff of text and map

* refactor: del span

* perf: tree state use deleted cache

* fix: some details

* fix: loro js tree create

* feat: add un exist tree node

* bench:  tree depth

* fix: check out should emit event

* refactor: event

* fix: fuzz err

* fix: pass all tests

* fix: fuzz err

* fix: list child cache err

* chore: rm debug code

* fix: encode enhanced err

* fix: encode enchanced

* fix: fix several richtext issue

* fix: richtext anchor err

* chore: rm debug code

* fix: richtext fuzz err

* feat: speedup text snapshot decode

* perf: optimize snapshot encoding

* perf: speed up decode & insert

* fix: fugue span merge err

* perf: speedup delete & id cursor map

* fix: fugue merge err

* chore: update utils

* fix: fix merge

* fix: return err apply op

* fix: fix merge

* fix: get map container as tree meta
  • Loading branch information
Leeeon233 authored Oct 30, 2023
1 parent 4d87725 commit e01e984
Show file tree
Hide file tree
Showing 45 changed files with 4,736 additions and 668 deletions.
14 changes: 13 additions & 1 deletion crates/loro-common/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use thiserror::Error;

use crate::{PeerID, ID};
use crate::{PeerID, TreeID, ID};

pub type LoroResult<T> = Result<T, LoroError>;

Expand Down Expand Up @@ -32,6 +32,8 @@ pub enum LoroError {
OutOfBound { pos: usize, len: usize },
#[error("Every op id should be unique. ID {id} has been used. You should use a new PeerID to edit the content. ")]
UsedOpID { id: ID },
#[error("Movable Tree Error")]
TreeError(#[from] LoroTreeError),
#[error("Invalid argument ({0})")]
ArgErr(Box<str>),
// #[error("the data for key `{0}` is not available")]
Expand All @@ -42,6 +44,16 @@ pub enum LoroError {
// Unknown,
}

#[derive(Error, Debug)]
pub enum LoroTreeError {
#[error("`Cycle move` occurs when moving tree nodes.")]
CyclicMoveError,
#[error("The parent of tree node is not found {0:?}")]
TreeNodeParentNotFound(TreeID),
#[error("TreeID {0:?} doesn't exist")]
TreeNodeNotExist(TreeID),
}

#[cfg(feature = "wasm")]
pub mod wasm {
use wasm_bindgen::JsValue;
Expand Down
126 changes: 125 additions & 1 deletion crates/loro-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ use std::{fmt::Display, sync::Arc};

use arbitrary::Arbitrary;
use enum_as_inner::EnumAsInner;

use fxhash::FxHashMap;
use serde::{Deserialize, Serialize};
mod error;
mod id;
mod span;
mod value;

pub use error::{LoroError, LoroResult};
pub use error::{LoroError, LoroResult, LoroTreeError};
pub use span::*;
pub use value::LoroValue;

use zerovec::ule::AsULE;
pub type PeerID = u64;
pub type Counter = i32;
Expand Down Expand Up @@ -58,6 +61,7 @@ pub enum ContainerType {
Text,
Map,
List,
Tree,
// TODO: Users can define their own container types.
// Custom(u16),
}
Expand All @@ -70,6 +74,7 @@ impl AsULE for ContainerType {
ContainerType::Map => 1,
ContainerType::List => 2,
ContainerType::Text => 3,
ContainerType::Tree => 4,
}
}

Expand All @@ -78,6 +83,7 @@ impl AsULE for ContainerType {
1 => ContainerType::Map,
2 => ContainerType::List,
3 => ContainerType::Text,
4 => ContainerType::Tree,
_ => unreachable!(),
}
}
Expand All @@ -89,6 +95,12 @@ impl ContainerType {
ContainerType::Map => LoroValue::Map(Arc::new(Default::default())),
ContainerType::List => LoroValue::List(Arc::new(Default::default())),
ContainerType::Text => LoroValue::String(Arc::new(Default::default())),
ContainerType::Tree => {
let mut map: FxHashMap<String, LoroValue> = FxHashMap::default();
map.insert("roots".to_string(), LoroValue::List(vec![].into()));
map.insert("deleted".to_string(), LoroValue::List(vec![].into()));
map.into()
}
}
}

Expand All @@ -97,6 +109,7 @@ impl ContainerType {
ContainerType::Map => 1,
ContainerType::List => 2,
ContainerType::Text => 3,
ContainerType::Tree => 4,
}
}

Expand All @@ -105,6 +118,7 @@ impl ContainerType {
1 => ContainerType::Map,
2 => ContainerType::List,
3 => ContainerType::Text,
4 => ContainerType::Tree,
_ => unreachable!(),
}
}
Expand Down Expand Up @@ -133,6 +147,7 @@ mod container {
ContainerType::Map => "Map",
ContainerType::List => "List",
ContainerType::Text => "Text",
ContainerType::Tree => "Tree",
})
}
}
Expand Down Expand Up @@ -227,10 +242,119 @@ mod container {
"Map" => Ok(ContainerType::Map),
"List" => Ok(ContainerType::List),
"Text" => Ok(ContainerType::Text),
"Tree" => Ok(ContainerType::Tree),
_ => Err(LoroError::DecodeError(
("Unknown container type".to_string() + value).into(),
)),
}
}
}
}

/// In movable tree, we use a specific [`TreeID`] to represent the root of **ALL** non-existent tree nodes.
///
/// When we create some tree node and then we checkout the previous vision, we need to delete it from the state.
/// If the parent of node is [`UNEXIST_TREE_ROOT`], we could infer this node is first created and delete it from the state directly,
/// instead of moving it to the [`DELETED_TREE_ROOT`].
///
/// This root only can be old parent of node.
pub const UNEXIST_TREE_ROOT: Option<TreeID> = Some(TreeID {
peer: PeerID::MAX,
counter: Counter::MAX - 1,
});

/// In movable tree, we use a specific [`TreeID`] to represent the root of **ALL** deleted tree node.
///
/// Deletion operation is equivalent to move target tree node to [`DELETED_TREE_ROOT`].
pub const DELETED_TREE_ROOT: Option<TreeID> = Some(TreeID {
peer: PeerID::MAX,
counter: Counter::MAX,
});

/// Each node of movable tree has a unique [`TreeID`] generated by Loro.
///
/// To further represent the metadata (a MapContainer) associated with each node,
/// we also use [`TreeID`] as [`ID`] portion of [`ContainerID`].
/// This not only allows for convenient association of metadata with each node,
/// but also ensures the uniqueness of the MapContainer.
///
/// Special ID:
/// - [`DELETED_TREE_ROOT`]: the root of all deleted nodes. To get it by [`TreeID::delete_root()`]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct TreeID {
pub peer: PeerID,
pub counter: Counter,
}

impl TreeID {
/// return [`DELETED_TREE_ROOT`]
pub const fn delete_root() -> Option<Self> {
DELETED_TREE_ROOT
}

/// return `true` if the `TreeID` is deleted root
pub fn is_deleted_root(target: Option<TreeID>) -> bool {
target == DELETED_TREE_ROOT
}

pub const fn unexist_root() -> Option<Self> {
UNEXIST_TREE_ROOT
}

/// return `true` if the `TreeID` is non-existent root
pub fn is_unexist_root(target: Option<TreeID>) -> bool {
target == UNEXIST_TREE_ROOT
}

pub fn from_id(id: ID) -> Self {
Self {
peer: id.peer,
counter: id.counter,
}
}

pub fn id(&self) -> ID {
ID {
peer: self.peer,
counter: self.counter,
}
}
}

impl Display for TreeID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.id().fmt(f)
}
}

impl TryFrom<&str> for TreeID {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
let mut parts = value.split('@');
let counter = parts.next().ok_or(())?.parse().map_err(|_| ())?;
let peer = parts.next().ok_or(())?.parse().map_err(|_| ())?;
Ok(TreeID { peer, counter })
}
}

#[cfg(feature = "wasm")]
pub mod wasm {
use crate::TreeID;
use wasm_bindgen::JsValue;
impl From<TreeID> for JsValue {
fn from(value: TreeID) -> Self {
JsValue::from_str(&format!("{}", value.id()))
}
}

impl TryFrom<JsValue> for TreeID {
type Error = ();
fn try_from(value: JsValue) -> Result<Self, Self::Error> {
let id = value.as_string().unwrap();
let mut parts = id.split('@');
let counter = parts.next().ok_or(())?.parse().map_err(|_| ())?;
let peer = parts.next().ok_or(())?.parse().map_err(|_| ())?;
Ok(TreeID { peer, counter })
}
}
}
94 changes: 94 additions & 0 deletions crates/loro-internal/benches/tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use criterion::{criterion_group, criterion_main, Criterion};
#[cfg(feature = "test_utils")]
mod tree {
use super::*;
use loro_internal::LoroDoc;
use rand::{rngs::StdRng, Rng};

pub fn tree_move(c: &mut Criterion) {
let mut b = c.benchmark_group("movable tree");
b.sample_size(10);
b.bench_function("10^3 tree move 10^5", |b| {
let loro = LoroDoc::default();
let tree = loro.get_tree("tree");
let mut ids = vec![];
let size = 1000;
for _ in 0..size {
ids.push(loro.with_txn(|txn| tree.create(txn)).unwrap())
}
let mut rng: StdRng = rand::SeedableRng::seed_from_u64(0);
let n = 100000;
b.iter(|| {
let mut txn = loro.txn().unwrap();
for _ in 0..n {
let i = rng.gen::<usize>() % size;
let j = rng.gen::<usize>() % size;
tree.mov(&mut txn, ids[i], ids[j]).unwrap_or_default();
}
drop(txn)
})
});

b.bench_function("1000 node checkout 10^3", |b| {
let mut loro = LoroDoc::default();
let tree = loro.get_tree("tree");
let mut ids = vec![];
let mut versions = vec![];
let size = 1000;
for _ in 0..size {
ids.push(loro.with_txn(|txn| tree.create(txn)).unwrap())
}
let mut rng: StdRng = rand::SeedableRng::seed_from_u64(0);
let mut n = 1000;
while n > 0 {
let i = rng.gen::<usize>() % size;
let j = rng.gen::<usize>() % size;
if loro.with_txn(|txn| tree.mov(txn, ids[i], ids[j])).is_ok() {
versions.push(loro.oplog_frontiers());
n -= 1;
};
}
b.iter(|| {
for _ in 0..1000 {
let i = rng.gen::<usize>() % 1000;
let f = &versions[i];
loro.checkout(f).unwrap();
}
})
});

b.bench_function("300 deep node random checkout 10^3", |b| {
let depth = 300;
let mut loro = LoroDoc::default();
let tree = loro.get_tree("tree");
let mut ids = vec![];
let mut versions = vec![];
let id1 = loro.with_txn(|txn| tree.create(txn)).unwrap();
ids.push(id1);
versions.push(loro.oplog_frontiers());
for _ in 1..depth {
let id = loro
.with_txn(|txn| tree.create_and_mov(txn, *ids.last().unwrap()))
.unwrap();
ids.push(id);
versions.push(loro.oplog_frontiers());
}
let mut rng: StdRng = rand::SeedableRng::seed_from_u64(0);
b.iter(|| {
for _ in 0..1000 {
let i = rng.gen::<usize>() % depth;
let f = &versions[i];
loro.checkout(f).unwrap();
}
})
});
}
}

pub fn dumb(_c: &mut Criterion) {}

#[cfg(feature = "test_utils")]
criterion_group!(benches, tree::tree_move);
#[cfg(not(feature = "test_utils"))]
criterion_group!(benches, dumb);
criterion_main!(benches);
26 changes: 26 additions & 0 deletions crates/loro-internal/examples/tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use std::time::Instant;

use loro_internal::LoroDoc;
use rand::{rngs::StdRng, Rng};

fn main() {
let s = Instant::now();
let loro = LoroDoc::default();
let tree = loro.get_tree("tree");
let mut ids = vec![];
let size = 10000;
for _ in 0..size {
ids.push(loro.with_txn(|txn| tree.create(txn)).unwrap())
}
let mut rng: StdRng = rand::SeedableRng::seed_from_u64(0);
let n = 1000000;

let mut txn = loro.txn().unwrap();
for _ in 0..n {
let i = rng.gen::<usize>() % size;
let j = rng.gen::<usize>() % size;
tree.mov(&mut txn, ids[i], ids[j]).unwrap_or_default();
}
drop(txn);
println!("{} ms", s.elapsed().as_millis());
}
6 changes: 6 additions & 0 deletions crates/loro-internal/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,9 @@ name = "import"
path = "fuzz_targets/import.rs"
test = false
doc = false

[[bin]]
name = "tree"
path = "fuzz_targets/tree.rs"
test = false
doc = false
5 changes: 5 additions & 0 deletions crates/loro-internal/fuzz/fuzz_targets/tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use loro_internal::fuzz::tree::{test_multi_sites, Action};

fuzz_target!(|actions: Vec<Action>| { test_multi_sites(5, &mut actions.clone()) });
Loading

0 comments on commit e01e984

Please sign in to comment.