feat: add with fractional index config (#442)

* feat: add `with_fractional_index` config

* fix: share config state
This commit is contained in:
Leon Zhao 2024-09-05 16:03:47 +08:00 committed by GitHub
parent 64e5d30da8
commit cb134fc7c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 102 additions and 42 deletions

1
Cargo.lock generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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