Skip to content

fix: big text insert after a concurrent change imports in linear time - #1236

Open
williamjakfar wants to merge 1 commit into
loro-dev:mainfrom
williamjakfar:fix/big-insert-import
Open

williamjakfar wants to merge 1 commit into
loro-dev:mainfrom
williamjakfar:fix/big-insert-import

Conversation

@williamjakfar

Copy link
Copy Markdown

Problem

Importing one big text insert made right after a concurrent change takes time quadratic in the insert's length:

Insert Import (1.17.1)
100k chars 0.05 s
300k chars 0.43 s
1M chars 5.1 s
3M chars 51.5 s

Repro: peer A imports a small edit from peer B, then inserts a big string and exports that one update; B imports it. Inserting the same text as many small commits, each imported on its own, takes milliseconds. We hit it running Loro in a Cloudflare Durable Object, where a 3M paste hung the object for over a minute. It reproduces natively, in Node (loro-crdt 1.17.1) and in WASM.

Cause

Almost all the time (98% of samples) is in IdToCursor::update_insert_batch, called from Tracker::update_insert_by_split.

IdToCursor stores the insert in fragments of 256 ops, so forward sends the rope one LeafUpdate per fragment, all naming the same leaf. CrdtRope::update returns a leaf index per update, so update_insert_by_split gets the same leaf about n/256 times, each with the same id_span() covering the whole insert. update_insert_batch then walks every fragment of the insert once per entry. With instrumentation, a 100k insert gave 390 updates, each 11~100011, and 152k fragments walked; at 3M, 11,718 updates and 137M fragments.

Fix

Skip leaves already seen in update_insert_by_split. A repeated entry is the same (leaf.id_span(), leaf) pair, so updating it again changes nothing.

Insert Before After
100k chars 0.05 s 0.001 s
1M chars 5.1 s 0.02 s
3M chars 51.5 s 0.13 s

Tests

  • crates/loro/tests/big_insert_import.rs:
    • a correctness test (both peers end with the same text);
    • an #[ignore]d timing test in the style of dead_container_cache_perf.rs, which imports 1M characters and fails above 1 s. It takes about 0.02 s with the fix and 5.1 s without.
  • cargo test --release --no-fail-fast -p loro-internal -p loro: 1,533 passed, 1 failed. The failure, tests::op_count::len_ops_prefers_exact_oplog_value_when_lock_order_allows, fails on main without this change too (macOS, Apple silicon).
  • I didn't run the fuzz targets; please say if you'd like any run.

A changeset for loro-crdt (patch) is included.

🤖 Generated with Claude Code

The rope reports a leaf once per update that touched it, and a big insert
reaches the id-to-cursor map as one update per 256-op fragment, all naming
the same leaf. update_insert_batch then walked every fragment of the insert
once per report: quadratic in the insert's length (3M characters: ~50 s).
Update each leaf once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant