| 1 | # Native replica typing probe — 2026-09-13 |
| 2 | |
| 3 | This bounded review experiment exercises the real `CRDTReplica` actor and its |
| 4 | statically linked WASM host. It is not a UI responsiveness, device, release-build, |
| 5 | or web/native performance parity benchmark. No production algorithm was changed. |
| 6 | The executable was an arm64 macOS Swift debug test build. Other integration work |
| 7 | was running on the machine, so these single samples are directional evidence. |
| 8 | |
| 9 | | Mode | Characters | Elapsed seconds | Snapshot bytes | Total emitted update bytes | Largest update bytes | Client IDs | |
| 10 | | --- | ---: | ---: | ---: | ---: | ---: | ---: | |
| 11 | | Fresh presentation after every character | 100 | 1.221 | 1,635 | 1,802 | 27 | 101 | |
| 12 | | Fresh presentation after every character | 1,000 | 26.734 | 15,901 | 17,867 | 27 | 1,001 | |
| 13 | | Fresh presentation after every character | 5,000 | stopped, no completed sample | — | — | — | — | |
| 14 | | Fresh presentation after each 100-character batch | 100 | 0.014 | 159 | 127 | 127 | 2 | |
| 15 | | Fresh presentation after each 100-character batch | 1,000 | 0.120 | 1,192 | 1,178 | 127 | 11 | |
| 16 | | Fresh presentation after each 100-character batch | 5,000 | 0.673 | 5,792 | 5,858 | 127 | 51 | |
| 17 | | One retained source, individual character commands | 100 | 0.137 | 159 | 1,810 | 28 | 2 | |
| 18 | | One retained source, individual character commands | 1,000 | 1.813 | 1,060 | 19,755 | 28 | 2 | |
| 19 | | One retained source, individual character commands | 5,000 | 11.442 | 5,060 | 99,755 | 28 | 2 | |
| 20 | |
| 21 | The fresh-source mode acknowledges each presentation immediately and retires the |
| 22 | old generation before the next publication. The retained-source control never |
| 23 | publishes another presentation, approximating a burst whose callbacks still refer |
| 24 | to one displayed generation; it is not a proposed safe replacement algorithm. |
| 25 | Batch mode inserts 100 ASCII characters per command and then presents. All modes |
| 26 | start with one paragraph and assert the resulting text length at every milestone. |
| 27 | Wall time includes actor commands, internal snapshots/projections, source retention, |
| 28 | and (where specified) presentations and their selection-anchor resolution. It |
| 29 | excludes initial open/paragraph creation, network, native text layout, and rendering. |
| 30 | Milestone snapshot/client queries add a little work to subsequent cumulative times. |
| 31 | |
| 32 | Per-character new client IDs materially increase work and persisted CRDT overhead |
| 33 | in this workload. At 1,000 characters the fresh-source snapshot is 15,901 bytes, |
| 34 | about 0.19% of the 8 MiB bridge boundary. The bridge does not expose an encoded |
| 35 | state-vector command, so exact vector bytes were not measured. For these fresh |
| 36 | workloads, client IDs and clocks fit unsigned 32-bit varints: a conservative |
| 37 | encoded-vector bound is `5 + 10 * clientCount` bytes (five bytes each for ID and |
| 38 | clock plus five for count). That is at most 10,015 bytes for the measured 1,001 |
| 39 | clients. This is a bound, not an observed size or a long-document capacity claim. |
| 40 | Latency becomes material well before the measured byte boundary. Rich formatting, |
| 41 | large documents, long sessions, peer updates, and device memory limits remain |
| 42 | unmeasured; batching is not validated here as a correctness-preserving fix. |
| 43 | |
| 44 | The first 5,000 fresh-generation run was stopped after roughly four minutes to |
| 45 | release SwiftPM's build lock for integration. SwiftPM buffered its XCTest output, |
| 46 | so no completed 5,000 result or intermediate metrics were recovered. The smaller |
| 47 | fresh-source sample and both controls were then run directly with `xctest`, which |
| 48 | does not hold the SwiftPM lock and emits milestones immediately. A separate 100 |
| 49 | character sample took 1.244 seconds with a 1,638-byte snapshot/101 clients; small |
| 50 | size differences reflect randomly assigned client IDs. |
| 51 | |
| 52 | ## Repeating the probe |
| 53 | |
| 54 | `NativePerformanceProbeTests.swift` is intentionally outside the normal test |
| 55 | source tree. Temporarily copy it into `native/apple/Tests/DocsKitTests/`, build the |
| 56 | test bundle once with a 100-character limit, and remove the copied source after |
| 57 | measurement. Use the actual built test-bundle path for your machine: |
| 58 | |
| 59 | ```sh |
| 60 | DOCS_PROBE_MODE=fresh DOCS_PROBE_COUNT=100 swift test --package-path native/apple --filter NativePerformanceProbeTests |
| 61 | DOCS_PROBE_MODE=fresh DOCS_PROBE_COUNT=1000 /Applications/Xcode.app/Contents/Developer/usr/bin/xctest -XCTest DocsKitTests.NativePerformanceProbeTests/testTypingProbe native/apple/.build/arm64-apple-macosx/debug/DocsNativePackageTests.xctest |
| 62 | ``` |
| 63 | |
| 64 | Set mode to `batch` or `retained` and count to `5000` for the controls. Run modes |
| 65 | sequentially and bound runtime. Recorded executable SHA-256: |
| 66 | `3ce4222fa0981fc444f85bb858cdebca921eecf26c5eb50d4e44657ed62b2485`. |
| 67 | Original local logs: `/private/tmp/docs-probe-fresh1000.log`, |
| 68 | `/private/tmp/docs-probe-batch.log`, `/private/tmp/docs-probe-retained.log`. |
| 69 | |
| 70 | ## Acknowledged identical-source reuse followup |
| 71 | |
| 72 | After the independently reviewed `presentation(..., allowReuse: true)` change, |
| 73 | mode `reuse` explicitly acknowledges the current published generation before every |
| 74 | next character and requests reuse. Every character still performs a presentation |
| 75 | and selection-anchor resolution. Only the same live source with byte-identical |
| 76 | committed state can be returned; this does not batch away commands or render steps. |
| 77 | |
| 78 | | Characters | Elapsed seconds | Snapshot bytes | Total update bytes | Largest update | Client IDs | |
| 79 | | --- | ---: | ---: | ---: | ---: | ---: | |
| 80 | | 100 | 0.329 | 159 | 1,810 | 28 | 2 | |
| 81 | | 1,000 | 3.303 | 1,060 | 19,755 | 28 | 2 | |
| 82 | | 5,000 | 30.845 | 5,060 | 99,755 | 28 | 2 | |
| 83 | |
| 84 | The 1,000-character sample falls from 26.734 to 3.303 seconds (about 8.1 times |
| 85 | faster in these individual runs), with client count bounded at two. The 5,000 |
| 86 | sample now completes, but there is no completed pre-change 5,000 timing to form a |
| 87 | ratio. It remains slower than the retained-source control, which omits all |
| 88 | presentation/anchor work, and much slower than 100-character batches. These data |
| 89 | support reduced source churn; they do not establish a device latency budget or |
| 90 | solve the cost of projecting and snapshotting long documents on every keystroke. |
| 91 | The source-reuse regression separately proves peer divergence and undo still |
| 92 | publish fresh generations and preserve peer content, and default/no-acknowledgement |
| 93 | calls still use fresh sources. |
| 94 | |
| 95 | This followup was also measured amid integration builds. Its initial separate |
| 96 | 100-character compile-phase sample took 2.727 seconds, versus 0.329 seconds in the |
| 97 | direct run, demonstrating machine-load sensitivity. The table is the single direct |
| 98 | run's cumulative milestones, not selected best timings. Direct log: |
| 99 | `/private/tmp/docs-probe-reuse5000.log`. Repeat using `DOCS_PROBE_MODE=reuse`. |
| 100 | Followup executable SHA-256: |
| 101 | `b08998210ac83bec46a8cb2d62c51c0e2792b0c65e647f52fa58eaee879a3c55`. |
| 102 | |