Repository navigation
fix: big text insert after a concurrent change imports in linear time - #1236
Open
williamjakfar wants to merge 1 commit into
Open
williamjakfar wants to merge 1 commit into
williamjakfar wants to merge 1 commit into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Importing one big text insert made right after a concurrent change takes time quadratic in the insert's length:
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-crdt1.17.1) and in WASM.Cause
Almost all the time (98% of samples) is in
IdToCursor::update_insert_batch, called fromTracker::update_insert_by_split.IdToCursorstores the insert in fragments of 256 ops, soforwardsends the rope oneLeafUpdateper fragment, all naming the same leaf.CrdtRope::updatereturns a leaf index per update, soupdate_insert_by_splitgets the same leaf about n/256 times, each with the sameid_span()covering the whole insert.update_insert_batchthen walks every fragment of the insert once per entry. With instrumentation, a 100k insert gave 390 updates, each11~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.Tests
crates/loro/tests/big_insert_import.rs:#[ignore]d timing test in the style ofdead_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 onmainwithout this change too (macOS, Apple silicon).A changeset for
loro-crdt(patch) is included.🤖 Generated with Claude Code