diff options
Diffstat (limited to 'desktop/modules')
| -rw-r--r-- | desktop/modules/perf/Perf.js | 19 | ||||
| -rw-r--r-- | desktop/modules/perf/PerfPanel.qml | 93 | ||||
| -rw-r--r-- | desktop/modules/perf/README.md | 7 | ||||
| -rw-r--r-- | desktop/modules/perf/test_perf.js | 17 |
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"); |
