use loro::{
event::{Diff, DiffBatch, ListDiffItem},
ContainerID, ContainerType, LoroDoc, LoroText, LoroValue, UndoManager, ValueOrContainer,
};
use std::time::{Duration, Instant};
const N: usize = 100_000;
fn median(mut v: Vec<Duration>) -> f64 {
v.sort();
v[v.len() / 2].as_secs_f64() * 1000.0
}
fn bench<S>(name: &str, setup: impl Fn() -> S, run: impl Fn(S)) {
let times = (0..7)
.map(|_| {
let s = setup();
let start = Instant::now();
run(s);
start.elapsed()
})
.collect();
println!("PERF {name}: median {:.2} ms", median(times));
}
fn doc_with_unknown_element() -> LoroDoc {
let src = LoroDoc::new();
src.set_peer_id(1).unwrap();
let list = src.get_movable_list("ml");
for i in 0..N {
list.push(i as i64).unwrap();
}
let text = list.insert_container(N / 2, LoroText::new()).unwrap();
src.commit();
let cid = loro::ContainerTrait::id(&text).to_string();
let json = serde_json::to_string(
&src.export_json_updates_without_peer_compression(&Default::default(), &src.oplog_vv())
.unwrap(),
)
.unwrap()
.replace(&cid, &cid.replace(":Text", ":Unknown(9)"));
let doc = LoroDoc::new();
doc.set_peer_id(2).unwrap();
doc.import_json_updates(json.as_str()).unwrap();
doc
}
#[test]
#[ignore]
fn perf_unknown_element_move() {
let base = doc_with_unknown_element();
let moved = || {
let doc = base.fork();
let undo = UndoManager::new(&doc);
let v0 = doc.state_frontiers();
doc.get_movable_list("ml").mov(N / 2, 0).unwrap();
doc.commit();
let v1 = doc.state_frontiers();
(doc, undo, v0, v1)
};
bench(
"unknown elem move in 100k movable list: apply_diff(diff(v1, v0))",
|| {
let (doc, _, v0, v1) = moved();
let diff = doc.diff(&v1, &v0).unwrap();
(doc, diff)
},
|(doc, diff)| doc.apply_diff(diff).unwrap(),
);
bench(
"unknown elem move in 100k movable list: revert_to(v0)",
moved,
|(doc, _, v0, _)| doc.revert_to(&v0).unwrap(),
);
bench(
"unknown elem move in 100k movable list: undo",
moved,
|(_, mut undo, _, _)| assert!(undo.undo().unwrap()),
);
}
#[test]
#[ignore]
fn perf_plain_diffs() {
let list_doc = {
let doc = LoroDoc::new();
let list = doc.get_list("l");
for i in 0..N {
list.push(i as i64).unwrap();
}
doc.commit();
doc
};
bench(
"apply_diff list 100k ints -> fresh doc",
|| {
(
LoroDoc::new(),
list_doc
.diff(&Default::default(), &list_doc.state_frontiers())
.unwrap(),
)
},
|(doc, diff)| doc.apply_diff(diff).unwrap(),
);
let map_doc = {
let doc = LoroDoc::new();
let map = doc.get_map("m");
for i in 0..N {
map.insert(&i.to_string(), i as i64).unwrap();
}
doc.commit();
doc
};
bench(
"apply_diff map 100k keys -> fresh doc",
|| {
(
LoroDoc::new(),
map_doc
.diff(&Default::default(), &map_doc.state_frontiers())
.unwrap(),
)
},
|(doc, diff)| doc.apply_diff(diff).unwrap(),
);
let children_doc = {
let doc = LoroDoc::new();
let list = doc.get_list("l");
for i in 0..N / 10 {
list.push_container(LoroText::new())
.unwrap()
.insert(0, &i.to_string())
.unwrap();
}
doc.commit();
doc
};
bench(
"apply_diff list 10k child containers -> fresh doc",
|| {
(
LoroDoc::new(),
children_doc
.diff(&Default::default(), &children_doc.state_frontiers())
.unwrap(),
)
},
|(doc, diff)| doc.apply_diff(diff).unwrap(),
);
bench(
"revert_to empty from 100k list",
|| list_doc.fork(),
|doc| doc.revert_to(&Default::default()).unwrap(),
);
bench(
"undo 100k list insert",
|| {
let doc = LoroDoc::new();
let undo = UndoManager::new(&doc);
let list = doc.get_list("l");
for i in 0..N {
list.push(i as i64).unwrap();
}
doc.commit();
(doc, undo)
},
|(_doc, mut undo)| assert!(undo.undo().unwrap()),
);
let ml = ContainerID::new_root("ml", ContainerType::MovableList);
bench(
"apply_diff 100k plain values into a movable list",
|| {
let mut batch = DiffBatch::default();
batch
.push(
ml.clone(),
Diff::List(vec![ListDiffItem::Insert {
insert: (0..N)
.map(|i| ValueOrContainer::Value(LoroValue::from(i as i64)))
.collect(),
is_move: false,
}]),
)
.unwrap();
(LoroDoc::new(), batch)
},
|(doc, batch)| doc.apply_diff(batch).unwrap(),
);
}
fn doc_with_mergeable_list(unknown: bool) -> LoroDoc {
let src = LoroDoc::new();
src.set_peer_id(1).unwrap();
let list = src.get_map("m").ensure_mergeable_list("s").unwrap();
for i in 0..N {
list.push(i as i64).unwrap();
}
if unknown {
list.insert_container(N / 2, LoroText::new()).unwrap();
}
src.commit();
let mut json = src
.export_json_updates_without_peer_compression(&Default::default(), &src.oplog_vv())
.unwrap();
for change in json.changes.iter_mut() {
for op in change.ops.iter_mut() {
if let loro::JsonOpContent::List(loro::JsonListOp::Insert { value, .. }) =
&mut op.content
{
for v in value.iter_mut() {
if let LoroValue::Container(ContainerID::Normal { peer, counter, .. }) = v {
*v = LoroValue::Container(ContainerID::new_normal(
loro::ID::new(*peer, *counter),
ContainerType::Unknown(9),
));
}
}
}
}
}
let doc = LoroDoc::new();
doc.set_peer_id(2).unwrap();
doc.import_json_updates(json).unwrap();
doc
}
#[test]
#[ignore]
fn perf_full_state_revival() {
for unknown in [false, true] {
let base = doc_with_mergeable_list(unknown);
bench(
&format!("full-state revive 100k mergeable list (unknown element: {unknown})"),
|| {
let doc = base.fork();
let target = doc.state_frontiers();
doc.get_map("m").delete("s").unwrap();
doc.commit();
let diff = doc.diff(&doc.state_frontiers(), &target).unwrap();
(doc, diff)
},
|(doc, diff)| doc.apply_diff(diff).unwrap(),
);
}
}
fn forge_texts(src: &LoroDoc) -> LoroDoc {
let mut json = src
.export_json_updates_without_peer_compression(&Default::default(), &src.oplog_vv())
.unwrap();
let forge = |v: &mut LoroValue| {
if let LoroValue::Container(ContainerID::Normal {
peer,
counter,
container_type: ContainerType::Text,
}) = v
{
*v = LoroValue::Container(ContainerID::new_normal(
loro::ID::new(*peer, *counter),
ContainerType::Unknown(9),
));
}
};
for change in json.changes.iter_mut() {
for op in change.ops.iter_mut() {
match &mut op.content {
loro::JsonOpContent::List(loro::JsonListOp::Insert { value, .. }) => {
value.iter_mut().for_each(forge)
}
loro::JsonOpContent::Map(loro::JsonMapOp::Insert { value, .. }) => forge(value),
_ => {}
}
}
}
let doc = LoroDoc::new();
doc.set_peer_id(2).unwrap();
doc.import_json_updates(json).unwrap();
doc
}
#[test]
#[ignore]
fn perf_many_full_state_targets() {
for unknown in [false, true] {
let src = LoroDoc::new();
src.set_peer_id(1).unwrap();
let m = src.get_map("m");
for i in 0..2000 {
let s = m.ensure_mergeable_map(&format!("s{i}")).unwrap();
for k in 0..50 {
s.insert(&format!("k{k}"), k as i64).unwrap();
}
if unknown {
s.insert_container("u", LoroText::new()).unwrap();
}
}
src.commit();
let base = forge_texts(&src);
let hide_all = |doc: &LoroDoc| {
let m = doc.get_map("m");
for i in 0..2000 {
m.delete(&format!("s{i}")).unwrap();
}
doc.commit();
};
bench(
&format!("full-state revive 2000 mergeable maps x50 keys (unknown: {unknown})"),
|| {
let doc = base.fork();
let target = doc.state_frontiers();
hide_all(&doc);
let diff = doc.diff(&doc.state_frontiers(), &target).unwrap();
(doc, diff)
},
|(doc, diff)| doc.apply_diff(diff).unwrap(),
);
bench(
&format!("undo of hiding 2000 mergeable maps (unknown: {unknown})"),
|| {
let doc = base.fork();
let undo = UndoManager::new(&doc);
hide_all(&doc);
(doc, undo)
},
|(_doc, mut undo)| assert!(undo.undo().unwrap()),
);
}
for unknown in [false, true] {
let src = LoroDoc::new();
src.set_peer_id(1).unwrap();
let s = src.get_map("m").ensure_mergeable_map("s").unwrap();
for k in 0..100_000 {
s.insert(&format!("k{k}"), k as i64).unwrap();
}
if unknown {
s.insert_container("u", LoroText::new()).unwrap();
}
src.commit();
let base = forge_texts(&src);
bench(
&format!("full-state revive mergeable map 100k keys (unknown: {unknown})"),
|| {
let doc = base.fork();
let target = doc.state_frontiers();
doc.get_map("m").delete("s").unwrap();
doc.commit();
let diff = doc.diff(&doc.state_frontiers(), &target).unwrap();
(doc, diff)
},
|(doc, diff)| doc.apply_diff(diff).unwrap(),
);
}
}