diff --git a/Cargo.lock b/Cargo.lock index d00e1c39..cc6380e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "benches" +version = "0.1.0" +dependencies = [ + "bench-utils", + "loro", + "tabled 0.15.0", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -798,7 +807,7 @@ dependencies = [ "smallvec", "static_assertions", "string_cache", - "tabled", + "tabled 0.10.0", "thiserror", "wasm-bindgen", ] @@ -1035,6 +1044,17 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "papergrid" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ad43c07024ef767f9160710b3a6773976194758c7919b17e63b863db0bdf7fb" +dependencies = [ + "bytecount", + "fnv", + "unicode-width", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -1645,8 +1665,19 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56c3ee73732ffceaea7b8f6b719ce3bb17f253fa27461ffeaf568ebd0cdb4b85" dependencies = [ - "papergrid", - "tabled_derive", + "papergrid 0.7.1", + "tabled_derive 0.5.0", + "unicode-width", +] + +[[package]] +name = "tabled" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c998b0c8b921495196a48aabaf1901ff28be0760136e31604f7967b0792050e" +dependencies = [ + "papergrid 0.11.0", + "tabled_derive 0.7.0", "unicode-width", ] @@ -1663,6 +1694,19 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "tabled_derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c138f99377e5d653a371cdad263615634cfc8467685dfe8e73e2b8e98f44b17" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2 1.0.67", + "quote 1.0.29", + "syn 1.0.107", +] + [[package]] name = "target-lexicon" version = "0.12.5" diff --git a/crates/bench-utils/src/draw.rs b/crates/bench-utils/src/draw.rs index f446a0e4..d1e34545 100644 --- a/crates/bench-utils/src/draw.rs +++ b/crates/bench-utils/src/draw.rs @@ -1,33 +1,27 @@ -use arbitrary::{Arbitrary, Unstructured}; +use arbitrary::Arbitrary; -#[derive(Arbitrary)] +#[derive(Debug, Arbitrary, PartialEq, Eq)] pub struct Point { pub x: i32, pub y: i32, } -#[derive(Arbitrary)] +#[derive(Debug, Arbitrary, PartialEq, Eq)] pub enum DrawAction { - DrawPath { + CreatePath { points: Vec, - color: i32, }, Text { - id: i32, text: String, pos: Point, - width: i32, - height: i32, + size: Point, + }, + CreateRect { + pos: Point, + size: Point, + }, + Move { + id: u32, + relative_to: Point, }, } - -pub fn gen_draw_actions(seed: u64, num: usize) -> Vec { - let be_bytes = seed.to_be_bytes(); - let mut gen = Unstructured::new(&be_bytes); - let mut ans = vec![]; - for _ in 0..num { - ans.push(gen.arbitrary().unwrap()); - } - - ans -} diff --git a/crates/bench-utils/src/lib.rs b/crates/bench-utils/src/lib.rs index b5955210..f006e88d 100644 --- a/crates/bench-utils/src/lib.rs +++ b/crates/bench-utils/src/lib.rs @@ -1,7 +1,8 @@ pub mod draw; -use arbitrary::Arbitrary; +pub mod sheet; +use arbitrary::{Arbitrary, Unstructured}; use enum_as_inner::EnumAsInner; -use rand::{rngs::StdRng, RngCore, SeedableRng}; +use rand::{RngCore, SeedableRng}; use std::io::Read; use flate2::read::GzDecoder; @@ -45,19 +46,20 @@ pub fn get_automerge_actions() -> Vec { actions } -#[derive(EnumAsInner, Arbitrary)] -pub enum Action { - Text { client: usize, action: TextAction }, +#[derive(Debug, EnumAsInner, Arbitrary, PartialEq, Eq)] +pub enum Action { + Action { peer: usize, action: T }, + Sync { from: usize, to: usize }, SyncAll, } -pub fn gen_realtime_actions(action_num: usize, client_num: usize, seed: u64) -> Vec { - let mut gen = StdRng::seed_from_u64(seed); - let size = Action::size_hint(1); - let size = size.1.unwrap_or(size.0); - let mut dest = vec![0; action_num * size]; - gen.fill_bytes(&mut dest); - let mut arb = arbitrary::Unstructured::new(&dest); +pub fn gen_realtime_actions<'a, T: Arbitrary<'a>>( + action_num: usize, + peer_num: usize, + seed: &'a [u8], + mut preprocess: impl FnMut(&mut Action), +) -> Result>, Box> { + let mut arb = Unstructured::new(seed); let mut ans = Vec::new(); let mut last_sync_all = 0; for i in 0..action_num { @@ -65,25 +67,82 @@ pub fn gen_realtime_actions(action_num: usize, client_num: usize, seed: u64) -> break; } - let mut action = arb.arbitrary().unwrap(); + let mut action: Action = arb + .arbitrary() + .map_err(|e| e.to_string().into_boxed_str())?; match &mut action { - Action::Text { client, action } => { - *client %= client_num; - if !action.ins.is_empty() { - action.ins = (action.ins.as_bytes()[0]).to_string(); - } + Action::Action { peer, .. } => { + *peer %= peer_num; } Action::SyncAll => { last_sync_all = i; } + Action::Sync { from, to } => { + *from %= peer_num; + *to %= peer_num; + } } + preprocess(&mut action); ans.push(action); - if i - last_sync_all > 100 { + if i - last_sync_all > 10 { ans.push(Action::SyncAll); last_sync_all = i; } } + Ok(ans) +} + +pub fn gen_async_actions<'a, T: Arbitrary<'a>>( + action_num: usize, + peer_num: usize, + seed: &'a [u8], + actions_before_sync: usize, + mut preprocess: impl FnMut(&mut Action), +) -> Result>, Box> { + let mut arb = Unstructured::new(seed); + let mut ans = Vec::new(); + let mut last_sync_all = 0; + while ans.len() < action_num { + if ans.len() >= action_num { + break; + } + + if arb.is_empty() { + return Err("not enough actions".into()); + } + + let mut action: Action = arb + .arbitrary() + .map_err(|e| e.to_string().into_boxed_str())?; + match &mut action { + Action::Action { peer, .. } => { + *peer %= peer_num; + } + Action::SyncAll => { + if ans.len() - last_sync_all < actions_before_sync { + continue; + } + + last_sync_all = ans.len(); + } + Action::Sync { from, to } => { + *from %= peer_num; + *to %= peer_num; + } + } + + preprocess(&mut action); + ans.push(action); + } + + Ok(ans) +} + +pub fn create_seed(seed: u64, size: usize) -> Vec { + let mut rng = rand::rngs::StdRng::seed_from_u64(seed); + let mut ans = vec![0; size]; + rng.fill_bytes(&mut ans); ans } diff --git a/crates/bench-utils/src/sheet.rs b/crates/bench-utils/src/sheet.rs new file mode 100644 index 00000000..e5c8810b --- /dev/null +++ b/crates/bench-utils/src/sheet.rs @@ -0,0 +1,37 @@ +use arbitrary::Arbitrary; + +#[derive(Debug, Arbitrary, PartialEq, Eq)] +pub enum SheetAction { + SetValue { + row: usize, + col: usize, + value: usize, + }, + InsertRow { + row: usize, + }, + InsertCol { + col: usize, + }, +} + +impl SheetAction { + pub const MAX_ROW: usize = 1_048_576; + pub const MAX_COL: usize = 16_384; + /// Excel has a limit of 1,048,576 rows and 16,384 columns per sheet. + // We need to normalize the action to fit the limit. + pub fn normalize(&mut self) { + match self { + SheetAction::SetValue { row, col, .. } => { + *row %= Self::MAX_ROW; + *col %= Self::MAX_COL; + } + SheetAction::InsertRow { row } => { + *row %= Self::MAX_ROW; + } + SheetAction::InsertCol { col } => { + *col %= Self::MAX_COL; + } + } + } +} diff --git a/crates/benches/Cargo.toml b/crates/benches/Cargo.toml new file mode 100644 index 00000000..77fced6f --- /dev/null +++ b/crates/benches/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "benches" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bench-utils = { path = "../bench-utils" } +loro = { path = "../loro" } +tabled = "0.15.0" diff --git a/crates/benches/examples/draw.rs b/crates/benches/examples/draw.rs new file mode 100644 index 00000000..391fbaca --- /dev/null +++ b/crates/benches/examples/draw.rs @@ -0,0 +1,136 @@ +use std::time::Instant; + +use benches::draw::{run_async_draw_workflow, run_realtime_collab_draw_workflow}; +use loro::LoroDoc; +use tabled::{settings::Style, Table, Tabled}; + +#[derive(Tabled)] +struct BenchResult { + task: &'static str, + action_size: usize, + peer_num: usize, + ops_num: usize, + changes_num: usize, + snapshot_size: usize, + updates_size: usize, + apply_duration: f64, + encode_snapshot_duration: f64, + encode_udpate_duration: f64, + decode_snapshot_duration: f64, + decode_update_duration: f64, +} + +pub fn main() { + let seed = 123123; + let ans = vec![ + run_async(1, 100, seed), + run_async(1, 1000, seed), + run_async(1, 10000, seed), + run_async(5, 100, seed), + run_async(5, 1000, seed), + run_async(5, 10000, seed), + run_async(10, 1000, seed), + run_async(10, 10000, seed), + run_async(10, 100000, seed), + run_async(10, 100000, 1000), + run_realtime_collab(5, 100, seed), + run_realtime_collab(5, 1000, seed), + run_realtime_collab(5, 10000, seed), + run_realtime_collab(10, 1000, seed), + run_realtime_collab(10, 10000, seed), + run_realtime_collab(10, 100000, seed), + run_realtime_collab(10, 100000, 1000), + ]; + let mut table = Table::new(ans); + let style = Style::markdown(); + table.with(style); + println!("{}", table); +} + +fn run_async(peer_num: usize, action_num: usize, seed: u64) -> BenchResult { + eprintln!( + "run_async(peer_num: {}, action_num: {})", + peer_num, action_num + ); + let (mut actors, start) = run_async_draw_workflow(peer_num, action_num, 200, seed); + actors.sync_all(); + let apply_duration = start.elapsed().as_secs_f64() * 1000.; + + let start = Instant::now(); + let snapshot = actors.docs[0].doc.export_snapshot(); + let encode_snapshot_duration = start.elapsed().as_secs_f64() * 1000.; + let snapshot_size = snapshot.len(); + + let start = Instant::now(); + let updates = actors.docs[0].doc.export_from(&Default::default()); + let encode_udpate_duration = start.elapsed().as_secs_f64() * 1000.; + let updates_size = updates.len(); + + let start = Instant::now(); + let doc = LoroDoc::new(); + doc.import(&snapshot).unwrap(); + let decode_snapshot_duration = start.elapsed().as_secs_f64() * 1000.; + + let start = Instant::now(); + doc.import(&updates).unwrap(); + let decode_update_duration = start.elapsed().as_secs_f64() * 1000.; + + BenchResult { + task: "async draw", + action_size: action_num, + peer_num, + snapshot_size, + ops_num: actors.docs[0].doc.len_ops(), + changes_num: actors.docs[0].doc.len_changes(), + updates_size, + apply_duration, + encode_snapshot_duration, + encode_udpate_duration, + decode_snapshot_duration, + decode_update_duration, + } +} + +fn run_realtime_collab(peer_num: usize, action_num: usize, seed: u64) -> BenchResult { + eprintln!( + "run_realtime_collab(peer_num: {}, action_num: {})", + peer_num, action_num + ); + let (mut actors, start) = run_realtime_collab_draw_workflow(peer_num, action_num, seed); + actors.sync_all(); + let apply_duration = start.elapsed().as_secs_f64() * 1000.; + + let start = Instant::now(); + let snapshot = actors.docs[0].doc.export_snapshot(); + let encode_snapshot_duration = start.elapsed().as_secs_f64() * 1000.; + let snapshot_size = snapshot.len(); + + let start = Instant::now(); + let updates = actors.docs[0].doc.export_from(&Default::default()); + let encode_udpate_duration = start.elapsed().as_secs_f64() * 1000.; + let updates_size = updates.len(); + + let start = Instant::now(); + let doc = LoroDoc::new(); + doc.import(&snapshot).unwrap(); + let decode_snapshot_duration = start.elapsed().as_secs_f64() * 1000.; + + let start = Instant::now(); + doc.import(&updates).unwrap(); + let decode_update_duration = start.elapsed().as_secs_f64() * 1000.; + + BenchResult { + task: "realtime draw", + action_size: action_num, + peer_num, + ops_num: actors.docs[0].doc.len_ops(), + changes_num: actors.docs[0].doc.len_changes(), + snapshot_size, + updates_size, + apply_duration, + encode_snapshot_duration, + encode_udpate_duration, + decode_snapshot_duration, + decode_update_duration, + } +} diff --git a/crates/benches/examples/init_sheet.rs b/crates/benches/examples/init_sheet.rs new file mode 100644 index 00000000..16f1a158 --- /dev/null +++ b/crates/benches/examples/init_sheet.rs @@ -0,0 +1,14 @@ +use benches::sheet::init_sheet; +use std::time::Instant; + +pub fn main() { + let start = Instant::now(); + let doc = init_sheet(); + let init_duration = start.elapsed().as_secs_f64() * 1000.; + println!("init_duration {}", init_duration); + + let start = Instant::now(); + let snapshot = doc.export_snapshot(); + let duration = start.elapsed().as_secs_f64() * 1000.; + println!("export duration {} size={}", duration, snapshot.len()); +} diff --git a/crates/benches/src/draw.rs b/crates/benches/src/draw.rs new file mode 100644 index 00000000..b42b47ab --- /dev/null +++ b/crates/benches/src/draw.rs @@ -0,0 +1,200 @@ +use std::{collections::HashMap, time::Instant}; + +use bench_utils::{create_seed, draw::DrawAction, gen_async_actions, gen_realtime_actions, Action}; +use loro::{ContainerID, ContainerType}; + +pub struct DrawActor { + pub doc: loro::LoroDoc, + paths: loro::LoroList, + texts: loro::LoroList, + rects: loro::LoroList, + id_to_obj: HashMap, +} + +impl DrawActor { + pub fn new(id: u64) -> Self { + let doc = loro::LoroDoc::new(); + doc.set_peer_id(id).unwrap(); + let paths = doc.get_list("all_paths"); + let texts = doc.get_list("all_texts"); + let rects = doc.get_list("all_rects"); + let id_to_obj = HashMap::new(); + Self { + doc, + paths, + texts, + rects, + id_to_obj, + } + } + + pub fn apply_action(&mut self, action: &mut DrawAction) { + match action { + DrawAction::CreatePath { points } => { + let path = self.paths.insert_container(0, ContainerType::Map).unwrap(); + let path_map = path.into_map().unwrap(); + let pos_map = path_map + .insert_container("pos", ContainerType::Map) + .unwrap() + .into_map() + .unwrap(); + pos_map.insert("x", 0).unwrap(); + pos_map.insert("y", 0).unwrap(); + let path = path_map + .insert_container("path", ContainerType::List) + .unwrap() + .into_list() + .unwrap(); + for p in points { + let map = path + .push_container(ContainerType::Map) + .unwrap() + .into_map() + .unwrap(); + map.insert("x", p.x).unwrap(); + map.insert("y", p.y).unwrap(); + } + let len = self.id_to_obj.len(); + self.id_to_obj.insert(len, path.id()); + } + DrawAction::Text { text, pos, size } => { + let text_container = self + .texts + .insert_container(0, ContainerType::Map) + .unwrap() + .into_map() + .unwrap(); + let text_inner = text_container + .insert_container("text", ContainerType::Text) + .unwrap() + .into_text() + .unwrap(); + text_inner.insert(0, text).unwrap(); + let map = text_container + .insert_container("pos", ContainerType::Map) + .unwrap() + .into_map() + .unwrap(); + map.insert("x", pos.x).unwrap(); + map.insert("y", pos.y).unwrap(); + let map = text_container + .insert_container("size", ContainerType::Map) + .unwrap() + .into_map() + .unwrap(); + map.insert("x", size.x).unwrap(); + map.insert("y", size.y).unwrap(); + + let len = self.id_to_obj.len(); + self.id_to_obj.insert(len, text_container.id()); + } + DrawAction::CreateRect { pos, .. } => { + let rect = self.rects.insert_container(0, ContainerType::Map).unwrap(); + let rect_map = rect.into_map().unwrap(); + let pos_map = rect_map + .insert_container("pos", ContainerType::Map) + .unwrap() + .into_map() + .unwrap(); + pos_map.insert("x", pos.x).unwrap(); + pos_map.insert("y", pos.y).unwrap(); + + let size_map = rect_map + .insert_container("size", ContainerType::Map) + .unwrap() + .into_map() + .unwrap(); + size_map.insert("width", pos.x).unwrap(); + size_map.insert("height", pos.y).unwrap(); + + let len = self.id_to_obj.len(); + self.id_to_obj.insert(len, rect_map.id()); + } + DrawAction::Move { id, relative_to } => { + let Some(id) = self.id_to_obj.get(&(*id as usize)) else { + return; + }; + + let map = self.doc.get_map(id); + let pos_map = map.get("pos").unwrap().unwrap_right().into_map().unwrap(); + let x = pos_map.get("x").unwrap().unwrap_left().into_i32().unwrap(); + let y = pos_map.get("y").unwrap().unwrap_left().into_i32().unwrap(); + pos_map.insert("x", x + relative_to.x).unwrap(); + pos_map.insert("y", y + relative_to.y).unwrap(); + } + } + } +} + +pub struct DrawActors { + pub docs: Vec, +} + +impl DrawActors { + pub fn new(size: usize) -> Self { + let docs = (0..size).map(|i| DrawActor::new(i as u64)).collect(); + Self { docs } + } + + pub fn apply_action(&mut self, action: &mut Action) { + match action { + Action::Action { peer, action } => { + self.docs[*peer].apply_action(action); + } + Action::Sync { from, to } => { + let vv = self.docs[*from].doc.oplog_vv(); + let data = self.docs[*from].doc.export_from(&vv); + self.docs[*to].doc.import(&data).unwrap(); + } + Action::SyncAll => self.sync_all(), + } + } + + pub fn sync_all(&mut self) { + let (first, rest) = self.docs.split_at_mut(1); + for doc in rest.iter_mut() { + let vv = first[0].doc.oplog_vv(); + first[0].doc.import(&doc.doc.export_from(&vv)).unwrap(); + } + for doc in rest.iter_mut() { + let vv = doc.doc.oplog_vv(); + doc.doc.import(&first[0].doc.export_from(&vv)).unwrap(); + } + } +} + +pub fn run_async_draw_workflow( + peer_num: usize, + action_num: usize, + actions_before_sync: usize, + seed: u64, +) -> (DrawActors, Instant) { + let seed = create_seed(seed, action_num * 32); + let mut actions = + gen_async_actions::(action_num, peer_num, &seed, actions_before_sync, |_| {}) + .unwrap(); + let mut actors = DrawActors::new(peer_num); + let start = Instant::now(); + for action in actions.iter_mut() { + actors.apply_action(action); + } + + (actors, start) +} + +pub fn run_realtime_collab_draw_workflow( + peer_num: usize, + action_num: usize, + seed: u64, +) -> (DrawActors, Instant) { + let seed = create_seed(seed, action_num * 32); + let mut actions = + gen_realtime_actions::(action_num, peer_num, &seed, |_| {}).unwrap(); + let mut actors = DrawActors::new(peer_num); + let start = Instant::now(); + for action in actions.iter_mut() { + actors.apply_action(action); + } + + (actors, start) +} diff --git a/crates/benches/src/lib.rs b/crates/benches/src/lib.rs new file mode 100644 index 00000000..196b7f0e --- /dev/null +++ b/crates/benches/src/lib.rs @@ -0,0 +1,2 @@ +pub mod draw; +pub mod sheet; diff --git a/crates/benches/src/sheet.rs b/crates/benches/src/sheet.rs new file mode 100644 index 00000000..e3b9cd5f --- /dev/null +++ b/crates/benches/src/sheet.rs @@ -0,0 +1,25 @@ +use loro::{LoroDoc, LoroList, LoroMap}; + +pub struct Actor { + pub doc: LoroDoc, + cols: LoroList, + rows: LoroList, +} + +impl Actor {} + +pub fn init_sheet() -> LoroDoc { + let doc = LoroDoc::new(); + doc.set_peer_id(0).unwrap(); + let cols = doc.get_list("cols"); + let rows = doc.get_list("rows"); + for i in 0..bench_utils::sheet::SheetAction::MAX_ROW { + rows.push_container(loro::ContainerType::Map).unwrap(); + } + + for i in 0..bench_utils::sheet::SheetAction::MAX_COL { + cols.push(i as i32).unwrap(); + } + + doc +} diff --git a/crates/loro-internal/benches/draw.rs b/crates/loro-internal/benches/draw.rs deleted file mode 100644 index 48ffaa2b..00000000 --- a/crates/loro-internal/benches/draw.rs +++ /dev/null @@ -1,36 +0,0 @@ -use bench_utils::draw::{gen_draw_actions, DrawAction}; -use criterion::{criterion_group, criterion_main, Criterion}; -use loro_internal::LoroDoc; - -pub fn draw(c: &mut Criterion) { - let mut data = None; - c.bench_function("simulate drawing", |b| { - if data.is_none() { - data = Some(gen_draw_actions(100, 1000)); - } - - let mut loro = LoroDoc::new(); - b.iter(|| { - loro = LoroDoc::new(); - let _paths = loro.get_list("all_paths"); - let _texts = loro.get_list("all_texts"); - for action in data.as_ref().unwrap().iter() { - match action { - DrawAction::DrawPath { points: _, color: _ } => {} - DrawAction::Text { - id: _, - text: _, - pos: _, - width: _, - height: _, - } => todo!(), - } - } - }); - - println!("Snapshot size = {}", loro.export_snapshot().len()) - }); -} - -criterion_group!(benches, draw); -criterion_main!(benches); diff --git a/crates/loro-internal/src/container.rs b/crates/loro-internal/src/container.rs index 16312766..efe75cf8 100644 --- a/crates/loro-internal/src/container.rs +++ b/crates/loro-internal/src/container.rs @@ -109,6 +109,12 @@ impl IntoContainerId for ContainerID { } } +impl IntoContainerId for &ContainerID { + fn into_container_id(self, _arena: &SharedArena, _kind: ContainerType) -> ContainerID { + self.clone() + } +} + impl IntoContainerId for ContainerIdx { fn into_container_id(self, arena: &SharedArena, kind: ContainerType) -> ContainerID { assert_eq!(self.get_type(), kind); @@ -116,6 +122,13 @@ impl IntoContainerId for ContainerIdx { } } +impl IntoContainerId for &ContainerIdx { + fn into_container_id(self, arena: &SharedArena, kind: ContainerType) -> ContainerID { + assert_eq!(self.get_type(), kind); + arena.get_container_id(*self).unwrap() + } +} + impl From for ContainerIdRaw { fn from(value: String) -> Self { ContainerIdRaw::Root { name: value.into() } diff --git a/crates/loro-internal/src/loro.rs b/crates/loro-internal/src/loro.rs index 4ad23a77..4fceb133 100644 --- a/crates/loro-internal/src/loro.rs +++ b/crates/loro-internal/src/loro.rs @@ -669,6 +669,16 @@ impl LoroDoc { pub(crate) fn weak_state(&self) -> Weak> { Arc::downgrade(&self.state) } + + pub fn len_ops(&self) -> usize { + let oplog = self.oplog.lock().unwrap(); + oplog.vv().iter().map(|(_, ops)| *ops).sum::() as usize + } + + pub fn len_changes(&self) -> usize { + let oplog = self.oplog.lock().unwrap(); + oplog.len_changes() + } } fn parse_encode_header(bytes: &[u8]) -> Result<(&[u8], EncodeMode), LoroError> { diff --git a/crates/loro/src/lib.rs b/crates/loro/src/lib.rs index 78024444..2203793a 100644 --- a/crates/loro/src/lib.rs +++ b/crates/loro/src/lib.rs @@ -186,6 +186,16 @@ impl LoroDoc { self.doc.state_vv() } + /// Get the total number of operations in the `OpLog` + pub fn len_ops(&self) -> usize { + self.doc.len_ops() + } + + /// Get the total number of changes in the `OpLog` + pub fn len_changes(&self) -> usize { + self.doc.len_changes() + } + pub fn get_deep_value(&self) -> LoroValue { self.doc.get_deep_value() } @@ -320,8 +330,14 @@ impl LoroList { } #[inline] - pub fn push(&self, v: LoroValue) -> LoroResult<()> { - self.handler.push(v) + pub fn push(&self, v: impl Into) -> LoroResult<()> { + self.handler.push(v.into()) + } + + #[inline] + pub fn push_container(&self, c_type: ContainerType) -> LoroResult { + let pos = self.handler.len(); + Ok(Container::from(self.handler.insert_container(pos, c_type)?)) } pub fn for_each(&self, f: I)