Skip to content
Open
Changes from 1 commit
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
970e9b7
perf: improve text checkout scalability
zxch3n Apr 22, 2026
e4a9181
docs: add text checkout performance plan
zxch3n Apr 22, 2026
ed54924
docs: remove temporary text checkout plan
zxch3n Apr 22, 2026
377afd4
perf: batch rich text style event deltas
zxch3n Apr 22, 2026
7b20b7e
Merge remote-tracking branch 'origin/main' into feat/scale-text-check…
zxch3n Apr 22, 2026
7d27feb
fix: handle fuzzed text checkout edge cases
zxch3n Apr 22, 2026
4ce926f
fix: handle shallow root frontiers in fuzzed imports
zxch3n Apr 22, 2026
c259b80
Merge remote-tracking branch 'origin/main' into feat/scale-text-check…
zxch3n Apr 26, 2026
67905cb
Merge remote-tracking branch 'origin/main' into feat/scale-text-check…
zxch3n May 7, 2026
87dd333
fix: clear deleted cache on checkout
zxch3n May 8, 2026
6e1b49a
fix: reject partial shallow root checkout
zxch3n May 8, 2026
162ff39
fix: reexport shallow root snapshots
zxch3n May 8, 2026
dccd172
fix: avoid shallow gca on reexport
zxch3n May 8, 2026
4ce3408
fix: reject unreachable shallow frontiers
zxch3n May 8, 2026
df2498c
fix: reject shallow root dependency frontiers
zxch3n May 8, 2026
fdedbd7
fix: guard shallow frontier utilities
zxch3n May 8, 2026
5d046be
fix: clamp shallow frontier conversions
zxch3n May 8, 2026
05a5b62
fix: clamp empty shallow version frontiers
zxch3n May 8, 2026
0b656b7
fix: normalize shallow reexport frontiers
zxch3n May 8, 2026
3e9edca
fix: normalize shallow snapshot targets
zxch3n May 8, 2026
20770e3
fix: normalize state-only export frontiers
zxch3n May 8, 2026
b7763b0
fix: normalize snapshot-at frontiers
zxch3n May 8, 2026
530b901
fix: keep richtext style pairs in shallow roots
zxch3n May 8, 2026
adbe9a3
fix: clamp shallow diff lca frontiers
zxch3n May 8, 2026
394134e
fix: normalize shallow state-only targets
zxch3n May 8, 2026
226cf27
fix: preserve independent shallow root frontiers
zxch3n May 8, 2026
9b45cd3
fix: handle multi-frontier shallow snapshot checkout
zxch3n May 8, 2026
ca54408
fix: reject malformed imported text diffs
zxch3n May 9, 2026
a38f5d5
fix: reject empty text marks in JSON import
zxch3n May 9, 2026
37078ae
fix: reject unpaired text marks in JSON import
zxch3n May 9, 2026
b0257ee
fix: canonicalize frontiers constructors
zxch3n May 9, 2026
3622d49
fix: preserve canonical state-only snapshot frontiers
zxch3n May 9, 2026
d3d84bb
fix: ignore cyclic tree moves in one-doc fuzz
zxch3n May 9, 2026
466c97e
fix: preserve commit options after failed change travel
zxch3n May 10, 2026
1bf267d
Merge branch 'main' into feat/scale-text-checkout-perf
zxch3n May 21, 2026
4a32d2e
fix: tighten import rollback followups
zxch3n May 21, 2026
2b9a599
Merge branch 'feat/scale-text-checkout-perf' of https://github.com/lo…
zxch3n May 21, 2026
d67d534
refactor: centralize import rollback container check
zxch3n May 21, 2026
507aff6
docs: plan fast diff calc span routing
zxch3n May 22, 2026
c350b0e
bench: add many text checkout scenario
zxch3n May 22, 2026
5c3cd62
refactor: route richtext checkout through spans
zxch3n May 22, 2026
91e5ceb
perf: filter richtext checkout spans by coverage
zxch3n May 22, 2026
b8ee18f
docs: record fast diff calc benchmark results
zxch3n May 22, 2026
57bfd67
bench: report checkout span averages
zxch3n May 22, 2026
f9fb539
test: compare filtered richtext diff
zxch3n May 22, 2026
434b35c
docs: update fast diff calc commit list
zxch3n May 22, 2026
f8d5752
fix: keep list diff calculator small
zxch3n May 22, 2026
82dd1dc
perf: reuse coverage-local richtext tracker versions
zxch3n May 22, 2026
091e3c9
fix: guard richtext tracker reuse
zxch3n May 22, 2026
3c2b53e
test: skip shallow peers in gc fuzzer sync
zxch3n May 22, 2026
3d31441
Merge remote-tracking branch 'origin/main' into feat/scale-text-check…
zxch3n May 26, 2026
079709f
fix: harden checkout replay invariants
zxch3n May 26, 2026
95b6353
Merge remote-tracking branch 'origin/main' into feat/scale-text-check…
zxch3n Jun 2, 2026
6305ea8
Merge remote-tracking branch 'origin/main' into feat/scale-text-check…
zxch3n Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
bench: add many text checkout scenario
  • Loading branch information
zxch3n committed May 22, 2026
commit c350b0e81387e0dcf486223b5c986f3056432fb9
168 changes: 160 additions & 8 deletions crates/loro-internal/benches/text_checkout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ mod text_checkout {
peer_count: usize,
change_count: usize,
base_len: usize,
text_container_count: usize,
large_text_container_count: usize,
large_text_len: usize,
version_count: usize,
subscribed: bool,
}
Expand Down Expand Up @@ -110,6 +113,13 @@ mod text_checkout {
let peer_count = env_usize("LORO_TEXT_CHECKOUT_PEERS", 1000).max(1);
let base_len = env_usize("LORO_TEXT_CHECKOUT_BASE_LEN", 8192).max(1);
let sequential_changes = env_usize("LORO_TEXT_CHECKOUT_CHANGES", peer_count.max(1000));
let text_container_count = env_usize("LORO_TEXT_CHECKOUT_TEXT_CONTAINERS", 10_000).max(1);
let large_text_container_count =
env_usize("LORO_TEXT_CHECKOUT_LARGE_TEXT_CONTAINERS", 8).min(text_container_count);
let small_text_len = env_usize("LORO_TEXT_CHECKOUT_SMALL_TEXT_LEN", 8);
let large_text_len = env_usize("LORO_TEXT_CHECKOUT_LARGE_TEXT_LEN", 65_536);
let container_edit_count =
env_usize("LORO_TEXT_CHECKOUT_CONTAINER_EDITS", text_container_count).max(1);

let mut group = c.benchmark_group("text checkout");
group.sample_size(10);
Expand Down Expand Up @@ -164,6 +174,19 @@ mod text_checkout {
"code/checkout-to-latest-linear",
build_code_like_history(sequential_changes, base_len, 1, false),
);
bench_checkout_latest_to_base_fixture(
&mut group,
"multi-container/latest-to-base",
build_many_text_container_history(
peer_count,
text_container_count,
large_text_container_count,
small_text_len,
large_text_len,
container_edit_count,
false,
),
);

group.finish();
}
Expand Down Expand Up @@ -210,6 +233,45 @@ mod text_checkout {
maybe_report_profile(name, stats, &totals, state_profile);
}

fn bench_checkout_latest_to_base_fixture(
group: &mut BenchmarkGroup<'_, WallTime>,
name: &str,
fixture: CheckoutFixture,
) {
let CheckoutFixture {
doc,
frontiers,
stats,
_subscription,
} = fixture;
let base_frontier = frontiers.first().unwrap().clone();
let latest_frontier = frontiers.last().unwrap().clone();
let mut totals = ProfileTotals::default();

group.bench_with_input(
BenchmarkId::new(name, stats.version_count),
&base_frontier,
|b, base_frontier| {
b.iter_custom(|iters| {
let mut measured = Duration::ZERO;
for _ in 0..iters {
doc.checkout(&latest_frontier).unwrap();
let start = std::time::Instant::now();
let profile = doc.checkout_with_profile(base_frontier).unwrap();
measured += start.elapsed();
totals.add(profile);
black_box(profile);
}

measured
});
},
);

let state_profile = doc.text_state_profile("text");
maybe_report_profile(name, stats, &totals, state_profile);
}

fn bench_checkout_to_latest_fixture(
group: &mut BenchmarkGroup<'_, WallTime>,
name: &str,
Expand Down Expand Up @@ -294,6 +356,9 @@ mod text_checkout {
peer_count,
change_count: peer_count,
base_len,
text_container_count: 1,
large_text_container_count: 0,
large_text_len: 0,
version_count: peer_count + 1,
subscribed,
},
Expand All @@ -315,9 +380,8 @@ mod text_checkout {
let mut frontiers = Vec::with_capacity(peer_count + 1);
frontiers.push(doc.oplog_frontiers());
let mut rng = StdRng::seed_from_u64(6);
let mut len = base_len;

for peer in 0..peer_count {
for (peer, len) in (0..peer_count).zip(base_len..) {
let snapshot = doc.export(ExportMode::snapshot()).unwrap();
let base_vv = doc.oplog_vv();
let peer_doc = doc_from_snapshot(&snapshot, peer as PeerID + 2);
Expand All @@ -327,7 +391,6 @@ mod text_checkout {
peer_doc.commit_then_renew();
let update = peer_doc.export(ExportMode::updates(&base_vv)).unwrap();
doc.import(&update).unwrap();
len += 1;
frontiers.push(doc.oplog_frontiers());
}

Expand All @@ -339,6 +402,9 @@ mod text_checkout {
peer_count,
change_count: peer_count,
base_len,
text_container_count: 1,
large_text_container_count: 0,
large_text_len: 0,
version_count: peer_count + 1,
subscribed,
},
Expand Down Expand Up @@ -386,6 +452,9 @@ mod text_checkout {
peer_count,
change_count: peer_count,
base_len,
text_container_count: 1,
large_text_container_count: 0,
large_text_len: 0,
version_count: peer_count + 1,
subscribed,
},
Expand Down Expand Up @@ -425,6 +494,9 @@ mod text_checkout {
peer_count,
change_count: peer_count,
base_len,
text_container_count: 1,
large_text_container_count: 0,
large_text_len: 0,
version_count: peer_count + 1,
subscribed,
},
Expand Down Expand Up @@ -478,13 +550,81 @@ mod text_checkout {
peer_count: 1,
change_count,
base_len,
text_container_count: 1,
large_text_container_count: 0,
large_text_len: 0,
version_count: change_count + 1,
subscribed,
},
subscribed,
)
}

fn build_many_text_container_history(
peer_count: usize,
text_container_count: usize,
large_text_container_count: usize,
small_text_len: usize,
large_text_len: usize,
edit_count: usize,
subscribed: bool,
) -> CheckoutFixture {
let doc = LoroDoc::new_auto_commit();
doc.set_peer_id(1).unwrap();
let small_text = repeated_text(small_text_len);
let large_text = repeated_text(large_text_len);
let mut texts = Vec::with_capacity(text_container_count);
let mut lens = Vec::with_capacity(text_container_count);

for idx in 0..text_container_count {
let name = text_container_name(idx);
let text = doc.get_text(name.as_str());
let initial = if idx < large_text_container_count {
&large_text
} else {
&small_text
};
if !initial.is_empty() {
text.insert(0, initial, PosType::Unicode).unwrap();
}
texts.push(text);
lens.push(initial.chars().count());
}

doc.commit_then_renew();
let mut frontiers = Vec::with_capacity(edit_count + 1);
frontiers.push(doc.oplog_frontiers());
let mut rng = StdRng::seed_from_u64(0x7e57_c001);

for edit in 0..edit_count {
let peer = edit % peer_count;
doc.set_peer_id(peer as PeerID + 2).unwrap();
let text_idx = edit % text_container_count;
let pos = rng.gen_range(0..=lens[text_idx]);
texts[text_idx].insert(pos, "x", PosType::Unicode).unwrap();
lens[text_idx] += 1;
doc.commit_then_renew();
frontiers.push(doc.oplog_frontiers());
}

attach_subscription(
doc,
frontiers,
FixtureStats {
scenario: "many text containers with wide multi-peer checkout",
peer_count,
change_count: edit_count,
base_len: small_text_len,
text_container_count,
large_text_container_count,
large_text_len,
version_count: edit_count + 1,
subscribed,
},
subscribed,
)
}

fn build_base_snapshot(base_len: usize) -> (Vec<u8>, loro_internal::VersionVector) {
let doc = LoroDoc::new_auto_commit();
doc.set_peer_id(1).unwrap();
Expand Down Expand Up @@ -543,6 +683,14 @@ mod text_checkout {
out
}

fn text_container_name(index: usize) -> String {
if index == 0 {
"text".to_string()
} else {
format!("text_{index}")
}
}

fn env_usize(name: &str, default: usize) -> usize {
std::env::var(name)
.ok()
Expand All @@ -562,15 +710,16 @@ mod text_checkout {

let samples = totals.samples as u32;
let state_profile = state_profile.unwrap_or_default();
let avg_future_scan_visited = if totals.richtext_insert_future_scan_count == 0 {
0
} else {
totals.richtext_insert_future_scan_visited / totals.richtext_insert_future_scan_count
};
let avg_future_scan_visited = totals
.richtext_insert_future_scan_visited
.checked_div(totals.richtext_insert_future_scan_count)
.unwrap_or(0);
eprintln!(
concat!(
"[text-checkout-profile] {name}: scenario={scenario}, peers={peers}, ",
"changes={changes}, base_len={base_len}, versions={versions}, ",
"text_containers={text_containers}, large_text_containers={large_text_containers}, ",
"large_text_len={large_text_len}, ",
"subscribed={subscribed}, samples={samples}, avg_total={avg_total:?}, ",
"avg_frontier_prepare={avg_frontier_prepare:?}, ",
"avg_frontiers_to_vv={avg_frontiers_to_vv:?}, avg_diff_calc={avg_diff_calc:?}, ",
Expand Down Expand Up @@ -601,6 +750,9 @@ mod text_checkout {
changes = stats.change_count,
base_len = stats.base_len,
versions = stats.version_count,
text_containers = stats.text_container_count,
large_text_containers = stats.large_text_container_count,
large_text_len = stats.large_text_len,
subscribed = stats.subscribed,
samples = totals.samples,
avg_total = totals.total / samples,
Expand Down
Loading