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

disk-speed

v0.0.2

Published

Sequential disk read/write throughput for Node, with a Blackmagic-style terminal dashboard

Downloads

274

Readme

disk-speed

Sequential disk read/write throughput for Node, with a Blackmagic-style terminal dashboard. Usable as a library or as a CLI.

the dashboard

                        write        read
Blackmagic Disk Speed  2635.6      3083.3
disk-speed             2865.1      3083.4

Like Blackmagic Disk Speed Test, it alternates write and read on a loop rather than measuring once. A drive's figures drift as it warms up and its write cache fills, so one round is a snapshot. Rounds idle for five seconds between cycles (--rest) instead of hammering the drive back to back.

The loop stops after 10 rounds, always. Blackmagic runs until you close it; this does not, so a dashboard left open overnight cannot quietly wear the drive down. Ten rounds at the default 2 GiB writes 20 GiB — negligible against a modern SSD's endurance rating, which an unbounded loop is not. The ceiling is enforced in the engine, so the library is capped too.

It ends with a report of the aggregate and of every round, so the drift is visible at a glance:

  ╭───────┬───────────┬──────┬──────┬──────┬───────╮
  │       │ sustained │  p50 │  min │  max │ total │
  ├───────┼───────────┼──────┼──────┼──────┼───────┤
  │ write │    2328.4 │ 8790 │  765 │ 9633 │ 0.35s │
  │ read  │    4763.7 │ 4872 │ 1534 │ 5235 │ 0.17s │
  ╰───────┴───────────┴──────┴──────┴──────┴───────╯
  MB/s

  sustained MB/s per round
  ╭───────┬──────┬──────┬──────╮
  │       │   r1 │   r2 │   r3 │
  ├───────┼──────┼──────┼──────┤
  │ write │ 2324 │ 2418 │ 2250 │
  │ read  │ 4797 │ 4664 │ 4833 │
  ╰───────┴──────┴──────┴──────╯

Why this needs a native call

A disk benchmark written in plain Node measures the wrong thing. The file it just wrote is still resident in the OS page cache, so reading it back reports memory bandwidth:

naive Node, cache on   :  write 3183 MB/s   read 19004 MB/s   <- RAM, not disk
with F_NOCACHE         :  write 2970 MB/s   read  3215 MB/s

macOS has no O_DIRECT, purge needs root, and evicting the cache by writing past its size is impractical (a 60 GiB write probe had not finished after four and a half minutes). The only workable escape is fcntl(fd, F_NOCACHE, 1) — one syscall, which node:fs does not expose. Hence koffi.

This is not a language limitation. C and Rust hit exactly the same page cache; they just get fcntl for free because it lives in libc. Copying the file and reading the copy — a common workaround — does not help either: the copy is written through the same cache and reads back at 6531 MB/s.

Note that fcntl is variadic. On arm64 variadic arguments are passed on the stack rather than in registers, so declaring it with fixed arity fails silently: the call returns 0 as though it succeeded while caching stays on.

Install

npx disk-speed                # run it once, without installing
npm install -g disk-speed     # install the `disk-speed` command
npm install disk-speed        # as a library

koffi is the only runtime dependency — everything else (citty, solid-js, @uniview/tui-*) is bundled into dist, so an install pulls three packages and about 4 MB rather than a dependency tree. koffi has to stay external because a .node binary cannot be inlined by a bundler.

CLI

disk-speed                    # test the current directory
disk-speed /Volumes/SSD       # test somewhere else
disk-speed -s 8g -b 1m        # 8 GiB in 1 MiB blocks
disk-speed --plain            # no TUI
disk-speed --json > out.json  # machine-readable

| Flag | Meaning | | --- | --- | | -s, --size | Bytes per pass, e.g. 2g, 512m. Default 2g. | | -b, --block | Bytes per I/O call. Default 4m. | | -d, --depth | Requests in flight. 1 matches Blackmagic's sequential figure. | | -r, --rounds | Write/read cycles, 1–10. Default 10 in the dashboard, 1 otherwise. | | --rest | Seconds idled between rounds, so a long run does not sit on the drive. Default 5, 0 to disable. | | --write / --read | Run only one pass. | | --no-cache-bypass | Leave the page cache on, to see how wrong that is. | | --plain / --json | Text or JSON instead of the dashboard. | | --samples | Include per-block samples in --json. |

q quits; Ctrl-C interrupts and exits 130 — including mid-rest, which aborts immediately rather than waiting the interval out. The scratch file is removed on every exit path that the process can observe: normal exit, q, SIGINT, SIGTERM and SIGHUP. SIGKILL cannot be caught, so kill -9 (or a power cut) will leave the scratch file behind.

Without the dashboard

The TUI is skipped automatically whenever stdout is not a terminal, so piping or redirecting just works. --plain forces text even on a terminal, and --json implies it.

The report goes to stdout, progress and headers to stderr, so both of these are clean. Colour is emitted only when stdout is a terminal, so a redirected report stays plain text:

disk-speed --plain > report.txt    # just the summary tables
disk-speed --json | jq .read.mbps  # nothing but JSON on stdout

Library

import { runDiskSpeedTest } from "disk-speed";

const result = await runDiskSpeedTest(
  { dir: process.cwd(), fileSize: 2 * 1024 ** 3 },
  { onSample: (s) => console.log(s.phase, s.averageMbps.toFixed(0)) },
);

console.log(result.write?.mbps, result.read?.mbps);

Check result.cacheBypassed. When it is false the platform could not keep the page cache out of the way and the read figure is memory bandwidth.

The engine is async on purpose. A blocking writeSync/readSync loop measured 2958 / 3193 MB/s while starving the event loop completely; the async form measured 2928 / 3156 — inside run-to-run noise — and leaves the loop free to repaint a TUI and answer Ctrl-C.

Reading the numbers

The headline is sustained throughput: total bytes over total seconds, with the closing flush counted. It is the only figure buffering cannot inflate, and it is the same quantity Blackmagic Disk Speed Test reports.

  write   2994.0 MB/s   p50 4400  ·  min 20  ·  max 9752
  • p50 / min / max — per-block quantiles. Useful on the read side, where they are stable. On the write side, expect them to disagree loudly with the headline and to swing between rounds. F_NOCACHE keeps the OS page cache out of the way, but the drive's own write cache still absorbs individual blocks and defers the real cost to the closing F_FULLFSYNC — which only the sustained figure accounts for. Three 4 GiB write rounds on one SSD measured a median of 5189 → 6131 → 2377 MB/s against a steady sustained 3727 / 3737 / 2146.
  • min — nearly always the first block of a pass.
  • max — the luckiest single block. An upper bound, not a result. The dial's full scale comes from the 95th percentile instead, so one outlier cannot flatten the needle.

Watch the figures move across rounds; that drift is the point. In the run above, write dropped from ~3730 to 2146 MB/s on the third round as the drive's SLC cache filled.

Sizing the test

Pick a --size well past the drive's SLC write cache, or the write figure reports burst rather than sustained speed. 2 GiB is the default; external SSDs often need more before the number settles.

--depth 1 is the sequential figure. Raising it measures something real but different — the same drive read 3156 MB/s at depth 1 and 3661 MB/s at depth 8.

Platform support

| | Mechanism | Status | | --- | --- | --- | | macOS | fcntl(F_NOCACHE), F_FULLFSYNC | verified against Blackmagic | | Linux | posix_fadvise(POSIX_FADV_DONTNEED) | implemented, untested | | Windows | needs FILE_FLAG_NO_BUFFERING | not implemented; reports cacheBypassed: false |

Windows would require CreateFileW, which yields a HANDLE rather than an fd, so the whole I/O path would have to move to FFI.

koffi ships one prebuilt binary per platform as optional dependencies, so a consumer installs a single package and their package manager picks the matching 1.2 MB native module. Note that a .node module cannot be inlined by a bundler, so this does not build into a single-file executable — and it is why koffi is the one dependency that cannot be bundled away.

Development

nub install
nub run dev                    # build, then run
nub run dev /tmp --plain -s 1g # arguments pass straight through
nub run build                  # dist/index.js (library) + dist/cli.js (bin) + types
nub run check-types
nub run snapshot               # render the dashboard to text without a terminal
nub run snapshot:svg           # regenerate docs/dashboard.svg

snapshot paints the real component tree onto a MemoryCellSurface (or SvgCellSurface), so the dashboard above is the actual layout and palette rather than a screenshot.

Colours must be RgbColor objects, never #hex strings: the ANSI surface resolves named tokens and rgb triples but silently drops hex, so color="#3fc7f4" emits SGR 0 and the whole UI renders monochrome. See src/cli/theme.ts.

Pass arguments to dev without a -- separator. Unlike npm, nub run forwards -- literally, and citty reads it as end-of-options — so nub run dev -- /tmp -s 1g silently drops the -s instead of applying it.

The TUI is Solid, via @uniview/tui-solid. Solid's fine-grained reactivity comes from a compile-time transform, so the build runs through Vite with the univiewSolid() plugin rather than plain tsc. dev therefore builds first — running the sources directly under nub or tsx fails with does not provide an export named 'jsx', because esbuild cannot lower Solid's JSX for the terminal renderer.

Every dependency except koffi is inlined by ssr.noExternal, so they live in devDependencies. This is not only an install-size choice: solid-js must be bundled. A solid-js resolved at runtime under Node picks its SSR build, whose signals are inert stubs — writes appear to work, no effect ever re-runs, and the dashboard renders exactly one frame and then freezes.

Declarations are emitted only for the library entry (tsconfig.build.json). A .d.ts for the CLI would import types from solid-js and @uniview/tui-core, which are bundled into dist and therefore absent from a consumer's node_modules.