Performance

Profilers give you samples and leave the interpretation to you. Squiggle records the causal graph alongside the timings, so a finding can name the component, the request or the query — and carry the row ids it was derived from.

Loading the live Studio…
A session’s performance at a glance. Live, in your browser.Open in Studio

It is always on

Every session — a flow run, a recording, a Studio connection — attaches a telemetry sink and leaves queryable data behind in .squiggle/perf/perf.db with zero flags. There is no “profiling mode” to remember to switch on before reproducing the bug.

Three producers feed it, and they are worth telling apart because their blind spots differ:

In-app
Renders, prop changes, navigation, gestures, network, storage, SQLite, the event loop, Hermes GC stats. Everything the JS side knows.
Host process sampling
CPU (with a JS/UI per-thread split), RSS, memory footprint, thread count and thermal state — sampled from outside the app, so it costs the app nothing and keeps reporting while JS is blocked.
The injected agent
Presented-frame truth from the display link — FPS, frame-time p95, dropped frames, stutters — plus memory-pressure transitions, Core Animation commit rates, keyboard-transition spans, permission prompts, and per-request network phase timings (DNS, connect, TLS, time to first byte). This layer is alive when JS is dead, which is exactly when you want it.

Exactly one producer writes each row: when Studio is open it samples the host in-process and a relay-joined CLI run stands its own sampler down, so a shared store never double-writes.

The detectors

Twelve rules run over a session and emit findings with a severity, a title that states the fix, and evidence: the span ids, node ids or metric ids the finding came from.

Rendering

render-storm
A component committing more than 15× inside one interaction, or 60× inside a sliding second.
memo-miss
Five or more re-renders where every changed prop was a recreated object, array or function and no changed object or array carried new data, grouped by prop signature. Objects are compared to a bounded depth and functions not at all, so a finding is a candidate: stabilise the props with useCallback / useMemo.
context-blast
Bursts of twenty-plus distinct components rendering for reason context inside 200 ms — a provider whose value identity changes too often.
long-task
JS tasks over 100 ms, with the concurrent CPU sample and the nearest render attached for attribution.

Frames

jank-attribution
A dropped-frame spike or an FPS dip, with the concurrent work in a ±500 ms window ranked as suspects by time consumed — renders, GC, long tasks, network. This is the detector that answers “why did it stutter” rather than telling you that it did.

Network and storage

duplicate-request · waterfall-request
The same request twice inside three seconds (a missing cache), or three-plus strictly sequential requests totalling over half a second (a chain that could parallelise).
oversized-response · slow-ttfb
Responses over 2 MB; time-to-first-byte over a second.
slow-query
SQLite work grouped by statement fingerprint — never raw parameters — flagged on p95 or on a lone spike.

Memory and startup

heap-growth
Memory strictly increasing across three or more visits to the same route. A leak candidate that needs repeat visits to be a claim at all, so it makes none without them.
startup-regression
A startup phase more than 25% and 50 ms slower than the same phase in a baseline.
nav-loop
The same two steps — a screen and a request, or two screens — repeated four or more times in a row, more than your app’s own stored sessions ever repeat them. Judged against the app’s other sessions in the local store, so it says nothing until there are 20 of them.

The Performance Center

The desktop screen is built entirely on store readers rather than live stream state, which is why it works identically on a live session and on a replay. While a session is live the timeline reads only the spans, renders and gestures written since its last refresh, not the whole session again. Top to bottom:

Metric tiles and band chart
Newest value per metric, and any metric plotted over time.
Unified timeline
Lanes for gestures, renders, network, long tasks and navigation, plus frame-drop and GC ticks and FPS/heap sparklines, all on one shared time scale. Its playhead is two-way scrub-synced with Replay Studio — drag the timeline and the recording seeks; scrub the recording and the timeline follows.
Flamegraph
Left-heavy merge of a captured CPU profile, with click-to-zoom and pixel-level horizontal zoom and pan.
Network waterfall
Requests with their DNS / connect / TLS / TTFB phase bars, error tint, and expandable detail.
Insights panel
Findings severest-first. Evidence chips highlight the matching waterfall span, jump to the inspector with the blamed component focused, or open the component’s declaration line in your editor. While a session is live, Studio runs the detectors every few seconds: a badge counts the findings, and a new problem raises one notification — a repeat of one already announced raises nothing.
Memory
Growth by constructor between a session’s two newest heap snapshots, plus a top-retainers walk for the biggest grower.
Baseline
Snapshot the session’s aggregate shape and diff the current one against it, with comparability warnings when the two are not fairly comparable.
Runtime SQL
A read-only query box with saved queries and starter presets. Saved queries live in a shared sidecar, so one saved headless appears here and vice versa.
Crashes and symbolication
Scan the system diagnostic reports for this app’s native crashes and ingest them; symbolicate JS stacks through Metro. The scan cursor is shared with the CLI so neither re-ingests the other’s finds.
Tools
Chrome-trace export, a copyable session report, and an on-demand retention sweep.

From the terminal

Terminal
squiggle perf sql "  select name, count(*) n, avg(dur_us)/1000.0 avg_ms  from spans where kind = 1 group by name order by avg_ms desc limit 10"squiggle perf top --watch          # live newest-value-per-metric stripsquiggle perf report               # human summary of a sessionsquiggle perf saved save slow-queries "select …"

The store is the API

Raw SQL against the store is a first-class surface, not an escape hatch — the schema is documented, and the same saved queries sync with the desktop. Twenty-four tables: twenty-two of data — actions, baselines, errors, escalations, gestures, heals, incidents, insight_revisions, insights, intent_runs, logs, memo_misses, metrics, metrics_1s, metrics_60s, navigations, profiles, recordings, renders, scrolls, sessions and spans — plus producer_leases and ingest_stats, which are the writer bookkeeping that keeps two producers from double-writing the same session.

The few that need a word: metrics, metrics_1s and metrics_60s are the raw, one-second and one-minute tiers of one series; gestures carry each touch’s track and scrolls every scroll’s offsets.

Queries are read-only, and the store file is shared: the desktop and the CLI open the same perf.db, so a query you save in one is there in the other, and an agent runs it through perf_saved_query_run.

Who writes what

Every consumer, flow and record session attaches a telemetry sink automatically — no flags. When Studio is open the desktop is the single writer for the app’s event stream and the CLI attaches an inject-only sink instead, so a relay-joined run lands its own host-side producers without double-writing every row the desktop already stored.

Findings, and what to do about them

Terminal
squiggle perf insights            # a session's findings, each with its file:linesquiggle perf watch               # follow the store; print each NEW finding oncesquiggle perf detector-stats      # which detectors fire more on failed runs than clean ones

A finding about a component names the line it is declared on, in perf insights, perf report and perf diff alike. perf detector-stats joins each flow run’s assertion outcome to the findings its session produced, so you can see which detectors actually predict a broken run before you gate on them.

For an agent, propose_fix turns a stored finding into an edit plan: the component’s declaration with the code there, what the detector measured, and a fix phrased only as firmly as the evidence allows. It never writes a file — the agent makes the edit with its own tools, as a reviewable change, and re-runs the flow to confirm the finding is gone. It refuses findings that name no component, code under node_modules, and files it cannot find.

Profiling and crash triage

Terminal
squiggle perf profile --duration 10   # Hermes CPU profile via Metro CDPsquiggle perf heap                   # heap snapshotsquiggle perf heapdiff               # growth by constructor between the last twosquiggle perf crashes                # scan + ingest native .ips reportssquiggle perf symbolicate --write    # resolve JS frames (and native, with atos)

Profiles and heap snapshots are captured through the Metro inspector protocol and stored as blobs the desktop can open. Native crash reports are parsed and matched to the session; JS frames symbolicate through Metro, and native address frames need a local binary — reported as such rather than left looking resolved.

A profile gives stacks. Some questions need a count: how many times did this provider render, how many animations are live right now. squiggle probe answers those from outside the app, over the same inspector, and reaches the Reanimated UI runtime as well as the JS one. On React Native macOS it is often the only way to see your React at all. See the CLI for its ten leaves.

Regression gating

The loop that turns all of this into something CI can enforce:

Terminal
squiggle perf snapshot --label v2.4.0        # freeze this session's shape# … change something, run the flow again …squiggle perf diff --against v2.4.0squiggle perf diff --against v2.4.0 --assert ci-thresholds.json   # exit non-zero on regression

A diff reports metric, span, startup and render deltas plus the evidence-linked findings, and warns when the two sessions are not fairly comparable rather than quietly comparing them anyway. With --assert it becomes a gate — on metric deltas, on finding severity, or with failOnDetectors on the detectors perf detector-stats shows are worth failing a build for.

For flow-level speed specifically, squiggle bench is the harness — see Speed & delivery. And a flow can assert on performance directly: assert: { rerenders: { CartRow: { lt: 3 } } } fails the test, in the test, at the moment the regression appears.