Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 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
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions crates/fuzz/src/crdt_fuzzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,15 @@ pub fn test_multi_sites_with_gc(
let (a, b) = array_mut_ref!(&mut this.actors, [i, j]);
let a_doc = &mut a.loro;
let b_doc = &mut b.loro;
let a_shallow = a_doc.is_shallow();
let b_shallow = b_doc.is_shallow();
// Shallow docs cannot export ops before the shallow root, so
// they cannot sync complete history to empty peers. This mirrors
// the non-GC `check_equal` guard.
if a_shallow || b_shallow {
continue;
}

info_span!("Attach", peer = i).in_scope(|| {
a_doc.attach();
});
Expand Down
12 changes: 10 additions & 2 deletions crates/fuzz/src/one_doc_fuzzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,11 @@ impl OneDocFuzzer {
peer: before.0,
counter: before.1,
};
tree.mov_before(target, before).unwrap();
if let Err(LoroError::TreeError(e)) =
tree.mov_before(target, before)
{
tracing::warn!("move error {}", e);
}
}
crate::container::TreeActionInner::MoveAfter { target, after } => {
let target = TreeID {
Expand All @@ -562,7 +566,11 @@ impl OneDocFuzzer {
peer: after.0,
counter: after.1,
};
tree.mov_after(target, after).unwrap();
if let Err(LoroError::TreeError(e)) =
tree.mov_after(target, after)
{
tracing::warn!("move error {}", e);
}
}
crate::container::TreeActionInner::Meta { meta: (k, v) } => {
let meta = tree.get_meta(target).unwrap();
Expand Down
186 changes: 186 additions & 0 deletions crates/fuzz/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,163 @@ fn all_fuzz_state_only_before_shallow_root() {
)
}

#[test]
fn all_fuzz_state_only_roundtrip_after_diff_apply_text_update() {
test_multi_sites(
5,
vec![FuzzTarget::All],
&mut [
Handle {
site: 196,
target: 0,
container: 151,
action: Generic(GenericAction {
value: Container(List),
bool: true,
key: 1835887981,
pos: 15359179523395251565,
length: 6845301837235606980,
prop: 4959913191460359423,
}),
},
SyncAll,
SyncAll,
SetCommitOptions {
site: 255,
origin: 255,
msg: 93,
},
Handle {
site: 0,
target: 0,
container: 0,
action: Generic(GenericAction {
value: Container(Unknown(255)),
bool: true,
key: 4294967295,
pos: 3225938275189391359,
length: 7885078839350357357,
prop: 3617008642897571181,
}),
},
DiffApply { from: 125, to: 178 },
SetCommitOptions {
site: 242,
origin: 242,
msg: 242,
},
DiffApply { from: 255, to: 255 },
SetCommitOptions {
site: 109,
origin: 109,
msg: 109,
},
SyncAll,
ForkAt {
site: 109,
to: 1835887981,
},
SyncAll,
SyncAll,
SyncAll,
SyncAll,
SyncAll,
SyncAll,
SyncAll,
SyncAll,
Sync { from: 91, to: 91 },
Sync { from: 91, to: 91 },
SyncAll,
StateOnlyRoundTrip { site: 213 },
SyncAll,
StateOnlyRoundTrip { site: 255 },
],
)
}

#[test]
fn all_fuzz_one_doc_ignores_cyclic_move_before_error() {
test_multi_sites_on_one_doc(
5,
&mut [
Handle {
site: 11,
target: 148,
container: 148,
action: Generic(GenericAction {
value: I32(0),
bool: false,
key: 65296,
pos: 18446744073709551600,
length: 18446744073709551615,
prop: 11240984665823117311,
}),
},
Query {
site: 125,
target: 125,
query_type: 119,
},
Handle {
site: 0,
target: 125,
container: 125,
action: Generic(GenericAction {
value: I32(2105376125),
bool: true,
key: 2105376125,
pos: 9042521604759584125,
length: 9042521604759584125,
prop: 9042521604759584125,
}),
},
ForkAt {
site: 125,
to: 2105376125,
},
Query {
site: 155,
target: 155,
query_type: 155,
},
SyncAll,
SyncAll,
SyncAll,
SyncAll,
SyncAll,
SyncAll,
SyncAll,
Handle {
site: 3,
target: 136,
container: 107,
action: Generic(GenericAction {
value: I32(1802075016),
bool: true,
key: 1802201963,
pos: 7740429931049413483,
length: 285424485,
prop: 29704420010754048,
}),
},
SyncAll,
Handle {
site: 3,
target: 136,
container: 107,
action: Generic(GenericAction {
value: I32(1802070920),
bool: true,
key: 1929407339,
pos: 3026537059180544374,
length: 232891317779694337,
prop: 3124043742624574344,
}),
},
],
)
}

#[test]
fn test_local_events() {
fuzz_local_events(vec![
Expand Down Expand Up @@ -10051,6 +10208,35 @@ fn shallow_arb_test() {
arbtest::builder().budget_ms(1000).run(|u| prop(u, 5))
}

#[test]
fn shallow_import_after_empty_shallow_export_and_text_edit_converges() {
test_multi_sites_with_gc(
5,
vec![FuzzTarget::All],
&mut [
Commit { site: 54 },
ExportShallow { site: 160 },
Handle {
site: 85,
target: 222,
container: 228,
action: Generic(GenericAction {
value: Container(Map),
bool: false,
key: 441515985,
pos: 11802333225252155855,
length: 9719789893245689708,
prop: 1572726038975133702,
}),
},
ImportShallow {
site: 21,
from: 240,
},
],
)
}

#[test]
fn shallow_fuzz_snapshot_after_shallow_import_and_diff_apply() {
test_multi_sites_with_gc(
Expand Down
20 changes: 20 additions & 0 deletions crates/loro-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,26 @@ impl ContainerType {
}
}

/// Returns whether importing ops for this container type may need import
/// rollback protection during state diff application.
///
/// This is used by import preflight: if an imported or newly-unblocked
/// pending change touches one of these container types, the oplog enables
/// rollback bookkeeping before applying the change to the document state.
/// Keep this list aligned with container states whose diff validation or
/// application can return an error after the oplog has already advanced.
///
/// Container types not listed here may still be complex, but their current
/// state-apply path does not report recoverable errors through
/// `LoroResult`, so enabling rollback for them would only add import
/// overhead.
pub fn may_need_state_apply_rollback(&self) -> bool {
matches!(
self,
ContainerType::List | ContainerType::Text | ContainerType::Tree
)
}

pub fn to_u8(self) -> u8 {
match self {
ContainerType::Map => 0,
Expand Down
4 changes: 4 additions & 0 deletions crates/loro-internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ jsonpath = []
name = "text_r"
harness = false

[[bench]]
name = "text_checkout"
harness = false

[[bench]]
name = "list"
harness = false
Expand Down
Loading
Loading