diff options
Diffstat (limited to 'desktop/modules/perf')
| -rw-r--r-- | desktop/modules/perf/Perf.js | 43 | ||||
| -rw-r--r-- | desktop/modules/perf/PerfPanel.qml | 76 | ||||
| -rw-r--r-- | desktop/modules/perf/README.md | 10 | ||||
| -rw-r--r-- | desktop/modules/perf/test_perf.js | 28 |
4 files changed, 76 insertions, 81 deletions
diff --git a/desktop/modules/perf/Perf.js b/desktop/modules/perf/Perf.js index 8294e94..03fd484 100644 --- a/desktop/modules/perf/Perf.js +++ b/desktop/modules/perf/Perf.js @@ -207,6 +207,49 @@ function procs(text) { return out; } +// One {key, name, rx, tx} per TCP socket in `ss -tinpH` output: a line +// naming the owner in users:((...)), then the -i line under it with the +// byte counters. Sockets ss names no owner for (other users', without root) +// are skipped. The key is both endpoints and the pid, so a reused port in +// another process is a new socket. +function sockets(text) { + const out = []; + let cur = null; + for (const line of text.split("\n")) { + const u = /^\S+\s+\d+\s+\d+\s+(\S+)\s+(\S+)\s+users:\(\("([^"]*)",pid=(\d+)/.exec(line); + if (u) { + cur = { key: u[1] + " " + u[2] + " " + u[4], name: u[3], rx: 0, tx: 0 }; + out.push(cur); + } else if (cur && /^\s/.test(line)) { + cur.rx = Number((/bytes_received:(\d+)/.exec(line) || [0, 0])[1]); + cur.tx = Number((/bytes_sent:(\d+)/.exec(line) || [0, 0])[1]); + cur = null; + } else { + cur = null; + } + } + return out; +} + +// Bytes/s per process name between two sockets() samples, busiest first, +// idle ones dropped. A socket new since the last sample counts from zero, +// since it opened inside the interval; one that closed takes its last +// bytes with it, which undercounts short transfers. +function netProcs(prev, cur, secs) { + if (!secs || secs <= 0) return []; + const before = {}; + for (const s of prev) before[s.key] = s; + const by = {}; + for (const s of cur) { + const p = before[s.key] || { rx: 0, tx: 0 }; + const e = by[s.name] || (by[s.name] = { name: s.name, rx: 0, tx: 0 }); + e.rx += Math.max(0, s.rx - p.rx) / secs; + e.tx += Math.max(0, s.tx - p.tx) / secs; + } + return Object.keys(by).map(k => by[k]).filter(e => e.rx + e.tx > 0) + .sort((a, b) => (b.rx + b.tx) - (a.rx + a.tx)); +} + function bytes(n) { if (n === null || n === undefined) return "--"; const u = ["B", "K", "M", "G", "T"]; diff --git a/desktop/modules/perf/PerfPanel.qml b/desktop/modules/perf/PerfPanel.qml index ff9f74b..8c9773d 100644 --- a/desktop/modules/perf/PerfPanel.qml +++ b/desktop/modules/perf/PerfPanel.qml @@ -15,7 +15,7 @@ import QtQuick import "../.." import "Perf.js" as P -// CPU, temperatures, GPU, network, disks and ~/.cache as cards, one per row: +// CPU, temperatures, GPU, disks and ~/.cache as cards, one per row: // the drawer page scrolls, so nothing has to share a line. Numbers come from // Perf; colours follow conky's bands, so they read as they did there. Column { @@ -36,7 +36,6 @@ Column { } function pct(part, whole) { return part === null || !whole ? null : 100 * part / whole; } - function last(list) { return list.length ? list[list.length - 1] : null; } // Conky's glyphs, re-checked in Inconsolata Nerd Font. function glyph(d) { if (d.host) return String.fromCodePoint(0xF06F3); // network share @@ -329,79 +328,6 @@ Column { } } - // --- network ------------------------------------------------------------ - - Card { - title: "NET · " + Perf.iface - - Canvas { - id: graph - width: parent.width - height: 80 - onPaint: { - const ctx = getContext("2d"); - ctx.reset(); - const max = Math.max(1, ...Perf.rx, ...Perf.tx); - const line = (data, color) => { - ctx.strokeStyle = color; - ctx.lineWidth = 1.5; - ctx.beginPath(); - data.forEach((v, i) => { - const x = width * i / 59; - const y = height - 1 - (height - 2) * v / max; - if (i) ctx.lineTo(x, y); else ctx.moveTo(x, y); - }); - ctx.stroke(); - }; - line(Perf.rx, Theme.teal); - line(Perf.tx, Theme.peach); - } - Connections { - target: Perf - function onRxChanged() { graph.requestPaint(); } - function onTxChanged() { graph.requestPaint(); } - } - } - - Pair { - label: "↓ " + P.speed(panel.last(Perf.rx)) - labelColor: Theme.teal - value: "↑ " + P.speed(panel.last(Perf.tx)) - valueColor: Theme.peach - } - Pair { label: "LAN"; value: Perf.lan || "--" } - - // The public IP, red when it differs from the pinned one. The pin - // stores the address shown; it is lit while they are the same. - Item { - width: parent.width - height: pubText.implicitHeight - - Label { text: "Public"; color: Theme.subtext } - Label { - id: pubText - anchors { right: pin.left; rightMargin: 6 } - text: Perf.ip.ip || "--" - color: Perf.ip.mismatch ? Theme.red : Theme.text - } - Text { - id: pin - anchors { right: parent.right; verticalCenter: pubText.verticalCenter } - text: String.fromCodePoint(0xF0403) - font { family: Theme.iconFamily; pixelSize: panel.fs + 2 } - color: Perf.ip.ip !== "" && Perf.ip.ip === Prefs.pinnedIp ? Theme.text : Theme.overlay - - MouseArea { - anchors.fill: parent - anchors.margins: -4 - enabled: Perf.ip.ip !== "" - cursorShape: Qt.PointingHandCursor - onClicked: Prefs.setPinnedIp(Perf.ip.ip) - } - } - } - } - // --- disks -------------------------------------------------------------- Card { diff --git a/desktop/modules/perf/README.md b/desktop/modules/perf/README.md index 8282a50..3d11be4 100644 --- a/desktop/modules/perf/README.md +++ b/desktop/modules/perf/README.md @@ -3,8 +3,7 @@ The figures the conky dashboard used to show, as a drawer page of cards, one per row; the page scrolls. CPU load with a bar per core, the CPU model and RAM; every temperature as a tile; the Arc B580's VRAM and fan (no -utilisation, xe exposes none); download and upload over the last minute, the -LAN address and the public IP; the five mounts with their usage; and +utilisation, xe exposes none); the five mounts with their usage; and `~/.cache` with its five largest children. `Perf.qml` reads them; `Perf.js` holds the parsing, tested by `node desktop/modules/perf/test_perf.js`. @@ -19,7 +18,7 @@ The tile shows CPU and RAM, and a dot for the worst of these, yellow or red: CPU load is left out, since spikes would flicker it. There is no hysteresis, so a sensor sitting on its threshold can toggle the dot. The pin beside the -public IP stores the address shown as `perf.pinnedIp` in +public IP, on the network page, stores the address shown as `perf.pinnedIp` in `$XDG_STATE_HOME/keystone/desktop.json`; the check exists because the VPN into the LAN depends on the ISP's fixed address. The fetch is `curl -4 https://ipinfo.io/ip` every 30 minutes from the last attempt, into @@ -27,8 +26,9 @@ into the LAN depends on the ISP's fixed address. The fetch is never a mismatch: the old value goes stale after 4 hours and shows as a dash. Everything is sampled always, including per-core load, the network rate, -VRAM (a scan of every fdinfo, ~14ms every 2s) and the fan, so the page opens -on the last minute of network history instead of an empty graph. The mount list, `br0`, the GPU name and the 12 GiB VRAM +VRAM (a scan of every fdinfo, ~14ms every 2s) and the fan. The network rate +is drawn by the network page, which reads it from here, so its graph opens +on the last minute of history instead of empty. The mount list, `br0`, the GPU name and the 12 GiB VRAM size are constants in `Perf.qml`. The CPU card ends with the five heaviest processes, by CPU or by RAM (a diff --git a/desktop/modules/perf/test_perf.js b/desktop/modules/perf/test_perf.js index f2caa94..b7d9dbb 100644 --- a/desktop/modules/perf/test_perf.js +++ b/desktop/modules/perf/test_perf.js @@ -20,7 +20,7 @@ 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, du, band, worst, isIPv4, parsePubip, formatPubip, " + - "ipCheck, due, push, bytes, speed, procs };")(); + "ipCheck, due, push, bytes, speed, procs, sockets, netProcs };")(); // cpuTimes: aggregate first, then cores; idle is idle + iowait. const stat = [ @@ -171,4 +171,30 @@ assert.deepStrictEqual(P.procs(frame), [ ]); assert.deepStrictEqual(P.procs(""), []); +// sockets: owned sockets with their -i counters; ownerless ones skipped. +const ss = [ + "ESTAB 0 0 10.0.0.5:35806 192.0.2.1:443 users:((\"firefox\",pid=12,fd=53))", + "\t cubic rto:340 bytes_sent:100 bytes_acked:101 bytes_received:5000 segs_out:2", + "ESTAB 0 0 10.0.0.5:22 192.0.2.9:5000", + "\t cubic bytes_sent:999 bytes_received:999", + "SYN-SENT 0 1 10.0.0.5:49916 192.0.2.2:65081 users:((\"ktorrent\",pid=7,fd=67))", + "\t cubic rto:1000 segs_out:3", +].join("\n"); +const socks = P.sockets(ss); +assert.deepStrictEqual(socks, [ + { key: "10.0.0.5:35806 192.0.2.1:443 12", name: "firefox", rx: 5000, tx: 100 }, + { key: "10.0.0.5:49916 192.0.2.2:65081 7", name: "ktorrent", rx: 0, tx: 0 }, +]); + +// netProcs: deltas over the interval summed by name; a new socket counts +// from zero, idle processes drop out. +const later = [ + { key: "a", name: "firefox", rx: 5000 + 4000, tx: 100 }, + { key: "b", name: "firefox", rx: 2000, tx: 0 }, + { key: "c", name: "ktorrent", rx: 0, tx: 0 }, +]; +assert.deepStrictEqual(P.netProcs([{ key: "a", name: "firefox", rx: 5000, tx: 100 }], later, 2), + [{ name: "firefox", rx: 3000, tx: 0 }]); +assert.deepStrictEqual(P.netProcs([], later, 0), []); + console.log("test_perf: ok"); |
