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

@vzn/vx-schedule-history

v0.0.646

Published

History-based scheduling for vx — a `schedule` plugin that orders ready tasks by their expected remaining critical path, learned from the local run history.

Readme

@vzn/vx-schedule-history

Website: https://vznjs.github.io/vx/ · this plugin: https://vznjs.github.io/vx/plugins/vx-schedule-history/ · source: https://github.com/vznjs/vx

History-based scheduling for @vzn/vx: a schedule plugin that orders ready tasks by their expected REMAINING critical-path duration — a task's own p50 plus the longest chain of dependents behind it — learned from the workspace's local run history. Zero dependencies.

A task's p50 is the median of its last five timings on the current branch, else on any branch, else unknown. The timings are shared by every worktree of the repo, so a new worktree orders by them on its first run.

Usage

bun add -d @vzn/vx @vzn/vx-schedule-history
// .vx/workspace.ts
import { scheduleHistoryPlugin } from '@vzn/vx-schedule-history'

export default { plugins: [scheduleHistoryPlugin()] }

scheduleHistoryPlugin({ window?, assume?, file?, defaultBranch?, resources?, memory?, reservations? }) learns from the last window invocations (default 20) through core's LocalHistoryProvider over the cache's own SQLite handle. With one worker and two chains of identical shape, the chain history says is slow starts first; with no history yet, the scheduler's structural order stands.

assume is for the run that has no history: a fresh CI runner. It names durations (task id → ms) for tasks the history has not seen, so a long leaf task — a docs build, a cross-compile — starts first instead of last:

export default {
  plugins: [scheduleHistoryPlugin({ assume: { '@vzn/vx-docs#build': 30_000 } })],
}

A recorded p50 always wins over an assumption, and assumptions never feed the workspace median: they are a hint for the cold run, not evidence. A duration that is not a finite non-negative number (Number() of an unset variable is NaN) is dropped with one warning naming its task ids; the rest still order the run. vx's own CI declares four, this one among them — its docs build was the 29 s tail of a 99 s cold gate, ready from the second second and started last (2026-09-10).

Timings through the cache

A fresh CI runner or a new branch has no timings of its own. With a remote cache declared, the plugin carries them there, with nothing to configure:

  • Write. A CI run writes each task's p50 under its commit after the run; on a branch that lands under the branch's cache scope. A commit's timings are written once and never replaced; a read-only cache policy keeps them local. A local run never writes.
  • Read. When some task has no timing on this branch, the plugin takes the merge-base of HEAD with defaultBranch (or origin/<defaultBranch>) and imports the newest timings at it or its 4 first-parent ancestors, once per merge-base for all its worktrees. On a branch it then reads HEAD's parent's timings from the branch's scope, once per commit: a task there overrides the default branch's, and a task missing there keeps it. The run never waits for them: it starts in the order local history gives, and when the timings land (within 500 ms) the tasks not yet started re-rank. A run whose tasks all have timings on its branch spawns no git and asks no cache.
  • Order. A forecast reads this branch's last five timings first, then the newest imported one, then any branch's, then assume.
  • Untrusted. A blob over 1 MiB, not { version: 1, tasks }, with more than 50 000 tasks, or with a time outside 0 to 24 h, is dropped, silently; ids not in the run's graph are dropped. It can only order a run, never change what runs. The first write of a commit's blob stays, so the cache's write credential is the boundary: give PR jobs a read-only token.

Timings between CI runners, without a remote cache

A fresh CI runner has no history. file names a JSON file, from the workspace root, that carries it: read before ordering, rewritten after each run with every task's p50 (its directory made if absent), so a CI cache keeps one file.

export default {
  plugins: [scheduleHistoryPlugin({ file: '.vx-timings.json' })],
}
# GitHub Actions
- uses: actions/cache@v4
  with:
    path: .vx-timings.json
    key: vx-timings-${{ github.run_id }}
    restore-keys: vx-timings-

The file is { "version": 1, "tasks": { "<task id>": <ms> } }, keys sorted. A p50 recorded on this machine wins over it, and it wins over assume; like assume, it never feeds the workspace median. A task the run did not see keeps its time. A file that is not one warns and is read as empty; --dry writes nothing.

Reservations learned from history

The same history answers what each task used: the runner records every execution's CPU time and peak RSS. Core's only gate is the worker count; this plugin's admit hook packs the finer numbers, and core never learns them. Each task's reservation is the largest peak RSS in the window times a headroom (1.25), rounded up to 64 MB; cores are never learned (the parallelism a build shows is what the machine let it have, and reserving the solo reading packed 92 real builds two wide on four cores for a 21% longer run, 2026-09-15), only declared. A task is admitted while its reservation and those of everything running beside it fit the budgets — cores are the run's worker count, memory is what this process may use (the machine's total, capped by the cgroup limit a container runs under) — and a task over a whole budget runs alone. A task with no execution in the window reserves nothing and runs freely, as it would with no plugin; a spike or a refactor ages out with the window. A cache hit counts too: the producing execution's usage rides the artifact, so a fresh CI runner that restored a task from a remote cache has its reservation on the next run without ever having executed it.

plugins: [
  scheduleHistoryPlugin({
    // MB. The default is the machine's total capped by the cgroup limit; set it to budget below either.
    memory: 8192,
    resources: { headroom: 1.5 },
    // Declared by hand for what history cannot size (a first run); a declaration wins.
    reservations: { 'app#e2e': { cpus: 2, memory: 4096 } },
  }),
]

window, memory and headroom must each be a finite number above 0 (window a whole one). One that is not — Number(process.env.X) with X unset is NaN, and no reservation fits a NaN budget, so every task that reserved memory would wait for an idle machine — runs on its default, named in one warning per run and in vx history. So does a declared reservations axis (cpus, memory): one that is not a number above 0 reserves nothing on that axis (a NaN held its task alone and every reserving task beside it waited); 0 is taken as nothing, silently. An entry that is not an object ({ "app#build": 2048 }) reserves nothing and is named the same way.

vx history [--format pretty|json] (a verb this plugin adds; --help prints its usage) shows what it learned per task and the reservation it will pack, with the budgets it packs into:

$ vx history
history: last 20 runs · budgets 4 cores (the default worker count; --concurrency changes it per run) · 13567 MB (what this process may use)
  task       runs      p50  peak rss    cpu  reserves
  app#build    12    8.41s    612 MB   1.9×  768 MB
  app#e2e       3   41.20s    1.4 GB   1.1×  4096 MB · 2 cores (declared)
  lib#test     12    1.02s         —   1.0×  —
  3 tasks with no execution in the window reserve nothing

--format json emits { window, budgets, tasks } with every task's runs, p50DurationMs, maxPeakRssBytes, maxCpuParallelism, reservation and declared; schemas/history.json (shipped) is its JSON Schema, held to the output by core's tests/plugin-json-schemas.unsafe.test.ts. A cache hit's row carries the producing execution's usage, so a task restored from a remote cache shows a reservation on a machine that never executed it. A task lighter than vx itself has no peak on record (the runner reports one only above its own footprint, since Linux hands a lighter child the parent's figure) and reserves nothing — what a task under vx's own footprint needs.

resources: false turns the learning off (declared reservations are still packed). Admission control, not enforcement: nothing is cgroup-limited or reniced, and a task that exceeds its reservation is the job of exec.timeout and the OS. Design: docs/design/resource-estimates-2026-09.md in @vzn/vx.

What it does and does not do

  • It is an ORDERING hint over the scheduler's structural baseline (how many tasks wait on each one, directly or through others), merged as VxPlugin.schedule returns it: task id → weight. It never changes what runs, only which ready task runs first.
  • Cache hits are not modelled as zero-cost: predicting cache state needs the key and a probe, which the scheduler handles at run time (a confirmed hit is backfill, never a critical-path task). The estimate is "what if everything ran", which is exactly the case where order matters.
  • It fails open: a broken history read warns ([vx] schedule-history: ordering falls back to the baseline) and leaves the baseline order and only the declared reservations. Observability never breaks a run.
  • Cost: one history read per run, serving both hooks, paid only by workspaces that declare the plugin. Core applies no plugin by default.

criticalPathPriorities(nodes, history, assume?), resourceEstimates(nodes, history, headroom), withDeclared(learned, declared) and admits(id, running, reservations, budgets) are exported for tests and for policies that want the same scoring over another history source, with the types ScheduleHistoryOptions, ResourceEstimate and Budgets.

History

Core's opt-in predictive mode until 2026-09-02, then core's one bundled plugin (@vzn/vx/plugins/schedule-history) until 2026-09-10, when it moved here so core ships no plugin at all.