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.

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 whose props carried no real value change, grouped by prop signature. The title names the React.memo + useCallback fix.
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.

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. 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, or jump to the inspector with the blamed component focused.
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 tables: eighteen of data — sessions, metrics and metrics_1s (raw and one-second rollups), spans, renders, memo_misses, gestures, navigations, actions, errors, logs, profiles, recordings, intent_runs, escalations, heals, insights, baselines — plus producer_leases and ingest_stats, which are the writer bookkeeping that keeps two producers from double-writing the same session.

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

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.

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.

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.