aboutsummaryrefslogtreecommitdiffstats
path: root/desktop
diff options
context:
space:
mode:
Diffstat (limited to 'desktop')
-rw-r--r--desktop/island/perf/Perf.js185
-rw-r--r--desktop/island/perf/test_perf.js151
2 files changed, 336 insertions, 0 deletions
diff --git a/desktop/island/perf/Perf.js b/desktop/island/perf/Perf.js
new file mode 100644
index 0000000..d23d900
--- /dev/null
+++ b/desktop/island/perf/Perf.js
@@ -0,0 +1,185 @@
+.pragma library
+
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// The performance panel's parsing and arithmetic, kept pure so it runs under
+// node (test_perf.js). Perf.qml does the reading; everything here takes text
+// or numbers and returns values. A reading that cannot be made is null, and
+// the panel draws null as a dash.
+
+const UNITS = { "": 1, B: 1, KiB: 1024, MiB: 1024 ** 2, GiB: 1024 ** 3 };
+
+// A public IP older than this is not shown: by then the fetch has failed
+// for hours, and an address that may no longer be ours is worse than a dash.
+const STALE = 4 * 3600;
+
+// One {total, idle} per cpu line of /proc/stat; index 0 is the aggregate,
+// then the cores in file order. idle counts iowait, as conky did.
+function cpuTimes(stat) {
+ const out = [];
+ for (const line of stat.split("\n")) {
+ const m = /^cpu\d*\s+(.*)$/.exec(line);
+ if (!m) continue;
+ const v = m[1].trim().split(/\s+/).map(Number);
+ if (v.length < 5) continue;
+ out.push({ total: v.reduce((a, b) => a + b, 0), idle: v[3] + v[4] });
+ }
+ return out;
+}
+
+// Percent busy between two samples.
+function load(prev, cur) {
+ if (!prev || !cur) return null;
+ const dt = cur.total - prev.total;
+ if (dt <= 0) return null;
+ return Math.max(0, Math.min(100, 100 * (1 - (cur.idle - prev.idle) / dt)));
+}
+
+// {total, used} in bytes; used is MemTotal - MemAvailable.
+function meminfo(text) {
+ const kb = key => {
+ const m = new RegExp("^" + key + ":\\s+(\\d+)", "m").exec(text);
+ return m ? Number(m[1]) * 1024 : null;
+ };
+ const total = kb("MemTotal"), avail = kb("MemAvailable");
+ if (total === null || avail === null) return null;
+ return { total: total, used: total - avail };
+}
+
+// Rows of `df -P -B1`. The header fails the percent match and is skipped.
+// host is the NFS server from the device field, "" for a local disk, so the
+// panel labels shares without an address in the repo.
+function df(text) {
+ const out = [];
+ for (const line of text.split("\n")) {
+ const m = /^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(\S+)\s*$/.exec(line);
+ if (!m) continue;
+ const h = /^([^/:]+):/.exec(m[1]);
+ out.push({ mount: m[6], host: h ? h[1] : "", size: Number(m[2]),
+ used: Number(m[3]), pct: Number(m[5]) });
+ }
+ return out;
+}
+
+// VRAM in bytes from the fdinfo scan: drm-resident-vram0 of every xe fd,
+// counted once per drm-client-id because a client holds several fds.
+function vram(text) {
+ const fds = {};
+ for (const line of text.split("\n")) {
+ const m = /^([^:]*):(drm-[\w-]+):\s*(.*)$/.exec(line);
+ if (!m) continue;
+ (fds[m[1]] = fds[m[1]] || {})[m[2]] = m[3];
+ }
+ const seen = {};
+ let total = null;
+ for (const path in fds) {
+ const fd = fds[path];
+ const id = fd["drm-client-id"];
+ const n = /^(\d+)\s*(\w*)/.exec(fd["drm-resident-vram0"] || "");
+ if (fd["drm-driver"] !== "xe" || !id || !n || seen[id]) continue;
+ seen[id] = true;
+ total = (total || 0) + Number(n[1]) * (UNITS[n[2]] || 1);
+ }
+ return total;
+}
+
+// Bytes per second between two counter readings. A counter that went
+// backwards (interface reset) gives null rather than a negative spike.
+function rate(prev, cur, secs) {
+ if (prev === null || cur === null || !(secs > 0) || cur < prev) return null;
+ return (cur - prev) / secs;
+}
+
+// `grep -H . /sys/class/hwmon/hwmon*/name` -> {chip: dir}. hwmonN numbers
+// change across boots, so chips are found by name.
+function hwmonMap(text) {
+ const map = {};
+ for (const line of text.split("\n")) {
+ const m = /^(.*)\/name:(.+)$/.exec(line);
+ if (m && !(m[2] in map)) map[m[2]] = m[1];
+ }
+ return map;
+}
+
+// `ip -4 -o addr show <iface>` -> the first address, without its prefix.
+function lanAddr(text) {
+ const m = /\binet (\d+\.\d+\.\d+\.\d+)\//.exec(text);
+ return m ? m[1] : "";
+}
+
+// The first model name in /proc/cpuinfo, trimmed the way conky trimmed it.
+function cpuModel(cpuinfo) {
+ const m = /^model name\s*:\s*(.+)$/m.exec(cpuinfo);
+ if (!m) return "";
+ return m[1].replace(/\((R|TM|tm)\)/g, "")
+ .replace(/^\s*\d+th Gen\s+/, "")
+ .replace(/^\s*(AMD|Intel)\s+/, "")
+ .replace(/\s+\d+-Core Processor.*$/, "")
+ .replace(/\s+/g, " ").trim();
+}
+
+// "ok", "warn", "crit", or "" for a value that could not be read.
+function band(v, warn, crit) {
+ if (v === null || v === undefined) return "";
+ return v >= crit ? "crit" : v >= warn ? "warn" : "ok";
+}
+
+// The worst of a list of bands; unknowns never raise it.
+function worst(bands) {
+ if (bands.indexOf("crit") >= 0) return "crit";
+ if (bands.indexOf("warn") >= 0) return "warn";
+ return "ok";
+}
+
+function isIPv4(s) {
+ return /^(\d{1,3}\.){3}\d{1,3}$/.test(s) && s.split(".").every(o => Number(o) <= 255);
+}
+
+// conky's cache format: "ip <addr>" and "fetched <epoch>" lines.
+function parsePubip(text) {
+ const ip = /^ip (\S+)$/m.exec(text), at = /^fetched (\d+)$/m.exec(text);
+ return ip && at && isIPv4(ip[1]) ? { ip: ip[1], fetched: Number(at[1]) } : null;
+}
+
+function formatPubip(ip, fetched) {
+ return "ip " + ip + "\nfetched " + fetched + "\n";
+}
+
+// What the panel shows and whether the icon warns. Only a fresh value can
+// mismatch: a failed fetch leaves the old one to go stale, never to alarm.
+function ipCheck(pub, now, pin) {
+ const fresh = !!pub && now - pub.fetched < STALE;
+ return { ip: fresh ? pub.ip : "", mismatch: fresh && !!pin && pub.ip !== pin };
+}
+
+// Whether a fetch is due: no value yet, or one at least `every` seconds old.
+function due(pub, now, every) {
+ return !pub || now - pub.fetched >= every;
+}
+
+// A history with v appended, oldest dropped past n.
+function push(list, v, n) {
+ const out = list.concat([v]);
+ return out.length > n ? out.slice(out.length - n) : out;
+}
+
+function bytes(n) {
+ if (n === null || n === undefined) return "--";
+ const u = ["B", "K", "M", "G", "T"];
+ let i = 0;
+ while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
+ return (i ? n.toFixed(1) : String(Math.round(n))) + u[i];
+}
+
+function speed(n) {
+ return n === null || n === undefined ? "--" : bytes(n) + "/s";
+}
diff --git a/desktop/island/perf/test_perf.js b/desktop/island/perf/test_perf.js
new file mode 100644
index 0000000..c336cf0
--- /dev/null
+++ b/desktop/island/perf/test_perf.js
@@ -0,0 +1,151 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// Run: node desktop/island/perf/test_perf.js
+// Perf.js is a QML library, so its .pragma line is stripped before node
+// evaluates it, as test_controls.js does for Controls.js.
+
+const fs = require("fs");
+const assert = require("assert");
+
+const src = fs.readFileSync(__dirname + "/Perf.js", "utf8")
+ .replace(/^\.pragma library$/m, "");
+const P = new Function(src + "\nreturn { cpuTimes, load, meminfo, df, vram, rate, " +
+ "hwmonMap, lanAddr, cpuModel, band, worst, isIPv4, parsePubip, formatPubip, " +
+ "ipCheck, due, push, bytes, speed };")();
+
+// cpuTimes: aggregate first, then cores; idle is idle + iowait.
+const stat = [
+ "cpu 100 0 50 800 50 0 0 0 0 0",
+ "cpu0 50 0 25 400 25 0 0 0 0 0",
+ "cpu1 50 0 25 400 25 0 0 0 0 0",
+ "intr 12345",
+ "ctxt 999",
+].join("\n");
+const t = P.cpuTimes(stat);
+assert.strictEqual(t.length, 3);
+assert.deepStrictEqual(t[0], { total: 1000, idle: 850 });
+assert.deepStrictEqual(t[1], { total: 500, idle: 425 });
+assert.deepStrictEqual(P.cpuTimes(""), []);
+
+// load: busy share of the delta; no prev, or no time passed, is null.
+assert.strictEqual(P.load({ total: 1000, idle: 850 }, { total: 1100, idle: 925 }), 25);
+assert.strictEqual(P.load(undefined, { total: 1, idle: 1 }), null);
+assert.strictEqual(P.load({ total: 5, idle: 5 }, { total: 5, idle: 5 }), null);
+assert.strictEqual(P.load({ total: 0, idle: 0 }, { total: 100, idle: 0 }), 100);
+
+// meminfo: used is total - available, in bytes.
+const mem = "MemTotal: 32000000 kB\nMemFree: 1000 kB\nMemAvailable: 24000000 kB\n";
+assert.deepStrictEqual(P.meminfo(mem), { total: 32000000 * 1024, used: 8000000 * 1024 });
+assert.strictEqual(P.meminfo("MemTotal: 5 kB\n"), null);
+
+// df: header skipped, NFS host from the device field, bytes as numbers.
+const dfOut = [
+ "Filesystem 1-blocks Used Available Capacity Mounted on",
+ "/dev/mapper/vg-root 263088050176 143921766400 105727733760 58% /",
+ "192.0.2.11:/Volume1/Library 3926310780928 3185272160256 541516365824 86% /mnt/nfs/Library",
+].join("\n");
+const d = P.df(dfOut);
+assert.strictEqual(d.length, 2);
+assert.deepStrictEqual(d[0], { mount: "/", host: "", size: 263088050176, used: 143921766400, pct: 58 });
+assert.strictEqual(d[1].host, "192.0.2.11");
+assert.strictEqual(d[1].mount, "/mnt/nfs/Library");
+assert.deepStrictEqual(P.df(""), []);
+
+// vram: summed once per xe client id, units honoured, other drivers ignored.
+const fd = [
+ "/proc/1/fdinfo/7:drm-driver:\txe",
+ "/proc/1/fdinfo/7:drm-client-id:\t38",
+ "/proc/1/fdinfo/7:drm-resident-vram0:\t1024 KiB",
+ "/proc/1/fdinfo/10:drm-driver:\txe",
+ "/proc/1/fdinfo/10:drm-client-id:\t38",
+ "/proc/1/fdinfo/10:drm-resident-vram0:\t1024 KiB",
+ "/proc/2/fdinfo/5:drm-driver:\txe",
+ "/proc/2/fdinfo/5:drm-client-id:\t40",
+ "/proc/2/fdinfo/5:drm-resident-vram0:\t2 MiB",
+ "/proc/3/fdinfo/4:drm-driver:\tamdgpu",
+ "/proc/3/fdinfo/4:drm-client-id:\t1",
+ "/proc/3/fdinfo/4:drm-resident-vram0:\t9 GiB",
+].join("\n");
+assert.strictEqual(P.vram(fd), 1024 * 1024 + 2 * 1024 * 1024);
+assert.strictEqual(P.vram(""), null);
+
+// rate: bytes per second; a counter that went backwards is null.
+assert.strictEqual(P.rate(1000, 3000, 2), 1000);
+assert.strictEqual(P.rate(3000, 1000, 1), null);
+assert.strictEqual(P.rate(null, 1000, 1), null);
+assert.strictEqual(P.rate(1000, 2000, 0), null);
+
+// hwmonMap: chip name -> directory, first one wins.
+const names = "/sys/class/hwmon/hwmon1/name:nvme\n/sys/class/hwmon/hwmon5/name:xe\n/sys/class/hwmon/hwmon7/name:nvme\n";
+assert.deepStrictEqual(P.hwmonMap(names),
+ { nvme: "/sys/class/hwmon/hwmon1", xe: "/sys/class/hwmon/hwmon5" });
+
+// lanAddr: the address without its prefix length.
+assert.strictEqual(P.lanAddr("4: br0 inet 192.0.2.5/24 brd 192.0.2.255 scope global br0\n"), "192.0.2.5");
+assert.strictEqual(P.lanAddr(""), "");
+
+// cpuModel: vendor, trademarks and the core-count tail dropped.
+assert.strictEqual(P.cpuModel("processor\t: 0\nmodel name\t: AMD Ryzen 9 7950X 16-Core Processor\n"), "Ryzen 9 7950X");
+assert.strictEqual(P.cpuModel("model name\t: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz\n"), "Core i7-1165G7 @ 2.80GHz");
+assert.strictEqual(P.cpuModel(""), "");
+
+// band and worst: null is "" (unknown), which never raises the worst.
+assert.strictEqual(P.band(10, 25, 75), "ok");
+assert.strictEqual(P.band(25, 25, 75), "warn");
+assert.strictEqual(P.band(75, 25, 75), "crit");
+assert.strictEqual(P.band(null, 25, 75), "");
+assert.strictEqual(P.worst(["ok", "", "warn"]), "warn");
+assert.strictEqual(P.worst(["warn", "crit", "ok"]), "crit");
+assert.strictEqual(P.worst(["", ""]), "ok");
+assert.strictEqual(P.worst([]), "ok");
+
+// isIPv4
+assert.ok(P.isIPv4("203.0.113.5"));
+assert.ok(!P.isIPv4("203.0.113.256"));
+assert.ok(!P.isIPv4("<html>"));
+assert.ok(!P.isIPv4(""));
+
+// The pubip cache, in conky's format, both ways.
+assert.deepStrictEqual(P.parsePubip("ip 203.0.113.5\nfetched 1700000000\n"), { ip: "203.0.113.5", fetched: 1700000000 });
+assert.strictEqual(P.parsePubip("ip nonsense\nfetched 1\n"), null);
+assert.strictEqual(P.parsePubip(""), null);
+assert.strictEqual(P.formatPubip("203.0.113.5", 1700000000), "ip 203.0.113.5\nfetched 1700000000\n");
+assert.deepStrictEqual(P.parsePubip(P.formatPubip("198.51.100.7", 42)), { ip: "198.51.100.7", fetched: 42 });
+
+// ipCheck: no pin ok; match ok; mismatch only when fresh; stale is a dash.
+const now = 1700010000;
+const fresh = { ip: "203.0.113.5", fetched: now - 60 };
+const stale = { ip: "198.51.100.7", fetched: now - 5 * 3600 };
+assert.deepStrictEqual(P.ipCheck(fresh, now, ""), { ip: "203.0.113.5", mismatch: false });
+assert.deepStrictEqual(P.ipCheck(fresh, now, "203.0.113.5"), { ip: "203.0.113.5", mismatch: false });
+assert.deepStrictEqual(P.ipCheck(fresh, now, "198.51.100.7"), { ip: "203.0.113.5", mismatch: true });
+assert.deepStrictEqual(P.ipCheck(stale, now, "203.0.113.5"), { ip: "", mismatch: false });
+assert.deepStrictEqual(P.ipCheck(null, now, "203.0.113.5"), { ip: "", mismatch: false });
+
+// due: no value, or one at least `every` seconds old.
+assert.ok(P.due(null, now, 1800));
+assert.ok(P.due({ ip: "x", fetched: now - 1800 }, now, 1800));
+assert.ok(!P.due({ ip: "x", fetched: now - 1799 }, now, 1800));
+
+// push: newest last, capped.
+assert.deepStrictEqual(P.push([1, 2, 3], 4, 3), [2, 3, 4]);
+assert.deepStrictEqual(P.push([], 1, 3), [1]);
+
+// bytes and speed
+assert.strictEqual(P.bytes(512), "512B");
+assert.strictEqual(P.bytes(1536), "1.5K");
+assert.strictEqual(P.bytes(9.1 * 1024 ** 3), "9.1G");
+assert.strictEqual(P.bytes(null), "--");
+assert.strictEqual(P.speed(1024 * 1024 * 1.2), "1.2M/s");
+assert.strictEqual(P.speed(null), "--");
+
+console.log("test_perf: ok");