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

@hona/strobe

v0.44.0

Published

Measure and explain performance: the strobe CLI and the types and helpers for its bench files

Readme

@hona/strobe

strobe: the referee for a performance claim in a pull request. This package is the strobe command and the types and helpers for its bench files, so a project pins one strobe version for everyone who runs its benches, people, agents and CI alike.

Two commands, compared in alternating pairs, with a verdict and its interval (no install needed):

bunx @hona/strobe run --base "<command on main>" --head "<command on your branch>"

A project's first bench:

bun add -d @hona/strobe
bunx strobe init                                          # strobe.json and a runnable perf/smoke.bench.ts
bunx strobe bench perf/smoke.bench.ts --head . --check    # each case once: does it work?

strobe init reads the folder's package.json (build, preview, start, dev, exports, module, types, main, bin, an electron dependency), Cargo.toml binaries or index.html, and writes a bench that passes --check as written: --target web|lib|server|native|electron|tty|cli picks the kind. It never replaces a file without --force. Then edit its cases to what your users do, or write your own:

import { bench, web } from "@hona/strobe"

export default bench({
  targets: { app: web({ url: "/" }) },
  cases: {
    "open settings": {
      primary: "main.work",
      run: (ctx) => ctx.page.click("[data-action=settings]", { until: "[role=dialog]" }),
      reset: (ctx) => ctx.page.press("Escape", { until: "idle" }),
    },
  },
})
bunx strobe bench perf/settings.bench.ts --check            # each case once: does it work?
bunx strobe bench perf/settings.bench.ts --base git:main    # is this branch faster?

A server: host it in a script() target and probe it from strobe's own threads while the runs load it. The harness is a module strobe loads in a fresh Bun process per block; cases call its exports with ctx.call.

// perf/server.ts
let server: ReturnType<typeof Bun.serve> | undefined

/** Starts the server; its address. It answers only the bearer token whose SHA-256 is `tokenHash`. */
export function start(tokenHash: string) {
  const hash = (s: string) => new Bun.CryptoHasher("sha256").update(s).digest("hex")
  server ??= Bun.serve({
    hostname: "127.0.0.1",
    port: 0,
    fetch: (req) =>
      hash(req.headers.get("authorization") ?? "") === tokenHash
        ? Response.json({ status: "ok" })
        : new Response("unauthorized", { status: 401 }),
  })
  return `http://127.0.0.1:${server.port}/health`
}

/** Holds the server's thread for `ms`, as a slow synchronous handler would: no request is answered meanwhile. */
export function stall(ms: number) {
  const end = performance.now() + ms
  let x = 0
  while (performance.now() < end) x += Math.sqrt(x + 1)
  return x > 0
}
// perf/server.bench.ts
import { bench, script } from "@hona/strobe"

export default bench({
  targets: { server: script("./server.ts") },
  async setup(ctx) {
    // The server gets the token's hash only; strobe keeps `authorization` in memory, never in a file.
    const token = crypto.randomUUID()
    const url = await ctx.call<string>("start", new Bun.CryptoHasher("sha256").update(`Bearer ${token}`).digest("hex"))
    // A probe every 100 ms, a new connection each (the defaults): a backlog stall shows too.
    await ctx.httpProbe({ url, authorization: `Bearer ${token}`, expect: { jsonPath: { path: "$.status", equals: "ok" } } })
  },
  cases: {
    "slow handler": {
      primary: "http.latency.p95",
      metrics: ["cpu.main", "cpu.main.busy", "http.timeouts"],
      // Four slow requests 100 ms apart: about 26 probes a run, enough for a p95 in a --check.
      run: async (ctx) => {
        for (let i = 0; i < 4; i++) {
          await ctx.call("stall", 550)
          await Bun.sleep(100)
        }
      },
    },
  },
})
bunx strobe bench perf/server.bench.ts --head . --check     # http.latency.p95 ≈ 500,000 µs: the held cohorts
bunx strobe record --bench "perf/server.bench.ts#slow handler" --head .   # then: strobe query <rec> http

http.latency.p95 is what a client waited while the runs held the server; cpu.main is the server's main thread alone (cpu.main.busy near 100%: its event loop never waited). Times print in microseconds; --unit ms or "unit": "ms" in strobe.json prints milliseconds.

A native app (native(…, { output: true })) that prints a line when it is ready: wait for it in setup, so the first run does not measure the launch's work. since: "launch" also takes a line the app wrote before the call (strobe keeps its output's last 2,000 lines); the default takes only lines written after it.

async setup(ctx) {
  await ctx.waitForLine(/listening on/, { since: "launch", timeout: 60_000 })
},

In GitHub Actions, strobe's action runs the same check on every pull request: each case once, and with noworse each exact counter held to the base commit's. version: project runs the strobe this package pins.

- uses: anomalyco/[email protected]
  with:
    bench: perf/settings.bench.ts
    noworse: net.requests=0,dom.nodes=0
    version: project

Timing verdicts need a machine of your own: mode: compare on a self-hosted runner (a GitHub-hosted runner's timings move with the jobs beside it, so strobe refuses them there). Every input is in the action's action.yml.

  • Platforms: the binary comes from @hona/strobe-<platform>-<arch>, installed with this package for your machine only: Windows x64 and Arm64, macOS Apple silicon and Intel, Linux x64 and Arm64 (static, glibc or musl).
  • One SDK per binary: strobe runs a bench file with the SDK it was built with, so the types you import and the runner always match.
  • Old name: @hona/strobe-bench re-exports this package. Import @hona/strobe instead.
  • Changes: each release's notes are on GitHub Releases.