From Paper to Production: SoftMaple Eg-walker
Engineering Retrospective and Performance Report
Collaborative text editing forces several costs into the same design: local edits must be immediate, offline branches must converge, documents must load quickly, and persisted state must remain small. Eg-walker offers an unusual answer: keep the event graph as durable truth, construct CRDT state only when a merge requires it, and discard that state again at critical versions. This article is both an engineering retrospective on bringing that idea into a production-oriented TypeScript package and a performance report comparing the result with the complete benchmark matrix published in the Eg-walker paper artifact.Version under review: SoftMaple commitThe headline result is that the implementation now has a reproducible, paper-aligned benchmark framework, fast incremental and snapshot restore paths, and explicit measurement boundaries. It is not yet meaningful to compare its absolute TypeScript timings directly with the paper’s optimized Rust implementation. The useful comparisons are:1bc089be4ec063491996fefcdd6d4f45ade1a144, merged through PR #801.
Engineering credit: Codex 5.6 Sol Ultra goal.
Measurement note: The official comparison tooling was rerun on the current machine and its working results were updated. This report uses that current-machine refresh where populated and labels retained publication values explicitly.
- SoftMaple against itself on the same machine and runtime;
- SoftMaple native load against other JavaScript native-load paths;
- Paper DT against the other systems measured by the paper;
- performance shapes across sequential, concurrent, and asynchronous traces.
Executive summary
- In the paper’s official benchmark, Rust Eg-walker merges sequential traces in 1.77–3.58 ms. It is approximately 7–10× faster than the like-for-like reference CRDT. Against the current-machine Yjs refresh, it is approximately 20–30× faster on those traces.
- On the asynchronous A1 trace, Paper DT is 4.8× faster than the reference CRDT, about 8.7× faster than the refreshed Yjs baseline, and about 707× faster than the tested OT implementation.
- On A2, Paper DT finishes in 23.47 ms while OT takes about 61 minutes, a difference of roughly 156,000×.
- Highly concurrent C1 and C2 remain the difficult shape for Eg-walker. Paper DT is close to the reference CRDT and slower than Yrs; on C2 it is also slower than Yjs.
- Paper DT’s steady-state memory is 0.07–0.97 MiB across the seven datasets, compared with 27–51 MiB for Yjs and 230–809 MiB for Automerge.
- In SoftMaple’s final full-trace snapshot baseline on Apple M1, native snapshot restore takes 87–191 ms for S1, S2, S3, and A1 without a full history replay.
- During the optimization cycle, the S3 native snapshot fell from 346 MB to 62.4 MB, while measured restore heap fell from approximately 1.43 GB to 135 MB.
- SoftMaple’s calibrated 1k/2k/4k operation-level gates pass. On a recent verification run of the same gate suite, the 4,000-event batch applied in 7.52 ms on average, or approximately 532,000 events per second.
Scope and terminology
The report uses the following names consistently:- Paper DT: the optimized Rust Eg-walker implementation in Diamond Types.
- Reference CRDT: the paper authors’ CRDT implementation sharing much of Paper DT’s infrastructure, included to reduce implementation-level bias.
- SoftMaple raw ingest: reading a paper JSON trace, converting paper positions and operations, allocating TypeScript event objects, and applying them through the public remote-event API.
- SoftMaple native graph load: decoding the EGW3 graph representation, constructing a replica, and replaying it to obtain the current text.
- SoftMaple portable snapshot: paper-aligned
EGWP1persistence containing current text and the event graph, but no temporary CRDT replay state. - SoftMaple native snapshot: optional
EGWS1resume state containing already-available runtime indexes and checkpoints. - Yjs native update: applying a precomputed Yjs binary update through
Y.applyUpdateV2.
Engineering retrospective
The reference commit is not a small optimization patch. It changes 158 files, adds approximately 39,000 lines, and touches the event graph, replay engine, text storage, persistence formats, network synchronization, conformance tests, property tests, and benchmark infrastructure. The central engineering challenge was not simply implementing the paper’s prepare/effect loop. It was preserving the meaning of that loop across JavaScript-specific boundaries while removing enough object allocation and repeated graph work to make large traces practical.The architectural boundary
SoftMaple separates three kinds of state:- Durable document state: current text and the event graph.
- Temporary replay state: sequence records, placeholders, delete-target indexes, and traversal state needed while crossing concurrent history.
- Optional resume state: a runtime optimization that can restore already-built replay indexes without claiming to be the paper-minimal persistent format.
EGWP1 snapshots represent the first category. Native EGWS1 snapshots may
include the third. Temporary replay structures do not silently become required
durable metadata.
Convergence before speed
The first gate was semantic: a valid event graph must converge independently of delivery order and topological traversal order. The commit addresses this through:- deterministic and transitive event-ID ordering;
- preservation of paper-trace agent identity;
- insertion integration against the event’s parent view;
- an independent scalar replay oracle;
- delivery-order and traversal-order differential tests;
- property tests covering batch application, pending dependencies, packed replay, snapshots, and randomized suffixes.
Atomic remote integration
Remote changes naturally arrive in batches, but the original single-event shape repeatedly planned replay and could expose partial state when a later event was invalid.applyRemoteEvents now treats a batch as a staged transaction:
- Clone and validate event IDs, parents, operations, and UTF-16 boundaries.
- Stage graph changes, frontier updates, and missing-parent queues.
- Drain causally ready events through an iterative work queue.
- Apply the resulting text and replay changes against staged state.
- Commit graph, text, frontier, pending events, checkpoints, caches, and diagnostics together.
Set objects.
Atomic batching improves correctness, but it is also a performance primitive:
the engine sees enough context to build one replay plan for a network batch.
Persistent text instead of repeated string splices
JavaScript strings are immutable. Replaying thousands of inserts and deletes with repeated slicing can copy large prefixes and suffixes, while materialized checkpoint strings multiply memory use. SoftMaple moved current text and checkpoint text to structurally shared UTF-16 ropes:- immutable roots make checkpoints cheap;
- edits copy only paths to affected leaves;
- unchanged chunks are shared across versions;
- a transient form batches replay edits;
getText()flattens lazily and caches the result.
Indexed and packed nonlinear replay
Correct object-oriented replay still allocated too much and repeatedly scanned large graph regions. The optimized path therefore stays in compact numeric representations for as long as possible:- packed event-graph columns;
- one-pass edge construction;
- run-based event-ID lookup;
- packed version differences and ranked replay order;
- Euler-rank ancestry indexes;
- segmented placeholders;
- packed delete-target arenas;
- batched typed runs and replay spans;
- reusable traversal and tree-update scratch storage.
FugueOrderIndex, which combines stable
order-maintenance labels with deterministic indexed sibling ordering. The
original linear integration logic remains as a test oracle, not the production
hot path.
The broader lesson was that the largest wins came from removing graph-wide work
and per-event object construction. Micro-optimizing a loop mattered less than
changing how often the loop had to run and what it operated on.
Persistence as two explicit products
The paper’s compact state and a fast process-resume image solve different problems. Treating them as one format would either make portable documents unnecessarily large or make resume benchmarks hide a full replay. SoftMaple exposes both:EGWP1is portable, paper-aligned state: current text plus the event graph;EGWS1is optional native resume state: compact runtime indexes and retained checkpoints when they are already available.
The hardest bugs lived at representation boundaries
Several of the most consequential failures were not mistakes in the headline algorithm. They appeared between representations:- paper offsets count Unicode scalar values; JavaScript APIs use UTF-16 code units;
- one paper transaction may contain many atomic events; a public edit may be a compound operation;
- an event’s position belongs to its parent frontier, not necessarily the receiver’s current text;
- in-memory parent sets are not directly JSON-serializable;
- lazy decoded bytes must not remain accidentally mutable through caller-owned buffers;
- restored resume state must describe the same frontier as the persisted graph;
- typed-run splits must update every dependent index atomically.
Test environments
Published paper artifact
The official comparison was collected on:- AMD Ryzen 7950X;
- Linux 6.5;
- 64 GB RAM;
- Rust 1.78 in release mode with
-C target-cpu=native; - Rust benchmarks pinned to one CPU core;
- Node.js 22.2.0 for Yjs;
- at least 100 iterations for reported timing results, except the hour-long OT A2 case, which used 10.
Current-machine refresh
The official comparison tooling has also been rerun on the current Apple M1 machine, and the artifact’s working results have been updated. The populated refresh available for this report is the Yjs lane:- Apple M1;
- macOS;
- 16 GB RAM;
- Node.js 24.12.0;
- all seven S/C/A paper datasets;
- updated CPU samples in
results/js.jsonandresults/timings.json; - updated memory samples in
results/yjs_memusage.json.
timings.json, so this report does not invent
current-machine values for them. The complete comparison tables retain the
paper’s published values for those systems and use the refreshed local values
for Yjs. Every table below marks this mixed provenance explicitly.
SoftMaple local baselines
The SoftMaple results recorded with the reference commit were collected on:- Apple M1;
- macOS;
- 16 GB RAM;
- Node.js 24.12.0.
Dataset characteristics
The paper artifact contains seven editing traces:
S1–S3 exercise long non-conflicting histories. C1 and C2 contain many
short-lived concurrent branches. A1 and A2 model Git-style asynchronous
branches, with A2 having by far the highest average concurrency.
Complete paper-artifact timing comparison
The following table reports mean time in milliseconds to merge the complete remote trace. Lower is better. Yrs is included because its results exist in the artifact, although the paper omits it from the final chart to save space. Paper DT, Reference CRDT, Yrs, Automerge, and OT are the artifact’s published values; the Yjs column is the updated current-machine rerun.Relative performance
The sequential results demonstrate the value of clearing temporary replay state
at critical versions. The concurrent results show the opposite side of the
algorithm: when the graph contains many live branches, Eg-walker performs work
similar to a conventional sequence CRDT.
A2 exposes OT’s quadratic long-branch behavior. It also shows that Eg-walker is
not automatically the fastest implementation on every graph: Yrs completes A2
in 12.80 ms versus 23.47 ms for Paper DT.
Optimized document load
Paper DT can persist the current document text separately from replay state. Its optimized load times are:
These numbers are not remote-merge times. They demonstrate the architectural
benefit of loading cached current text without first materializing all
historical CRDT metadata.
Complete paper-artifact memory comparison
Each cell ispeak / steady-state memory in MiB. As with the timing table, the
Yjs column contains the updated current-machine measurement; all other columns
retain the artifact’s published results.
Memory interpretation
- Paper DT’s steady state is 0.07–0.97 MiB because temporary merge structures can be discarded.
- Yjs retains 27–51 MiB across the same traces, approximately 29–730× Paper DT’s steady-state footprint.
- Automerge retains 230–809 MiB, approximately two to three orders of magnitude more than Paper DT.
- Paper DT’s peak rises to 65–76 MiB for C1/C2 because replay must represent substantial concurrency.
- OT shares Eg-walker’s compact steady-state shape, but reaches approximately 6.3 GiB at peak on A2 due to memoized transformations.
Complete paper-artifact storage comparison
The following figures are KiB:
The full DT event history is smaller than uncompressed Automerge on every
trace. The compact DT comparison form is smaller than Yjs on all seven traces,
although the margin narrows for C2 because a highly concurrent event graph has
many edges to encode.
Compression policy matters. The paper disables DT’s LZ4 and Automerge’s gzip
for the like-for-like storage comparison. These figures therefore describe
format overhead rather than the smallest possible archive.
SoftMaple TypeScript performance
Full-trace raw-ingest baseline
The reference commit records the following three-run Apple M1 baselines:
The first four rows use the explicitly labelled patch-level import-stress lane.
They are useful for tracking implementation regressions but are not
paper-conformant timing results. Operation-level conversion emits one event per
paper keystroke and is required for faithful concurrent/asynchronous reporting.
The gap between EGW3 decode and native graph load shows that decoding bytes is
not the dominant full sequential cost. Reconstructing and materializing replay
state is.
For A1 and bounded A2, native graph load is much faster than raw ingest. Their
dominant cost is JSON conversion and per-event application rather than byte
decode.
Concurrent replay improvement
Before checkpoint and replay-order optimization, the bounded C1/C2 cases took approximately 4.5 seconds. Avoiding repeated full checkpoint ancestry expansion reduced them to approximately 2.0 seconds. Reusing the already computed partial-replay suffix order reduced them again:
The resulting improvement is approximately 2.9× for both traces.
The larger 10,000-transaction smoke cases complete in 16.37 seconds for C1 and
14.85 seconds for C2. They are practical profiling targets but remain too slow
for routine CI.
Native snapshot evolution
The native snapshot lane measures an optional runtime extension, not the paper-minimal portable format. Initial snapshot adoption produced:
After moving runtime state into compact binary columns, eliminating avoidable
section copies, keeping content bytes as views, and deferring event indexes,
the final recorded baseline became:
The change is substantial:
- S1 restore improved about 10.6× and snapshot size fell 77.7%.
- S2 restore improved about 10.7× and snapshot size fell 81.7%.
- S3 restore improved about 21.1× and snapshot size fell 82.0%.
- A1 restore improved about 11.7× and snapshot size fell 82.6%.
Calibrated operation-level gates
The reference commit adds fixed S1 operation-level gates at 1,000, 2,000, and 4,000 events. A three-run verification on Apple M1 produced:
The 4,000-event case also reported:
- portable decode: 0.16 ms;
- lazy portable restore: 0.09 ms;
- explicit graph materialization: 24.35 ms;
- portable snapshot heap delta: approximately 0.70 MiB;
- native snapshot size: approximately 6.62 KiB;
- native snapshot heap delta: approximately 0.15 MiB;
- 4,000 incremental applies;
- zero retreats, advances, partial replays, or full replays.
Bottleneck analysis
Sequential traces
Sequential edits predominantly use the incremental fast path. The remaining full-history cost is replay and materialization, not binary decoding. Persistent ropes and typed runs reduce the amount of copied text and per-character state, but operation-level full S1–S3 still represents 779,000 to 2.34 million atomic events.Concurrent traces
C1/C2 stress:- version difference calculation;
- retreat/advance planning;
- Fugue sibling integration;
- ranked sequence weight updates;
- delete-target lookup;
- checkpoint selection.
Asynchronous A2
A2 is both a correctness and scalability challenge. Later operations may refer to positions inside a long earlier insertion, so collapsing that insertion into one compound event is not faithful. The correct operation-level path expands the trace to atomic events. A 300-transaction sample contains approximately 46,540 events and takes about 11 seconds. A 600-transaction sample contains approximately 95,257 events and takes about 130 seconds. Full operation-level A2 is therefore not yet a routine benchmark target for the TypeScript implementation.Persistence
Portable snapshot byte decode and lazy replica construction are fast. Materializing the event graph is more expensive, as shown by the 24.35 ms materialization time in the 4,000-event gate. Native resume snapshots eliminate full replay, but they trade additional bytes for faster restoration. Reporting them separately is essential: otherwise a runtime cache can be mistaken for the compact durable format described by the paper.Correctness gates behind the numbers
The benchmark fails if:- remote events remain buffered;
- an unbounded faithful trace does not match its expected final text;
- portable snapshot round-trip changes the text or event count;
- native snapshot restoration performs a full replay;
- native binary encode/decode fails to reproduce the graph.
- the pinned paper conformance corpus;
- independent scalar replay;
- traversal-order convergence;
- randomized delivery;
- batch rollback;
- missing-parent buffering;
- packed versus object replay differentials;
- randomized edits after snapshot restoration;
- UTF-16 and surrogate-pair boundary checks.
Engineering lessons
Four lessons from the work apply beyond Eg-walker. First, benchmark boundaries are part of the API. JSON import, native update application, graph decoding, lazy restoration, and full materialization answer different product questions. Combining them into one number makes both optimization and comparison less useful. Second, correctness oracles enable aggressive optimization. The packed replay path could replace object graphs and linear scans because an independent scalar implementation, differential indexes, and randomized delivery tests remained available to challenge it. Third, representation changes beat isolated micro-optimizations. The largest improvements came from eliminating repeated ancestry expansion, using shared rope roots, keeping graph data packed, and postponing object materialization. Finally, persistent truth should be smaller than runtime convenience. The portable/native snapshot split makes that rule visible. Fast resume state is valuable, but it should remain optional and honestly measured.Limitations
- Different hardware: Paper DT and SoftMaple were measured on different CPU architectures and operating systems.
- Different runtimes: optimized Rust and Node.js have fundamentally different allocation and warm-up behavior.
- Different inputs: Yjs native update, SoftMaple raw JSON ingest, EGW3 graph load, and snapshot restore are distinct operations.
- Historical patch results: the full SoftMaple S1/S2/S3/A1 baseline uses patch-level import stress. It must not be labelled paper-conformant.
- Limited operation-level scale: current calibrated gates stop at 4,000 events; full faithful traces contain hundreds of thousands or millions.
- Small sample count: the local verification uses three runs, unlike the paper’s 100 or more.
- Memory measurement: Node heap deltas do not include every form of native or process memory and can vary with garbage collection.
Conclusions
Commit1bc089be4ec063491996fefcdd6d4f45ade1a144 establishes a credible
performance baseline for SoftMaple Eg-walker:
- paper datasets and operation semantics are represented explicitly;
- raw ingest, native graph load, portable persistence, and native resume state are measured separately;
- concurrent replay smoke tests improved by approximately 2.9×;
- native snapshot restore improved by roughly 10–21× across the full recorded traces;
- full snapshot sizes fell by approximately 78–83%;
- native restoration no longer performs a full replay;
- fixed operation-level performance and memory gates now protect the portable persistence path.
Reproduction
Build and test the package:@softmaple/bench so Turborepo builds the engine dependency
first.
Run the calibrated operation-level persistence gates: