Skip to content

Debugging & profiling

When a frame paints the wrong thing — a stale region, an unexpected full repaint, a layer that should be cached but isn’t — renderer.debug is the single source of truth. It is a core-owned probe that emits a rich per-frame record (which layers drew, why the frame was full, the exact damage rects, the cache state) and keeps a rolling history ring you can copy into a bug report.

Frame debug HUD Read live per-frame cost from the renderer's onFrameTiming callback.

It is zero-cost when off: constructing the probe does nothing observable, and the renderer assembles no record unless the probe is capturing. Touching renderer.debug on a hot renderer is safe.

const probe = renderer.debug; // lazily constructed; does NOT start capturing
const unsubscribe = probe.onFrame((d) => {
console.log(d.frame, d.kind, d.fullCause?.reason, d.drawCalls);
for (const layer of d.layers) {
console.log(layer.label, layer.cacheState, layer.instances, layer.glyphs);
}
});
// or capture without a listener (e.g. just to fill the history ring):
probe.enable();
// ...later...
probe.disable();
unsubscribe();

renderer.debug is a RendererDebug:

MemberDescription
onFrame(cb)Subscribe to the per-frame FrameDebug event. Returns an unsubscribe fn. Attaching a listener turns capture on.
enable() / disable()Toggle explicit capture. Capture stays on while any listener is attached.
isCapturingWhether the renderer will assemble a record this frame (enabled OR ≥1 listener).
ringThe FrameRing of frame summaries.
lastLayersThe exact Layer[] from the most recent captured render(), or null — mutate visibility / cache-hint by identity.
requestRender()Re-render the retained last layers as a full frame.
layerAabb(layer, space?)Project the union of a layer’s instance AABBs into a DebugRect in space (default "ui"), or null.

FrameDebugListener is (d: FrameDebug) => void.

The live event carries direct Layer references and a per-frame SpaceMap. Key fields:

FieldDescription
frame / kindMonotonic frame index; "full" (clear whole canvas) or "partial" (scissored).
fullCause{ reason, changed? } — why a full frame fired. On a "view-changed" full, changed lists the fingerprint parts that moved.
regionsCaller / regionsFinalDamage rects as the caller passed them, and post-pad/clamp (what the GPU scissored). Space-tagged "ui".
autoDamagePer-layer provenance for damage: "auto" frames (which layer produced which rects).
spacesThe frame’s SpaceMap.
layersOne LayerDebugInfo per Layer, in z-sorted draw order.
cacheBake budget / used bytes / per-bake state / evictions / demotions this frame.
emphasisDecoded setEmphasis mirror ({ focusedKey, dimAlpha, t }).
totalMs / uploadMs / encodeMs / drawCalls / damageRectsFrame timing reused from FrameTiming.

Each LayerDebugInfo carries layer (direct ref), label, zIndex, space, visible, instances, glyphs, cost, cacheState ("live" | "baked-below" | "baked-above" | "bake-demoted" | "hidden"), and the layer’s debugData.

A FingerprintPart (in fullCause.changed and config types) is one of "camera" | "size" | "dpr" | "background" | "view-key" | "z-order" | "visibility" | "buffer-layer" | "emphasis" — so you can see exactly which view input forced a full repaint.

The probe keeps a fixed-capacity ring of lean per-frame summaries (no Layer refs, no matrices) so history survives without retaining a scene graph, and a bug report can paste a timeline.

import { FRAME_RING_CAPACITY } from "insomni/internal"; // 240
const timeline = renderer.debug.ring.toArray(); // oldest → newest
const json = renderer.debug.ring.exportJson(); // paste into a bug report
const { full, partial } = renderer.debug.ring.ratio();
ExportDescription
FrameRingpush, toArray() (oldest→newest), ratio(), exportJson(), clear(), size, capacity.
FRAME_RING_CAPACITY240 — default ring capacity.
summarize(d)Derive a scalar FrameSummary from a rich FrameDebug.
FrameSummaryPer-frame scalars: frame, kind, reason, changed, rectCount, rectArea, timing, and layers.
LayerSummaryPer-layer scalar row: label, cacheState, instances, glyphs.

Debug rects cross between four spaces. SpaceMap is the per-frame snapshot needed to convert between them, and convertRect is the one pure converter — ui is the hub all conversions route through.

DebugSpaceMeaning
"world"Camera-transformed authoring coordinates.
"ui"device ÷ dpr (“CSS px”). Core’s damage regions live here.
"device"Raw backing-store px (canvas.width); ui × dpr.
"css-layout"The canvas CSS layout box (clientWidth/clientHeight).
import { captureSpaceMap, convertRect, type RectXYWH } from "insomni/internal";
const map = captureSpaceMap({
camera, // ResolvedCameraState
deviceSize: { w: canvas.width, h: canvas.height },
dpr,
cssBox: { w: canvas.clientWidth, h: canvas.clientHeight }, // optional
});
const uiRect: RectXYWH = { x: 10, y: 10, width: 40, height: 20 };
const worldRect = convertRect(uiRect, "ui", "world", map);
ExportDescription
captureSpaceMap({ camera, deviceSize, dpr, cssBox? })Build a SpaceMap (pure, no DOM). worldToUi is the exact world→ui map the draw/scissor path uses, including under HiDPI.
convertRect(rect, from, to, map)Convert a RectXYWH between spaces via the ui hub. World↔ui transforms all four corners (correct under rotation).
DebugSpaceThe four-space union above.
SpaceMap{ dpr, deviceSize, uiSize, cssBox, worldToUi }.
DebugSize{ w, h }.
RectXYWH{ x, y, width, height } (no space tag — caller passes from/to).

These config-surface types describe the debug record’s space-tagged rects and fingerprint parts: FrameDebug, LayerDebugInfo, DebugRect ({ space, x, y, width, height }), and FingerprintPart. They are exported from the package barrel for presenters that type the event.

The render-debug probe answers what the renderer did this frame. For CPU/GPU timing of named spans across your own code, use the profiler; for a ready-made UI that presents the probe, use the monitor.

  • insomni-profilerprofile(name, fn) / startSpan(name).end() record named CPU/GPU spans into pluggable sinks (DevTools marks/measures). Disabled by default; enable via setEnabled(true) or the ?profile=1 URL flag. The disabled path is a fast inline branch.
  • insomni-monitor — a lightweight telemetry/control panel that subscribes to renderer.debug and presents frames, regions, layer state, and the cache budget without you wiring a panel by hand.