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-perf-gate

v1.2.1

Published

Zero-GC and performance regression gate for node:test. Scavenge-counting, scaling verdict, detector self-validation. Proves your hot path allocates nothing -- or names what did.

Readme

@zakkster/lite-perf-gate

npm version Zero-GC sponsor npm bundle size npm downloads npm total downloads TypeScript Dependencies License: MIT

Prove your hot path allocates nothing -- or name what did.

Zero-GC and performance regression gate for node:test. Scavenge-counting with scaling verdict, built-in detector self-validation, and CI pass/fail. No sampling, no heuristics, no false passes.

npm install @zakkster/lite-perf-gate

Why this exists

benchmark.js is dead. tinybench and mitata measure throughput but don't gate -- they can't fail a CI build when a hot path starts allocating. The V8 sampling heap profiler reports live-at-stop bytes, silently missing transient garbage -- the exact GC pressure a zero-alloc claim is about. Retained-heap delta (heapUsed before/after + gc()) misses it too: freed before snapshot.

Scavenge-counting is the only reliable detector of transient allocation. This library packages the methodology into a node:test-native harness that validates its own detector on every run.

suiteGate -- SPP stream-fed budgets (v1.1)

Gate live probe streams, not just measured scenarios. suiteGate() reads SPP records (a Float64Array slab or any forEach(cb(packed, t, a, b)) source -- a @zakkster/lite-scope memory sink qualifies), reduces each budget's metric, and delegates every comparison to verdict():

import { suiteGate, toNDJSON } from '@zakkster/lite-perf-gate';

const gate = suiteGate({
    name: 'ci-gate',
    source: sink.toSlab(),          // or the sink itself (forEach source)
    budgets: [
        { name: 'gc.pause.max', stream: 2, op: 0x0201, slot: 'a', reduce: 'max',   max: 8 },
        { name: 'leak.orphans',            op: 0x0801,            reduce: 'count', max: 0 },
        { name: 'inp.worst',               op: 0x0601, slot: 'a', reduce: 'max',   max: 200 }
    ]
});

process.stdout.write(toNDJSON(gate, { pkg: 'my-lib', run: process.env.CI_RUN }));
if (!gate.pass) process.exitCode = 1;   // exit codes stay in YOUR runner

No package coupling: lite-perf-gate speaks the Scope Probe Protocol (SPP v1, PROTOCOL.md in lite-scope) and never imports lite-scope. CONT continuation records are never budget targets (base-record slots only in v1.1). suiteGate returns structured per-budget verdicts and leaves exit codes to the runner layer.

toNDJSON -- CI artifacts (v1.2)

One JSON object per line: budget lines, then the suite-gate summary; measure() results serialize as measure lines. Optional meta fields (package, run id, versions) merge into every line. Pipe to a file in your gate script and attach it as a CI artifact.

Quick start

// test/zero-gc.test.mjs
import { zgcSuite } from '@zakkster/lite-perf-gate';

zgcSuite({
    scenarios: [
        {
            name: 'steady-state propagation',
            setup() {
                const engine = buildYourGraph();
                return engine;
            },
            hot(engine, n) {
                for (let i = 0; i < n; i++) engine.update(i);
            },
            statsOf(engine) {
                return engine.stats();  // { poolGrowths, totalAllocations, ... }
            }
        }
    ],
    counters: { poolGrowths: 0, totalAllocations: 0 }
});
node --expose-gc --max-semi-space-size=4 --test test/zero-gc.test.mjs

What it does

  1. Validates the detector. A positive control (allocates per iteration) must force scavenges. A negative control (pure arithmetic) must force ~0. If either fails, the gate refuses to judge -- you get an explicit "detector broken" error, not a silent false pass.

  2. Measures at two scales. Each scenario runs at N and k*N iterations. A zero-alloc path shows ~0 scavenges at both; an allocating path shows scavenges scaling with bytes. The comparison is not hostage to one absolute threshold.

  3. Asserts the verdict. Scavenges, retained-heap, and custom counter deltas are checked against thresholds. A failure names the signal that tripped.

  4. Self-tests the gate. mustFail scenarios inject a known allocation and assert the gate catches it. If your gate can't catch a leak, it tells you.

Three signals, each for what it can see

| Signal | Detects | Misses | |--------|---------|--------| | Scavenge count (perf_hooks GC minor) | Transient allocation (the GC pressure zero-alloc claims are about) | Nothing in the young-gen domain | | Custom counters (statsOf deltas) | Exact engine internals (pool growth, node allocation) | Anything outside the engine's own bookkeeping | | Retained-heap delta (memoryUsage + gc()) | Leaks (memory that survives GC) | Transient garbage (freed before snapshot) |

API

zgcSuite(config)

Register node:test cases for detector validation, scenario gating, and must-fail self-tests.

interface GateConfig {
    scenarios: Scenario[];          // your hot-path scenarios
    N?: number;                     // iteration count, default 200000
    k?: number;                     // scale factor, default 8
    maxScavenges?: number;          // threshold at k*N, default 2
    maxRetainedKB?: number;         // heap growth threshold, default 64
    counters?: Record<string, number>; // per-counter max delta
    positiveControl?: Scenario;     // override built-in
    negativeControl?: Scenario;     // override built-in
    mustFail?: Scenario[];          // must trip the gate
}

measure(scenario, options?)

Raw measurement at N and k*N. Returns MeasureResult with scavenge counts, retained heap, and counter deltas.

verdict(result, thresholds?)

Evaluate a MeasureResult against thresholds. Returns { pass: boolean, reasons: string[] }.

runGate(config)

Standalone human-readable report. Same config as zgcSuite. Returns { passed, results }.

Scenario

interface Scenario<State = any> {
    name: string;
    setup(): State;
    hot(state: State, n: number): void;
    statsOf?(state: State): Record<string, number>;
    teardown?(state: State): void;
}

The hot function is the ONLY code inside the measurement window. setup and teardown run outside.

Measurement traps (why it's built this way)

Two approaches were tried and rejected -- both produce false passes:

  • Retained-heap delta alone only sees retention. A hot path that allocates and frees every iteration shows ~0 delta -- yet that transient churn is exactly the GC pressure the claim is about.
  • V8 sampling heap profiler (HeapProfiler.startSampling) reports live-at-stop sampled bytes, so it likewise misses transient garbage.

Two V8 gotchas the controls handle:

  • Escape analysis / scalar replacement erases function-local throwaway allocations entirely. The positive control allocates into a module-level sink that genuinely escapes.
  • GC PerformanceObserver entries are async. The window is followed by a timer tick before the count is read, and the count is snapshotted before the harness's own gc() calls.

--max-semi-space-size=4

The default V8 semi-space is 16MB. At 200k iterations of 64-byte objects, you produce ~12MB -- possibly fitting without a single scavenge. --max-semi-space-size=4 shrinks the young generation to 4MB, forcing scavenges on smaller cumulative allocation and sharpening sensitivity. The four-field positive control ({x,y,z,w}) provides margin even without this flag, but the flag is recommended for production gates.

CI tuning

Node's perf_hooks delivers GC entries to the observer asynchronously. After the hot loop the harness waits before reading the entry count so the observer has time to flush. Default wait is 100ms, which is safe on a dev machine but can be marginal on saturated CI runners.

If you see intermittent zero-scavenge reports on scenarios that clearly allocate:

  • Bump globally via env var: PERF_GATE_FLUSH_MS=300 node --test ...
  • Per-suite: zgcSuite({ scenarios, flushMs: 300 })
  • Per-call: measure(scenario, { flushMs: 500 })

gc2() (the harness's forced-collection helper) does one gc(), yields a setImmediate tick to let FinalizationRegistry cleanups run, then does a second gc(). Without the yield, WeakRef/finalizer-driven teardown (used across the ecosystem in lite-cleanup, lite-observe, lite-floating) can leave the heap in an intermediate state between the two collections and inflate the retained-heap delta.

License

MIT (c) Zahary Shinikchiev