|
| 1 | +# Runtime performance |
| 2 | + |
| 3 | +Run the local runtime benchmark suite against the compiled package and the |
| 4 | +pinned XState comparison versions: |
| 5 | + |
| 6 | +```sh |
| 7 | +pnpm perf:runtime |
| 8 | +``` |
| 9 | + |
| 10 | +The command reports: |
| 11 | + |
| 12 | +- pure `Machine.plan` counter-transition throughput; |
| 13 | +- end-to-end useful-increment throughput for a burst sent to one running machine; |
| 14 | +- machine start-and-stop throughput; |
| 15 | +- idle heap and resident-memory growth at 100, 500, and 1,000 live machines. |
| 16 | + |
| 17 | +The comparison dependencies use package aliases, so XState 5 and 6 can be |
| 18 | +loaded by the same process: |
| 19 | + |
| 20 | +- `xstate-v5`: `xstate@5.32.5`, the stable v5 baseline; |
| 21 | +- `xstate-v6`: `xstate@6.0.0-alpha.27`, the latest published v6 alpha available |
| 22 | + when the harness was added. |
| 23 | + |
| 24 | +All implementations use the same flat counter topology, immutable events, and |
| 25 | +terminal fence. The XState adapter uses `assign` in v5 and the v6 transition |
| 26 | +function API because `assign` is not exported by that alpha. |
| 27 | + |
| 28 | +The burst benchmark reports useful counter increments per second. It enqueues |
| 29 | +one final fence event after all counter events and awaits the machine's terminal |
| 30 | +output, so the measured duration also amortizes that fence and terminal |
| 31 | +cleanup. This measures complete queue drainage, not only the enqueue time |
| 32 | +returned by `MachineRef.send`. |
| 33 | + |
| 34 | +Results are informational. Compare runs on the same machine while it is idle, |
| 35 | +using the same Node.js and dependency versions. Tinybench warms each scenario |
| 36 | +before collecting samples, and the memory measurements force garbage |
| 37 | +collection before every observation. Each implementation's memory curve runs |
| 38 | +in a fresh child process so garbage from one library cannot distort another |
| 39 | +library's baseline. |
| 40 | + |
| 41 | +These scenarios compare observable work, not identical internals. Effect |
| 42 | +Machine plans through an `Effect`, validates schema-backed state and events, |
| 43 | +and its running machine provisions Effect queues, fibers, synchronization, |
| 44 | +change publication, and child/invoke lifecycle machinery. XState's counter is |
| 45 | +a smaller synchronous actor. Treat the comparison as an application-level |
| 46 | +cost baseline, not a claim that the libraries provide the same runtime |
| 47 | +guarantees. |
| 48 | + |
| 49 | +The fitted heap slope is the primary idle-capacity metric. Resident memory is |
| 50 | +reported as a raw diagnostic because V8 and the operating-system allocator can |
| 51 | +reuse already committed pages. The capacity-per-GiB value is a linear estimate |
| 52 | +that excludes shared process overhead; it is not a run-until-OOM limit. |
| 53 | + |
| 54 | +Use a shorter smoke run while changing the harness: |
| 55 | + |
| 56 | +```sh |
| 57 | +pnpm perf:runtime -- --quick |
| 58 | +``` |
| 59 | + |
| 60 | +Display the complete versioned report as JSON, or write a machine-readable JSON |
| 61 | +file alongside the terminal report: |
| 62 | + |
| 63 | +```sh |
| 64 | +pnpm perf:runtime -- --json |
| 65 | +pnpm perf:runtime -- --output runtime-performance.json |
| 66 | +``` |
| 67 | + |
| 68 | +Prefer `--output` for scripts: pnpm and the preceding build may add their own |
| 69 | +lines to standard output before `--json` is printed. |
| 70 | + |
| 71 | +Pull requests run the suite three times for both the base and pull request |
| 72 | +revisions on the same GitHub-hosted runner. The workflow publishes the median |
| 73 | +of those process-level results to the job summary and a sticky pull request |
| 74 | +comment. The benchmark workflow has read-only repository access; a separate |
| 75 | +trusted `workflow_run` workflow validates the uploaded JSON before receiving |
| 76 | +permission to update the comment. |
| 77 | + |
| 78 | +The implementation lives in `scripts/runtime-performance.mjs`; the Effect |
| 79 | +Machine fixture is in `perf/runtime/counter.mjs`, and the comparison adapter is |
| 80 | +in `perf/runtime/xstate.mjs`. Add new scenarios only when every implementation |
| 81 | +performs equivalent observable work and the result is consumed and checked so |
| 82 | +the JavaScript engine cannot discard it. |
0 commit comments