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

tv-fps

v0.5.0

Published

Framework-agnostic FPS / frame-time instrumentation for TV / set-top-box UIs. Allocation-free ring buffer, rAF + render-driven sampling, LoAF, OTLP exporter, a zero-render React HUD, Playwright helper and an e2e soak harness — one package, independent sub

Readme

fps

Framework-agnostic FPS / frame-time instrumentation for TV / set-top-box UIs (WPE WebKit on BCM-class silicon, concurrent GC off). One package, independent subpath exports — import only what you need.

fps                 core sampler — allocation-free ring buffer, stall counters,
                    LoAF observer, rAF + render-driven sampling   (zero deps)
fps/react           zero-render React HUD + signal pump
fps/hud             the same overlays in pure DOM — FPS meter, result dialog, key readout
fps/bench           turnkey benchmark — one call mounts HUD + hotkey + sweep + publish
fps/sweep           configurable D-pad sweep driver — drive any TV UI's nav, measure it
fps/publish         post a finished sweep to a results collector / leaderboard
fps/otel            OTLP/HTTP-JSON exporter (Grafana/Honeycomb/Collector)
fps/playwright      page-FPS measurement helper for e2e CI gates
fps/soak            e2e soak harness (driver + collector + aggregator, `fps-soak` CLI)
fps/mock-catalog    license-clean 20×20 TV catalog fixtures + placeholder posters
dist/inject.js      the benchmark as one self-contained IIFE for pages you don't own —
                    script tag or console paste, installs `window.__bench`

Install

Published to npm as tv-fps (the bare fps name was taken). Alias it to fps so imports read the same everywhere:

// package.json — pick one
"dependencies": { "fps": "npm:tv-fps@^0.3.0" }        // npm registry
"dependencies": { "fps": "github:dobreadi/tv-fps" }   // git dependency

The prepare hook builds dist/ on install, so the git form works without a published artifact (releases are tagged v* — .github/workflows/publish.yml publishes with provenance via npm trusted publishing). import "fps" is dependency-free; React / Playwright stay optional peers and only load on their subpath. No npm at all? — load the console build from the deployed benchmark site (/fps.js, or the version-pinned /fps-<version>.js — see “Console benchmark” below), or point a webpack alias / tsconfig paths entry at a checked-out tv-fps/dist.

Core sampler

import { startFrameSampler, sharedFrameRing, sharedStats, getStallCounts } from "fps";

startFrameSampler();            // always-on rAF loop → shared ring (ref-counted)
sharedFrameRing.stats(sharedStats);   // { p50, p95, max } — no allocation
const { stalls16, stalls33 } = getStallCounts(); // superset bands: stalls16 ⊇ stalls33

For render-on-demand apps, drive the ring from your scheduler instead of a rAF loop (no idle wakeups):

import { recordFrameDelta, startIdleHeartbeat } from "fps";
recordFrameDelta(dtMs);         // called per rendered frame by your render pump
const beat = startIdleHeartbeat(); // low-rate idle probe; no rAF, no fake samples

Zero-render HUD (fps/react)

Drop-in overlay that renders once and thereafter writes the numbers straight to the DOM via a @preact/signals-core effect — it never triggers a React re-render, so it can't perturb the measurement it reports. Works over DOM, Pixi, or mixed render paths.

import { FpsHud } from "fps/react";

<FpsHud position="top-right" budgetMs={33} />   // self-starts the sampler

Need the numbers elsewhere (a custom overlay, a Pixi useTick)? Read the same signals — still zero React renders:

import { startFpsSignals } from "fps/react";
const { signals, stop } = startFpsSignals();
// signals.fps.value, signals.p95.value, …

Vanilla HUD (fps/hud)

The same overlays in pure DOM (self-injected inline styles, no framework), so any app — including WebGL engines (Lightning / Blits) that only have a canvas — gets the live meter, the 10-foot result dialog, and the last-key readout:

import { mountFpsMeter, mountSweepResult, mountKeyHud } from "fps/hud";

const meter = mountFpsMeter({ position: "top-right" });  // self-starts the sampler
const overlay = mountSweepResult();  // auto-raises on "benchmark:done" — or overlay.show(result)
mountKeyHud();                       // which key/keyCode did the remote actually send?

mountSweepResult({ onPublish }) grows a D-pad-focusable Publish results button (wire it to fps/publish). meter.setVisible(false) hides the meter while timing without stopping the sampler — the quiet path to the cleanest reading.

Turnkey benchmark (fps/bench)

One call mounts the meter + result overlay, starts the sampler, and binds a hotkey (b or a colour button) that runs a deterministic D-pad sweep and shows the result. The sweep drives the app's REAL input path — synthetic KeyboardEvents into its own navigation — so the host must move focus on arrow keys.

import { installBenchmark, installDomBenchmark } from "fps/bench";

const bench = installBenchmark({ rows: 20, cols: 20 }); // grid known → walks every cell
installDomBenchmark({ steps: 40, cycles: 8 });          // grid unknown → directional fling
// bench.run();  // trigger programmatically; bench.dispose() removes everything

Publishing auto-wires when a collector resolves (same-origin /api/results on https). A registered 3rd-party app targets a board explicitly:

installBenchmark({
  rows: 20, cols: 20,
  publish: { board: "https://tv-benchmark.pages.dev", app: "acme-tv", key: ACME_BOARD_KEY },
});

publish: false disables the button, quiet: true hides the HUD while timing, and the sweep knobs (stepMs, target, eventTypes, keyMap, …) pass straight through to the sweep driver below.

Sweep driver (fps/sweep)

Drive any TV UI's D-pad navigation through synthetic key events (the real input path — the same window/document keydown handler a remote hits, not a private scroll/focus API) and measure the result. Configurable on both axes (rows × per-row cols), plus cadence, cycles, dispatch target / event types / key map, and phase order.

import { measureGridSweep } from "fps/sweep";

const run = measureGridSweep({
  rows: rails.length,
  cols: (r) => rails[r].tiles.length, // a constant or per-row fn (variable rails)
  verticalCycles: 10,
  rowReturn: "back",                  // "rewind" | "back" | "none"
  stepMs: 90,                         // ≈ key-repeat cadence
  target: window,                     // document + ["keydown","keyup"] for some WebGL engines
  exposeAs: "__benchmarkResult",      // expose for an e2e poller
  emitEvent: "benchmark:done",        // dispatch for an on-screen result line
});
const result = await run.result;      // { frames, fps, p50, p95, max, stalls16, stalls33, ... }

Results also carry cadence-relative smoothness metrics, streamed over the whole sweep (the ring only keeps the last 1024 frames): hitches / hitchMax / hitchLostMs (frames > max(2× running cadence, 28ms) — a steady 30fps engine scores 0, a 60fps engine's freezes all score), cv (frame-time coefficient of variation), cluster33Max (longest >33ms run), and — when the optional trackMotion: () => number probe is passed (a scalar scroll offset of ONE continuous surface; allocation-free, never a layout read) — jumps/jumpMaxPx displacement discontinuities (the freeze-then-jump a wall-clock tween produces). Why these exist and how to read them: the consuming benchmark's SMOOTHNESS.md. The building blocks are exported from the root: getSmoothnessStats, setMotionSampler, resetStallCounts.

Compose the lower-level pieces directly when you need to: buildGridSweep(shape) (pure step list, no DOM), createKeyDispatcher(opts) (the press(dir)), and runSweep({ steps, press }) (the interval runner). The measurement variant reads the shared fps ring, so keep the sampler running (startFrameSampler() or a mounted <FpsHud/>).

Publishing (fps/publish)

Post a finished sweep to a results collector — the tv-benchmark leaderboard worker's POST /api/results, or any worker implementing that endpoint (pass its base URL as board). Deliberately tiny: one fetch, no retries; resolves (never rejects) with { ok, rank?, error? } so a TV UI can render "Published #4" without try/catch.

import { createPublisher } from "fps/publish";

// First-party app on the deployed site — everything defaults from the page URL.
const publish = createPublisher();      // undefined = no collector → hide the button

// Registered 3rd-party app (id + key issued by the board admin).
const publishAcme = createPublisher({
  board: "https://tv-benchmark.pages.dev",
  app: "acme-tv",
  key: ACME_BOARD_KEY,                  // per-app secret — sent as x-bench-key
});
await publishAcme?.(result);            // → { ok: true, rank: 4 }

A key without an explicit board also yields undefined — the same-origin default exists for the first-party site only, and a secret must never ride to an endpoint the caller didn't name.

Three ways through the collector's door:

  • first-party — same-origin (and localhost-dev) posts need no key;
  • registered apps — the board admin issues an app id + per-app secret from the PIN dashboard's Registered apps section (only the key's sha256 is stored; origin binding optional). Their rows are first-class: no test marking, ranked like everyone else. Rotation = revoke + re-register;
  • local-build hatch — the shared BENCH_PUBLISH_KEY lets a local/pre-merge build on the LAN publish to the prod board; those rows are force-marked server-side (build gets a -test suffix). Never bake it into production.

Helpers: publishSweepResult (the single POST), defaultAppId / defaultBenchFlags / defaultDeviceLabel (the URL-derived defaults), resolveBoardEndpoint (base URL or full endpoint; a scheme-less host like "tv-benchmark.pages.dev" gets https://), and buildPublishPayload — the exact JSON body the worker validates, exported so client and validator can be unit-tested against each other (the schema is hand-mirrored; change both together).

Console benchmark (inject.js)

dist/inject.js is the whole benchmark as one self-contained IIFE — no imports, no framework — for a page you can't (or won't) modify. Load it with a script tag (the deployed copy lives at https://tv-benchmark.pages.dev/fps.js):

<script src="https://tv-benchmark.pages.dev/fps.js" data-app="acme-tv"
        data-board="https://tv-benchmark.pages.dev" data-key="…"
        data-rows="12" data-cols="8"></script>

…or paste it into the running app's devtools console (set window.__benchConfig = {…} before pasting, or call __bench.configure({…}) after). Either way it mounts the live meter, the result overlay (with a Publish button when a board is configured — publishing REQUIRES one here: inject runs on a foreign page, so there is no same-origin default, and a key without a board never publishes anywhere), a b/colour-button hotkey — which always runs quiet — and window.__bench:

__bench.sweep({rows, cols, axis, steps, stepMs, keyMap, target, quiet, …})
__bench.run(…)            alias of sweep
__bench.configure({…})    merge config at runtime
__bench.config            effective merged config (defensive copy)
__bench.publish(result?)  push the last (or a given) result to the board
__bench.keys(show?)       toggle the last-key readout
__bench.stop()            remove everything

Config merges in order (later wins): defaults ← script data-* attributes ← window.__benchConfigconfigure(). Each field's attribute is the kebab-case data-* of its name; malformed values are ignored. keyMap / trackMotion / flags ride any config-object layer (window.__benchConfig, configure()) — only data-* attributes can't carry them; keyMap also works per call via sweep() opts. Published flags default to {} — the host page's URL query is NOT captured (a 3rd-party URL can carry tokens); set them via a config object:

data-app                  app id results publish under (a registered app's id)
data-board                collector base URL — REQUIRED for publishing
                          (no same-origin default on a foreign page);
                          unset = publishing off (no button)
data-key                  x-bench-key — registered-app secret or the local-build hatch key
data-device-label         friendly device name (boxes with generic UAs); ?device= also works
data-build                build provenance (short commit SHA, optionally -dirty)
data-rows / data-cols     BOTH set → the full V-H-V grid sweep (sweepType "full",
                          directly comparable to the first-party apps); else a fling
data-vertical-cycles / data-row-return   grid knobs (default 8 / "rewind")
data-axis / data-steps / data-cycles     fling knobs (default "vertical" / 40 / 8)
data-step-ms / data-settle-ms            key cadence / pre-sweep settle (default 90 / 200)
data-target               "window" | "document" | "body" | "active" — where synthetic
                          keys are dispatched. Spatial-nav TV UIs often act ONLY on
                          events targeted at document.body ("body") or the focused
                          element ("active", re-read each press), not window/document.
data-active-target        "1" — same as target="active"
data-event-types          comma list — add "keyup" for engines that need it ("keydown,keyup")
data-quiet                "1" hides the HUD while timing (cleanest numbers)
data-hotkey               "sweep" (default) | "measure" — what 'b'/a colour button starts;
                          "measure" = keyless window you drive with the real remote
                          (for trusted-input-only boxes)
data-key-hud              "1" mounts the last-key readout on install

Caveat: the sweep drives the page with synthetic KeyboardEvents (isTrusted: false) — an app that ignores untrusted input won't move. Configure keyMap / target / eventTypes when it uses non-arrow keys or listens elsewhere (__bench.keys() shows what the remote actually sends).

When the sweep moves nothing — first suspect the dispatch target, not isTrusted. Many spatial-nav TV UIs attach the key handler to document.body or the focused element, and ignore events dispatched at window/document. Try target: "body" first, then target: "active" (fires at document.activeElement, re-read each press as focus moves). Diagnose directly — fire one synthetic key and see which target moves focus:

document.body.dispatchEvent(new KeyboardEvent('keydown', {key:'ArrowRight', keyCode:39, bubbles:true}))

Whichever moves focus is your target. Both drive the app even with isTrusted:false — the block was the target, not the trust bit.

Only if that still moves nothing (a build that genuinely gates on e.isTrusted, or takes input entirely outside the DOM — e.g. an Rx handler that preventDefaults injected keys) fall back to __bench.measure({ durationMs }) — it records a timed window with the same metrics and publish path but dispatches no keys; you drive the real D-pad while it times. With rows/cols configured it stamps the same grid so the row stays comparable; replay the V-H-V pattern for the window, then __bench.publish().

For a SOTB over SSH, scripts/rcu-sweep.mjs automates the remote half — it replays the exact buildGridSweep V-H-V pattern as trusted rcuemulator keys in lockstep with a measure() window:

node scripts/rcu-sweep.mjs --host <box-ip> --rows 7 --cols 10 --cycles 10
#   prints the __bench.measure({durationMs}) one-liner to paste, then on Enter
#   drives UP/DOWN/LEFT/RIGHT over one SSH session at the configured cadence.
#   --dry-run shows the plan without sending; SSHPASS=<pw> for password auth.

Agent skill

skills/bench-integrate/SKILL.md teaches a coding agent to instrument any TV/STB app with this package: the integration decision tree, the registered-app flow, the comparability rules, and a publish-failure troubleshooting table.

Develop

npm install
npm run build       # vite (multi-entry ESM) + tsc declarations
npm test            # vitest
npm run typecheck