npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zakkster/lite-gpu-profiler-signal

v1.0.0

Published

Reactive boundary for @zakkster/lite-gpu-profiler: GPU command counters and timing as lite-signal signals, with a live baseline gate. The hot path stays allocation-free; a throttled pulse lifts coarse telemetry into signals.

Readme

@zakkster/lite-gpu-profiler-signal

npm version Zero-GC sponsor npm bundle size npm downloads npm total downloads lite-signal peer license tests types

A reactive boundary for @zakkster/lite-gpu-profiler. It lifts GPU command counters (draw calls, instances, floats uploaded) and GPU-time timing into @zakkster/lite-signal signals through a throttled pulse, and exposes a live baseline gate for regression.

The profiler's per-frame path stays exactly as it was: allocation-free, writing into zero-GC ring buffers, touching no signal. This wrapper is the seam between that imperative hot loop and a reactive UI.

The reactive-profiler trap

The naive way to make a profiler reactive is to write a signal per metric every frame. That inverts the cost you were trying to measure: the graph now runs inside the frame it is profiling, and allocates as it goes.

This bridge does the opposite. pulse(), called once per frame, does one thing: it increments an internal integer signal. A lite-throttle window over that integer gates the recompute, which at most once per window (~10Hz by default) calls profiler.summary() once and writes a fixed, bounded set of output signals inside batch().

  • Per-frame graph cost is O(1), independent of counter count or frame rate.
  • No graph nodes are created per frame.
  • The recompute -- the one place that allocates -- runs ~10x a second, off the frame.

test/01-signals.test.js drives 200 frames and asserts summary() ran no more than three times: the throttle coalesced the rest.

Install

npm i @zakkster/lite-gpu-profiler-signal @zakkster/lite-signal

@zakkster/lite-signal is a peer dependency (>=1.3.0). @zakkster/lite-gpu-profiler, @zakkster/lite-throttle, and @zakkster/lite-watch-ex are dependencies.

Quick start

import { GpuProfiler } from '@zakkster/lite-gpu-profiler';
import { effect } from '@zakkster/lite-signal';
import { createGpuProfilerView } from '@zakkster/lite-gpu-profiler-signal';

const gpu = new GpuProfiler(1024);            // default counters: drawCalls, instances, floatsUploaded
const view = createGpuProfilerView(gpu, { label: 'scene', engine: '[email protected]' });

// one effect binds the coarse telemetry to the DOM; it runs at the pulse cadence, not per frame
effect(() => {
    hud.textContent =
        `${view.gpuP99.get().toFixed(2)} ms p99 | ` +
        `${view.counters.floatsUploaded.max.get()} floats/frame | ` +
        `${view.counters.drawCalls.max.get()} draws`;
});

function frame() {
    gpu.beginFrame();
    gpu.recordDraw(instanceCount);            // 1 draw, N instances
    gpu.recordUpload(floatCount);             // bytes-worth of vertex/uniform data, in floats
    gpu.recordGpuTime(lastQueryMs);           // optional: a resolved GPU timer query
    gpu.endFrame();
    view.pulse();                             // <- one integer set; the throttle does the rest
    requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

In the browser you can let the view drive its own pulse:

const detach = view.attach();                 // pulses on requestAnimationFrame
// ... later
detach();

Signals

Read-only. Never call .set() on them yourself.

| Signal | Type | Meaning | | --- | --- | --- | | gpuAvg, gpuP99, gpuMax | Signal<number> | GPU time (ms) over the window | | gpuSamples | Signal<number> | GPU-time samples in the window | | framesDrawn, framesSkipped | Signal<number> | frames that did / didn't record a draw | | regressed | Signal<boolean> | live gate vs the armed baseline / ceilings | | counters[tag] | { sum, max, avg, last } | one Signal<number> bundle per counter |

counter(tag) returns a bundle or null for an unknown tag.

Regression gating

Counters are gated exactly, not by tolerance. GPU command counts are deterministic: a fixed scene uploads the same floats and issues the same draws every frame. So a counter regression isn't "10% slower" -- it's "this build touches the bus more than the baseline did, at all". The default rules:

{
  'counter.floatsUploaded.max': { exact: true },   // any drift up OR down trips
  'counter.drawCalls.max':      { exact: true },
  'gpu.p99':                    { tolerance: 0.15 } // timing is noisy -> a tolerance
}
view.captureBaseline();                 // snapshot the current window as the baseline

view.onRegression((report) => {         // fires once each time `regressed` goes true
    console.warn('GPU regression:', report.regressions);
});

// or pull a full report on demand:
const report = view.checkAgainstBaseline();   // GpuGateResult, or null if no baseline

Rule kinds (from @zakkster/lite-gpu-profiler):

  • { exact: true } -- deterministic counters; the live value must equal the baseline. A decrease is still drift from an exact baseline, so it trips -- recapture on purpose.
  • { max: N } -- an absolute ceiling. Arms without a baseline.
  • { tolerance: 0.15 } -- lower-is-better; trips when (live - base) / base > tolerance.

A vanished exact-gated counter is a regression (metric missing in candidate), not a silent pass. A vanished max/tolerance metric is skipped -- there is no basis to compare against.

Replace the rule set at runtime with setRules(rules); it re-arms the live gate.

Why no counter-spike watcher? A rolling-baseline "spiked 3x above recent average" watcher is the right tool for a noisy signal that moves both ways (phase p99). Exact counts don't move for a fixed workload, so there is no rolling baseline to spike against -- the exact gate is the correct mechanism, and it's the one shipped here.

Environments without requestAnimationFrame

raf: true drives the recompute off requestAnimationFrame. When rAF isn't available (node, workers), the view falls back to an intervalMs timer instead of throwing at construction, so the same code runs in tests and headless harnesses.

Testing

npm test        # node --test

16 tests across signal mirroring, the anti-trap coalescing proof, the exact/tolerance/max gate (including vanished-metric integrity and the arms-without-baseline ceiling), and watcher/lifecycle teardown.

License

MIT (c) Zahary Shinikchiev