aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules
diff options
context:
space:
mode:
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");