Source of the YABS board

These are the exact bytes the server runs. Compare the hash with your own sha256sum of the downloaded copy: if the two match, the page you read is the program you reviewed.
history.php · 251 lines · 9.7 KiB · sha256 0085dc91691c233f1e66ddf52844493224da111d1375ce9e812a996f94d47a20 · download · board · README

<?php
// History of every stored run, one chart per metric, plain server-rendered SVG.
// No JavaScript and no chart library: a few polylines and circles.
// Reads the board's database read-only.

declare(strict_types=1);
error_reporting(E_ALL & ~E_DEPRECATED);
ini_set('display_errors', '0');
date_default_timezone_set('UTC');

const 
DB_PATH = '/home/vasilves/yabs-data/yabs.sqlite';
const 
COLOURS = ['#58a6ff', '#7ee787', '#f0883e', '#d2a8ff', '#ffa198', '#76e3ea', '#e3b341', '#79c0ff'];
const 
METRICS = [
    
'd4k'       => ['4k read-write', 'MB/s', 2],
    
'd4k_iops'  => ['4k IOPS', 'IOPS', 0],
    
'd64k'      => ['64k read-write', 'MB/s', 2],
    
'd512k'     => ['512k read-write', 'MB/s', 2],
    
'd1m'       => ['1M read-write', 'MB/s', 2],
    
'send_mbit' => ['Network send', 'Mbit/s', 0],
    
'recv_mbit' => ['Network receive', 'Mbit/s', 0],
    
'psi_full'  => ['IO stall during the run', '%', 1],
];

function 
h(?string $s): string
{
    return 
htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');
}

function 
num($v, int $d): string
{
    return 
$v === null ? 'n/a' : number_format((float) $v, $d);
}

$error = null;
$rows = [];
try {
    
$db = new PDO('sqlite:' . DB_PATH, null, null, [
        
PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]);
    
// psi_full is added by index.php's migration; read NULL until it exists so
    // this page never breaks on a fresh deployment order.
    
$cols = array_column($db->query('PRAGMA table_info(runs)')->fetchAll(), 'name');
    
$psi = in_array('psi_full', $cols, true) ? 'psi_full' : 'NULL AS psi_full';
    
$rows = $db->query(
        
'SELECT node, tested_at, d4k, d4k_iops, d64k, d512k, d1m, send_mbit, recv_mbit, ' . $psi . '
           FROM runs WHERE tested_at IS NOT NULL ORDER BY tested_at'
    
)->fetchAll();
} catch (
Throwable $e) {
    
$error = $e->getMessage();
}

$series = [];   // node => list of rows
// ?nodes=a,b narrows the page to those nodes: with a flash disk on the same
// chart as platters, the y axis scales to the fastest run and the rest flattens.
$filter = array_values(array_filter(array_map('trim', explode(',', (string) ($_GET['nodes'] ?? ''))),
    static fn(
$n) => $n !== ''));
if (
$filter) {
    
$rows = array_values(array_filter($rows, static fn(array $r) => in_array((string) $r['node'], $filter, true)));
}
foreach (
$rows as $row) {
    
$series[(string) $row['node']][] = $row;
}
ksort($series);
$colours = [];
$i = 0;
foreach (
array_keys($series) as $node) {
    
$colours[$node] = COLOURS[$i++ % count(COLOURS)];
}

$xs = array_map(static fn($r) => (int) $r['tested_at'], $rows);
$t0 = $xs ? min($xs) : 0;
$t1 = $xs ? max($xs) : 1;
if (
$t1 - $t0 < 3600) {          // one run, or runs within the hour: widen the axis
    
$t0 -= 1800;
    
$t1 += 1800;
}
$span = $t1 - $t0;
$stamp = static fn(int $t): string => gmdate($span > 3 * 86400 ? 'm-d' : 'H:i', $t);

// Keeps a y axis readable: round the ceiling up to 1, 2 or 5 times a power of ten.
function ceiling(float $max): float
{
    if (
$max <= 0) {
        return 
1.0;
    }
    
$mag = 10 ** floor(log10($max));
    foreach ([
1, 2, 5, 10] as $step) {
        if (
$max <= $step * $mag) {
            return 
$step * $mag;
        }
    }
    return 
10 * $mag;
}

function 
chart(string $key, array $meta, array $series, array $colours, int $t0, int $t1, callable $stamp): string
{
    [
$label, $unit, $decimals] = $meta;
    
$w = 420; $hgt = 190; $left = 58; $right = 12; $top = 12; $bottom = 26;
    
$pw = $w - $left - $right;
    
$ph = $hgt - $top - $bottom;

    
$max = 0.0;
    
$points = 0;
    foreach (
$series as $list) {
        foreach (
$list as $r) {
            if (
$r[$key] !== null) {
                
$max = max($max, (float) $r[$key]);
                
$points++;
            }
        }
    }
    
$ymax = ceiling($max);
    
$sx = static fn(int $t): float => $left + ($t - $t0) / max(1, $t1 - $t0) * $pw;
    
$sy = static fn(float $v): float => $top + $ph - ($v / $ymax) * $ph;

    
$svg = '<svg viewBox="0 0 ' . $w . ' ' . $hgt . '" role="img" aria-label="' . h($label) . '">';
    
// Grid and y labels at 0, half and full scale.
    
for ($g = 0; $g <= 2; $g++) {
        
$v = $ymax * $g / 2;
        
$y = $sy($v);
        
$svg .= sprintf('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="#21262d"/>', $left, $y, $left + $pw, $y);
        
$tick = $v === 0.0 ? '0' : number_format($v, $ymax < 10 ? 2 : 0);
        
$svg .= sprintf('<text x="%d" y="%.1f" fill="#6e7681" font-size="10" text-anchor="end">%s</text>',
            
$left - 6, $y + 3.5, h($tick));
    }
    
// x labels at both ends.
    
$svg .= sprintf('<text x="%d" y="%d" fill="#6e7681" font-size="10">%s</text>', $left, $hgt - 8, h($stamp($t0)));
    
$svg .= sprintf('<text x="%d" y="%d" fill="#6e7681" font-size="10" text-anchor="end">%s</text>',
        
$left + $pw, $hgt - 8, h($stamp($t1)));

    foreach (
$series as $node => $list) {
        
$pts = [];
        foreach (
$list as $r) {
            if (
$r[$key] === null) {
                continue;
            }
            
$pts[] = [$sx((int) $r['tested_at']), $sy((float) $r[$key]), $r];
        }
        if (!
$pts) {
            continue;
        }
        if (
count($pts) > 1) {
            
$d = [];
            foreach (
$pts as $p) {
                
$d[] = sprintf('%.1f,%.1f', $p[0], $p[1]);
            }
            
$svg .= sprintf('<polyline fill="none" stroke="%s" stroke-width="1.6" points="%s"/>',
                
$colours[$node], implode(' ', $d));
        }
        foreach (
$pts as $p) {
            
$svg .= sprintf('<circle cx="%.1f" cy="%.1f" r="2.6" fill="%s"><title>%s</title></circle>',
                
$p[0], $p[1], $colours[$node],
                
h(sprintf("%s\n%s UTC\n%s %s", $node, gmdate('Y-m-d H:i', (int) $p[2]['tested_at']), num($p[2][$key], $decimals), $unit)));
        }
    }

    
$svg .= '</svg>';
    return 
$points ? $svg : '<p class="empty">No ' . h(strtolower($label)) . ' data yet.</p>';
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>YABS history</title>
<link rel="icon" href="data:,">
<style>
  :root { color-scheme: dark; }
  body { margin: 0; background: #0d1117; color: #c9d1d9;
         font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
  header { padding: 14px 18px 10px; border-bottom: 1px solid #21262d; }
  h1 { margin: 0 0 4px; font-size: 15px; font-weight: 600; letter-spacing: .04em; }
  h2 { margin: 0 0 6px; font-size: 12px; font-weight: 600; color: #8b949e; }
  main { display: grid; grid-template-columns: repeat(auto-fit, minmax(440px, 1fr));
         gap: 18px; padding: 16px 18px 40px; }
  section { border: 1px solid #21262d; border-radius: 7px; padding: 10px 12px 4px; background: #0f141b; }
  svg { width: 100%; height: auto; display: block; }
  .mut { color: #8b949e; }
  a { color: #58a6ff; text-decoration: none; }
  a:hover { text-decoration: underline; }
  .empty { color: #6e7681; padding: 12px 4px; }
  .legend { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 6px; }
  .chip { display: inline-flex; align-items: center; gap: 6px; }
  .dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
  .delta.up { color: #7ee787; }
  .delta.down { color: #ffa198; }
</style>
</head>
<body>
<header>
  <h1>YABS history</h1>
  <nav class="mut" style="margin-bottom:6px">
    <a href="./">board</a> &middot;
    <a href="./?do=api">JSON</a> &middot;
    <a href="racks">3D hangar</a> &middot;
    <a href="source?f=history.php">source</a>
  </nav>
  <div class="mut">
    Every stored run, one point per run, one colour per node. Hover a point for
    its date and value. The delta compares the last run with the one before it.
    The y axis scales to the fastest run on the page, so click a node name to
    hide the others and keep the slow ones readable.
  </div>
  <div class="legend">
    <?php foreach ($series as $node => $list):
        
$vals = array_values(array_filter(array_column($list, 'd1m'), static fn($v) => $v !== null));
        
$last = $vals ? (float) end($vals) : null;
        
$prev = count($vals) > 1 ? (float) $vals[count($vals) - 2] : null;
        
$delta = ($last !== null && $prev !== null && $prev > 0) ? ($last - $prev) / $prev * 100 : null; ?>
      <span class="chip">
        <span class="dot" style="background:<?= h($colours[$node]) ?>"></span>
        <a href="?nodes=<?= h(rawurlencode($node)) ?>"><?= h($node) ?></a>
        <span class="mut"><?= $last === null ? '1m n/a' : num($last, 2) . ' MB/s' ?></span>
        <?php if ($delta !== null): ?>
          <span class="delta <?= $delta >= 0 ? 'up' : 'down' ?>"><?= ($delta >= 0 ? '+' : '') . number_format($delta, 1) ?>%</span>
        <?php endif; ?>
      </span>
    <?php endforeach; ?>
    <?php if ($filter): ?><span class="chip"><a href="./">&larr; all nodes</a></span><?php endif; ?>
  </div>
</header>

<main>
<?php if ($error !== null): ?>
  <section><p class="empty">Database error: <?= h($error) ?></p></section>
<?php elseif (!$series): ?>
  <section><p class="empty"><?= $filter
    
? 'No run for ' . h(implode(', ', $filter)) . '. <a href="./">Show every node</a>.'
    
: 'No runs yet. Install the probe, then reload.' ?></p></section>
<?php else: ?>
  <?php foreach (METRICS as $key => $meta): ?>
    <section>
      <h2><?= h($meta[0] . ' (' . $meta[1] . ')') ?></h2>
      <?= chart($key, $meta, $series, $colours, $t0, $t1, $stamp) ?>
    </section>
  <?php endforeach; ?>
<?php 
endif; ?>
</main>

<footer style="padding:10px 18px;border-top:1px solid #21262d" class="mut">
  <?= count($rows) ?> run(s), <?= count($series) ?> node(s) &middot;
  <a href="./">board</a> &middot;
  <a href="./?do=api">JSON</a> &middot;
  <a href="racks">3D hangar</a> &middot;
  <a href="source?f=history.php">source</a>
</footer>
</body>
</html>