Benchmark a flow
For AI agents: this page is Markdown at https://squiggle.sh/docs/run/bench.md. Every docs page is listed at https://squiggle.sh/llms.txt, and all of them are one file at https://squiggle.sh/llms-full.txt.
squiggle bench runs a flow, splits each step's time into five parts, and compares the run with the last one that used the same settings. Use it to see where a run spends its time, and to catch a flow getting slower.
Run a benchmark
squiggle bench login10.5s -57ms (-1%) vs 10.5s on 2026-07-30 13:41 (74193f7+dirty)flags inproc · transport directsettleGate ██████████████████████████████ 3.1s +36ms (+1%)deliver █████████████░░░░░░░░░░░░░░░░░ 1.4s +50ms (+4%)stepDelay ██████████░░░░░░░░░░░░░░░░░░░░ 1.1s ±0ms (0%)rectStable ███████░░░░░░░░░░░░░░░░░░░░░░░ 685ms +23ms (+3%)resolve █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 76ms -7ms (-8%)phase totals · vs the last same-flags rundropped frames 67 (-20)peak RSS 512.5 MB (-99.5 MB)GC 55 (-19) runs / 132 µs (+19 µs)The first line compares the total with the last benchmark of this flow that used the same flags. A run with different flags, such as --direct-scroll, is never compared with one without them. The bars share one scale, so the part that grew is easy to spot. The last lines are the app’s own numbers for the run. A number Squiggle couldn’t measure shows as null, never 0.
Where a step’s time goes
| Part | What it is |
|---|---|
settleGate | Waiting for the app to stop moving before the step acts. Usually the largest. squiggle flow --settle-debug names what kept it waiting. |
resolve | Finding the control the selector names. |
rectStable | Waiting for the control to stop moving, so the tap doesn’t land where it used to be. |
deliver | The tap, text or scroll itself. |
stepDelay | The short pause after each step. |
Every squiggle flow run also ends with a chart of its three slowest steps, and writes each step’s timings to .squiggle/runs/.
Watch the trend
squiggle bench login --history--history lists the recent benchmarks of a flow, so slow drift shows as a trend instead of a series of small changes that each look like noise. The history is kept in .squiggle/bench/ on your Mac.
Run it several times
squiggle bench login --runs 3--runs 3 runs the flow three times and records one of the runs, not an average: the median one when all three pass. If any run fails, it records the failed run, shows it as red in the history, and exits 1, because a benchmark is also a check that the flow still works:
1.9s · 2.6s · 2.7s → recorded the failed run (1.9s)A failed run is never used as the run to compare against. The next benchmark compares with the last one that passed, and says how many failed runs it passed over. A flow that fails only on some runs usually starts from a different screen each time; give it a prerequisite.
Find the commit that slowed a flow
squiggle bisect runs a flow on the commits between one where it was fine and one where it isn’t, and names the first bad one. Each commit’s JavaScript is served to the app through its own Metro, so nothing is rebuilt, and your checkout and your own Metro are left alone. With --budget-ms, a run slower than the budget counts as bad, so it finds a slowdown as well as a failure:
squiggle bisect login --good main~8 --budget-ms 3000 --hunksbisect login: 8 commits, main~8..HEAD … 3e1c90ab Prefetch order history on sign-in: bad, 4630ms b72d0f15 Rename the sign-in screen: good, 2710ms …first bad commit: 3e1c90ab Prefetch order history on sign-in (confirmed by a re-run of it and its parent), 7 runs in 4.6 minhunks in 3e1c90ab: 2 src/orders/useOrders.ts (+9/-2) @@ -14,8 +14,15 @@ export function useOrders() {: without it good, 2740ms, accounts for 1890 of 1920ms src/auth/signIn.ts (+3/-0) @@ -40,6 +40,9 @@ export async function signIn() {: without it bad, 4610ms, accounts for 20 of 1920msreverting src/orders/useOrders.ts recovers it: 2740msA culprit is confirmed when it fails and its parent passes on a second run. --hunks then reverts each change in that commit on its own and reports how much of the slowdown each one accounts for, and which to revert to get the time back. Run times vary, so use --runs 3 to judge each commit by the median of three runs.
Before you start, check that you have:
- A booted simulator running an Expo development build of the app, with
expo.schemeset inapp.json. bisect points the development build at each commit through that scheme. If the app isn’t at the project root, pass--app-dir. - A range that only changes JavaScript. A commit that changes native code, a dependency,
package.json, a lockfile or the Expo config stops bisect before anything runs, withE_BISECT_NATIVEand the commit and file. Bisect the commits after it, or rebuild and test across it by hand. - Your app’s Metro at
http://127.0.0.1:8081. When bisect finishes it points the app back at it. If your Metro runs elsewhere, setSQUIGGLE_METRO_URL, for exampleSQUIGGLE_METRO_URL=http://127.0.0.1:8082.
If you press Ctrl-C, bisect points the app back at your Metro and removes its worktree before it exits. If the bad end passes, or the good end already fails, bisect says so and exits 1. The flags and exit codes are in the CLI reference.
Compare two implementations
When you can’t decide between two ways to build something, such as a FlatList or a mapped ScrollView, or one store or another, commit each on its own branch and let squiggle bakeoff measure them. It runs one flow on your checkout and on each branch you name, serving each one’s JavaScript through its own Metro the way bisect does, and prints one table:
squiggle bakeoff checklist-scroll --against checklist-flatlist --runs 3bakeoff checklist-scroll: HEAD vs checklist-flatlist, 3 runs each …| revision | passed | flow ms | renders / interaction | dropped frames | memory growth MB | startup ms || --- | --- | --- | --- | --- | --- | --- || HEAD | 3/3 | 8843 | 169.5 | 9 | 49.4 | 1097 || checklist-flatlist | 3/3 | 8782 | 174.5 | 9 | 52.6 | 1097 |The branches take turns, run by run, so a Mac that gets busier halfway through slows them all alike. Each number is the median of the runs that passed, and each is measured only while its own run was running: renders per tap, swipe or other interaction, frames dropped, how much the app’s memory grew, and how long the app took to start. Here the two lists are level, and the FlatList costs five more renders an interaction, so the ScrollView stays. The table is Markdown, so you can paste it into the pull request.
The same checks as bisect apply: an Expo development build with expo.scheme set, --app-dir when the app isn’t at the project root, and SQUIGGLE_METRO_URL if your Metro isn’t on port 8081. A branch whose difference from your checkout touches native code or a dependency is refused before anything runs, with E_BAKEOFF_NATIVE. Ctrl-C cleans up the same way it does for bisect.