Skip to main content

Diskio

@magistr/diskiov2026.10.25.10· MIT· 1d agoBEHAVIORS
01README

Answer "what is actually reading this disk?" on a Linux host whose storage sits behind device-mapper and FUSE — where the obvious tools all lie.

Model:

  • @magistr/diskio — probes a host over SSH. Methods: device-map (resolve every dm/md/loop layer to its physical spindle and Unraid array slot, plus the alias groups a per-device metric would double-count), readers (rank processes by disk I/O from /proc//io, container-attributed, reporting requested bytes and block I/O separately), open-files (walk /proc/*/fd and re-attribute FUSE-held descriptors to the containers that really hold them), and attribute (all three for one disk, plus a one-line summary).
02Release Notes

2026.10.25.10

  • Repo-wide republish; no functional change.
03Behaviors1
@magistr/diskiov2026.10.25.10extensions/models/diskio.ts

Global Arguments

ArgumentTypeDescription
sshHoststringSSH address of the host whose disks to read
sshUser?stringSSH username (default: root)
fuseProxies?stringComma-separated process names that hold file descriptors on behalf of other processes. I/O charged to one of these is a proxy for a real consumer, never the consumer itself.
fn device-map()
Resolve the host's block stack: each dm-N/mdXpN/loopN down to the physical sdX/nvme it sits on, joined with Unraid array slots and mountpoints. Use the aliasGroups to collapse layers before ranking devices by utilisation — otherwise one spindle is counted two or three times.
fn readers(sampleSeconds?: number, topN?: number)
Sample /proc/<pid>/io over a window and rank every process by disk I/O, attributing each to its container. Reports requested bytes (rchar) alongside block I/O (read_bytes): a FUSE daemon such as shfs carries the block I/O while the container that wanted the data shows up only in rchar, and cgroup/cadvisor metrics miss it entirely.
ArgumentTypeDescription
sampleSeconds?numberSampling window in seconds. Short windows catch bursts; 60s reflects steady load.
topN?numberHow many processes to keep
fn open-files(path: string, limit?: number)
List files open under a path by walking /proc/<pid>/fd for every process. Unlike lsof on a FUSE mount — which names only the FUSE daemon — a proxy-held file is matched back to the containers holding the same file under their own mount path.
ArgumentTypeDescription
pathstringAbsolute path prefix to report on, e.g. /mnt/disk4
limit?numberMax files to report, largest first
fn attribute(target: string, sampleSeconds?: number, topN?: number, limit?: number)
Answer 'what is reading this disk?' for one device, array slot or mountpoint: resolve it through the block stack, sample the readers, and list the open files with their real consumers. One method because the three probes are only meaningful together — the readers are unattributable without the device map, and the map is inert without the readers.
ArgumentTypeDescription
targetstringDevice (sdl, dm-3), Unraid array slot (disk4, cache), or mountpoint (/mnt/disk4)
sampleSeconds?numberSampling window in seconds
topN?numberHow many readers to keep
limit?numberHow many open files to keep

Resources

deviceMap(infinite)— Block-device topology: every dm/md/loop layer resolved to its physical spindle and Unraid array slot, plus the alias groups a per-device metric would double-count
readers(infinite)— Processes ranked by disk I/O over a sampling window, with requested (rchar) and block (read_bytes) rates side by side and container attribution
openFiles(infinite)— Files open under a path, with FUSE-held descriptors re-attributed to the containers that actually hold them
attribution(infinite)— Full answer for one disk: what it physically is, who is reading it, and which files they have open
04Previous Versions13
2026.10.25.1MIT

2026.10.25.1

Changed

  • Document every exported symbol (JSDoc), so the generated API docs cover the whole module.
2026.10.15.9MIT

2026.10.15.9

Changed

  • Repo-wide maintenance release: version bump to republish the current source. No schema change.
2026.10.15.8MIT

2026.10.15.8

Changed

  • Repo-wide maintenance release: version bump to republish the current source. No schema change.
2026.10.15.6MIT

2026.10.15.6

Changed

  • Repo-wide maintenance release: version bump to republish the current source. No schema change.
2026.10.15.5MIT

2026.10.15.5

Changed

  • Repo-wide maintenance release: version bump to republish the current source. No schema change.
2026.10.15.4MIT

2026.10.15.4

Changed

  • Repo-wide maintenance release: version bump to republish the current source. No schema change.
2026.10.15.3MIT

2026.10.15.3

Changed

  • Repo-wide maintenance release: version bump to republish the current source. No schema change.
2026.10.15.2MIT

2026.10.15.2

Changed

  • Repo-wide maintenance release: version bump to republish the current source. No schema change.
2026.10.15.1MIT

2026.10.15.1

Changed

  • Repo-wide maintenance release: version bump to republish the current source. No schema change.
2026.10.14.2MIT(was none)

2026.10.14.2

Changed

  • Repo-wide maintenance release: version bump to republish the current source. No schema change.

Modified 1 behaviors

2026.09.19.2

2026.09.19.2

Changed

  • Repo-wide maintenance release: version bump to republish the current source. No schema change.
2026.09.17.1

2026.09.17.1

Changed

  • Maintenance republish. Property-test coverage hardened (credit detection now matches exact summary entries); no model behavior or schema change.
2026.08.31.1

2026.08.31.1

First release.

Answers "what is actually reading this disk?" on a Linux host whose storage sits behind device-mapper and FUSE. Built after a homelab disk-saturation alert named dm-3 and sdl as two failing drives when they are one 14.6TB spindle, and lsof /mnt/disk4 could name only shfs while the real reader was a torrent daemon seeding a large library.

  • device-map — resolves every dm/md/loop layer to its physical spindle and Unraid array slot, and returns aliasGroups: the layer sets a per-device metric double-counts.
  • readers — samples /proc/<pid>/io over a window and ranks processes by disk I/O with container attribution, reporting requested bytes (rchar) and block I/O (read_bytes) separately. The gap between them is the FUSE indirection made visible.
  • open-files — walks /proc/<pid>/fd for every process and re-attributes a FUSE-held descriptor to the container holding the same file under its own mount path, matched on basename and size.
  • attribute — all three for one device, array slot or mountpoint, plus a one-line summary.

Every probe is one SSH round trip: the script is generated locally, base64-encoded and piped to bash, so no caller-supplied value ever becomes a remote shell word. path and target are additionally allow-listed. Helpers take an injected SshRunner, so no test spawns ssh.

Three host-behaviour traps are encoded here because each one shipped green and was only caught against the live host:

  • md4p1 -> sdl exists only in /var/local/emhttp/disks.ini. Unraid's md driver is not Linux md — /sys/block/md4 exposes no slaves, so walking /sys/block alone returns physical: null for every encrypted array slot.
  • stat -Lc %n echoes its argument. cd /proc; stat "$pid"/fd/* emits relative keys, so every size parsed as null — which sorted real media files below zero-length rows and silently disabled the size check that stops two files sharing a basename being conflated (Postgres relfilenode 2688 exists under every database on the host).
  • rchar counts sockets and pipes. A websocket-heavy service outranked the real disk reader, so attribute sets onTarget per reader — true only when that process, or a proxy acting for it, holds a descriptor under the disk's mountpoint — and the summary credits nobody else. null (not evaluated) is kept distinct from false.

The fd scan costs two forks per process (ls -l plus stat -L), not per descriptor: ~13 000 open fds in ~25s, against over two minutes for the per-descriptor version.

05Stats
A
100 / 100
Downloads
33
Archive size
23.1 KB
  • Has README or module doc2/2earned
  • README has a code example1/1earned
  • README is substantive1/1earned
  • Most symbols documented1/1earned
  • No slow types (deprecated)1/1earned
  • Dependencies pass trust audit2/2earned
  • Docs keep secrets in a vault1/1earned
  • Has description1/1earned
  • Platform support declared (or universal)2/2earned
  • License2/2earned
  • Verified public repository2/2earned
06Platforms
07Labels