# Profiling, memory and crashes

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.

> **Before you start**
>
> Profiles and heap snapshots are taken through Metro, so Metro must be running. Each command attaches to `localhost:8081`; pass `--metro <url>` for another port, and `--app <id>` when several apps share one Metro.

## Profile the JS thread

```shell
squiggle perf profile --duration 10           # sample the JS thread for 10 seconds
squiggle 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

```shell
squiggle perf heap        # take a heap snapshot
# … use the screen you suspect a few times …
squiggle perf heap        # take another
squiggle 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.

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.

```shell
squiggle perf crashes                     # find this app's new crash reports
squiggle perf crashes --commit            # and don't list them again next time
squiggle perf symbolicate --write         # turn JS frames back into your source lines
squiggle 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](https://squiggle.sh/docs/replay/incidents).

## Open a session in another tool

```shell
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.

```shell
squiggle motion sheet --tap open-sheet                       # fit the spring the sheet opens with
squiggle motion sheet --tap open-sheet,close-sheet --json > sheet.json
squiggle 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](https://squiggle.sh/docs/reference/cli#flags-for-motion).

## 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](https://squiggle.sh/docs/reference/cli).

## 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.

```shell
squiggle probe inputs                                   # what a rule can read
squiggle probe when 'fps < 45' 'scroll.moved == 1' --id janky-scroll
squiggle 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:

```shell
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.
