Skip to content

insomni-monitor

insomni-monitor is a small, framework-free overlay UI for insomni apps. It has two halves:

  1. 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).
  2. enableRenderDebug / attachPanel — a presenter for the core renderer’s renderer.debug probe. 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.

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.

OptionTypeDescription
monitorMonitorThe Monitor to embed the per-renderer sections into. When omitted, a dedicated Monitor is created lazily and disposed by dispose().

Returns a RenderDebugController:

MemberTypeDescription
dispose()voidDetach every panel and unsubscribe the registry listener. Idempotent.

The presenter subscribes to renderer.debug (probe.onFrame) and renders, per renderer:

  • Damage overlay — a position:fixed overlay 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 live Layer and call probe.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 / budgetBytes budget bar plus one row per resident bake.
  • Emphasis readout — a key · alpha · t line that pulses while an emphasis ramp is in flight.
  • Copy JSON — dumps renderer.debug.ring.exportJson() to the clipboard for bug reports.

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 probe

attachPanel(renderer, host, canvas?, label?)

Section titled “attachPanel(renderer, host, canvas?, label?)”
ParamTypeDefaultDescription
rendererRenderer2DThe renderer whose renderer.debug probe is presented.
hostRenderDebugHost = Monitor | HTMLElementA Monitor (embed as a section) or a container (standalone floating panel).
canvasHTMLCanvasElementThe canvas the damage overlay anchors to. When omitted, the panel works but the overlay never flashes.
labelstring"Render Debug"Titles the panel / section.

Returns a RenderDebugHandle:

MemberTypeDescription
flashLayerAabb(layer)voidFlash one layer’s projected AABB on the overlay. No-op if no frame has been captured.
detach()voidDetach the probe listener and remove all DOM. The probe disables capture when its listener count hits 0. Idempotent.

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);
OptionTypeDefaultDescription
containerHTMLElementdocument.bodyWhere to mount the panel.
controlsRecord<string, ControlDef>The controls to render. Keys are echoed back to onControlChange.
onControlChange(key, value) => voidFires on user input. value is number | boolean | string | File per the control type.
telemetryThrottleMsnumber120Minimum ms between DOM refreshes of the perf panel. (Sparkline samples are still recorded every call.)

ControlType is "range" | "toggle" | "select" | "file".

TypeShapeEmits
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
MethodDescription
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.

The per-frame stats object passed to updateTelemetry.

FieldTypeDescription
cpuMsnumberCPU ms inside render() up to queue submit.
renderMsnumber | nullGPU ms for the main render pass. null / omitted → "—".
computeMsnumber | nullGPU ms across all compute passes this frame. null / omitted → "—".
resolutionLabelstringShown in the meta row.
elementsLabelstringShown in the meta row (e.g. an element count).
sectionsMonitorSection[]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).

  • Debugging & profiling — the core renderer.debug probe that the presenter consumes.
  • insomni-profiler — named CPU/GPU spans that produce the cpuMs / renderMs / computeMs numbers you feed into the telemetry panel.