Add benchmark utils that simulate drawing workflow (#229)

* chore: add benchmark utils that simulate drawing workflow

* chore: use markdown as table default style

* chore: init sheet simulating
This commit is contained in:
Zixuan Chen 2023-12-28 18:00:34 +08:00 committed by GitHub
parent 6a2d0f8fef
commit 727b5c2518
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 604 additions and 79 deletions

50
Cargo.lock generated
View file

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

View file

@ -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<Point>,
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<DrawAction> {
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
}

View file

@ -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<TextAction> {
actions
}
#[derive(EnumAsInner, Arbitrary)]
pub enum Action {
Text { client: usize, action: TextAction },
#[derive(Debug, EnumAsInner, Arbitrary, PartialEq, Eq)]
pub enum Action<T> {
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<Action> {
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<T>),
) -> Result<Vec<Action<T>>, Box<str>> {
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<T> = 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<T>),
) -> Result<Vec<Action<T>>, Box<str>> {
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<T> = 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<u8> {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let mut ans = vec![0; size];
rng.fill_bytes(&mut ans);
ans
}

View file

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

11
crates/benches/Cargo.toml Normal file
View file

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

View file

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

View file

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

200
crates/benches/src/draw.rs Normal file
View file

@ -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<usize, ContainerID>,
}
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<DrawActor>,
}
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<DrawAction>) {
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::<DrawAction>(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::<DrawAction>(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)
}

View file

@ -0,0 +1,2 @@
pub mod draw;
pub mod sheet;

View file

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

View file

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

View file

@ -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<String> for ContainerIdRaw {
fn from(value: String) -> Self {
ContainerIdRaw::Root { name: value.into() }

View file

@ -669,6 +669,16 @@ impl LoroDoc {
pub(crate) fn weak_state(&self) -> Weak<Mutex<DocState>> {
Arc::downgrade(&self.state)
}
pub fn len_ops(&self) -> usize {
let oplog = self.oplog.lock().unwrap();
oplog.vv().iter().map(|(_, ops)| *ops).sum::<i32>() 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> {

View file

@ -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<LoroValue>) -> LoroResult<()> {
self.handler.push(v.into())
}
#[inline]
pub fn push_container(&self, c_type: ContainerType) -> LoroResult<Container> {
let pos = self.handler.len();
Ok(Container::from(self.handler.insert_container(pos, c_type)?))
}
pub fn for_each<I>(&self, f: I)