Profiling, memory and crashes

For AI agents: this page is Markdown at https://squiggle.sh/docs/performance/profiling.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.

When a finding points at slow JS, growing memory or a crash, these commands get you the detail: a CPU profile, heap snapshots to compare, and native crash reports matched to the session.

Profile the JS thread

Terminal
squiggle perf profile --duration 10           # sample the JS thread for 10 secondssquiggle perf profile --duration 10 --out ./checkout.cpuprofile

Use the app while it samples. The profile is saved with the session, and the Performance screen’s Timeline section draws it as a flamegraph: widest first, click a frame to zoom into it. --out also writes a .cpuprofile file that Chrome DevTools can open. It needs a React Native version whose DevTools has the Performance panel.

In Studio, the Data section’s Run via CLI section runs these commands for you: Profile (10s), Heap snapshot, Crashes (CLI) and Symbolicate (CLI).

Stalls are profiled for you

While squiggle flow, squiggle record or another command that records a session is attached to an app launched with squiggle up, Squiggle starts the profiler as soon as the JS thread has been busy for 200 ms, and stops it when the thread goes idle. The long-task finding for that stall then names the function that was running, and its title ends in (profiled), because profiling slows the app a little while it runs.

  • It needs Metro. It uses localhost:8081; set SQUIGGLE_METRO_URL for another address.
  • At most three stalls are profiled per session, and a capture stops after 10 seconds.
  • Squiggle connects to the app’s debugger when the command attaches and stays connected until it ends, or until three stalls are profiled. Metro allows one debugger per app, so React Native DevTools disconnects for that time. Reconnect it afterwards.
  • A stall already under way when the command attaches is not profiled.
  • It needs a React Native version whose DevTools has the Performance panel. On older versions stalls are not profiled.
  • To turn it off, set SQUIGGLE_STALL_PROFILE=0.

Find a memory leak

Terminal
squiggle perf heap        # take a heap snapshot# … use the screen you suspect a few times …squiggle perf heap        # take anothersquiggle perf heapdiff    # what grew between the last two, and what holds on to it

heapdiff lists what grew between the two snapshots, by type, and for the biggest one, what is keeping it in memory. The Resources section shows the same comparison for the session’s two newest snapshots, and its Take heap snapshot button takes a snapshot without the terminal.

Loading the live Studio…
Memory and network requests. Live, in your browser.Open in Studio

The heap-growth detector points you here: memory that grows on every visit to the same screen.

Crashes

A native crash leaves a report on your Mac, not in the app. perf crashes looks for new reports from your app in ~/Library/Logs/DiagnosticReports and stores them with the session they happened in. In Studio, press Scan for native crashes on the Insights section; the crashes found are listed there. Studio and the CLI remember how far they have scanned, together, so neither stores a report twice.

Terminal
squiggle perf crashes                     # find this app's new crash reportssquiggle perf crashes --commit            # and don't list them again next timesquiggle perf symbolicate --write         # turn JS frames back into your source linessquiggle perf symbolicate --write --binary ./build/Checkout.app/Checkout

JS frames are resolved through Metro; Studio’s Symbolicate JS stacks button in the Data section does the same. Native frames need your app’s binary or dSYM, passed with --binary; without it they are reported as unresolved rather than left looking resolved.

Studio can also save a recording of the seconds before a crash; see Incident capture.

Open a session in another tool

Terminal
squiggle perf export --trace ./checkout-trace.json

In Studio, Export trace in the Data section does the same for the session you are looking at. The file opens in Perfetto or chrome://tracing, with the JS thread, the network, the UI and React renders on their own tracks, and metrics as counters.

Match an animation’s spring

To copy a motion you like, or check that a refactor kept it, squiggle motion taps a control, records how a view moves after the tap, and prints the withSpring config that reproduces it. Give it the view’s testID and the testID to tap; with two, it taps them in turn, so a motion that goes there and back is recorded both ways.

Terminal
squiggle motion sheet --tap open-sheet                       # fit the spring the sheet opens withsquiggle motion sheet --tap open-sheet,close-sheet --json > sheet.jsonsquiggle motion sheet --tap open-sheet,close-sheet --match sheet.json   # after you change the config

The printed config sets mass: 1. Reanimated’s default mass is 4, so keep the mass when you paste it. A motion that is not a plain spring, such as one retargeted while it runs, is reported as NOT a plain spring from rest with no config. It needs an iOS simulator and the app launched with squiggle up. Every flag is in the CLI reference.

Count things in a running app

Some questions need a number rather than a profile: how many animations are running right now, or how many times a component rendered. squiggle probe asks the running app over Metro, and can reach the Reanimated UI runtime as well as the JS one. See the CLI reference.

Watch for a condition inside the app

A probe is a set of rules the app checks for you while you use it, with no rebuild: fps < 45 while a list scrolls, or a component rendering five times in one tick. You need a development build on a simulator; a release build never runs probes.

Terminal
squiggle probe inputs                                   # what a rule can readsquiggle probe when 'fps < 45' 'scroll.moved == 1' --id janky-scrollsquiggle probe when 'render.watch0 >= 5' --component ProductCard --for 30

A rule is an input, a comparison (>=, >, <=, <, ==, !=) and a number. The probe fires when all of its rules hold, or any of them with --any. Squiggle prints a line each time the rules start or stop holding, with the inputs at that moment, and removes the probe when you press Ctrl-C or --for runs out. To count one component’s renders, name it with --component (up to eight) and read render.watch0, render.watch1 and so on, in the same order.

The app refuses a probe it cannot run, and says why: an input it does not have, a range wider than the input’s, more than 256 instructions in one probe, or more than eight probes at once. Each check costs about 0.6 µs per instruction on the simulator, so a probe of a few rules costs a few microseconds a tick.

Keep a probe as a detector

When a probe has caught what you were looking for, export it with the same rules and id. Squiggle writes a TypeScript function that checks the rules, the firings it recorded as a fixture, and a test that checks the function against every one of them:

Terminal
squiggle probe export 'fps < 45' 'scroll.moved == 1' --id janky-scroll --out src/detectors

Export refuses rules that do not reproduce what the probe sent, so the fixture always matches the function. The test is written for Bun; pass --runner vitest or --runner jest for yours. The firings come from the newest session that has any for that id; --session picks another.