StatsRecorder ​
StatsRecorder records frame timing and application metrics. It loads no Lit and registers no element.
import { StatsRecorder } from "@jolly-pixel/ui/stats";
const recorder = new StatsRecorder();
function renderFrame(): void {
recorder.begin();
world.tick();
renderer.render(scene, camera);
recorder.end();
}Constructor ​
new StatsRecorder(options?: StatsRecorderOptions)
interface StatsRecorderOptions {
historySize?: number; // snapshots kept per metric, default 60
refreshInterval?: number; // ms between flushes, default 250
performance?: { // default globalThis.performance
now(): number;
memory?: {
usedJSHeapSize: number;
jsHeapSizeLimit?: number;
};
};
}- An invalid or non-positive
historySizeorrefreshIntervaluses its default. - Inject
performancefor deterministic tests and headless hosts.
Recording frames ​
begin(): void ​
Stores the current time.
end(): void ​
Records the elapsed duration and counts the frame. Without a matching begin(), does nothing.
Once refreshInterval has elapsed, end() also flushes: it aggregates pending values, samples pull metrics, appends to each history and notifies subscribers. There is no timer.
skipGap(): void ​
Call it when frames stop for a while, such as a loop going to sleep, and before they resume. It flushes the frames counted since the last flush, measured up to the last end(). The next window then starts at the next end(), so the pause never shows up as a low FPS sample.
Built-in metrics ​
Registered in this order:
fps(FPS): frames over the window time. 0 to 100, higher is better.ms(MS): averagebegin()toend()duration. 0 to 200, lower.worstMs(WORST MS): longest duration. 0 to 200, lower.mb(MB): used JavaScript heap in MiB, up to the reported limit. Lower. Only when the performance source hasmemory.usedJSHeapSize.
Custom metrics ​
addMetric(definition: MetricDefinition): () => void ​
Registers a metric and returns its unregister function. Throws an Error for an empty or duplicate id.
track(id: string, value: number): void ​
Pushes a value, reduced by the metric's aggregate at the next flush. Ignores NaN and infinite values. Throws for an unknown id.
recorder.addMetric({
id: "triangles",
label: "TRIS",
better: "lower",
aggregate: "last"
});
recorder.track("triangles", renderer.info.render.triangles);A metric with sample() is pulled once per flush instead. Use it for a cheap live value, and track() for values that arrive as events or need aggregation.
recorder.addMetric({
id: "drawCalls",
label: "CALLS",
better: "lower",
sample: () => renderer.info.render.drawCalls
});addSource(source: MetricSource): () => void ​
Registers every metric of a MetricSource.
- The returned function unregisters them all. Call it when the source dies first, or its
sample()closures keep running. - A colliding id throws, and nothing of the source stays registered.
const release = recorder.addSource(engine.inspector);
release();removeMetric(id: string): boolean ​
Whether a metric was registered under that id.
revision: number ​
Changes whenever metrics are added or removed, so a readout can tell that definitions went stale.
Reading ​
snapshot(): Record<string, number> ​
The current value of every metric. A metric starts at 0.
history(id: string): number[] ​
An oldest-first copy, at most historySize long. Empty for an unknown metric or before the first sample.
definitions: readonly MetricDefinition[] ​
Copies, in registration order.
subscribe(listener: StatsListener): () => void ​
The listener receives a snapshot once per flush. Call the returned function on teardown.
const unsubscribe = recorder.subscribe((snapshot) => {
console.log(snapshot.fps);
});To display the recorder, assign it to jolly-stats.