Skip to content

perf: speed up concurrent imports with large peer histories - #1222

Open
serg-and wants to merge 3 commits into
loro-dev:mainfrom
serg-and:perf/concurrent-import-frontier-replay
Open

serg-and wants to merge 3 commits into
loro-dev:mainfrom
serg-and:perf/concurrent-import-frontier-replay

Conversation

@serg-and

@serg-and serg-and commented Oct 22, 2026 •

Copy link
Copy Markdown

Documents with thousands of historical peer IDs can stall on small concurrent imports. We encountered this during collaborative editing: the first import was quick, but subsequent causally independent updates spent seconds traversing mostly unrelated history. This reproduces on unpatched loro-crdt 1.17.0.

The patch reuses the maintained frontier for the current version, removes multiple frontier candidates per ancestor traversal, and skips unrelated container changes before materializing replay version vectors. It preserves replay-base selection, causal context, and the fallible APIs in 1.17.0.

Performance

Measured the exact reproduction below (4,999 historical map commits from distinct peer IDs, four concurrent updates) with Bun, using identical release-build tooling for unpatched 1.17.0 (d2f3dfb9) and the patched build. Medians of three alternating fresh-process runs per build, after one excluded warm-up each; separate verified WASM binaries.

Type Second import: unpatched Patched Speedup
Text 977.11 ms 7.11 ms 137×
Tree 968.40 ms 10.67 ms 91×
List 946.96 ms 3.63 ms 261×
MovableList 3,223.18 ms 25.95 ms 124×
Map 11.19 ms 10.40 ms ~unchanged
Counter 1.22 ms 1.15 ms ~unchanged

The fourth movable-list import dropped from 5,220.04 ms to 17.57 ms (297×). First imports were broadly unchanged. These are local reproduction timings, not end-to-end server throughput measurements.

MovableList tracker rebuilds also benefit from filtering unrelated container history before constructing version vectors; the frontier optimizations are shared across types. With 16 concurrent movable-list updates, total import time fell from 149.6 s to 320 ms (467×), and the 16th import from 16.75 s to 21.70 ms (772×) (medians of three runs, same 5,000-history setup).

Reproduction

import { LoroDoc } from 'loro-crdt'
import assert from 'assert'

const historical = 5000
const concurrent = 4

const base = new LoroDoc()

for (let i = 1; i < historical; i++) {
  base.setPeerId(BigInt(i))
  base.getMap('x').set('x', i)
  base.commit()
}

const snapshot = base.export({ mode: 'snapshot' })

const testType = (mutate: (doc: LoroDoc, iteration: number) => void) => {
  const concurrentUpdates = Array.from({ length: concurrent }, (_, i) => {
    let update: Uint8Array | undefined
    const branch = new LoroDoc()
    branch.import(snapshot)
    branch.setPeerId(BigInt(historical + i))
    branch.subscribeLocalUpdates(u => (update = u))
    mutate(branch, i)
    branch.commit()
    assert.notEqual(update, undefined)
    return update!
  })

  const doc1 = new LoroDoc()
  doc1.import(snapshot)
  return Object.fromEntries(
    concurrentUpdates.map((update, i) => {
      const start = performance.now()
      doc1.import(update)
      return [`import concurrent update ${i + 1}`, `${(performance.now() - start).toFixed(2)}ms`]
    })
  )
}

console.table({
  LoroText: testType(d => d.getText('text').insert(0, d.peerIdStr)),
  LoroMap: testType((d, i) => d.getMap('map').set(`${d.peerIdStr}${i}`, `${d.peerIdStr}${i}`)),
  LoroTree: testType(d => d.getTree('tree').createNode()),
  LoroCounter: testType(d => d.getCounter('counter').increment(1)),
  LoroList: testType(d => d.getList('list').insert(0, d.peerIdStr)),
  LoroMovableList: testType(d => d.getMovableList('movableList').insert(0, d.peerIdStr)),
})

Validation

  • Affected Cargo suites: 1,847 passed, 0 failed, 38 ignored, including unit, integration, and documentation tests for loro-internal, loro, loro-wasm, examples, fuzz, and json-float-consumer, with test_utils,jsonpath.
  • Rebuilt WASM on 1.17.0: 524 tests passed, plus TypeScript declarations, CommonJS, Deno, and Bun checks.
  • Regression coverage checks frontier equivalence, causal context across filtered history, concurrent replay, import order, and checkout.

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