aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-24 10:09:11 +0200
committerDanilo M. <danix@danix.xyz>2026-10-24 10:09:11 +0200
commitc083b40500b0496e1e8e1a32fc31b70b5e9b960d (patch)
tree18d4c6aa1a91084aeb7c7bfb3e6aae880f1ca160 /desktop/modules
parent7e14b275e5a9759f0877ef13b9ad338127bec728 (diff)
downloadquickshell-c083b40500b0496e1e8e1a32fc31b70b5e9b960d.tar.gz
quickshell-c083b40500b0496e1e8e1a32fc31b70b5e9b960d.zip
perf: the five heaviest processes in the CPU card, by CPU or RAM
A table under RAM lists the top five processes with their CPU and resident memory, sorted by either through a toggle in the card. The toggle re-sorts the frame already held and is not saved. The rows come from one `top -b -d2` that runs only while the page is open and dies with it, checked with pgrep. ps was not used: its %CPU is a lifetime average. top's first frame is the same lifetime figure, so it is skipped. top marks no frame end but writes each frame in one burst, so a frame is published once its output has been quiet for 150ms, and rows appear about 2s after opening, not 4s as when waiting for the next header. SplitParser delivered every header after the first as "\ntop - ...", so the header match is a regex that allows leading whitespace; AGENTS.md records it. The parser is Perf.js procs(), covered in test_perf.js. Columns are right-aligned at fixed widths with tabular figures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/modules')
-rw-r--r--desktop/modules/perf/Perf.js19
-rw-r--r--desktop/modules/perf/PerfPanel.qml93
-rw-r--r--desktop/modules/perf/README.md7
-rw-r--r--desktop/modules/perf/test_perf.js17
4 files changed, 135 insertions, 1 deletions
diff --git a/desktop/modules/perf/Perf.js b/desktop/modules/perf/Perf.js
index d84c694..8294e94 100644
--- a/desktop/modules/perf/Perf.js
+++ b/desktop/modules/perf/Perf.js
@@ -188,6 +188,25 @@ function push(list, v, n) {
return out.length > n ? out.slice(out.length - n) : out;
}
+// The process rows of one `top -b` frame, run under LC_ALL=C: [{pid, name,
+// cpu, mem}], cpu in percent of one core as top shows it, mem the resident
+// size in bytes. RES is KiB unless top scaled it to a suffixed unit (1.0g),
+// and COMMAND is the last column and may hold spaces ("Isolated Web Co").
+function procs(text) {
+ const out = [];
+ let rows = false;
+ for (const line of text.split("\n")) {
+ if (/^\s*PID\s+USER\b/.test(line)) { rows = true; continue; }
+ if (!rows) continue;
+ const f = line.trim().split(/\s+/);
+ const res = /^([\d.]+)([kmgtp]?)$/.exec(f[5] || "");
+ if (f.length < 12 || !res) continue;
+ const scale = 1024 ** (1 + Math.max(0, "kmgtp".indexOf(res[2] || "k")));
+ out.push({ pid: Number(f[0]), name: f.slice(11).join(" "), cpu: Number(f[8]), mem: Number(res[1]) * scale });
+ }
+ return out;
+}
+
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 aa2e49f..ff9f74b 100644
--- a/desktop/modules/perf/PerfPanel.qml
+++ b/desktop/modules/perf/PerfPanel.qml
@@ -131,6 +131,39 @@ Column {
// --- CPU ----------------------------------------------------------------
+ // The five heaviest processes, from one `top -b` streaming a frame every
+ // 2s for as long as the page is open; it dies with the panel. top marks
+ // no frame's end but writes each in one burst, so a frame is published
+ // once its output goes quiet. The first is skipped, since top computes
+ // it over each process's whole life rather than the last interval, so
+ // rows load about 2s after the page opens.
+ property var procs: []
+ property string procBy: "cpu"
+ property string frame: ""
+ property int frames: 0
+
+ Process {
+ running: true
+ command: ["env", "LC_ALL=C", "top", "-b", "-d2", "-w512"]
+ stdout: SplitParser {
+ onRead: line => {
+ // Every header after the first reaches here with the blank
+ // line before it still attached, as "\ntop - ...".
+ if (/^\s*top - /.test(line)) {
+ panel.frames++;
+ panel.frame = "";
+ }
+ panel.frame += line + "\n";
+ frameEnd.restart();
+ }
+ }
+ }
+ Timer {
+ id: frameEnd
+ interval: 150
+ onTriggered: if (panel.frames >= 2) panel.procs = P.procs(panel.frame)
+ }
+
Card {
title: "CPU"
headline: Perf.load === null ? "--" : Math.round(Perf.load) + "%"
@@ -174,6 +207,66 @@ Column {
frac: Perf.mem ? Perf.mem.used / Perf.mem.total : 0
fill: panel.tone(panel.pct(Perf.mem ? Perf.mem.used : null, Perf.mem ? Perf.mem.total : 0), 25, 75)
}
+
+ // Sorted by CPU or RAM, the active one lit; switching re-sorts the
+ // frame already held rather than waiting for the next.
+ Item {
+ width: parent.width
+ height: byRow.implicitHeight
+ Label { text: "Processes"; color: Theme.subtext }
+ Row {
+ id: byRow
+ anchors.right: parent.right
+ spacing: 10
+ Repeater {
+ model: [{ key: "cpu", text: "CPU" }, { key: "mem", text: "RAM" }]
+ delegate: Label {
+ required property var modelData
+ text: modelData.text
+ font.bold: panel.procBy === modelData.key
+ color: panel.procBy === modelData.key ? Theme.text : Theme.overlay
+ MouseArea {
+ anchors.fill: parent
+ anchors.margins: -4
+ cursorShape: Qt.PointingHandCursor
+ onClicked: panel.procBy = modelData.key
+ }
+ }
+ }
+ }
+ }
+ Repeater {
+ model: panel.procs.slice().sort((a, b) => b[panel.procBy] - a[panel.procBy]).slice(0, 5)
+ // Two right-aligned columns of fixed width, so the numbers line
+ // up down the table whatever their length.
+ delegate: Item {
+ required property var modelData
+ width: parent.width
+ implicitHeight: nameText.implicitHeight
+ Label {
+ id: nameText
+ anchors { left: parent.left; right: cpuText.left; rightMargin: 8 }
+ text: modelData.name
+ elide: Text.ElideRight
+ }
+ Label {
+ id: cpuText
+ anchors.right: memText.left
+ width: panel.fs * 4
+ horizontalAlignment: Text.AlignRight
+ text: modelData.cpu.toFixed(1) + "%"
+ font.features: { "tnum": 1 }
+ }
+ Label {
+ id: memText
+ anchors.right: parent.right
+ width: panel.fs * 4.5
+ horizontalAlignment: Text.AlignRight
+ text: P.bytes(modelData.mem)
+ font.features: { "tnum": 1 }
+ }
+ }
+ }
}
// --- temperatures -------------------------------------------------------
diff --git a/desktop/modules/perf/README.md b/desktop/modules/perf/README.md
index 236f5b4..8282a50 100644
--- a/desktop/modules/perf/README.md
+++ b/desktop/modules/perf/README.md
@@ -31,6 +31,13 @@ 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
size are constants in `Perf.qml`.
+The CPU card ends with the five heaviest processes, by CPU or by RAM (a
+toggle in the card, not saved). They come from one `top -b -d2` that runs only
+while the page is open. A frame is published once top's output goes quiet,
+and the first is skipped because top computes it over each process's whole
+life, so rows appear about 2s after opening. CPU is per core, as top shows it, so a
+busy process can exceed 100%.
+
The cache card runs `du -B1 -d1` once each time the page opens, 28ms over
2 GiB, instead of reading conky's 15-minute `cachesize.txt`. The largest
child is yellow, red when it holds over half the total, as conky had it.
diff --git a/desktop/modules/perf/test_perf.js b/desktop/modules/perf/test_perf.js
index ac5859d..f2caa94 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 };")();
+ "ipCheck, due, push, bytes, speed, procs };")();
// cpuTimes: aggregate first, then cores; idle is idle + iowait.
const stat = [
@@ -156,4 +156,19 @@ assert.strictEqual(cache.total, 20);
assert.deepStrictEqual(cache.items, [{ name: "a b", bytes: 9 }, { name: "b", bytes: 5 }]);
assert.strictEqual(P.du(""), null);
+// procs: rows after the header only, RES suffixes scaled, spaced names kept.
+const frame = [
+ "top - 09:50:01 up 34 min, 1 user, load average: 1.02, 0.80, 0.60",
+ "Tasks: 412 total, 1 running",
+ "",
+ " PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND",
+ " 8733 you 20 0 3824328 1.0g 140312 S 26.9 3.2 2:23.27 Isolated Web Co",
+ " 3772 you 20 0 1311072 191708 126420 S 5.0 0.6 2:07.86 Hyprland",
+].join("\n");
+assert.deepStrictEqual(P.procs(frame), [
+ { pid: 8733, name: "Isolated Web Co", cpu: 26.9, mem: 1024 ** 3 },
+ { pid: 3772, name: "Hyprland", cpu: 5, mem: 191708 * 1024 },
+]);
+assert.deepStrictEqual(P.procs(""), []);
+
console.log("test_perf: ok");