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-profiler-scheduler

v1.0.0

Published

Profiles @zakkster/lite-scheduler priority lanes through its public surface only -- per-lane task-time distributions (avg/p99/max), counts, and single-task budget overruns. No private-field poking, no monkeypatching. Built on lite-ring-buffer + lite-stats

Readme

@zakkster/lite-profiler-scheduler

npm version Zero-GC sponsor npm bundle size npm downloads npm total downloads lite-signal peer TypeScript License: MITlicense types module public surface only

A profiling facade for @zakkster/lite-scheduler. Point it at a scheduler, route your tasks through it, and read back where each priority lane spends its time -- per-lane task-time distributions (avg / p99 / max), task counts, and single-task budget overruns -- so you can see which lane is eating the frame.

It is an adapter in the lite-profiler family: like the core profiles a render loop and lite-profiler-signal makes that telemetry reactive, this profiles a scheduler. It composes the same primitives -- per-lane samples land in lite-ring-buffer ring buffers and are reduced by lite-stats-math.

It never touches scheduler internals. No private fields, no monkeypatching. See how.


Measuring through the public surface

lite-scheduler is a strict-priority frame-budget manager. Its public API is schedule(fn, priority), shouldYield(), isBusy(), stats(), destroy(). There is no task-start/end hook and no per-lane timing exposed. So how do you profile lanes without reaching inside?

The one public seam where a task's execution can be observed is the function you hand to schedule(). This profiler wraps it:

  profiler.schedule(fn, lane)
        |
        v
  scheduler.schedule(  () => { t0 = now(); fn(); record(lane, now() - t0); }  , lane)
        |                       \_______________ the only thing added _______________/
        v
  ...drains on its own MessageChannel, exactly as before...

Samples flow into a per-lane ring buffer; lite-stats-math turns them into avg / p99 / max on demand. The scheduler runs unchanged.

Why overruns are duration-based, not shouldYield()-based

The tempting move is to call scheduler.shouldYield() after each task and call it an overrun when true. It does not mean what it looks like. Measured on a real scheduler (budgetMs: 5, four ~4ms tasks):

  task 0  ran 4.02ms  shouldYield-after: false
  task 1  ran 4.01ms  shouldYield-after: true
  task 2  ran 4.03ms  shouldYield-after: true
  task 3  ran 4.01ms  shouldYield-after: true

Once a flush blows its budget, the deadline stays passed across the catch-up flushes, so shouldYield() flags the entire backlog -- not the task that crossed the line. Useless for attribution.

So an overrun here is defined deterministically: a task whose own duration exceeds budgetMs -- a single task that cannot fit in a frame, attributed to the exact lane that scheduled it. No flakiness, no false cascade.


Install

npm install @zakkster/lite-profiler-scheduler @zakkster/lite-scheduler

No peer dependencies. Pulls @zakkster/lite-ring-buffer and @zakkster/lite-stats-math (>= 1.0.1).


Quick start

import { createScheduler, Priority } from "@zakkster/lite-scheduler";
import { createSchedulerProfiler } from "@zakkster/lite-profiler-scheduler";

const scheduler = createScheduler({ budgetMs: 8 });
const profiler  = createSchedulerProfiler(scheduler, { budgetMs: 8 });  // match the scheduler's budget

// Route scheduling through the profiler instead of the bare scheduler:
profiler.schedule(() => handleTap(),  Priority.UserInput);
profiler.schedule(() => layoutPass(), Priority.Normal);
profiler.schedule(() => prefetch(),   Priority.Background);

// Read telemetry whenever you like -- e.g. once a second, off the hot path:
const snap = profiler.snapshot();
console.log(`${snap.totalExecuted} tasks | ${snap.totalOverruns} budget busts | busiest: ${snap.busiestLane}`);
for (const lane of snap.lanes) {
  console.log(`${lane.name.padEnd(10)} p99 ${lane.p99Ms.toFixed(2)}ms  (${(lane.utilization * 100) | 0}% of frame)  x${lane.count}  overruns=${lane.overruns}`);
}

API

createSchedulerProfiler(scheduler, options?) -> SchedulerProfiler

| option | default | meaning | | ---------- | -------------- | ------------------------------------------------------------------------ | | capacity | 256 | per-lane ring-buffer size (number of task-duration samples kept) | | budgetMs | 10 | frame budget; should match the scheduler's config. Drives overruns + utilization. | | lanes | [0,1,2,3,4] | which Priority lanes to track |

Throws TypeError if scheduler is not a Scheduler.

Methods

| method | description | | -------------------------- | ------------------------------------------------------------------------------------ | | schedule(fn, priority?) | Instrumented enqueue: wraps fn, times it, forwards to the scheduler. | | wrap(fn, priority?) | Returns a self-timing task you schedule yourself. Reuse to avoid per-call allocation.| | lane(priority) | LaneStats for a tracked lane, or null. | | snapshot() | Aggregate across all tracked lanes. | | reset() | Clear all per-lane buffers and counters. | | dispose() | Destroy buffers. After dispose, schedule() forwards untimed. | | budgetMs / tracked | Accessors (the configured budget; the tracked lane ids). |

LaneStats

| field | meaning | | ------------- | -------------------------------------------------------- | | priority | the Priority value (0-4) | | name | "immediate" \| "userInput" \| "normal" \| "background" \| "idle" | | count | tasks executed in this lane | | totalMs | summed task time | | lastMs | most recent task duration | | avgMs / p99Ms / maxMs | task-duration distribution over the window | | overruns | tasks whose own duration exceeded budgetMs | | utilization | p99Ms / budgetMs -- the worst-ish task as a fraction of one frame |

snapshot()

{
  budgetMs: number;
  totalExecuted: number;
  totalOverruns: number;
  busiestLane: string | null;     // by totalMs
  schedulerExecuted: number;      // scheduler.stats().totalExecuted, for reconciliation
  lanes: LaneStats[];
}

schedulerExecuted lets you confirm everything went through the facade: if it exceeds totalExecuted, some tasks were scheduled on the bare scheduler and went unmeasured.


Zero-GC notes

The measurement path allocates nothing: durations land in pre-allocated ring buffers, stats reduce into a reused scratch object, and snapshot()'s per-lane objects are the only allocation -- and only when you ask for them.

schedule() allocates one wrapper closure per call. For hot tasks scheduled every frame, wrap() the task once and schedule the wrapped function repeatedly to make even that allocation-free:

const step = profiler.wrap(() => stepSimulation(), Priority.Normal);
function frame() {
  scheduler.schedule(step, Priority.Normal);   // no per-schedule closure
  requestAnimationFrame(frame);
}

Recipes

Watch a single lane. Only care about background work? createSchedulerProfiler(s, { lanes: [Priority.Background] }) -- tasks on other lanes run normally, just untimed.

Alert on budget busts. Poll snapshot().totalOverruns on an interval and warn when it climbs; drill in via the per-lane overruns to find the offending lane.

Reconcile coverage. Compare snapshot().schedulerExecuted to totalExecuted to catch scheduling paths that bypass the profiler.

Reactive lane dashboards. Sample snapshot() from a lite-signal effect (or feed it through lite-profiler-signal's pulse pattern) to drive a live HUD without touching the scheduler's hot path.


Testing

npm test          # node --test, against a real lite-scheduler instance
  • lanes -- task time is attributed to the right lane; snapshot() reconciles with the scheduler's own totalExecuted and names the busiest lane.
  • overruns -- single-task budget busts are flagged per lane; none reported when every task fits.
  • lifecycle -- wrap() reuse, reset(), idempotent dispose(), and lane-subset tracking.

Compatibility

| package | range | role | | ----------------------------- | ---------------- | ---- | | @zakkster/lite-scheduler | ^1.0.0 | dep | | @zakkster/lite-ring-buffer | ^1.0.1 | dep | | @zakkster/lite-stats-math | ^1.0.1 | dep |

ESM only. sideEffects: false. Node 18+ (uses performance.now; the scheduler uses MessageChannel).

Changelog

See CHANGELOG.md.

License

MIT (c) Zahary Shinikchiev [email protected]