insomni-monitor
insomni-monitor is a small, framework-free overlay UI for insomni apps. It has
two halves:
Monitor— a draggable-free, collapsible HUD with FPS / CPU / render / compute sparklines, free-form telemetry sections, and a configurable controls panel (sliders, toggles, selects, file pickers).enableRenderDebug/attachPanel— a presenter for the core renderer’srenderer.debugprobe. It does not compute any debug data; it subscribes to the probe and renders the damage overlay, frame strip, layer table, cache inspector, and emphasis readout. See Debugging & profiling for the probe itself.
It depends on insomni (for the Renderer2D type, the onRendererCreated
registry, and the renderer.debug probe). It pairs naturally with
insomni-profiler for the CPU/GPU timings you feed into
the telemetry panel.
The Monitor injects its own scoped CSS on construction — there is nothing to import or wire up beyond the JS.
The render-debug presenter
Section titled “The render-debug presenter”For most apps this is the only thing you need. enableRenderDebug() attaches a
per-renderer debug panel to every insomni renderer — both those already alive
and any created later — into one shared Monitor.
import { enableRenderDebug } from "insomni-monitor";
// App owns the gate — there is no URL parsing inside the package.if (new URLSearchParams(location.search).has("debug")) { const controller = enableRenderDebug(); // later: controller.dispose();}Calling it is unconditional — it always enables the tooling, so gate the call
yourself (the ?debug=1 pattern above is typical). It subscribes to insomni’s
onRendererCreated event, so it fires immediately for every live renderer and
again for each future one. Each renderer gets its own collapsible section labeled
Render Debug · canvas#<id> (or an index when the canvas has no id). Destroyed
renderers are reclaimed lazily via WeakRef — their panels are swept on the next
renderer creation.
enableRenderDebug(options?)
Section titled “enableRenderDebug(options?)”| Option | Type | Description |
|---|---|---|
monitor | Monitor | The Monitor to embed the per-renderer sections into. When omitted, a dedicated Monitor is created lazily and disposed by dispose(). |
Returns a RenderDebugController:
| Member | Type | Description |
|---|---|---|
dispose() | void | Detach every panel and unsubscribe the registry listener. Idempotent. |
What each panel shows
Section titled “What each panel shows”The presenter subscribes to renderer.debug (probe.onFrame) and renders, per
renderer:
- Damage overlay — a
position:fixedoverlay re-anchored to the canvas each flash. Partial frames flash their final/caller damage regions; full frames pulse a whole-canvas border colored by the frame’s reason bucket. Hovering a layer row flashes that layer’s projected AABB. - Frame strip — the core ring’s recent frames as clickable ticks (green = partial, bucket color = full). Click a tick to pin the frame inspector to it.
- Layer table — one row per layer: label, z-index, space + cache-state
badges, instance/glyph/cost counts, a live visibility checkbox, and a live
cache-hint select (
auto/always/never). Both mutate the liveLayerand callprobe.requestRender(). - Counters — bakes alive, cumulative demoted count, full/partial ratio, and the last frame’s bake events.
- Frame inspector — kind, full cause + changed list, region count/area, and timing breakdown for the pinned (or latest) frame.
- Cache inspector — a
usedBytes / budgetBytesbudget bar plus one row per resident bake. - Emphasis readout — a
key · alpha · tline that pulses while an emphasis ramp is in flight. - Copy JSON — dumps
renderer.debug.ring.exportJson()to the clipboard for bug reports.
Lower-level: attachPanel
Section titled “Lower-level: attachPanel”enableRenderDebug calls attachPanel once per renderer. Use it directly when
you want to attach exactly one panel to one known renderer, or to host the panel
in a plain element instead of a Monitor.
import { attachPanel } from "insomni-monitor";
const handle = attachPanel(renderer, document.body, renderer.canvas, "Tree view");// handle.flashLayerAabb(layer); // flash one layer's AABB// handle.detach(); // remove overlay + panel, unsubscribe probeattachPanel(renderer, host, canvas?, label?)
Section titled “attachPanel(renderer, host, canvas?, label?)”| Param | Type | Default | Description |
|---|---|---|---|
renderer | Renderer2D | — | The renderer whose renderer.debug probe is presented. |
host | RenderDebugHost = Monitor | HTMLElement | — | A Monitor (embed as a section) or a container (standalone floating panel). |
canvas | HTMLCanvasElement | — | The canvas the damage overlay anchors to. When omitted, the panel works but the overlay never flashes. |
label | string | "Render Debug" | Titles the panel / section. |
Returns a RenderDebugHandle:
| Member | Type | Description |
|---|---|---|
flashLayerAabb(layer) | void | Flash one layer’s projected AABB on the overlay. No-op if no frame has been captured. |
detach() | void | Detach the probe listener and remove all DOM. The probe disables capture when its listener count hits 0. Idempotent. |
The Monitor HUD
Section titled “The Monitor HUD”Monitor is the standalone telemetry + controls panel. You can use it on its own
(feed it timings each frame) and/or as the host that enableRenderDebug mounts
into.
import { Monitor } from "insomni-monitor";
const monitor = new Monitor({ controls: { pointSize: { type: "range", label: "point size", min: 1, max: 12, step: 0.5, value: 4 }, showGrid: { type: "toggle", label: "grid", value: true }, layout: { type: "select", label: "layout", value: "radial", options: [ { value: "radial", label: "Radial" }, { value: "linear", label: "Linear" }, ], }, tree: { type: "file", label: "tree", accept: ".nwk,.tree", buttonLabel: "Open tree…" }, }, onControlChange(key, value) { // value is number | boolean | string | File depending on the control type console.log(key, value); },});
// Per frame: feed timings (e.g. from insomni-profiler).function frame(now: number) { monitor.updateTelemetry(now, { cpuMs, renderMs, // null/omitted → renders as "—" computeMs, // null/omitted → renders as "—" resolutionLabel: "1920 × 1080", elementsLabel: "12,430 items", sections: [{ title: "Camera", fields: [{ label: "zoom", value: "2.4×" }] }], }); requestAnimationFrame(frame);}requestAnimationFrame(frame);MonitorOptions
Section titled “MonitorOptions”| Option | Type | Default | Description |
|---|---|---|---|
container | HTMLElement | document.body | Where to mount the panel. |
controls | Record<string, ControlDef> | — | The controls to render. Keys are echoed back to onControlChange. |
onControlChange | (key, value) => void | — | Fires on user input. value is number | boolean | string | File per the control type. |
telemetryThrottleMs | number | 120 | Minimum ms between DOM refreshes of the perf panel. (Sparkline samples are still recorded every call.) |
Control definitions (ControlDef)
Section titled “Control definitions (ControlDef)”ControlType is "range" | "toggle" | "select" | "file".
| Type | Shape | Emits |
|---|---|---|
RangeControl | { type: "range"; label; min; max; step; value } | number |
ToggleControl | { type: "toggle"; label; value: boolean } | boolean |
SelectControl | { type: "select"; label; value; options: { value, label }[] } | string |
FileControl | { type: "file"; label; accept?; buttonLabel? } | File |
Monitor methods
Section titled “Monitor methods”| Method | Description |
|---|---|
updateTelemetry(nowMs, stats?, opts?) | Push one frame of telemetry. Records FPS from frame deltas and any provided cpuMs / renderMs / computeMs sparkline samples, then refreshes the DOM (throttled). Pass { fresh: false } to mark the panel idle and pause the clock (so the next active frame doesn’t register a huge delta). |
setControlValue(key, value) | Programmatically set a control’s value without firing onControlChange. No-ops for unknown keys or mismatched value shapes. |
setControlOutput(key, text) | Set the displayed output text next to a control. |
mountSection(title, content) | Mount a caller-owned collapsible section into the body; returns { body, remove }. The Monitor never touches a mounted section’s body (unlike telemetry-driven sections). This is how attachPanel embeds itself. |
resetTelemetry() | Clear all sparkline history and reset the perf readouts. |
dispose() | Remove the panel from the DOM. |
MonitorTelemetry
Section titled “MonitorTelemetry”The per-frame stats object passed to updateTelemetry.
| Field | Type | Description |
|---|---|---|
cpuMs | number | CPU ms inside render() up to queue submit. |
renderMs | number | null | GPU ms for the main render pass. null / omitted → "—". |
computeMs | number | null | GPU ms across all compute passes this frame. null / omitted → "—". |
resolutionLabel | string | Shown in the meta row. |
elementsLabel | string | Shown in the meta row (e.g. an element count). |
sections | MonitorSection[] | Free-form telemetry sections, reconciled every frame. |
A MonitorSection is { title; fields: MonitorField[]; collapsed? }, and a
MonitorField is { label; value: string | string[] } (an array renders as
multiple value chips).
See also
Section titled “See also”- Debugging & profiling — the core
renderer.debugprobe that the presenter consumes. - insomni-profiler — named CPU/GPU spans that produce the
cpuMs/renderMs/computeMsnumbers you feed into the telemetry panel.