mirror of
https://github.com/loro-dev/loro.git
synced 2025-01-22 21:07:43 +00:00
feat: add with fractional index
config (#442)
* feat: add `with_fractional_index` config * fix: share config state
This commit is contained in:
parent
64e5d30da8
commit
cb134fc7c7
12 changed files with 102 additions and 42 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1349,6 +1349,7 @@ dependencies = [
|
||||||
"criterion 0.5.1",
|
"criterion 0.5.1",
|
||||||
"fractional_index",
|
"fractional_index",
|
||||||
"imbl",
|
"imbl",
|
||||||
|
"once_cell",
|
||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
|
|
@ -16,6 +16,7 @@ imbl = "^3.0"
|
||||||
smallvec = { workspace = true }
|
smallvec = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive", "rc"], optional = true }
|
serde = { workspace = true, features = ["derive", "rc"], optional = true }
|
||||||
rand = { version = "^0.8" }
|
rand = { version = "^0.8" }
|
||||||
|
once_cell = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
fraction_index = { version = "^2.0", package = "fractional_index" }
|
fraction_index = { version = "^2.0", package = "fractional_index" }
|
||||||
|
|
|
@ -9,6 +9,8 @@ use serde::{Deserialize, Serialize};
|
||||||
mod jitter;
|
mod jitter;
|
||||||
|
|
||||||
const TERMINATOR: u8 = 128;
|
const TERMINATOR: u8 = 128;
|
||||||
|
static DEFAULT_FRACTIONAL_INDEX: once_cell::sync::Lazy<FractionalIndex> =
|
||||||
|
once_cell::sync::Lazy::new(|| FractionalIndex(Arc::new(vec![TERMINATOR])));
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
@ -16,7 +18,7 @@ pub struct FractionalIndex(Arc<Vec<u8>>);
|
||||||
|
|
||||||
impl Default for FractionalIndex {
|
impl Default for FractionalIndex {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
FractionalIndex(Arc::new(vec![TERMINATOR]))
|
DEFAULT_FRACTIONAL_INDEX.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,8 @@ use super::{
|
||||||
container::MapActor,
|
container::MapActor,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_WITH_FRACTIONAL_INDEX: bool = false;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Undo {
|
pub struct Undo {
|
||||||
pub undo: UndoManager,
|
pub undo: UndoManager,
|
||||||
|
@ -45,6 +47,7 @@ impl Actor {
|
||||||
pub fn new(id: PeerID) -> Self {
|
pub fn new(id: PeerID) -> Self {
|
||||||
let loro = LoroDoc::new();
|
let loro = LoroDoc::new();
|
||||||
loro.set_peer_id(id).unwrap();
|
loro.set_peer_id(id).unwrap();
|
||||||
|
loro.set_with_fractional_index(DEFAULT_WITH_FRACTIONAL_INDEX);
|
||||||
let undo = UndoManager::new(&loro);
|
let undo = UndoManager::new(&loro);
|
||||||
let tracker = Arc::new(Mutex::new(ContainerTracker::Map(MapTracker::empty(
|
let tracker = Arc::new(Mutex::new(ContainerTracker::Map(MapTracker::empty(
|
||||||
ContainerID::new_root("sys:root", ContainerType::Map),
|
ContainerID::new_root("sys:root", ContainerType::Map),
|
||||||
|
|
|
@ -7,7 +7,7 @@ use loro::{Container, ContainerID, ContainerType, LoroDoc, LoroMap, LoroValue};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actions::{Actionable, FromGenericAction, GenericAction},
|
actions::{Actionable, FromGenericAction, GenericAction},
|
||||||
actor::{ActionExecutor, ActorTrait},
|
actor::{assert_value_eq, ActionExecutor, ActorTrait},
|
||||||
crdt_fuzzer::FuzzValue,
|
crdt_fuzzer::FuzzValue,
|
||||||
value::{ApplyDiff, ContainerTracker, MapTracker, Value},
|
value::{ApplyDiff, ContainerTracker, MapTracker, Value},
|
||||||
};
|
};
|
||||||
|
@ -66,7 +66,11 @@ impl ActorTrait for MapActor {
|
||||||
let map = self.loro.get_map("map");
|
let map = self.loro.get_map("map");
|
||||||
let value_a = map.get_deep_value();
|
let value_a = map.get_deep_value();
|
||||||
let value_b = self.tracker.lock().unwrap().to_value();
|
let value_b = self.tracker.lock().unwrap().to_value();
|
||||||
assert_eq!(&value_a, value_b.into_map().unwrap().get("map").unwrap());
|
assert_value_eq(
|
||||||
|
&value_a,
|
||||||
|
value_b.into_map().unwrap().get("map").unwrap(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn container_len(&self) -> u8 {
|
fn container_len(&self) -> u8 {
|
||||||
|
|
|
@ -14,7 +14,7 @@ use tracing::{debug, trace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actions::{Actionable, FromGenericAction, GenericAction},
|
actions::{Actionable, FromGenericAction, GenericAction},
|
||||||
actor::{ActionExecutor, ActorTrait},
|
actor::{assert_value_eq, ActionExecutor, ActorTrait},
|
||||||
crdt_fuzzer::FuzzValue,
|
crdt_fuzzer::FuzzValue,
|
||||||
value::{ApplyDiff, ContainerTracker, MapTracker, Value},
|
value::{ApplyDiff, ContainerTracker, MapTracker, Value},
|
||||||
};
|
};
|
||||||
|
@ -135,7 +135,11 @@ impl ActorTrait for TreeActor {
|
||||||
let tree = loro.get_tree("tree");
|
let tree = loro.get_tree("tree");
|
||||||
let result = tree.get_value_with_meta();
|
let result = tree.get_value_with_meta();
|
||||||
let tracker = self.tracker.lock().unwrap().to_value();
|
let tracker = self.tracker.lock().unwrap().to_value();
|
||||||
assert_eq!(&result, tracker.into_map().unwrap().get("tree").unwrap());
|
assert_value_eq(
|
||||||
|
&result,
|
||||||
|
tracker.into_map().unwrap().get("tree").unwrap(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_new_container(&mut self, container: Container) {
|
fn add_new_container(&mut self, container: Container) {
|
||||||
|
|
|
@ -5,6 +5,9 @@ pub struct Configure {
|
||||||
pub(crate) text_style_config: Arc<RwLock<StyleConfigMap>>,
|
pub(crate) text_style_config: Arc<RwLock<StyleConfigMap>>,
|
||||||
record_timestamp: Arc<AtomicBool>,
|
record_timestamp: Arc<AtomicBool>,
|
||||||
pub(crate) merge_interval: Arc<AtomicI64>,
|
pub(crate) merge_interval: Arc<AtomicI64>,
|
||||||
|
/// Whether the tree has fractional index. `false` by default. If false, the fractional index is always [`FractionalIndex::default`] and
|
||||||
|
/// `tree_position_jitter` is not used.
|
||||||
|
pub(crate) tree_with_fractional_index: Arc<AtomicBool>,
|
||||||
/// do not use `jitter` by default
|
/// do not use `jitter` by default
|
||||||
pub(crate) tree_position_jitter: Arc<AtomicU8>,
|
pub(crate) tree_position_jitter: Arc<AtomicU8>,
|
||||||
}
|
}
|
||||||
|
@ -16,6 +19,7 @@ impl Default for Configure {
|
||||||
record_timestamp: Arc::new(AtomicBool::new(false)),
|
record_timestamp: Arc::new(AtomicBool::new(false)),
|
||||||
merge_interval: Arc::new(AtomicI64::new(1000 * 1000)),
|
merge_interval: Arc::new(AtomicI64::new(1000 * 1000)),
|
||||||
tree_position_jitter: Arc::new(AtomicU8::new(0)),
|
tree_position_jitter: Arc::new(AtomicU8::new(0)),
|
||||||
|
tree_with_fractional_index: Arc::new(AtomicBool::new(false)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,6 +42,10 @@ impl Configure {
|
||||||
self.tree_position_jitter
|
self.tree_position_jitter
|
||||||
.load(std::sync::atomic::Ordering::Relaxed),
|
.load(std::sync::atomic::Ordering::Relaxed),
|
||||||
)),
|
)),
|
||||||
|
tree_with_fractional_index: Arc::new(AtomicBool::new(
|
||||||
|
self.tree_with_fractional_index
|
||||||
|
.load(std::sync::atomic::Ordering::Relaxed),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +63,11 @@ impl Configure {
|
||||||
.store(record, std::sync::atomic::Ordering::Relaxed);
|
.store(record, std::sync::atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_with_fractional_index(&self, with_fractional_index: bool) {
|
||||||
|
self.tree_with_fractional_index
|
||||||
|
.store(with_fractional_index, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_fractional_index_jitter(&self, jitter: u8) {
|
pub fn set_fractional_index_jitter(&self, jitter: u8) {
|
||||||
self.tree_position_jitter
|
self.tree_position_jitter
|
||||||
.store(jitter, std::sync::atomic::Ordering::Relaxed);
|
.store(jitter, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
|
@ -210,7 +210,6 @@ impl HandlerTrait for TreeHandler {
|
||||||
self.inner.attached_handler()
|
self.inner.attached_handler()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO:
|
|
||||||
fn get_value(&self) -> LoroValue {
|
fn get_value(&self) -> LoroValue {
|
||||||
match &self.inner {
|
match &self.inner {
|
||||||
MaybeDetached::Detached(t) => {
|
MaybeDetached::Detached(t) => {
|
||||||
|
|
|
@ -173,6 +173,12 @@ impl LoroDoc {
|
||||||
self.config.set_merge_interval(interval);
|
self.config.set_merge_interval(interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set whether to use fractional index for Tree Position.
|
||||||
|
#[inline]
|
||||||
|
pub fn set_with_fractional_index(&self, with_fractional_index: bool) {
|
||||||
|
self.config.set_with_fractional_index(with_fractional_index);
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the jitter of the tree position(Fractional Index).
|
/// Set the jitter of the tree position(Fractional Index).
|
||||||
///
|
///
|
||||||
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
|
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::{
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
io::Write,
|
io::Write,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicU64, AtomicU8, Ordering},
|
atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering},
|
||||||
Arc, Mutex, RwLock, Weak,
|
Arc, Mutex, RwLock, Weak,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -310,8 +310,18 @@ impl State {
|
||||||
Self::RichtextState(Box::new(RichtextState::new(idx, config)))
|
Self::RichtextState(Box::new(RichtextState::new(idx, config)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_tree(idx: ContainerIdx, peer: PeerID, jitter: Arc<AtomicU8>) -> Self {
|
pub fn new_tree(
|
||||||
Self::TreeState(Box::new(TreeState::new(idx, peer, jitter)))
|
idx: ContainerIdx,
|
||||||
|
peer: PeerID,
|
||||||
|
jitter: Arc<AtomicU8>,
|
||||||
|
with_fractional_index: Arc<AtomicBool>,
|
||||||
|
) -> Self {
|
||||||
|
Self::TreeState(Box::new(TreeState::new(
|
||||||
|
idx,
|
||||||
|
peer,
|
||||||
|
jitter,
|
||||||
|
with_fractional_index,
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_unknown(idx: ContainerIdx) -> Self {
|
pub fn new_unknown(idx: ContainerIdx) -> Self {
|
||||||
|
@ -1476,6 +1486,7 @@ fn create_state_(idx: ContainerIdx, config: &Configure, peer: u64) -> State {
|
||||||
idx,
|
idx,
|
||||||
peer,
|
peer,
|
||||||
config.tree_position_jitter.clone(),
|
config.tree_position_jitter.clone(),
|
||||||
|
config.tree_with_fractional_index.clone(),
|
||||||
))),
|
))),
|
||||||
ContainerType::MovableList => State::MovableListState(Box::new(MovableListState::new(idx))),
|
ContainerType::MovableList => State::MovableListState(Box::new(MovableListState::new(idx))),
|
||||||
#[cfg(feature = "counter")]
|
#[cfg(feature = "counter")]
|
||||||
|
|
|
@ -13,7 +13,7 @@ use serde::Serialize;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::sync::atomic::{AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||||
use std::sync::{Arc, Mutex, Weak};
|
use std::sync::{Arc, Mutex, Weak};
|
||||||
|
|
||||||
use super::{ContainerState, DiffApplyContext};
|
use super::{ContainerState, DiffApplyContext};
|
||||||
|
@ -41,8 +41,10 @@ pub struct TreeState {
|
||||||
idx: ContainerIdx,
|
idx: ContainerIdx,
|
||||||
trees: FxHashMap<TreeID, TreeStateNode>,
|
trees: FxHashMap<TreeID, TreeStateNode>,
|
||||||
children: TreeChildrenCache,
|
children: TreeChildrenCache,
|
||||||
|
/// Whether the tree has fractional index. If false, the fractional index is always [`FractionalIndex::default`]
|
||||||
|
with_fractional_index: Arc<AtomicBool>,
|
||||||
rng: Option<rand::rngs::StdRng>,
|
rng: Option<rand::rngs::StdRng>,
|
||||||
jitter: u8,
|
jitter: Arc<AtomicU8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
@ -597,16 +599,22 @@ impl NodePosition {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TreeState {
|
impl TreeState {
|
||||||
pub fn new(idx: ContainerIdx, peer_id: PeerID, config: Arc<AtomicU8>) -> Self {
|
pub fn new(
|
||||||
let jitter = config.load(Ordering::Relaxed);
|
idx: ContainerIdx,
|
||||||
|
peer_id: PeerID,
|
||||||
|
jitter_config: Arc<AtomicU8>,
|
||||||
|
with_fractional_index: Arc<AtomicBool>,
|
||||||
|
) -> Self {
|
||||||
|
let jitter = jitter_config.load(Ordering::Relaxed);
|
||||||
let use_jitter = jitter != 1;
|
let use_jitter = jitter != 1;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
idx,
|
idx,
|
||||||
trees: FxHashMap::default(),
|
trees: FxHashMap::default(),
|
||||||
children: Default::default(),
|
children: Default::default(),
|
||||||
|
with_fractional_index,
|
||||||
rng: use_jitter.then_some(rand::rngs::StdRng::seed_from_u64(peer_id)),
|
rng: use_jitter.then_some(rand::rngs::StdRng::seed_from_u64(peer_id)),
|
||||||
jitter,
|
jitter: jitter_config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -677,10 +685,6 @@ impl TreeState {
|
||||||
!self.is_node_deleted(&target)
|
!self.is_node_deleted(&target)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contains_internal(&self, target: &TreeID) -> bool {
|
|
||||||
self.trees.contains_key(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the parent of the node, if the node is deleted or does not exist, return None
|
/// Get the parent of the node, if the node is deleted or does not exist, return None
|
||||||
pub fn parent(&self, target: &TreeID) -> TreeParentId {
|
pub fn parent(&self, target: &TreeID) -> TreeParentId {
|
||||||
self.trees
|
self.trees
|
||||||
|
@ -823,11 +827,15 @@ impl TreeState {
|
||||||
parent: &TreeParentId,
|
parent: &TreeParentId,
|
||||||
index: usize,
|
index: usize,
|
||||||
) -> FractionalIndexGenResult {
|
) -> FractionalIndexGenResult {
|
||||||
|
if !self.with_fractional_index.load(Ordering::Relaxed) {
|
||||||
|
return FractionalIndexGenResult::Ok(FractionalIndex::default());
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(rng) = self.rng.as_mut() {
|
if let Some(rng) = self.rng.as_mut() {
|
||||||
self.children
|
self.children
|
||||||
.entry(*parent)
|
.entry(*parent)
|
||||||
.or_default()
|
.or_default()
|
||||||
.generate_fi_at_jitter(index, target, rng, self.jitter)
|
.generate_fi_at_jitter(index, target, rng, self.jitter.load(Ordering::Relaxed))
|
||||||
} else {
|
} else {
|
||||||
self.children
|
self.children
|
||||||
.entry(*parent)
|
.entry(*parent)
|
||||||
|
@ -936,28 +944,26 @@ impl ContainerState for TreeState {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Otherwise, it's a normal move inside deleted nodes, no event is needed
|
// Otherwise, it's a normal move inside deleted nodes, no event is needed
|
||||||
|
} else if was_alive {
|
||||||
|
// normal move
|
||||||
|
ans.push(TreeDiffItem {
|
||||||
|
target,
|
||||||
|
action: TreeExternalDiff::Move {
|
||||||
|
parent: parent.into_node().ok(),
|
||||||
|
index: self.get_index_by_tree_id(&target).unwrap(),
|
||||||
|
position: position.clone(),
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
if was_alive {
|
// create event
|
||||||
// normal move
|
ans.push(TreeDiffItem {
|
||||||
ans.push(TreeDiffItem {
|
target,
|
||||||
target,
|
action: TreeExternalDiff::Create {
|
||||||
action: TreeExternalDiff::Move {
|
parent: parent.into_node().ok(),
|
||||||
parent: parent.into_node().ok(),
|
index: self.get_index_by_tree_id(&target).unwrap(),
|
||||||
index: self.get_index_by_tree_id(&target).unwrap(),
|
position: position.clone(),
|
||||||
position: position.clone(),
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// create event
|
|
||||||
ans.push(TreeDiffItem {
|
|
||||||
target,
|
|
||||||
action: TreeExternalDiff::Create {
|
|
||||||
parent: parent.into_node().ok(),
|
|
||||||
index: self.get_index_by_tree_id(&target).unwrap(),
|
|
||||||
position: position.clone(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1490,8 +1496,12 @@ mod snapshot {
|
||||||
peers.push(PeerID::from_le_bytes(buf));
|
peers.push(PeerID::from_le_bytes(buf));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut tree =
|
let mut tree = TreeState::new(
|
||||||
TreeState::new(idx, ctx.peer, ctx.configure.tree_position_jitter.clone());
|
idx,
|
||||||
|
ctx.peer,
|
||||||
|
ctx.configure.tree_position_jitter.clone(),
|
||||||
|
ctx.configure.tree_with_fractional_index.clone(),
|
||||||
|
);
|
||||||
let encoded: EncodedTree = serde_columnar::from_bytes(bytes)?;
|
let encoded: EncodedTree = serde_columnar::from_bytes(bytes)?;
|
||||||
let fractional_indexes = PositionArena::decode(&encoded.fractional_indexes).unwrap();
|
let fractional_indexes = PositionArena::decode(&encoded.fractional_indexes).unwrap();
|
||||||
let fractional_indexes = fractional_indexes.parse_to_positions();
|
let fractional_indexes = fractional_indexes.parse_to_positions();
|
||||||
|
|
|
@ -155,6 +155,12 @@ impl LoroDoc {
|
||||||
self.doc.set_change_merge_interval(interval);
|
self.doc.set_change_merge_interval(interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set whether to use fractional index for Tree Position.
|
||||||
|
#[inline]
|
||||||
|
pub fn set_with_fractional_index(&self, with_fractional_index: bool) {
|
||||||
|
self.doc.set_with_fractional_index(with_fractional_index);
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the jitter of the tree position(Fractional Index).
|
/// Set the jitter of the tree position(Fractional Index).
|
||||||
///
|
///
|
||||||
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
|
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
|
||||||
|
|
Loading…
Reference in a new issue