'TEXT', 'cores' => 'INTEGER', 'ram_gib' => 'REAL', 'swap_gib' => 'REAL', 'disk_gb' => 'REAL', 'os' => 'TEXT', 'vm' => 'TEXT', 'gpu' => 'TEXT', ]; // Measured once, during the run, by the probe: neither can be recomputed later, // so a run from an older probe keeps NULL and the board falls back to no weight. const RUN_EXTRAS = [ 'psi_some' => 'REAL', 'psi_full' => 'REAL', 'disk_rota' => 'INTEGER', 'disk_tran' => 'TEXT', 'disk_virt' => 'INTEGER', ]; // PMSS forwards the panel path with this prefix stripped, so the page rebuilds // it to print a URL that works from another node. const PANEL_PREFIX = '/public-vasilves'; function db(): PDO { if (!is_dir(DB_DIR)) { @mkdir(DB_DIR, 0770, true); } $p = new PDO('sqlite:' . DB_FILE); $p->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $p->exec('CREATE TABLE IF NOT EXISTS runs ( id INTEGER PRIMARY KEY AUTOINCREMENT, node TEXT NOT NULL, uploaded_at INTEGER NOT NULL, tested_at INTEGER, yabs_version TEXT, raw TEXT NOT NULL, d4k REAL, d4k_iops REAL, d64k REAL, d512k REAL, d1m REAL, send_mbit REAL, recv_mbit REAL, gb6_single REAL, gb6_multi REAL, uploader TEXT )'); $p->exec('CREATE INDEX IF NOT EXISTS idx_runs_node ON runs(node, tested_at)'); // One row per node and run start: re-posting the same JSON is a no-op. try { $p->exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_uniq ON runs(node, tested_at)'); } catch (PDOException $e) { // Older duplicates survive; inserts still work, dedupe only applies to new rows. } // A node asks for its own token here instead of sharing the admin one. $p->exec('CREATE TABLE IF NOT EXISTS nodes ( name TEXT PRIMARY KEY, token TEXT NOT NULL, gpu TEXT, registered_at INTEGER NOT NULL, last_seen INTEGER )'); $have = array_column($p->query('PRAGMA table_info(runs)')->fetchAll(PDO::FETCH_ASSOC), 'name'); // Additive only: nodes own their rows. Never recreate, drop or rewrite the // table, and never delete the database file: a measurement cannot be re-run // on demand, so a lost row is lost data. foreach (array_merge(HW_COLUMNS, RUN_EXTRAS) as $col => $type) { if (!in_array($col, $have, true)) { $p->exec("ALTER TABLE runs ADD COLUMN $col $type"); } } return $p; } function upload_token(): string { if (!file_exists(TOKEN_FILE)) { $t = bin2hex(random_bytes(16)); file_put_contents(TOKEN_FILE, $t); chmod(TOKEN_FILE, 0600); } return trim((string)file_get_contents(TOKEN_FILE)); } // KBps or MBps string field from yabs fio output, normalised to MB/s. function fio_mbps(array $r, string $key): ?float { if (!isset($r[$key]) || !is_numeric($r[$key])) return null; $unit = strtoupper((string)($r['speed_units'] ?? 'KBps')); return str_contains($unit, 'MB') ? (float)$r[$key] : (float)$r[$key] / 1024; } // iperf3 result strings look like "5.20 Gbits/sec"; return Mbit/s. function rate_mbit($v): ?float { if (!is_string($v) || !preg_match('~([\d.]+)\s*([KMGT])?bits/sec~i', $v, $m)) return null; $n = (float)$m[1]; switch (strtoupper($m[2] ?? 'M')) { case 'G': return $n * 1000; case 'T': return $n * 1000000; case 'K': return $n / 1000; default: return $n; } } // Flatten the yabs JSON (v2026-10-07 layout) into the columns the board shows. function extract_numbers(array $y): array { $out = [ 'd4k'=>null,'d4k_iops'=>null,'d64k'=>null,'d512k'=>null,'d1m'=>null, 'send_mbit'=>null,'recv_mbit'=>null,'gb6_single'=>null,'gb6_multi'=>null, ]; // v2026 puts fio results at the top level; keep the older disk.fio path too. $fio = $y['fio'] ?? ($y['disk']['fio'] ?? []); foreach ((array)$fio as $r) { if (!is_array($r)) continue; $bs = (string)($r['bs'] ?? ''); $mbps = fio_mbps($r, 'speed_rw'); if ($mbps === null) { // older layouts report read and write separately $r2 = fio_mbps($r, 'speed_r'); $w2 = fio_mbps($r, 'speed_w'); if ($r2 !== null && $w2 !== null) $mbps = ($r2 + $w2) / 2; } if ($mbps === null) continue; $iops = isset($r['iops_rw']) && is_numeric($r['iops_rw']) ? (float)$r['iops_rw'] : null; if ($bs === '4k') { $out['d4k'] = $mbps; $out['d4k_iops'] = $iops; } if ($bs === '64k') { $out['d64k'] = $mbps; } if ($bs === '512k') { $out['d512k'] = $mbps; } if ($bs === '1m') { $out['d1m'] = $mbps; } } // iperf3 runs against 7 locations and half of them time out; a dead // location still reports a few Kbit/s. Keep the best one and drop those. $sends = []; $recvs = []; foreach ((array)($y['iperf'] ?? []) as $r) { if (!is_array($r)) continue; $s = rate_mbit($r['send'] ?? null); $v = rate_mbit($r['recv'] ?? null); if ($s !== null && $s >= 1) $sends[] = $s; if ($v !== null && $v >= 1) $recvs[] = $v; } if ($sends) $out['send_mbit'] = max($sends); if ($recvs) $out['recv_mbit'] = max($recvs); // geekbench is a list of runs (4/5/6/7); keep the first one that scored. foreach ((array)($y['geekbench'] ?? []) as $gb) { if (is_array($gb) && (isset($gb['single']) || isset($gb['multi']))) { $out['gb6_single'] = is_numeric($gb['single'] ?? null) ? (float)$gb['single'] : null; $out['gb6_multi'] = is_numeric($gb['multi'] ?? null) ? (float)$gb['multi'] : null; break; } } return $out; } // cpu/mem/os blocks of the yabs JSON, normalised to the columns the board shows. function extract_hardware(array $y): array { $cpu = is_array($y['cpu'] ?? null) ? $y['cpu'] : []; $mem = is_array($y['mem'] ?? null) ? $y['mem'] : []; $os = is_array($y['os'] ?? null) ? $y['os'] : []; return [ 'cpu' => isset($cpu['model']) ? substr((string)$cpu['model'], 0, 120) : null, 'cores' => is_numeric($cpu['cores'] ?? null) ? (int)$cpu['cores'] : null, // yabs reports KiB and KB, so GiB is /1048576 and GB is /1e6. 'ram_gib' => is_numeric($mem['ram'] ?? null) ? round($mem['ram'] / 1048576, 1) : null, 'swap_gib' => is_numeric($mem['swap'] ?? null) ? round($mem['swap'] / 1048576, 1) : null, 'disk_gb' => is_numeric($mem['disk'] ?? null) ? round($mem['disk'] / 1000000) : null, 'os' => isset($os['distro']) ? substr((string)$os['distro'], 0, 80) : null, 'vm' => isset($os['vm']) ? substr((string)$os['vm'], 0, 20) : null, ]; } // What the node's kernel says about the disk behind its home directory. The // probe reads /sys/block//queue/rotational, `lsblk -dno TRAN` and, when the // device exposes it, the ATA rotation rate. NVMe is flash by definition, so the // transport alone settles it. A virtio disk declares nothing: QEMU leaves the // rotational flag at 1 whatever the backing store is, so `?virt=1` wins and the // board prints `virtio` rather than publishing that default as HDD. The board // never guesses from the measured IOPS: a stalled disk measures low, not slow. function disk_media($rota, $tran, $virt = null): string { if ((int)$virt === 1) return 'virtio'; if (strtolower(trim((string)$tran)) === 'nvme') return 'NVMe'; if (!is_numeric($rota)) return '—'; return ((int)$rota) === 0 ? 'SSD' : 'HDD'; } // A run measured while the disk was stalled understates the hardware, so it // counts less in the average. The 0.25 floor keeps a permanently slow disk from // weighting its own bad numbers away. NULL pressure (old run) counts as 1. function wavg(string $col): string { $w = 'CASE WHEN psi_full IS NULL THEN 1.0 ELSE MAX(0.25, 1.0 - psi_full / 100.0) END'; return "SUM($col * $w) / SUM($w)"; } // yabs writes "time":"20261027-164500" (local time of the node, UTC on ours). function parse_tested_at(array $y): ?int { $raw = $y['time'] ?? null; if (!is_string($raw) || $raw === '') return null; $t = DateTimeImmutable::createFromFormat('Ymd-His', $raw, new DateTimeZone('UTC')); if ($t === false) return null; $ts = $t->getTimestamp(); return ($ts > 946684800 && $ts < time() + 86400) ? $ts : null; } // PMSS terminates TLS on its own proxy, so the visitor address arrives here. function client_ip(): string { $xff = (string)($_SERVER['HTTP_X_FORWARDED_FOR'] ?? ''); $ip = trim(explode(',', $xff)[0]); if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip; return (string)($_SERVER['REMOTE_ADDR'] ?? ''); } function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); } // Node names come from ?node=, the form or a token; keep them hostname-shaped. function clean_name($raw): ?string { $n = trim((string)$raw); return preg_match('~^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$~', $n) ? $n : null; } function gib($v, string $unit = 'GiB'): string { return $v === null ? '—' : number_format((float)$v, 1) . ' ' . $unit; } // Hardware barely changes, so the board shows the newest row of each node. function latest_hardware(PDO $pdo): array { $sql = 'SELECT r.node, r.cpu, r.cores, r.ram_gib, r.swap_gib, r.disk_gb, r.os, r.vm, r.gpu, r.disk_rota, r.disk_tran, r.disk_virt, r.tested_at FROM runs r JOIN (SELECT node, MAX(tested_at) m FROM runs GROUP BY node) l ON l.node = r.node AND l.m = r.tested_at GROUP BY r.node'; $out = []; foreach ($pdo->query($sql, PDO::FETCH_ASSOC) as $h) { $out[$h['node']] = $h; } return $out; } function fmt($v): string { if ($v === null) return '—'; $v = (float)$v; return $v >= 100 ? number_format($v, 1) : ($v >= 10 ? number_format($v, 2) : number_format($v, 3)); } function json_out($x, int $code = 200): void { http_response_code($code); header('Content-Type: application/json'); echo json_encode($x); exit; } function clean_nodes($raw): array { if (is_string($raw)) $raw = explode(',', $raw); if (!is_array($raw)) return []; $out = []; foreach (array_slice($raw, 0, 20) as $n) { if (!is_scalar($n)) continue; $n = trim((string)$n); if ($n !== '' && strlen($n) <= 100) $out[$n] = true; } return array_keys($out); } function node_filter(array $pdoParams, string $sql, array $nodes): array { if (!$nodes) return [$sql, $pdoParams]; $ph = []; foreach ($nodes as $i => $n) { $ph[] = ":n$i"; $pdoParams[":n$i"] = $n; } return [$sql . ' AND node IN (' . implode(',', $ph) . ')', $pdoParams]; } // ---------- routing ---------- $pdo = db(); $nodesFilter = clean_nodes($_GET['nodes'] ?? ''); $do = (string)($_GET['do'] ?? ''); // A node installs the probe, which calls this once to get its own token. if ($do === 'register') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { json_out(['ok'=>false,'error'=>'POST {"name":""}'], 405); } $in = json_decode((string)file_get_contents('php://input', false, null, 0, 4096), true); if (!is_array($in)) { json_out(['ok'=>false,'error'=>'body must be JSON'], 400); } $name = clean_name($in['name'] ?? ''); if ($name === null) { json_out(['ok'=>false,'error'=>'name must look like a hostname'], 400); } if ((int)$pdo->query('SELECT COUNT(*) FROM nodes')->fetchColumn() >= 500) { json_out(['ok'=>false,'error'=>'registry is full'], 507); } $st = $pdo->prepare('SELECT name FROM nodes WHERE name = ?'); $st->execute([$name]); if ($st->fetchColumn() !== false) { // No token is ever handed out twice: a lost token needs a manual delete. json_out(['ok'=>false,'error'=>"$name is already registered; ask the board owner to delete its row"], 409); } $token = bin2hex(random_bytes(16)); $gpu = clean_name($in['gpu'] ?? '') ?: null; $pdo->prepare('INSERT INTO nodes (name, token, gpu, registered_at) VALUES (?,?,?,?)') ->execute([$name, $token, $gpu, time()]); json_out(['ok'=>true, 'name'=>$name, 'token'=>$token, 'gpu'=>$gpu]); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $raw = (string)file_get_contents('php://input', false, null, 0, MAX_BODY + 1); if (strlen($raw) > MAX_BODY) { json_out(['ok'=>false,'error'=>'body too large'], 413); } // Header for the web form; query string so a plain `yabs -s ` works too. // The shared admin token can post any node; a registered node token only its // own name, so a leaked node token cannot overwrite another node. $tok = (string)($_SERVER['HTTP_X_UPLOAD_TOKEN'] ?? ($_GET['token'] ?? '')); $ownNode = null; if (!hash_equals(upload_token(), $tok)) { $st = $pdo->prepare('SELECT name FROM nodes WHERE token = ?'); $st->execute([$tok]); $ownNode = $st->fetchColumn(); if ($ownNode === false) { json_out(['ok'=>false,'error'=>'invalid token'], 401); } } $in = json_decode($raw, true); if (!is_array($in)) { json_out(['ok'=>false,'error'=>'body must be JSON'], 400); } // Accepted shapes: the raw `yabs.sh -j` JSON (node from ?node=), or // {"nodes":["le4"], "yabs": {...}}. $yabs = isset($in['yabs']) && is_array($in['yabs']) ? $in['yabs'] : $in; $nodes = $ownNode !== null ? [$ownNode] : clean_nodes($in['nodes'] ?? ($_GET['node'] ?? ($_GET['nodes'] ?? ''))); if (!$nodes) { json_out(['ok'=>false,'error'=>'missing node: pass ?node=le4 or {"nodes":[...]}'], 400); } $nums = extract_numbers($yabs); $hasData = false; foreach ($nums as $k => $v) { if ($v !== null && $k !== 'gb6_single' && $k !== 'gb6_multi') $hasData = true; } if (!$hasData) { json_out(['ok'=>false,'error'=>'no YABS disk or net numbers recognised'], 422); } // Data cleaning at the door: small blocks cannot be fast on any disk, so // these ceilings catch a swapped field or a unit mistake. foreach (['d4k'=>200, 'd64k'=>2000, 'd512k'=>20000, 'd1m'=>20000] as $k => $max) { if ($nums[$k] !== null && ($nums[$k] <= 0 || $nums[$k] > $max)) { json_out(['ok'=>false,'error'=>"$k implausible: {$nums[$k]} MB/s"], 422); } } foreach (['send_mbit','recv_mbit'] as $k) { if ($nums[$k] !== null && ($nums[$k] <= 0 || $nums[$k] > 100000)) { json_out(['ok'=>false,'error'=>"$k implausible: {$nums[$k]} Mbit/s"], 422); } } $tested = parse_tested_at($yabs) ?? time(); $hw = extract_hardware($yabs); // The probe passes its GPU name on the query string: yabs cannot report one. $gpu = clean_name($_GET['gpu'] ?? '') ?: null; // Same for the IO pressure during the run and the disk's media type. $pct = fn(string $k) => is_numeric($_GET[$k] ?? null) && (float)$_GET[$k] >= 0 && (float)$_GET[$k] <= 100 ? round((float)$_GET[$k], 1) : null; $rota = in_array((string)($_GET['rota'] ?? ''), ['0', '1'], true) ? (int)$_GET['rota'] : null; $tran = preg_match('~^[a-z0-9]{1,12}$~', strtolower((string)($_GET['tran'] ?? ''))) ? strtolower($_GET['tran']) : null; $virt = ((string)($_GET['virt'] ?? '') === '1') ? 1 : null; $cols = array_keys(HW_COLUMNS); $st = $pdo->prepare('INSERT OR IGNORE INTO runs (node, uploaded_at, tested_at, yabs_version, raw, d4k, d4k_iops, d64k, d512k, d1m, send_mbit, recv_mbit, gb6_single, gb6_multi, uploader, psi_some, psi_full, disk_rota, disk_tran, disk_virt, ' . implode(', ', $cols) . ') VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,' . rtrim(str_repeat('?,', count($cols)), ',') . ')'); $saved = []; $skipped = []; foreach ($nodes as $node) { $st->execute(array_merge([ $node, time(), $tested, (string)($yabs['version'] ?? 'unknown'), json_encode($yabs), $nums['d4k'], $nums['d4k_iops'], $nums['d64k'], $nums['d512k'], $nums['d1m'], $nums['send_mbit'], $nums['recv_mbit'], $nums['gb6_single'], $nums['gb6_multi'], substr(client_ip(), 0, 45), $pct('psi_some'), $pct('psi_full'), $rota, $tran, $virt, ], array_values($hw), [$gpu])); if ($st->rowCount() > 0) { $saved[] = $node; } else { $skipped[] = $node; } } if ($ownNode !== null) { $pdo->prepare('UPDATE nodes SET gpu = COALESCE(?, gpu), last_seen = ? WHERE name = ?') ->execute([$gpu, time(), $ownNode]); } json_out([ 'ok' => (bool)$saved, 'saved' => $saved, 'duplicate' => $skipped, 'error' => $saved ? null : 'already uploaded for this run time', ], $saved ? 200 : 409); } if ($do === 'api') { $cut = time() - CUTOFF_MONTHS * 30 * 86400; [$q, $p] = node_filter(['cut'=>$cut], 'SELECT node, COUNT(*) n, MIN(tested_at) first, MAX(tested_at) last, ' . wavg('d4k') . ' a4k, MIN(d4k) mn4k, MAX(d4k) mx4k, ' . wavg('d4k_iops') . ' aiops, ' . wavg('d64k') . ' a64, ' . wavg('d512k') . ' a512, ' . wavg('d1m') . ' a1m, AVG(send_mbit) asend, AVG(recv_mbit) arecv, AVG(gb6_single) agbs, AVG(gb6_multi) agbm, AVG(psi_full) astall FROM runs WHERE tested_at >= :cut', $nodesFilter); $q .= ' GROUP BY node ORDER BY node'; $st = $pdo->prepare($q); $st->execute($p); $rows = $st->fetchAll(PDO::FETCH_ASSOC); $hw = latest_hardware($pdo); foreach ($rows as $i => $r) { $rows[$i] = array_merge($r, array_diff_key($hw[$r['node']] ?? [], ['node' => 1])); $rows[$i]['disk'] = disk_media($rows[$i]['disk_rota'] ?? null, $rows[$i]['disk_tran'] ?? null, $rows[$i]['disk_virt'] ?? null); } foreach ($nodesFilter as $n) { if (!in_array($n, array_column($rows, 'node'), true)) $rows[] = ['node'=>$n, 'n'=>0]; } usort($rows, fn($a,$b) => strcmp($a['node'], $b['node'])); json_out(['cutoff_months'=>CUTOFF_MONTHS, 'nodes'=>$rows]); } if ($do === 'list') { [$q, $p] = node_filter([], 'SELECT id, node, tested_at, uploaded_at, yabs_version, d4k, d4k_iops, d64k, d512k, d1m, send_mbit, recv_mbit, gb6_single, gb6_multi, psi_some, psi_full, disk_rota, disk_tran, disk_virt FROM runs WHERE 1=1', $nodesFilter); $q .= ' ORDER BY tested_at DESC LIMIT 500'; $st = $pdo->prepare($q); $st->execute($p); json_out(['runs'=>$st->fetchAll(PDO::FETCH_ASSOC)]); } // ---------- HTML view ---------- $allNodes = $pdo->query('SELECT DISTINCT node FROM runs ORDER BY node')->fetchAll(PDO::FETCH_COLUMN); $cut = time() - CUTOFF_MONTHS * 30 * 86400; echo ' PM node YABS board '; echo '

Node benchmark board

'; echo '

Verified YABS runs. Averages roll on a ' . CUTOFF_MONTHS . '-month cutoff (run date), so stale numbers drop out.

'; // PMSS serves the same docroot at two addresses: /public-/ on its own // domain, and / on a customer domain such as 05877bc1b477317f.mcx.fi. The // customer vhost rewrites to the prefixed path internally, so strip the prefix // and put it back only for the panel hostname: a copied URL must not name the // account. $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; $path = strtok($_SERVER['REQUEST_URI'], '?'); if (strpos($path, PANEL_PREFIX) === 0) { $path = substr($path, strlen(PANEL_PREFIX)) ?: '/'; } if (strpos($host, 'pulsedmedia.com') !== false && strpos($path, PANEL_PREFIX) !== 0) { $path = PANEL_PREFIX . $path; } $scheme = (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https' || !empty($_SERVER['HTTPS'])) ? 'https' : 'http'; $board = $scheme . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . rtrim($path, '/') . '/'; $self = $board . '?token=TOKEN&node=$(hostname -s)'; $cmds = 'curl -fsSLO ' . $board . "probe.sh\n" . "sh probe.sh # register this node, monthly cron\n" . "sh probe.sh install daily hourly # rates: benchmark, self-update\n" . "sh probe.sh update # refresh the probe.sh copy\n" . "sh probe.sh now # one run on screen, 10 to 20 minutes\n" . "sh probe.sh status # running or not, last log line\n"; echo '

Another node, run these on it:

'; echo '
' . h($cmds) . '
'; echo '

From a node with the admin token: yabs.sh -j -s ' . h($self) . '

'; echo '

README · llms.txt' . ' (machine-readable install and API summary) · JSON' . ' · 3D hangar · history' . ' · source

'; echo '
Add a run (paste YABS JSON)

Node name(s), comma-separated:   Upload token:

'; if (!$allNodes) { echo '

No runs yet. Post one from a node, or paste JSON above.

'; exit; } $q = 'SELECT node, COUNT(*) n, MAX(tested_at) last, ' . wavg('d4k') . ' a4k, ' . wavg('d4k_iops') . ' aiops, ' . wavg('d64k') . ' a64, ' . wavg('d512k') . ' a512, ' . wavg('d1m') . ' a1m, AVG(send_mbit) asend, AVG(recv_mbit) arecv, AVG(gb6_single) agbs, AVG(gb6_multi) agbm, AVG(psi_full) astall FROM runs WHERE tested_at >= ? GROUP BY node ORDER BY node'; $st = $pdo->prepare($q); $st->execute([$cut]); $avgs = array_values(array_filter($st->fetchAll(PDO::FETCH_ASSOC), fn($a) => ($a['n'] ?? 0) > 0)); foreach ($nodesFilter as $n) { if (!in_array($n, array_column($avgs, 'node'), true)) $avgs[] = ['node'=>$n, 'n'=>0]; } usort($avgs, fn($a,$b) => strcmp($a['node'], $b['node'])); echo '

Nodes

'; echo ''; $hw = latest_hardware($pdo); foreach ($hw as $h) { $disk = $h['disk_gb'] === null ? '—' : ((float)$h['disk_gb'] >= 1000 ? number_format((float)$h['disk_gb'] / 1000, 1) . ' TB' : number_format((float)$h['disk_gb'], 0) . ' GB'); echo '' . '' . '' . '' . '' . '' . '' . '' . ''; } echo '
nodecpucoresramswapdiskgpuosvmlast run
' . h($h['node']) . '' . h((string)($h['cpu'] ?? '—')) . '' . ($h['cores'] === null ? '—' : (int)$h['cores']) . '' . gib($h['ram_gib']) . '' . gib($h['swap_gib']) . '' . $disk . '' . h((string)($h['gpu'] ?? '—')) . '' . h((string)($h['os'] ?? '—')) . '' . h((string)($h['vm'] ?? '—')) . '' . date('Y-m-d', (int)$h['tested_at']) . '
'; echo '

Averages, last ' . CUTOFF_MONTHS . ' months

'; echo ''; foreach ($avgs as $a) { if (!$a['n']) { echo ''; continue; } echo '' . '' . '' . '' . '' . '' . '' . '' . ''; } echo '
nodemediaruns4k MB/s4k IOPS64k512k1mio stallsend Gbit/srecv Gbit/sGB s/m
' . h($a['node']) . '—0no run in the window
' . h($a['node']) . '' . h(disk_media($hw[$a['node']]['disk_rota'] ?? null, $hw[$a['node']]['disk_tran'] ?? null, $hw[$a['node']]['disk_virt'] ?? null)) . '' . (int)$a['n'] . '' . fmt($a['a4k']) . '' . ($a['aiops']!==null?number_format((float)$a['aiops'],0):'—') . '' . fmt($a['a64']) . '' . fmt($a['a512']) . '' . fmt($a['a1m']) . '' . ($a['astall']!==null?number_format((float)$a['astall'],0).'%':'—') . '' . ($a['asend']!==null?number_format($a['asend']/1000,2):'—') . '' . ($a['arecv']!==null?number_format($a['arecv']/1000,2):'—') . '' . ($a['agbs']!==null?fmt($a['agbs']).'/'.fmt($a['agbm']):'—') . '
'; echo '

Per-node runs (last ' . CUTOFF_MONTHS . ' months)

'; foreach ($avgs as $a) { $node = $a['node']; $st2 = $pdo->prepare('SELECT id, tested_at, yabs_version, d4k, d4k_iops, d64k, d512k, d1m, send_mbit, recv_mbit, psi_full FROM runs WHERE node = ? AND tested_at >= ? ORDER BY tested_at DESC'); $st2->execute([$node, $cut]); $runs = $st2->fetchAll(PDO::FETCH_ASSOC); echo '

' . h($node) . '

'; if (!$runs) { echo '

no run in the window (last overall: ' . (!empty($a['last']) ? date('Y-m-d', (int)$a['last']) : 'never') . ')

'; continue; } echo ''; foreach ($runs as $r) { echo '' . '' . '' . '' . '' . '' . ''; } echo '
date4k MB/sIOPS64k512k1mio stallsend Gbit/srecv Gbit/syabs
' . date('Y-m-d', (int)$r['tested_at']) . '' . fmt($r['d4k']) . '' . ($r['d4k_iops']!==null?number_format((float)$r['d4k_iops'],0):'—') . '' . fmt($r['d64k']) . '' . fmt($r['d512k']) . '' . fmt($r['d1m']) . '' . ($r['psi_full']!==null?number_format((float)$r['psi_full'],0).'%':'—') . '' . ($r['send_mbit']!==null?number_format($r['send_mbit']/1000,2):'—') . '' . ($r['recv_mbit']!==null?number_format($r['recv_mbit']/1000,2):'—') . '' . h((string)$r['yabs_version']) . '
'; } echo '

Disk numbers are the fio mixed read/write figures (speed_rw, IOPS). Net numbers are the best iperf3 location, since a timed-out location still reports a few Kbit/s. On shared storage these swing widely between runs: read them as a trend, not a spec.

'; echo '

Media is what the node kernel says about the disk under its home directory: lsblk transport nvme, else the rotational flag, with the ATA rotation rate overriding it. A virtio disk declares no media at all, because QEMU leaves the rotational flag at 1 whatever the backing store is, so the board names it virtio instead of publishing that default; a RAID shows its array, not the members. io stall is the share of the run window that a task spent blocked on IO (kernel PSI, /proc/pressure/io): a high value means the disk was congested while measured, not necessarily slow. The disk averages are therefore weighted by 1 - stall, floored at 0.25, so a congested run counts less; the per-run tables keep the raw numbers. Runs from before 2026-10-27 carry no stall and weigh 1, and a virtual disk among them still reads HDD because it has no virt flag.

'; echo '';