# YABS node board
A public board of [YABS](https://github.com/masonr/yet-another-bench-script)
runs (disk and network benchmarks) from several nodes. One PHP file, one
SQLite database, no framework.
Board URL:
PMSS serves this account's docroot (`~/www/public`) at two addresses: the
panel path `https://le4-0-78-102caroline.pulsedmedia.com/public-vasilves/yabs/`
and the root of a customer domain. Use the customer domain: the panel path
names the account in every URL a stranger copies, and PMSS's proxy only
forwards that path, so `/yabs/` on the panel hostname answers PMSS's own 404.
## Install on a node
Requirements: `bash`, `curl`, a writable `$HOME`, and the right to edit your
own crontab. No root.
```sh
curl -fsSLO https://05877bc1b477317f.mcx.fi/yabs/probe.sh
sh probe.sh
```
This does three things:
1. downloads `yabs.sh` from into `~/.yabs-probe/`;
2. registers the short hostname with the board and stores the node token in
`~/.yabs-probe/conf` (mode 0600);
3. adds one crontab line: a monthly benchmark at a random time between 01:00
and 02:59.
Re-running `probe.sh` is safe: it keeps the existing token and does not add a
second cron line.
A node can have two cron lines: the benchmark (`run.sh`) and a self-update
(`probe.sh update`). Each command below changes one line, never the other:
```sh
sh probe.sh # install: benchmark monthly if no line exists
sh probe.sh schedule weekly # benchmark rate only
sh probe.sh update hourly # self-update rate only
sh probe.sh update off # drop the self-update line
```
`schedule` takes `daily`, `weekly` or `monthly`; `update` takes `hourly`,
`daily`, `weekly`, `monthly` or `off`. The benchmark refuses `hourly`: one run
takes 10 to 20 minutes, where an update is a single 7 KB download. A bare
`sh probe.sh` never resets a rate and never adds a line that already exists.
`schedule` draws a new random slot for the benchmark between 01:00 and 02:59, so
several nodes installed the same day run at different times instead of all
hitting their disks at once. That hour is the quiet part of the night: it stays
clear of the 00:13 disk probe, the 03:00 backup tarball and the 04:00 configarr
run. `update` never moves the benchmark. Move the window with `BENCH_FROM` and
`BENCH_TO`:
```sh
BENCH_FROM=5 BENCH_TO=9 sh probe.sh schedule daily # daily between 05:00 and 08:59
```
## History
[history](https://05877bc1b477317f.mcx.fi/yabs/history) plots every
stored run, one chart per metric (4k, IOPS, 64k, 512k, 1M, send, receive) and
one colour per node. Hover a point for its date and value; the legend compares
the last 1M value with the previous run. The page is server-rendered SVG, so it
needs no JavaScript and no chart library.
## Read the source
[source](https://05877bc1b477317f.mcx.fi/yabs/source) prints the exact
bytes of every file this board serves, with a SHA-256 per file and a download
link (`?f=probe.sh&raw=1`). Download a file, run `sha256sum` on it, compare with
the hash on the page: a match proves the code you read is the code that runs.
## 3D hangar
[racks](https://05877bc1b477317f.mcx.fi/yabs/racks) draws one hall per
hostname family (`le4`, `le6`) and one rack per node. Rack height and colour
come from the latest 1M fio throughput; hover a rack for its full numbers, drag
to orbit, wheel to zoom. The page reads the same database, read-only, and adds
no dependency: the projection is about 100 lines of canvas code.
The hall is the hostname family, and its label adds the datacenter row that the
first two letters of the name describe: `le` and `lt` are Lauttasaari, `ke` and
`kt` are Kerava, and a second Kerava DC will be `k2`. Names older than that
scheme show the family alone. Neither YABS nor the database records where a node
really sits, so any grouping here follows the name.
## Use it
```sh
sh probe.sh now # one run in the foreground, output on screen
sh probe.sh status # running or not, elapsed, last log line
sh probe.sh refresh # fetch the newest probe.sh and re-install
```
`refresh` fetches `${BOARD}probe.sh`, checks it with `sh -n`, stores it as
`~/.yabs-probe/probe.sh` and re-runs it with no argument. The token, the
benchmark rate and both cron lines stay as they are, so `refresh` never rewrites
the crontab. It prints nothing when the copy it already has is current, which
keeps the hourly cron run from mailing anything. A bare `sh probe.sh update` is
the old spelling of `refresh`: the cron lines installed before 2026-10-27 call
it that way.
The update cron line is separate from the benchmark one, so changing one never
disturbs the other.
`now` and `status` install nothing: they never register, never add a cron line
and never print the token. Run `sh probe.sh` once first; without a registration
`now` stops with `not installed`.
A run takes 10 to 20 minutes, mostly generating the fio test file. The cron run
logs to `~/.yabs-probe/last.log` and writes its state to
`~/.yabs-probe/progress`. Geekbench is skipped (`-g`): one score costs several
GB of downloads.
A node token can only post its own name. The board owner also holds an admin
token that can post any name; you do not need it.
## What the board stores
Per run: CPU model, cores, RAM and swap (GiB), disk size (GB), distribution,
kernel, virtualisation type, GPU, fio 4k/64k/512k/1m in MB/s with IOPS,
iperf3 send and receive in Mbit/s, and Geekbench single and multi when present.
YABS cannot report a GPU, so `probe.sh` sends the `/dev/dri/renderD*` name.
The HTML pages print every number the same way: a dot for the decimal mark and
a space for the thousands separator, with two decimals for MB/s. So you read
`6.69`, `43.70` and `10 644 IOPS`, and a bigger number is always the bigger
figure, whatever its column. The JSON API returns raw numbers, not strings.
Two more fields come from the node's kernel, because YABS reports neither:
- **Disk media** (`virtio`, `HDD`, `SSD`, `NVMe`): `probe.sh` resolves the
device under `$HOME` in `/sys/block`, reads its `queue/rotational` flag and
its `lsblk` transport, and lets `ID_ATA_ROTATION_RATE_RPM` override the flag.
NVMe settles it on the transport alone. A device under a `virtio` node in
`/sys` gets `virtio`: QEMU leaves the rotational flag at 1 whatever the
backing store is, so the flag says nothing there. The board never guesses the
type from the measured IOPS: a stalled disk measures low, not slow.
- **IO stall** (%): the share of the run window that a task spent blocked on IO,
from the kernel's pressure stall information (`/proc/pressure/io`). The probe
reads the cumulative `some` and `full` counters before and after the run, so
the difference over the elapsed time is the average stall of that window.
The disk averages are therefore weighted. A run measured while the disk was
stalled understates the hardware, so it counts as `1 - stall` (floored at 0.25)
instead of 1. The per-run tables keep the raw numbers, and a run with no stall
reading weighs 1, so numbers from an older probe stay in the average.
Averages cover the last 6 months, by run date. The header counts every stored
run, and how many started today (Europe/Helsinki, the node's own clock).
The chips above the node table filter the page without JavaScript. Tick one or
more node names: the node table, the averages and the per-node run sections keep
only those rows. Tick none and every node comes back. The header counters do not
follow the chips, because the server counts them before the page is sent, and a
reload clears the choice.
## API
| Request | Answer |
|---|---|
| `POST ?do=register` with `{"name":"node","gpu":"renderD128"}` | 200 `{"ok":true,"token":"..."}`; the token is returned once |
| `POST /` with header `X-Upload-Token: ` and the raw `yabs.sh -j` JSON | 200 `{"ok":true,"saved":["node"]}` |
| `GET ?do=api` | JSON per node: hardware, run count, first and last run, averages |
| `GET ?do=list` | JSON, the 500 newest runs |
The node name comes from `?node=NAME` (or `?nodes=a,b` for one run posted
under several names). A node token forces its own name, whatever `?node=` says.
`?gpu=NAME` is optional and overrides the stored GPU for that run.
| Code | Meaning |
|---|---|
| 400 | not JSON, no node name, or a name that is not hostname-shaped |
| 401 | wrong or missing token |
| 405 | `?do=register` called with GET |
| 409 | this node already posted at this run time, or the name is taken |
| 413 | body over 256 KB |
| 422 | no disk or network number found, or a value outside the plausible range |
| 507 | the registry is full (500 nodes) |
## Known limits
- **A name is a label, not a proof.** Registration is open and uploads are not
signed, so anyone can claim a free name and post numbers under it.
- **The token travels in the URL** when you use `yabs.sh -s`, so it lands in
the web server log. A node token can only write its own name.
- **Numbers vary between runs.** The nodes share storage with other accounts,
so a second run on the same box can differ by a large factor.
- **IO stall is the node's own view.** Pressure stall information counts the
tasks on that machine. A slow host disk makes its requests slow, so the stall
rises, but the number also rises when the node itself is busy: it says the
measurement was congested, not who caused it.
- **The disk media comes from the node.** A virtual disk declares nothing:
QEMU reports `rotational=1` for an SSD too, so the board shows `virtio`
instead of `HDD`. A RAID array reports itself, not its members, and the probe
does not read SMART, so a case like "flash on a SATA bridge" can be wrong.
- **Rows from before 2026-10-27 carry neither stall nor `virt`.** They keep
their numbers and weigh 1 in the averages. Their KVM rows were backfilled to
`virtio` on 2026-10-27, because a virtual disk declares nothing either way;
the rows from bare-metal nodes keep the flag verdict.
- **No delete.** Ask the board owner to remove a row.
- **The update runs what the board serves.** `sh probe.sh refresh` downloads
`${BOARD}probe.sh` and runs it, and with the hourly line it does so from cron.
Whoever controls that URL runs code on every node that installed the update.
The download is checked with `sh -n` and rejected if it is not shell, but that
only stops a broken file, not a hostile one. Use `sh probe.sh update off` on
a node that must not trust the board.
- **The token stays out of cron mail.** `probe.sh` prints it only on a terminal.
## For an AI agent
Use this checklist when you are asked to benchmark a machine and publish the
result:
1. Read `llms.txt` next to this file. It holds the same facts in one screen.
2. Check that you are on the machine to be measured: `hostname -s` must match
the name you will register. Do not post a run executed elsewhere.
3. Install and register:
`curl -fsSLO /probe.sh && sh probe.sh`.
4. Run once and wait for the result:
`sh probe.sh now`. Expect 10 to 20 minutes; do not interrupt it.
5. Confirm the upload:
`curl -s '?do=api' | head -c 400`. The node must appear with its
hardware and at least one run.
6. If the node is already registered and you only need a fresh run, skip step 3
and call `~/.yabs-probe/run.sh`.
Rules: do not re-register to work around a 409; the default is one run per
month, and each node's owner decides its own rate. Report the exact node name
and the run time with the numbers.