---
title: "profiler"
description: "The profiler namespace — the engine's Luau API reference for profiler."
section: "API Reference"
slug: "api-profiler"
canonical: "https://origozero.ai/docs/api-profiler"
updated: "2026-09-05T23:13:47.041366815+00:00"
tags: ["api", "reference"]
---

# profiler

The `profiler` namespace — 63 functions.

## globals/profiler/begin {#globals-profiler-begin}

```lua
profiler.begin(name: string)
```

Start a named profiling block. Call `profiler.finish(name)` to
record the duration. Blocks appear in `profiler.stats()` under
`"script.<name>"` and inside captures.

**Parameters**

- `name` `string` — Block name (e.g. "MyComponent.update").

```lua
profiler.begin("MyComponent.update"); ...; profiler.finish()
```

## globals/profiler/disableRing {#globals-profiler-disablering}

```lua
profiler.disableRing()
```

Disable the ring buffer and clear its history.

```lua
profiler.disableRing()
```

## globals/profiler/enableRing {#globals-profiler-enablering}

```lua
profiler.enableRing(seconds: number?) -> boolean
```

Enable the always-recording ring buffer, retaining the last
`seconds` of per-frame data (default 20). Query it AFTER the fact
with `profiler.retro()` — latency-immune, since the data is
historical. Editor profile only: returns false in the runtime
profile. The enable is the gate; the ring costs nothing until on.

**Parameters**

- `seconds` `number` _(optional)_ — Seconds of history to retain (default 20).

**Returns** `boolean` — True if enabled, false if refused (runtime profile).

```lua
if profiler.enableRing(30) then ... end
```

## globals/profiler/finish {#globals-profiler-finish}

```lua
profiler.finish(name: string?) -> number?
```

Finish a profiling block and record the elapsed duration as
`"script.<name>"`. Without an argument, closes the most-recently-
begun block (LIFO stack). With a name, closes the most recent
block whose name matches — useful when blocks of different names
are nested.

**Parameters**

- `name` `string` _(optional)_ — Block name to finish. Omit to pop the top of the stack.

**Returns** `number?` — Elapsed milliseconds, or nil if no matching block was active.

```lua
local ms = profiler.finish("MyComponent.update")
```

## globals/profiler/gpuFrame {#globals-profiler-gpuframe}

```lua
profiler.gpuFrame() -> GpuFrameReport
```

Label-aggregated GPU pass timings over the last `window_frames`
resolved frames, measured with GPU timestamp queries. `supported`
is false when the device lacks timestamp queries — `spans` stays
empty. Each span covers every render/compute pass recorded under
one label — `compute.<shader>` per compute dispatch, `scene.*` for
the scene passes, `post.<effect>` per post-process effect,
`feature.*` for render-feature passes: `ms` is the median of its
per-frame totals, `min_ms`/`max_ms` the range that median sits in,
`count` the passes per frame and `frames` how much of the window
carried it. `at_floor` marks a label whose every sample landed
within a few ticks of the device's timestamp counter (`tick_ms`) —
those passes ran and the device resolved no duration for them,
which is not the same as a measured zero. `ran` is whether the
label recorded a measured pass in the newest resolved frame, and
`last_frame` the newest frame that did. The window outlives the
work it describes, so a pass that stops being recorded leaves a row
standing for up to `window_frames` frames carrying the median of
the frames it did run in: read `ran` to answer whether a pass is
running, `frame - last_frame` for how many resolved frames ago it
last did, and `ms` as the cost of the frames it ran in.
`frame_span_ms` (first pass begin to last pass end) and
`total_ms` are medians too, so
rows do not sum to `total_ms`, and the GPU may overlap passes so
`total_ms` can exceed `frame_span_ms`. The readback is
asynchronous: the window lags the live frame by a few frames.

**Returns** `GpuFrameReport` — GPU timing window, spans ranked by median ms descending.

```lua
local g = profiler.gpuFrame(); print(g.frame_span_ms, g.spans[1] and g.spans[1].label)
```

## globals/profiler/hits {#globals-profiler-hits}

```lua
profiler.hits(label: string?) -> string?
```

Drain the watchdog's recorded hit frames into a capture stored
under `label` (default `"watch_hits"`) and clear the buffer.
Returns the capture JSON (same shape as `stopCapture`), or nil if
there were no hits.

**Parameters**

- `label` `string` _(optional)_ — Capture label to store under (default "watch_hits").

**Returns** `string?` — Capture JSON of the hit frames, or nil if none.

```lua
local json = profiler.hits()
```

## globals/profiler/isCapturing {#globals-profiler-iscapturing}

```lua
profiler.isCapturing() -> boolean
```

Check if a profiler capture is currently active.

**Returns** `boolean` — True if a capture is in progress.

```lua
if profiler.isCapturing() then ... end
```

## globals/profiler/lastCapture {#globals-profiler-lastcapture}

```lua
profiler.lastCapture() -> string?
```

Get the most recent completed capture result as a JSON string.
Same shape as `profiler.stopCapture()`. Returns nil if no capture
has been completed yet.

**Returns** `string?` — JSON string of the last capture, or nil.

```lua
local last = profiler.lastCapture()
```

## globals/profiler/retro {#globals-profiler-retro}

```lua
profiler.retro(seconds: number?, label: string?) -> { [string]: any }?
```

Retroactively aggregate the last `seconds` of the ring (default:
the whole ring). The full per-frame capture is retained under `label`
(default `"retro"`) for in-engine drill-down (the `frame` / `hotspots`
tools);
this RETURNS a compact structured aggregate table (frame-time distribution
+ per-system summary), never the raw per-frame array — bounded, so it is
safe over the ZeroMind bridge. Code-facing primitive; the `retro` tool
renders the agent-facing report. Latency-immune: the data is historical.

**Parameters**

- `seconds` `number` _(optional)_ — How many seconds back to include (default: whole ring).
- `label` `string` _(optional)_ — Capture label to store under (default "retro").

**Returns** `{ [string]: any }?` — A compact aggregate `{ label, source, frames, seconds, exclude_agent, agent_frames, dt = { avg, p50, p90, p99, max, min }, summary = {...} }`, or nil if the ring holds nothing.

```lua
local agg = profiler.retro(8, "collapse")
```

## globals/profiler/ringStatus {#globals-profiler-ringstatus}

```lua
profiler.ringStatus() -> string
```

Ring buffer status as a JSON string:
`{ enabled, frames, capacity, span_seconds }`.

**Returns** `string` — JSON status string.

```lua
local s = profiler.ringStatus()
```

## globals/profiler/startCapture {#globals-profiler-startcapture}

```lua
profiler.startCapture(label: string?) -> boolean
```

Start recording per-frame profiler data. Each frame's system
timings are captured until `stopCapture()` is called. Results are
accessible via `profiler.lastCapture()` and VFS at
`/zero/runtime/profiler/<label>.json`.

**Parameters**

- `label` `string` _(optional)_ — Capture label (default `"capture"`).

**Returns** `boolean` — True if capture started, false if a capture is already active.

```lua
if profiler.startCapture("frame-spike") then ... end
```

## globals/profiler/stats {#globals-profiler-stats}

```lua
profiler.stats(pattern: string?) -> { ProfilerStat }
```

Get current EMA profiling statistics from the SystemProfiler.
Optional glob pattern filters by metric name (supports `*` and `?`
wildcards). Each entry carries two averages: `avg_ms` averages one
RUN of the block and is folded when the block runs, so a block that
has stopped running keeps the last value it saw; `avg_frame_ms`
averages one FRAME and is folded every frame, including the frames
the block did not run in, so it is the block's share of the current
frame and falls back to zero once the block stops running.

**Parameters**

- `pattern` `string` _(optional)_ — Filter pattern (e.g. "schedule.*", "system.schedule.render.*").

**Returns** `{ ProfilerStat }` — Array of profiler block stats.

```lua
for _, s in ipairs(profiler.stats("schedule.*")) do print(s.name, s.avg_frame_ms) end
```

## globals/profiler/stopCapture {#globals-profiler-stopcapture}

```lua
profiler.stopCapture() -> string?
```

Stop the active profiler capture and return its result as a
JSON string. The capture is also saved to VFS at
`/zero/runtime/profiler/<label>.json`. Top-level fields: `label`,
`frame_count`, `started_at`, `ended_at`, `frames`, `summary`.
Compute duration as `ended_at - started_at`.

**Returns** `string?` — JSON capture result, or nil if no capture was active.

```lua
local json = profiler.stopCapture()
```

## globals/profiler/unwatch {#globals-profiler-unwatch}

```lua
profiler.unwatch()
```

Disarm the watchdog. Recorded hits are kept for a final
`profiler.hits()`.

```lua
profiler.unwatch()
```

## globals/profiler/watch {#globals-profiler-watch}

```lua
profiler.watch(ceilingMs: number, mode: string?, excludeAgent: boolean?, maxHits: number?) -> boolean
```

Arm the frame-time watchdog. When a frame's EFFECTIVE time
(total minus agent-injected `execute` cost) crosses `ceilingMs`,
mode `"record"` logs every offending frame (read with
`profiler.hits()`), and mode `"pause"` pauses gameplay ONCE to
freeze the bad state, then disarms. Editor profile only: returns
false in the runtime profile. `excludeAgent` (default true) keeps
the agent's own calls from tripping it.

**Parameters**

- `ceilingMs` `number` — Effective frame-time ceiling in ms.
- `mode` `string` _(optional)_ — "record" (default) or "pause".
- `excludeAgent` `boolean` _(optional)_ — Subtract agent cost before comparing (default true).
- `maxHits` `number` _(optional)_ — Max frames retained in record mode (default 240).

**Returns** `boolean` — True if armed, false if refused (runtime profile).

```lua
if profiler.watch(50, "pause") then ... end
```

## globals/profiler/watchStatus {#globals-profiler-watchstatus}

```lua
profiler.watchStatus() -> string
```

Watchdog status as a JSON string: `{ armed, ceiling_ms, mode,
exclude_agent, hits, dropped_hits, tripped }`.

**Returns** `string` — JSON status string.

```lua
local s = profiler.watchStatus()
```

## modules/profiler/README {#modules-profiler-readme}

```lua
require("@builtin/modules/api/engine/profiler") -- profiler (also available as global 'profiler')
```

Frame-level profiler capture, EMA stats, and named profiling blocks. Public Luau surface over the `__profiler` Internal FFI namespace.

Usage: local profiler = require("@builtin/modules/api/engine/profiler")
Also available as global: profiler

## modules/profiler/begin {#modules-profiler-begin}

```lua
begin(name: string)
```

Start a named profiling block. Call `profiler.finish(name)` to
record the duration. Blocks appear in `profiler.stats()` under
`"script.<name>"` and inside captures.

**Parameters**

- `name` `string` — Block name (e.g. "MyComponent.update").

```lua
profiler.begin("MyComponent.update"); ...; profiler.finish()
```

## modules/profiler/disableRing {#modules-profiler-disablering}

```lua
disableRing()
```

Disable the ring buffer and clear its history.

```lua
profiler.disableRing()
```

## modules/profiler/enableRing {#modules-profiler-enablering}

```lua
enableRing(seconds: number?): boolean
```

Enable the always-recording ring buffer, retaining the last
`seconds` of per-frame data (default 20). Query it AFTER the fact
with `profiler.retro()` — latency-immune, since the data is
historical. Editor profile only: returns false in the runtime
profile. The enable is the gate; the ring costs nothing until on.

**Parameters**

- `seconds` `number?` _(optional)_ — Seconds of history to retain (default 20).

```lua
if profiler.enableRing(30) then ... end
```

## modules/profiler/finish {#modules-profiler-finish}

```lua
finish(name: string?): number?
```

Finish a profiling block and record the elapsed duration as
`"script.<name>"`. Without an argument, closes the most-recently-
begun block (LIFO stack). With a name, closes the most recent
block whose name matches — useful when blocks of different names
are nested.

**Parameters**

- `name` `string?` _(optional)_ — Block name to finish. Omit to pop the top of the stack.

```lua
local ms = profiler.finish("MyComponent.update")
```

## modules/profiler/gpuFrame {#modules-profiler-gpuframe}

```lua
gpuFrame(): GpuFrameReport
```

Label-aggregated GPU pass timings over the last `window_frames`
resolved frames, measured with GPU timestamp queries. `supported`
is false when the device lacks timestamp queries — `spans` stays
empty. Each span covers every render/compute pass recorded under
one label — `compute.<shader>` per compute dispatch, `scene.*` for
the scene passes, `post.<effect>` per post-process effect,
`feature.*` for render-feature passes: `ms` is the median of its
per-frame totals, `min_ms`/`max_ms` the range that median sits in,
`count` the passes per frame and `frames` how much of the window
carried it. `at_floor` marks a label whose every sample landed
within a few ticks of the device's timestamp counter (`tick_ms`) —
those passes ran and the device resolved no duration for them,
which is not the same as a measured zero. `ran` is whether the
label recorded a measured pass in the newest resolved frame, and
`last_frame` the newest frame that did. The window outlives the
work it describes, so a pass that stops being recorded leaves a row
standing for up to `window_frames` frames carrying the median of
the frames it did run in: read `ran` to answer whether a pass is
running, `frame - last_frame` for how many resolved frames ago it
last did, and `ms` as the cost of the frames it ran in.
`frame_span_ms` (first pass begin to last pass end) and
`total_ms` are medians too, so
rows do not sum to `total_ms`, and the GPU may overlap passes so
`total_ms` can exceed `frame_span_ms`. The readback is
asynchronous: the window lags the live frame by a few frames.

```lua
local g = profiler.gpuFrame(); print(g.frame_span_ms, g.spans[1] and g.spans[1].label)
```

## modules/profiler/hits {#modules-profiler-hits}

```lua
hits(label: string?): string?
```

Drain the watchdog's recorded hit frames into a capture stored
under `label` (default `"watch_hits"`) and clear the buffer.
Returns the capture JSON (same shape as `stopCapture`), or nil if
there were no hits.

**Parameters**

- `label` `string?` _(optional)_ — Capture label to store under (default "watch_hits").

```lua
local json = profiler.hits()
```

## modules/profiler/isCapturing {#modules-profiler-iscapturing}

```lua
isCapturing(): boolean
```

Check if a profiler capture is currently active.

```lua
if profiler.isCapturing() then ... end
```

## modules/profiler/lastCapture {#modules-profiler-lastcapture}

```lua
lastCapture(): string?
```

Get the most recent completed capture result as a JSON string.
Same shape as `profiler.stopCapture()`. Returns nil if no capture
has been completed yet.

```lua
local last = profiler.lastCapture()
```

## modules/profiler/measure<T...> {#}

```lua
measure<T...>(name: string, fn: () -> T...): T...
```

Run a function inside a profiling block. Equivalent to a
begin/finish pair but handles errors correctly. Returns the
function's return values.

**Parameters**

- `name` `string` — Block name.
- `fn` `() -> T...` — Function to profile.

```lua
local count = profiler.measure("walk", function() return walk() end)
```

## modules/profiler/retro {#modules-profiler-retro}

```lua
retro(seconds: number?, label: string?): { [string]: any }?
```

Retroactively aggregate the last `seconds` of the ring (default:
the whole ring). The full per-frame capture is retained under `label`
(default `"retro"`) for in-engine drill-down (the `frame` / `hotspots`
tools);
this RETURNS a compact structured aggregate table (frame-time distribution
+ per-system summary), never the raw per-frame array — bounded, so it is
safe over the ZeroMind bridge. Code-facing primitive; the `retro` tool
renders the agent-facing report. Latency-immune: the data is historical.

**Parameters**

- `seconds` `number?` _(optional)_ — How many seconds back to include (default: whole ring).
- `label` `string?` _(optional)_ — Capture label to store under (default "retro").

```lua
local agg = profiler.retro(8, "collapse")
```

## modules/profiler/ringStatus {#modules-profiler-ringstatus}

```lua
ringStatus(): string
```

Ring buffer status as a JSON string:
`{ enabled, frames, capacity, span_seconds }`.

```lua
local s = profiler.ringStatus()
```

## modules/profiler/startCapture {#modules-profiler-startcapture}

```lua
startCapture(label: string?): boolean
```

Start recording per-frame profiler data. Each frame's system
timings are captured until `stopCapture()` is called. Results are
accessible via `profiler.lastCapture()` and VFS at
`/zero/runtime/profiler/<label>.json`.

**Parameters**

- `label` `string?` _(optional)_ — Capture label (default `"capture"`).

```lua
if profiler.startCapture("frame-spike") then ... end
```

## modules/profiler/stats {#modules-profiler-stats}

```lua
stats(pattern: string?): { ProfilerStat }
```

Get current EMA profiling statistics from the SystemProfiler.
Optional glob pattern filters by metric name (supports `*` and `?`
wildcards). Each entry carries two averages: `avg_ms` averages one
RUN of the block and is folded when the block runs, so a block that
has stopped running keeps the last value it saw; `avg_frame_ms`
averages one FRAME and is folded every frame, including the frames
the block did not run in, so it is the block's share of the current
frame and falls back to zero once the block stops running.

**Parameters**

- `pattern` `string?` _(optional)_ — Filter pattern (e.g. "schedule.*", "system.schedule.render.*").

```lua
for _, s in ipairs(profiler.stats("schedule.*")) do print(s.name, s.avg_frame_ms) end
```

## modules/profiler/stopCapture {#modules-profiler-stopcapture}

```lua
stopCapture(): string?
```

Stop the active profiler capture and return its result as a
JSON string. The capture is also saved to VFS at
`/zero/runtime/profiler/<label>.json`. Top-level fields: `label`,
`frame_count`, `started_at`, `ended_at`, `frames`, `summary`.
Compute duration as `ended_at - started_at`.

```lua
local json = profiler.stopCapture()
```

## modules/profiler/unwatch {#modules-profiler-unwatch}

```lua
unwatch()
```

Disarm the watchdog. Recorded hits are kept for a final
`profiler.hits()`.

```lua
profiler.unwatch()
```

## modules/profiler/watch {#modules-profiler-watch}

```lua
watch(ceilingMs: number, mode: string?, excludeAgent: boolean?, maxHits: number?): boolean
```

Arm the frame-time watchdog. When a frame's EFFECTIVE time
(total minus agent-injected `execute` cost) crosses `ceilingMs`,
mode `"record"` logs every offending frame (read with
`profiler.hits()`), and mode `"pause"` pauses gameplay ONCE to
freeze the bad state, then disarms. Editor profile only: returns
false in the runtime profile. `excludeAgent` (default true) keeps
the agent's own calls from tripping it.

**Parameters**

- `ceilingMs` `number` — Effective frame-time ceiling in ms.
- `mode` `string?` _(optional)_ — "record" (default) or "pause".
- `excludeAgent` `boolean?` _(optional)_ — Subtract agent cost before comparing (default true).
- `maxHits` `number?` _(optional)_ — Max frames retained in record mode (default 240).

```lua
if profiler.watch(50, "pause") then ... end
```

## modules/profiler/watchStatus {#modules-profiler-watchstatus}

```lua
watchStatus(): string
```

Watchdog status as a JSON string: `{ armed, ceiling_ms, mode,
exclude_agent, hits, dropped_hits, tripped }`.

```lua
local s = profiler.watchStatus()
```

## tools/profiler/compare {#tools-profiler-compare}

```lua
profiler.compare(before?: string, after?: string) -> string
```

Diff two recordings (made with `record`): the change in avg/p90 frametime, and the systems that moved the most between them. Record a baseline, make a change, record again, then compare to see whether the change helped and which system it moved. A positive frametime delta means B is slower than A.

**Parameters**

- `before` `string` _(optional)_
- `after` `string` _(optional)_

**Returns** `string`

```lua
"baseline", "optimized"  -- did 'optimized' beat 'baseline', and where
```

## tools/profiler/flamegraph {#tools-profiler-flamegraph}

```lua
profiler.flamegraph(seconds?: number, mode?: ("run" | "start" | "stop" | "snapshot"), label?: string) -> string
```

Sample the Luau call stack to find the hottest code paths — the depth view under a hot component or `task_scheduler`. `flamegraph(seconds)` runs the sampler for that long (resetting first) and returns the top stacks plus a folded-stack file for flamegraph rendering. `mode` picks a manual phase instead: "start" / "stop" a long session, or "snapshot" the current top stacks without stopping.

**Parameters**

- `seconds` `number` _(optional)_
- `mode` `("run" | "start" | "stop" | "snapshot")` _(optional)_
- `label` `string` _(optional)_

**Returns** `string`

```lua
-- 3s sample, top stacks + folded file
5          -- 5s sample
0, "snapshot" -- top stacks right now without stopping a session
```

## tools/profiler/frame {#tools-profiler-frame}

```lua
profiler.frame(source?: string, which?: ("worst" | "typical"), minMs?: number) -> string
```

The current frame's time as a self-accounting tree: schedule -> system -> sub-timing, each row showing SELF (own time excluding children), TOTAL, calls, and % of frame. Time no schedule covers (the frame limiter / vsync wait, GPU present, event loop) shows as `present / idle`; each expensive component's update(dt) is listed by component + entity under `lua_update.vm_call`. Read the live frame with source "now", or a stopped recording by its label (its worst or typical frame). Read from top: the row with the largest SELF time is where the frame time actually goes.

**Parameters**

- `source` `string` _(optional)_
- `which` `("worst" | "typical")` _(optional)_
- `minMs` `number` _(optional)_

**Returns** `string`

```lua
-- live frame, full attributed tree
"combat", "worst"     -- the worst frame of the 'combat' recording
"combat", "typical"   -- the typical (average) frame of that recording
```

## tools/profiler/gpu {#tools-profiler-gpu}

```lua
profiler.gpu(n?: number) -> string
```

GPU pass timings over the last resolved frames, from GPU timestamp queries. Each row is one label: the median of its per-frame total, the min/max that median sits in, the passes per frame, and how many of the window's frames carried it. A row marked `floor` ran but the device resolved no duration for it — its two timestamps retired within a few ticks of each other. The `last` column is how many resolved frames ago the label last recorded a pass: `now` is a pass running in the frame the table describes, and anything else is a row the window still holds after the work under it stopped. The readback is asynchronous, so the window lags the live frame by a few frames. Use this when `frame` shows the time under `present / idle` (GPU-bound) and you need to know which passes the GPU spends it on.

**Parameters**

- `n` `number` _(optional)_

**Returns** `string`

```lua
-- top 20 GPU spans of the window
40     -- top 40
```

## tools/profiler/hits {#tools-profiler-hits}

```lua
profiler.hits(label?: string, spikeMs?: number) -> string
```

Drain the frames a record-mode `watch` caught and report them as a spike-cluster distribution — every caught frame grouped by its dominant hotspot, so repeated hitches collapse to their handful of causes instead of a wall of individual frames. Draining clears the buffer. The snapshot is stored under `label` — drill into any cluster with `frame <label>` / `hotspots <label>`. Arm the watchdog first with `watch <ceilingMs>`.

**Parameters**

- `label` `string` _(optional)_
- `spikeMs` `number` _(optional)_

**Returns** `string`

```lua
-- review everything the watchdog caught
"collapse_hits"    -- store under a name for later drill-down
```

## tools/profiler/hotspots {#tools-profiler-hotspots}

```lua
profiler.hotspots(n?: number, source?: string, which?: ("worst" | "typical")) -> string
```

Rank the frame's costs by SELF time (a node's own cost, excluding its children) and return the top `n` as a flat table. Because it ranks by SELF, the top rows are the actual expensive leaves — a system's own work or a single heavy component's update(dt) — not the schedules that merely contain them. This is the "just tell me what's slow" tool; follow a hit down with `frame`. Reads the live frame ("now") or a stopped recording's worst/typical frame (pass its label).

**Parameters**

- `n` `number` _(optional)_
- `source` `string` _(optional)_
- `which` `("worst" | "typical")` _(optional)_

**Returns** `string`

```lua
-- top 12 costs in the live frame
20           -- top 20
12, "combat" -- top 12 in the worst frame of the 'combat' recording
```

## tools/profiler/memory {#tools-profiler-memory}

```lua
profiler.memory(n?: number) -> string
```

The Luau VM's memory: total heap + GC state, then the components retaining the most memory (per instance). Use it to catch a growing script — take it, play/test, take it again, and watch which component's retained bytes climb. Complements the frame-time tools: memory pressure shows up as GC cost in `frame` (the `gc` node) and as crashes under load, not as one slow system.

**Parameters**

- `n` `number` _(optional)_

**Returns** `string`

```lua
-- VM total + top 20 components by retained memory
10    -- top 10
```

## tools/profiler/record {#tools-profiler-record}

```lua
profiler.record(action?: ("start" | "stop" | "status"), label?: string, spikeMs?: number) -> string
```

Start / stop / check a background profiling recording that spans a play session. `record("start", label)` begins capturing every frame; play or test the game across as many turns as you want, then `record("stop", label)` returns the windowed breakdown: avg / p50 / p90 / p99 / max frametime, spike count, the single WORST frame's attributed tree, and a typical-frame tree. `record("status")` reports whether a recording is running. A stopped recording is kept under its label — analyse it later with `frame`, `hotspots`, or `scripts` (pass the label as their source), or diff two of them with `compare`.

**Parameters**

- `action` `("start" | "stop" | "status")` _(optional)_
- `label` `string` _(optional)_
- `spikeMs` `number` _(optional)_

**Returns** `string`

```lua
"start", "combat"      -- begin recording a combat encounter
"stop", "combat"       -- end it, get the windowed breakdown
"status"               -- is a recording running right now?
```

## tools/profiler/retro {#tools-profiler-retro}

```lua
profiler.retro(seconds?: number, label?: string, spikeMs?: number) -> string
```

Retroactively read the ring buffer's last `seconds` of frames (default: the whole ring) as a spike-cluster report — the latency-immune profiler. Enable the ring first (`ring on`), drive the scene, then call this AFTER the spike: the data is historical, so your call's timing does not matter. The report groups every spike frame by its dominant hotspot (so one call shows the full distribution of what's slow, not one anecdote), on EFFECTIVE frame time (your own `execute` cost excluded). The snapshot is stored under `label` — drill into any cluster with `frame <label>` / `hotspots <label>`.

**Parameters**

- `seconds` `number` _(optional)_
- `label` `string` _(optional)_
- `spikeMs` `number` _(optional)_

**Returns** `string`

```lua
-- the whole ring, clustered
8               -- just the last 8 seconds
8, "collapse"   -- last 8s, stored as 'collapse' for drill-down
```

## tools/profiler/ring {#tools-profiler-ring}

```lua
profiler.ring(action?: ("on" | "off" | "status"), seconds?: number) -> string
```

Control the retroactive ring buffer — an always-recording, bounded history of the last N seconds of per-frame data you query AFTER the fact with `retro`. `ring("on", seconds)` starts it (default 20s); `ring("off")` stops and clears it; `ring("status")` reports whether it's on, how many frames and seconds it holds. Editor profile only — a no-op in the runtime profile. The ring is off until you turn it on, so it costs nothing until then. This is the fix for "I can't profile a spike I only see afterwards".

**Parameters**

- `action` `("on" | "off" | "status")` _(optional)_
- `seconds` `number` _(optional)_

**Returns** `string`

```lua
"on", 30      -- keep the last 30 seconds, always
"status"      -- is the ring on? how much does it hold?
"off"         -- stop recording and clear the history
```

## tools/profiler/scripts {#tools-profiler-scripts}

```lua
profiler.scripts(n?: number, minMs?: number, type_?: string) -> string
```

Rank components by update(dt) cost — the content view of where the frame's script time goes. Rolled up per component type by default (many instances of a type collapse to `Type xN` with summed cost; a lone instance keeps its `@ entity`), so it stays readable whether a world has three scripts or three hundred. Pass a `type` to drill into that one type's individual instances (which entity is the heavy one). Use it after `hotspots`/`frame` point at `lua_update.vm_call`. Counts COMPONENT update loops; a scene entrypoint's per-frame `update` / `editorUpdate` runs on the scheduler, and `scene.cost` ranks the loaded scenes by what theirs costs.

**Parameters**

- `n` `number` _(optional)_
- `minMs` `number` _(optional)_
- `type_` `string` _(optional)_

**Returns** `string`

```lua
-- every component type, heaviest first
10       -- the 10 heaviest types
20, 0.5, "MyMover" -- instances of MyMover costing >= 0.5ms, by entity
```

## tools/profiler/tasks {#tools-profiler-tasks}

```lua
profiler.tasks(n?: number, minMs?: number) -> string
```

Rank components by the time the scheduler spent resuming their coroutines this frame — the content breakdown of `task_scheduler`. Rolled up per component type (many instances collapse to `Type xN`; a lone instance keeps its `@ entity`). Use this when `frame`/`hotspots` show `task_scheduler` hot and you need to know whose `task.spawn` / `task.wait` work is behind it.

**Parameters**

- `n` `number` _(optional)_
- `minMs` `number` _(optional)_

**Returns** `string`

```lua
-- every component's coroutine cost, heaviest first
10       -- the 10 heaviest
```

## tools/profiler/watch {#tools-profiler-watch}

```lua
profiler.watch(ceilingMs?: (number | "off" | "status"), mode?: ("record" | "pause"), excludeAgent?: boolean) -> string
```

Arm a frame-time watchdog that catches bad frames without you having to poll (which always lands seconds late). Call with a number to arm: `watch(50)` records every frame whose EFFECTIVE time (agent cost excluded) is >= 50ms; `watch(50, "pause")` instead pauses gameplay the first time the ceiling is crossed, freezing the bad state for you to inspect (then read it with `retro`). `watch("off")` disarms; `watch("status")` (or no arg) reports state and hit count. Read recorded hits with the `hits` tool. Editor profile only — returns a refusal in the runtime profile. `excludeAgent` (default true) keeps your own `execute`/write frames from tripping it.

**Parameters**

- `ceilingMs` `(number | "off" | "status")` _(optional)_
- `mode` `("record" | "pause")` _(optional)_
- `excludeAgent` `boolean` _(optional)_

**Returns** `string`

```lua
50                 -- record every frame over 50ms effective
50, "pause"        -- pause gameplay the first time a frame exceeds 50ms
"status"           -- armed? how many hits so far?
"off"              -- disarm
```

## typed/builtin//modules/api/engine/profiler/profiler/begin {#typed-builtin-modules-api-engine-profiler-profiler-begin}

```lua
profiler.begin(name: string)
```

Start a named profiling block. Call `profiler.finish(name)` to
record the duration. Blocks appear in `profiler.stats()` under
`"script.<name>"` and inside captures.

**Parameters**

- `name` `string` — Block name (e.g. "MyComponent.update").

```lua
profiler.begin("MyComponent.update"); ...; profiler.finish()
```

## typed/builtin//modules/api/engine/profiler/profiler/disableRing {#typed-builtin-modules-api-engine-profiler-profiler-disablering}

```lua
profiler.disableRing()
```

Disable the ring buffer and clear its history.

```lua
profiler.disableRing()
```

## typed/builtin//modules/api/engine/profiler/profiler/enableRing {#typed-builtin-modules-api-engine-profiler-profiler-enablering}

```lua
profiler.enableRing(seconds: number?) -> boolean
```

Enable the always-recording ring buffer, retaining the last
`seconds` of per-frame data (default 20). Query it AFTER the fact
with `profiler.retro()` — latency-immune, since the data is
historical. Editor profile only: returns false in the runtime
profile. The enable is the gate; the ring costs nothing until on.

**Parameters**

- `seconds` `number` _(optional)_ — Seconds of history to retain (default 20).

**Returns** `boolean` — True if enabled, false if refused (runtime profile).

```lua
if profiler.enableRing(30) then ... end
```

## typed/builtin//modules/api/engine/profiler/profiler/finish {#typed-builtin-modules-api-engine-profiler-profiler-finish}

```lua
profiler.finish(name: string?) -> number?
```

Finish a profiling block and record the elapsed duration as
`"script.<name>"`. Without an argument, closes the most-recently-
begun block (LIFO stack). With a name, closes the most recent
block whose name matches — useful when blocks of different names
are nested.

**Parameters**

- `name` `string` _(optional)_ — Block name to finish. Omit to pop the top of the stack.

**Returns** `number?` — Elapsed milliseconds, or nil if no matching block was active.

```lua
local ms = profiler.finish("MyComponent.update")
```

## typed/builtin//modules/api/engine/profiler/profiler/gpuFrame {#typed-builtin-modules-api-engine-profiler-profiler-gpuframe}

```lua
profiler.gpuFrame() -> GpuFrameReport
```

Label-aggregated GPU pass timings over the last `window_frames`
resolved frames, measured with GPU timestamp queries. `supported`
is false when the device lacks timestamp queries — `spans` stays
empty. Each span covers every render/compute pass recorded under
one label — `compute.<shader>` per compute dispatch, `scene.*` for
the scene passes, `post.<effect>` per post-process effect,
`feature.*` for render-feature passes: `ms` is the median of its
per-frame totals, `min_ms`/`max_ms` the range that median sits in,
`count` the passes per frame and `frames` how much of the window
carried it. `at_floor` marks a label whose every sample landed
within a few ticks of the device's timestamp counter (`tick_ms`) —
those passes ran and the device resolved no duration for them,
which is not the same as a measured zero. `ran` is whether the
label recorded a measured pass in the newest resolved frame, and
`last_frame` the newest frame that did. The window outlives the
work it describes, so a pass that stops being recorded leaves a row
standing for up to `window_frames` frames carrying the median of
the frames it did run in: read `ran` to answer whether a pass is
running, `frame - last_frame` for how many resolved frames ago it
last did, and `ms` as the cost of the frames it ran in.
`frame_span_ms` (first pass begin to last pass end) and
`total_ms` are medians too, so
rows do not sum to `total_ms`, and the GPU may overlap passes so
`total_ms` can exceed `frame_span_ms`. The readback is
asynchronous: the window lags the live frame by a few frames.

**Returns** `GpuFrameReport` — GPU timing window, spans ranked by median ms descending.

```lua
local g = profiler.gpuFrame(); print(g.frame_span_ms, g.spans[1] and g.spans[1].label)
```

## typed/builtin//modules/api/engine/profiler/profiler/hits {#typed-builtin-modules-api-engine-profiler-profiler-hits}

```lua
profiler.hits(label: string?) -> string?
```

Drain the watchdog's recorded hit frames into a capture stored
under `label` (default `"watch_hits"`) and clear the buffer.
Returns the capture JSON (same shape as `stopCapture`), or nil if
there were no hits.

**Parameters**

- `label` `string` _(optional)_ — Capture label to store under (default "watch_hits").

**Returns** `string?` — Capture JSON of the hit frames, or nil if none.

```lua
local json = profiler.hits()
```

## typed/builtin//modules/api/engine/profiler/profiler/isCapturing {#typed-builtin-modules-api-engine-profiler-profiler-iscapturing}

```lua
profiler.isCapturing() -> boolean
```

Check if a profiler capture is currently active.

**Returns** `boolean` — True if a capture is in progress.

```lua
if profiler.isCapturing() then ... end
```

## typed/builtin//modules/api/engine/profiler/profiler/lastCapture {#typed-builtin-modules-api-engine-profiler-profiler-lastcapture}

```lua
profiler.lastCapture() -> string?
```

Get the most recent completed capture result as a JSON string.
Same shape as `profiler.stopCapture()`. Returns nil if no capture
has been completed yet.

**Returns** `string?` — JSON string of the last capture, or nil.

```lua
local last = profiler.lastCapture()
```

## typed/builtin//modules/api/engine/profiler/profiler/retro {#typed-builtin-modules-api-engine-profiler-profiler-retro}

```lua
profiler.retro(seconds: number?, label: string?) -> { [string]: any }?
```

Retroactively aggregate the last `seconds` of the ring (default:
the whole ring). The full per-frame capture is retained under `label`
(default `"retro"`) for in-engine drill-down (the `frame` / `hotspots`
tools);
this RETURNS a compact structured aggregate table (frame-time distribution
+ per-system summary), never the raw per-frame array — bounded, so it is
safe over the ZeroMind bridge. Code-facing primitive; the `retro` tool
renders the agent-facing report. Latency-immune: the data is historical.

**Parameters**

- `seconds` `number` _(optional)_ — How many seconds back to include (default: whole ring).
- `label` `string` _(optional)_ — Capture label to store under (default "retro").

**Returns** `{ [string]: any }?` — A compact aggregate `{ label, source, frames, seconds, exclude_agent, agent_frames, dt = { avg, p50, p90, p99, max, min }, summary = {...} }`, or nil if the ring holds nothing.

```lua
local agg = profiler.retro(8, "collapse")
```

## typed/builtin//modules/api/engine/profiler/profiler/ringStatus {#typed-builtin-modules-api-engine-profiler-profiler-ringstatus}

```lua
profiler.ringStatus() -> string
```

Ring buffer status as a JSON string:
`{ enabled, frames, capacity, span_seconds }`.

**Returns** `string` — JSON status string.

```lua
local s = profiler.ringStatus()
```

## typed/builtin//modules/api/engine/profiler/profiler/startCapture {#typed-builtin-modules-api-engine-profiler-profiler-startcapture}

```lua
profiler.startCapture(label: string?) -> boolean
```

Start recording per-frame profiler data. Each frame's system
timings are captured until `stopCapture()` is called. Results are
accessible via `profiler.lastCapture()` and VFS at
`/zero/runtime/profiler/<label>.json`.

**Parameters**

- `label` `string` _(optional)_ — Capture label (default `"capture"`).

**Returns** `boolean` — True if capture started, false if a capture is already active.

```lua
if profiler.startCapture("frame-spike") then ... end
```

## typed/builtin//modules/api/engine/profiler/profiler/stats {#typed-builtin-modules-api-engine-profiler-profiler-stats}

```lua
profiler.stats(pattern: string?) -> { ProfilerStat }
```

Get current EMA profiling statistics from the SystemProfiler.
Optional glob pattern filters by metric name (supports `*` and `?`
wildcards). Each entry carries two averages: `avg_ms` averages one
RUN of the block and is folded when the block runs, so a block that
has stopped running keeps the last value it saw; `avg_frame_ms`
averages one FRAME and is folded every frame, including the frames
the block did not run in, so it is the block's share of the current
frame and falls back to zero once the block stops running.

**Parameters**

- `pattern` `string` _(optional)_ — Filter pattern (e.g. "schedule.*", "system.schedule.render.*").

**Returns** `{ ProfilerStat }` — Array of profiler block stats.

```lua
for _, s in ipairs(profiler.stats("schedule.*")) do print(s.name, s.avg_frame_ms) end
```

## typed/builtin//modules/api/engine/profiler/profiler/stopCapture {#typed-builtin-modules-api-engine-profiler-profiler-stopcapture}

```lua
profiler.stopCapture() -> string?
```

Stop the active profiler capture and return its result as a
JSON string. The capture is also saved to VFS at
`/zero/runtime/profiler/<label>.json`. Top-level fields: `label`,
`frame_count`, `started_at`, `ended_at`, `frames`, `summary`.
Compute duration as `ended_at - started_at`.

**Returns** `string?` — JSON capture result, or nil if no capture was active.

```lua
local json = profiler.stopCapture()
```

## typed/builtin//modules/api/engine/profiler/profiler/unwatch {#typed-builtin-modules-api-engine-profiler-profiler-unwatch}

```lua
profiler.unwatch()
```

Disarm the watchdog. Recorded hits are kept for a final
`profiler.hits()`.

```lua
profiler.unwatch()
```

## typed/builtin//modules/api/engine/profiler/profiler/watch {#typed-builtin-modules-api-engine-profiler-profiler-watch}

```lua
profiler.watch(ceilingMs: number, mode: string?, excludeAgent: boolean?, maxHits: number?) -> boolean
```

Arm the frame-time watchdog. When a frame's EFFECTIVE time
(total minus agent-injected `execute` cost) crosses `ceilingMs`,
mode `"record"` logs every offending frame (read with
`profiler.hits()`), and mode `"pause"` pauses gameplay ONCE to
freeze the bad state, then disarms. Editor profile only: returns
false in the runtime profile. `excludeAgent` (default true) keeps
the agent's own calls from tripping it.

**Parameters**

- `ceilingMs` `number` — Effective frame-time ceiling in ms.
- `mode` `string` _(optional)_ — "record" (default) or "pause".
- `excludeAgent` `boolean` _(optional)_ — Subtract agent cost before comparing (default true).
- `maxHits` `number` _(optional)_ — Max frames retained in record mode (default 240).

**Returns** `boolean` — True if armed, false if refused (runtime profile).

```lua
if profiler.watch(50, "pause") then ... end
```

## typed/builtin//modules/api/engine/profiler/profiler/watchStatus {#typed-builtin-modules-api-engine-profiler-profiler-watchstatus}

```lua
profiler.watchStatus() -> string
```

Watchdog status as a JSON string: `{ armed, ceiling_ms, mode,
exclude_agent, hits, dropped_hits, tripped }`.

**Returns** `string` — JSON status string.

```lua
local s = profiler.watchStatus()
```
