Editor performance measurements
Run the same Rust workloads on the native host and in Chrome/WASM:
cargo run -p openwebide-editor-bench --features candidates --release
CHROMEDRIVER=<matching-driver> cargo test -p openwebide-editor-bench --features candidates --target wasm32-unknown-unknown --release -- --nocapture
The browser command needs the matching wasm-bindgen-test-runner on PATH and Chrome configuration described in editor controls. Copy the runner configuration to tools/editor-bench/webdriver.json; each package’s test runner reads its own working directory. CI runs both workloads. Timings are observations, not pass/fail thresholds; correctness checks and native/WASM builds are required. Setup and final text verification are outside timed edits.
Each edit workload performs 50 insert/delete or document edit/Undo pairs (100 edits) at the middle of a Unicode/CRLF buffer. Document history stores replacement deltas. Candidate rope runs compare isolated edits and edits that also materialize the complete display string after every operation. Queries repeat 100 times. document_100_textarea_queries exercises actual CRLF-aware browser offsets; raw UTF-16 candidate queries measure their indexing primitives and are not interchangeable with textarea offsets.
Rope candidates are optional dependencies of this measurement tool and are absent from application runtime dependencies. Crop exposes byte edits and an optional UTF-16 metric. Ropey uses character edits with byte/character conversion. Ropey enables SIMD while disabling extra Unicode/bare-CR line separators to match the editor’s LF/CRLF line semantics.
Recorded comparison
One local observation on 2026-10-06, Rust 1.98.1, macOS arm64, Chrome 154.0.8037.98. All numbers are total milliseconds for 100 operations. Repeat on deployment hardware before choosing thresholds.
| Workload | Bytes | Native ms | Browser WASM ms |
|---|---|---|---|
document_100_edits |
2,097,144 | 4.888 | 3.145 |
crop_100_edits |
2,097,144 | 0.004 | 0.015 |
ropey_100_edits |
2,097,144 | 0.009 | 0.060 |
crop_100_edits_with_view |
2,097,144 | 6.833 | 5.565 |
ropey_100_edits_with_view |
2,097,144 | 8.512 | 6.860 |
document_100_textarea_queries |
2,097,144 | 77.741 | 120.825 |
document_100_edits |
16,777,194 | 51.277 | 24.380 |
crop_100_edits |
16,777,194 | 0.005 | 0.055 |
ropey_100_edits |
16,777,194 | 0.013 | 0.060 |
crop_100_edits_with_view |
16,777,194 | 110.173 | 45.565 |
ropey_100_edits_with_view |
16,777,194 | 131.001 | 55.185 |
document_100_textarea_queries |
16,777,194 | 579.388 | 964.545 |
Full records: native CSV, browser CSV.
Both ropes make isolated edits and indexed queries much cheaper. In this workload, recreating the full display string makes candidate edits slower than the current document. Retain current production storage while implementing worker/viewport rendering; choose storage against the resulting access pattern rather than adding a mirrored rope alongside a full string. Whole-source reference conversions remain in the comparison; production document queries use the incremental index described below.
Incremental coordinates and shared projections
The document now maintains logical-line and raw/native UTF-16 prefixes across edit transactions, grouped Undo/Redo and IME previews. Updates re-scan the changed rows and shift suffix coordinates; queries binary-search a row and scan only within it. Line/comment/reindent/selection commands reuse those rows. Folded projections share immutable text, normalized textarea text and visible-row coordinates until source or folds change. Read-only view preparation does not change document identity.
One observation on the same native host and Chrome version on 2026-10-06:
| Workload | Bytes | Native ms | Browser WASM ms |
|---|---|---|---|
document_100_edits |
2,097,144 | 5.850 | 5.270 |
document_100_textarea_queries |
2,097,144 | 98.997 | 124.210 |
document_indexed_100_textarea_queries |
2,097,144 | 0.004 | 0.010 |
document_cold_projection |
2,097,144 | 3.209 | 3.035 |
document_1000_warm_projections |
2,097,144 | 0.007 | 0.015 |
document_100_edits |
16,777,194 | 69.710 | 42.660 |
document_100_textarea_queries |
16,777,194 | 767.601 | 1025.380 |
document_indexed_100_textarea_queries |
16,777,194 | 0.013 | 0.025 |
document_cold_projection |
16,777,194 | 29.591 | 24.435 |
document_1000_warm_projections |
16,777,194 | 0.008 | 0.015 |
Full records: native CSV, browser CSV. The indexed query workload performs 100 paired byte-to-native/native-to-byte queries; the legacy reference performs 100 byte-to-native queries. Cold projection includes its first allocation; warm access clones shared handles 1,000 times. Setup is outside the timed section, and browser clock resolution limits precision for the shortest workloads.
This removes whole-prefix coordinate scans on ordinary short-line files and repeated projection allocations. It does not bound long-line scans, avoid full String copies on edits, or eliminate suffix-coordinate shifts. Native textarea input still owns full projected text. These microbenchmarks do not establish input/scroll latency, total memory use or full-editor large-file limits.
Long wrapped lines
The existing both-mode Unicode/CRLF regression keeps two cursors deep inside the same large wrapped line. It checks exact Down/Up restoration, unchanged source/history and fewer than 2,000 DOM range measurements. Paint now splits oversized tokens into Unicode-safe text runs, retaining complete grapheme clusters and escaped source. In the local debug browser fixture, splitting runs reduced elapsed fixture time from 9.05 seconds to 1.27 seconds; a Down press used 225 ranges in approximately 130 ms. This is a regression observation, not a claim of final large-file responsiveness.
Syntax preparation now runs in a Rust/WASM worker, with bounded shared cache retention, validated coordinates and coalesced requests. The built-worker Chrome check exercises all providers, incremental Unicode/CRLF, a UI event during preparation and oversized-source fallback. These correctness checks do not measure total editor memory or latency.
Remaining work: bounded cold and fine wrapped viewport paint, incremental input/source access, responsiveness and memory validation at the admission boundaries, and real-device verification. The production baselines below establish observations rather than completing that validation. Storage microbenchmarks do not prove those items complete.
Production view and process memory
python3 tools/measure-editor-view.py runs the built app against disposable
Spin/SQLite accounts and fresh Chrome process trees. It hydrates the same saved
editor source in local and remote modes, inserts text through Chrome's native input
path, scrolls to 70% of the document, and waits for current source/layout paint.
Wrapped readiness additionally requires an exact height table. Local cases do not
grant an OS directory handle; these measure editor behavior rather than filesystem
permissions. Each source contains Unicode, and multi-line sources retain CRLF.
The harness records load-to-paint, input-to-paint, scroll-to-paint, frame intervals,
main-thread long tasks, main WASM committed memory before/after input, DOM size, and
Chrome process-tree RSS at 200 ms intervals. Linux additionally reports apportioned
PSS when every owned process's smaps_rollup is readable. Summed RSS counts shared
pages repeatedly; it is not unique resident memory. Main WASM allocation excludes
worker instances; process-tree measurements include their hosting renderer. Samples
can miss brief peaks. These are complete application workloads, including recovery,
background preparation and deferred saves, rather than isolated core operations.
Input timing includes the native event through paint observation; cold timing also
includes navigation and recovery transport. Results are single observations, not
percentiles or pass/fail latency budgets.
On 2026-10-06, macOS arm64 and Chrome 148, the built geometry checkpoint 645c91f
(with concurrent branding/welcome working changes) produced these observations.
The raw records identify the compiled JS module hash, browser version and checkout.
| Workload | Mode | Wrap | Cold ms | Input ms | Scroll ms | Main WASM after input MiB | Sampled peak summed RSS GiB |
|---|---|---|---|---|---|---|---|
| 58 KB / 1,001 rows | Local | Off | 550 | 37 | 31 | 22.8 | 1.02 |
| 58 KB / 1,001 rows | Remote | Off | 525 | 36 | 6 | 22.9 | 1.02 |
| 2 MiB / 36,158 rows | Local | Off | 1,035 | 254 | 124 | 89.0 | 1.34 |
| 2 MiB / 36,158 rows | Remote | Off | 1,027 | 265 | 124 | 91.0 | 1.38 |
| 100,000 rows | Local | Off | 896 | 202 | 134 | 46.1 | 1.38 |
| 100,000 rows | Remote | Off | 1,177 | 200 | 131 | 46.1 | 1.40 |
| Near 8 MiB byte limit | Local | Off | 1,541 | 358 | 148 | 239.9 | 1.66 |
| Near 8 MiB byte limit | Remote | Off | 1,631 | 331 | 145 | 234.2 | 1.68 |
| 58 KB / 1,001 rows | Local | On | 694 | 167 | 26 | 25.9 | 1.10 |
| 58 KB / 1,001 rows | Remote | On | 721 | 174 | 24 | 26.1 | 1.10 |
| Near 1 MiB single-line limit | Local | On | 2,122 | 447 | 17 | 48.4 | 3.76 |
| Near 1 MiB single-line limit | Remote | On | 2,159 | 402 | 11 | 49.1 | 4.39 |
Full records: unwrapped JSONL, wrapped JSONL. To repeat:
NO_COLOR=true spin build
CHROMEDRIVER=<driver> CHROME=<chrome> python3 tools/measure-editor-view.py \
--cases small medium line-limit byte-limit
CHROMEDRIVER=<driver> CHROME=<chrome> python3 tools/measure-editor-view.py \
--cases small long-line --wrap
An earlier separate 2 MiB wrapped local attempt timed out waiting for WebDriver to observe cold paint. It did not establish a completed latency or memory result, and remote completion at that size was not checked. The near-1 MiB wrapped line also produced frame stalls above 1.5 seconds and several GiB of sampled summed RSS despite a bounded final DOM. A bounded warm row count alone therefore does not prove bounded shaping, cold layout, native input or overall memory.
These results contradict treating the current admission limits as validated responsiveness limits. Finish bounded cold measurement/paint, finer rendering within very long wrapped rows, and incremental input/source access; then repeat these cases with memory sampling, including Linux PSS, and verify the previously unresponsive wrapped workloads before closing the roadmap item.
Cold measurement batches
The batched-layout working tree on 2026-10-06 (checkout f628df9, concurrent
branding changes, built JS openwebide-frontend-1866e76a7cfeee5f.js) completed
both previously unverified 2 MiB wrapped cases. The same Chrome 148/macOS harness
used bounded temporary row DOM, task yields and a frame turn every eight batches.
These are single observations, including concurrent host workloads; summed RSS
retains the shared-page caveat above.
| Workload | Mode | Cold ms | Input ms | Scroll ms | Main WASM after input MiB | Sampled peak summed RSS GiB |
|---|---|---|---|---|---|---|
| 58 KB / 1,001 rows | Local | 711 | 91 | 17 | 24.4 | 1.08 |
| 58 KB / 1,001 rows | Remote | 762 | 107 | 32 | 24.6 | 1.09 |
| 2 MiB / 36,158 rows | Local | 2,366 | 2,579 | 89 | 105.4 | 2.02 |
| 2 MiB / 36,158 rows | Remote | 2,616 | 2,628 | 89 | 104.6 | 2.03 |
Full records: batched wrapped JSONL. The larger cases still produce frame stalls around 533 ms and rebuild the complete height table after input. Progressing preparation accepts ordinary native typing and retains queued arrows, but ordered edits/IME/clipboard after a queued arrow still need work. Incremental height reuse, finer shaping within long logical rows, native input/source access and Linux PSS/device validation remain open. Completing these cases does not validate the admission boundaries as responsiveness limits.
Incremental styled-row reuse
The follow-on working tree at checkout 166ba6b, built JS
openwebide-frontend-9099131cd6992d5e.js, reuses exact unchanged styled rows after
edits. The same Chrome 148/macOS workload on 2026-10-06 produced:
| Workload | Mode | Cold ms | Input ms | Scroll ms | Main WASM after input MiB | Sampled peak summed RSS GiB |
|---|---|---|---|---|---|---|
| 2 MiB / 36,158 rows | Local | 2,375 | 315 | 89 | 102.6 | 1.81 |
| 2 MiB / 36,158 rows | Remote | 2,673 | 303 | 87 | 103.4 | 1.82 |
Full records: incremental wrapped JSONL. Localized and disjoint edits reuse unchanged paint only when text, token styles, line endings, font/width and ownership match. Identical rows with conflicting previous heights are remeasured. Browser contracts count actual measurement DOM for single-row changes, insertion/deletion, undo and two distant edits, and verify full remeasurement after font invalidation in both modes.
Input-to-paint improves from roughly 2.6 seconds to 303–315 ms. Cold preparation still takes 2.4–2.7 seconds, with frame stalls around 550 ms; native input retains full projected text. The long-line/byte/row admission boundaries, Linux PSS and real-device workflows still need the remaining validation described above. These single observations establish improvement, not final responsiveness limits.
Sparse long-line coordinates
The follow-on working tree at checkout 9392227 on 2026-10-06 adds immutable
checkpoints approximately every 512 bytes inside long logical rows. Document
native-UTF-16/byte and character-column queries binary-search a checkpoint, then
scan only its tail. Fold projections share the same row indexes, and transactions
rebuild the edit region while retaining indexes outside it. Short rows allocate no
checkpoint array. The same macOS arm64 host and Chrome 148 ran these release
workloads; setup and correctness assertions are outside timed queries.
| Workload | Bytes | Native ms | Browser WASM ms |
|---|---|---|---|
| Long-line document construction | 65,522 | 0.124 | 0.120 |
| Full-prefix reference, 100 native queries | 65,522 | 3.699 | 4.910 |
| Indexed document, 100 paired byte/native queries | 65,522 | 0.071 | 0.170 |
| Cold folded projection | 65,522 | 0.054 | 0.055 |
| Indexed projection, 100 paired byte/native queries | 65,522 | 0.072 | 0.160 |
| Indexed character columns, 100 queries | 65,522 | 0.003 | 0.010 |
| Long-line document construction | 1,048,574 | 1.895 | 1.835 |
| Full-prefix reference, 100 native queries | 1,048,574 | 56.506 | 76.030 |
| Indexed document, 100 paired byte/native queries | 1,048,574 | 0.005 | 0.015 |
| Cold folded projection | 1,048,574 | 0.739 | 0.840 |
| Indexed projection, 100 paired byte/native queries | 1,048,574 | 0.004 | 0.010 |
| Indexed character columns, 100 queries | 1,048,574 | 0.001 | 0.010 |
Full records: native CSV, browser CSV. Queries repeat at one fixed position three quarters into Unicode/tab/standalone-CR text ending in CRLF; checkpoint alignment differs between sizes. Tiny timings approach browser clock resolution and are observations rather than thresholds. Reference queries are one direction while indexed queries include both directions. These measurements do not establish native input/shaping, fine wrapped paint, memory or admission-boundary responsiveness. Grapheme indexing for browser geometry still scans a complete line.