Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat movable tree #120

Merged
merged 114 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
d4b3d75
feat: richtext wip
zxch3n Sep 7, 2023
4c0c8a4
feat: add insert to style range map wip
zxch3n Sep 7, 2023
b1b142b
feat: richtext state
zxch3n Sep 12, 2023
195389f
fix: fix style state inserting and style map
zxch3n Sep 13, 2023
ce34b61
fix: tiny vec merge err
zxch3n Sep 13, 2023
48eb20f
fix: comment err
zxch3n Sep 13, 2023
4081b31
refactor: use new generic-btree & refine impl
zxch3n Sep 27, 2023
89e468d
feat: fugue tracker
zxch3n Oct 3, 2023
f728df4
Merge branch 'main' into feat-richtext
zxch3n Oct 3, 2023
a4d3aab
feat: tracker
zxch3n Oct 4, 2023
1fef374
feat: tracker
zxch3n Oct 4, 2023
978b40e
fix: fix a few err in impl
zxch3n Oct 4, 2023
024a63f
feat: init richtext content state
zxch3n Oct 5, 2023
8845bb9
feat: refactor arena
zxch3n Oct 5, 2023
8182a47
feat: extract anchor_type info out of style flag
zxch3n Oct 6, 2023
486a55d
refactor: state apply op more efficiently
zxch3n Oct 6, 2023
e98ae4b
fix: new clippy errors
zxch3n Oct 6, 2023
72d1033
refactor: use state chunk as delta item
zxch3n Oct 6, 2023
2db8d9b
refactor: use two op to insert style start and style end
zxch3n Oct 6, 2023
0c16ec6
feat: diff calc
zxch3n Oct 6, 2023
2ead9a7
feat: handler
zxch3n Oct 7, 2023
0a4aff0
fix: tracker checkout err
zxch3n Oct 7, 2023
d93a1ad
fix: pass basic richtext handler tests
zxch3n Oct 7, 2023
b272817
fix: pass handler basic marking tests
zxch3n Oct 8, 2023
3de37b6
fix: pass all peritext criteria
zxch3n Oct 9, 2023
17645f9
feat: snapshot encoding for richtext init
zxch3n Oct 10, 2023
2b7863c
refactor: replace Text with Richtext
zxch3n Oct 10, 2023
044d7b4
refacotr: rm text code
zxch3n Oct 10, 2023
179c707
fix: richtext checkout err
zxch3n Oct 10, 2023
a36ab52
feat: tree state
Leeeon233 Oct 11, 2023
d5c557e
feat: tree value
Leeeon233 Oct 11, 2023
2fc6cf8
feat: tree handler
Leeeon233 Oct 11, 2023
8987116
fix: tree diff
Leeeon233 Oct 11, 2023
27a0190
test: fuzz tree
Leeeon233 Oct 11, 2023
3010097
feat: tree snapshot
Leeeon233 Oct 11, 2023
b9ba107
fix: tree default value
Leeeon233 Oct 11, 2023
019ab41
fix: test new node
Leeeon233 Oct 11, 2023
3adb71a
fix: tree diff
Leeeon233 Oct 11, 2023
e78f364
fix: tree unresolved value
Leeeon233 Oct 11, 2023
a2597a4
fix: tree fuzz
Leeeon233 Oct 11, 2023
a4077a7
fix: tree fuzz move
Leeeon233 Oct 11, 2023
bf20427
fix: sort by tree id
Leeeon233 Oct 11, 2023
89300ba
fix: tree diff sorted by lamport
Leeeon233 Oct 11, 2023
2738e04
fix: sort roots before tree converted to string
Leeeon233 Oct 11, 2023
1b3f352
fix: rebase main
Leeeon233 Oct 11, 2023
7678202
fix: tree fuzz
Leeeon233 Oct 11, 2023
61d4d52
fix: delete undo
Leeeon233 Oct 11, 2023
80491af
fix: tree to json children sorted
Leeeon233 Oct 11, 2023
b2ec422
fix: diff calculate
Leeeon233 Oct 11, 2023
b123659
fix: diff cycle move
Leeeon233 Oct 11, 2023
45c1aed
fix: tree old parent cache
Leeeon233 Oct 11, 2023
272b644
feat: cache
Leeeon233 Oct 11, 2023
e9e211a
fix: local op add tree cache
Leeeon233 Oct 11, 2023
ab065c4
fix: don't add same tree move to cache
Leeeon233 Oct 11, 2023
2622620
fix: need update cache
Leeeon233 Oct 11, 2023
d375f78
feat: new cache
Leeeon233 Oct 11, 2023
32facab
bench: add checkout bench
Leeeon233 Oct 11, 2023
c17a578
chore: clean
Leeeon233 Oct 11, 2023
f0bc30e
fix: apply node uncheck
Leeeon233 Oct 11, 2023
1cb0c97
perf: lamport bound
Leeeon233 Oct 11, 2023
2efa6be
fix: calc old parent
Leeeon233 Oct 11, 2023
ef8b937
feat: tree wasm
Leeeon233 Oct 11, 2023
14da537
fix: change tree diff
Leeeon233 Oct 11, 2023
89c3085
fix: tree diff retreat
Leeeon233 Oct 11, 2023
47866e6
fix: tree diff should not apply when add node
Leeeon233 Oct 11, 2023
cf8b3b6
feat: new tree loro value
Leeeon233 Oct 11, 2023
5a3fda1
chore: typo
Leeeon233 Oct 11, 2023
5edfcde
fix: tree deep value
Leeeon233 Oct 11, 2023
b82d850
fix: snapshot tree index -1
Leeeon233 Oct 11, 2023
aeb6bc0
fix: decode tree snapshot use state
Leeeon233 Oct 11, 2023
0c22dc5
fix: release state lock when emit event
Leeeon233 Oct 11, 2023
f21e740
fix: tree node meta container
Leeeon233 Oct 11, 2023
d6f93c2
fix: need set map container when covert to local tree op
Leeeon233 Oct 11, 2023
59dd30f
fix: tree value add deleted
Leeeon233 Oct 11, 2023
e7bcc4a
fix: more then one op in a change
Leeeon233 Oct 11, 2023
b8c690b
fix: tree fuzz deleted equal
Leeeon233 Oct 11, 2023
177c05d
fix: tree calc min lamport
Leeeon233 Oct 11, 2023
9799dea
feat: tree encoding v2
Leeeon233 Oct 11, 2023
ed6d240
doc: movable tree
Leeeon233 Oct 12, 2023
67d8691
fix: test tree meta
Leeeon233 Oct 12, 2023
afc9864
test: remove import bytes check
Leeeon233 Oct 12, 2023
6a6a44e
refactor: diff of text and map
zxch3n Oct 13, 2023
21b988b
refactor: del span
zxch3n Oct 13, 2023
a2baa74
perf: tree state use deleted cache
Leeeon233 Oct 13, 2023
d19a092
fix: some details
Leeeon233 Oct 13, 2023
dc2b431
fix: loro js tree create
Leeeon233 Oct 13, 2023
0333f09
feat: add un exist tree node
Leeeon233 Oct 16, 2023
971c0d7
bench: tree depth
Leeeon233 Oct 16, 2023
09a98d0
fix: check out should emit event
Leeeon233 Oct 16, 2023
3e1ff9c
refactor: event
zxch3n Oct 17, 2023
7eac42f
fix: fuzz err
zxch3n Oct 17, 2023
1d50571
fix: pass all tests
zxch3n Oct 17, 2023
64af52a
fix: fuzz err
zxch3n Oct 17, 2023
b07c3cf
fix: list child cache err
zxch3n Oct 18, 2023
33cf3e7
chore: rm debug code
zxch3n Oct 18, 2023
50ce64d
fix: encode enhanced err
zxch3n Oct 18, 2023
2afcde6
fix: encode enchanced
zxch3n Oct 19, 2023
30d12fa
fix: fix several richtext issue
zxch3n Oct 19, 2023
a57936e
fix: richtext anchor err
zxch3n Oct 19, 2023
116f382
chore: rm debug code
zxch3n Oct 19, 2023
bc4f802
fix: richtext fuzz err
zxch3n Oct 19, 2023
6023c7f
feat: speedup text snapshot decode
zxch3n Oct 20, 2023
638dba9
perf: optimize snapshot encoding
zxch3n Oct 20, 2023
fdf05b0
perf: speed up decode & insert
zxch3n Oct 27, 2023
b0a1ac6
fix: fugue span merge err
zxch3n Oct 27, 2023
fe65bcd
perf: speedup delete & id cursor map
zxch3n Oct 27, 2023
2b95996
fix: fugue merge err
zxch3n Oct 27, 2023
6a968ab
chore: update utils
zxch3n Oct 27, 2023
08f10b9
Merge branch 'feat-richtext' into feat-movable-tree
Leeeon233 Oct 27, 2023
4b5e8ca
fix: fix merge
Leeeon233 Oct 28, 2023
d10360e
fix: return err apply op
Leeeon233 Oct 28, 2023
9097402
Merge branch 'main' into feat-movable-tree
Leeeon233 Oct 29, 2023
d6f3270
fix: fix merge
Leeeon233 Oct 29, 2023
f7f2f65
fix: get map container as tree meta
Leeeon233 Oct 29, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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