From 11c4c745c7ed5477c74abe7de3458924a0567a3a Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 28 Oct 2026 09:18:17 +0100 Subject: perf: sum VRAM in a shell script instead of in QML A process holding thousands of DRM fds makes the fdinfo scan a few hundred KB. Parsing that in QML every 2s drove Qt's unmanaged-heap counter to underflow, after which the V4 engine ran a GC on every allocation and the shell sat at 100% CPU. vram.sh does the scan and the per-client-id sum in awk and prints one number, or nothing when no xe client is found, which the panel draws as a dash. Perf.js loses vram() and its test; test_vram.sh carries the same cases against the script, which takes a file argument instead of scanning so the test can feed it. Co-Authored-By: Claude Opus 5.5 --- desktop/modules/perf/Perf.js | 24 ----------------- desktop/modules/perf/Perf.qml | 6 ++--- desktop/modules/perf/test_perf.js | 20 +-------------- desktop/modules/perf/test_vram.sh | 40 +++++++++++++++++++++++++++++ desktop/modules/perf/vram.sh | 54 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 46 deletions(-) create mode 100755 desktop/modules/perf/test_vram.sh create mode 100755 desktop/modules/perf/vram.sh (limited to 'desktop') diff --git a/desktop/modules/perf/Perf.js b/desktop/modules/perf/Perf.js index 03fd484..d067d3b 100644 --- a/desktop/modules/perf/Perf.js +++ b/desktop/modules/perf/Perf.js @@ -16,8 +16,6 @@ // 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; @@ -86,28 +84,6 @@ function du(text) { return { total: rows[0].bytes, items: rows.slice(1) }; } -// 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) { diff --git a/desktop/modules/perf/Perf.qml b/desktop/modules/perf/Perf.qml index 9ebfe45..d165110 100644 --- a/desktop/modules/perf/Perf.qml +++ b/desktop/modules/perf/Perf.qml @@ -187,11 +187,11 @@ Singleton { // ponytail: a shell glob over every fd on the system, ~14ms every 2s, // always; gate it on the panel again if that shows up. Only the user's own processes are readable, so a // system service's GPU use is not counted, as in conky. + // Summed in vram.sh, so JS gets one number, never the scan itself. Process { id: vramProc - command: ["sh", "-c", "grep -sH -e '^drm-driver' -e '^drm-client-id' " - + "-e '^drm-resident-vram0' /proc/[0-9]*/fdinfo/* 2>/dev/null"] - stdout: StdioCollector { onStreamFinished: root.vram = P.vram(text) } + command: ["sh", `${Quickshell.shellDir}/modules/perf/vram.sh`] + stdout: StdioCollector { onStreamFinished: root.vram = text.trim() === "" ? null : Number(text) } } // --- network ------------------------------------------------------------ diff --git a/desktop/modules/perf/test_perf.js b/desktop/modules/perf/test_perf.js index b7d9dbb..7c108a8 100644 --- a/desktop/modules/perf/test_perf.js +++ b/desktop/modules/perf/test_perf.js @@ -18,7 +18,7 @@ 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, " + +const P = new Function(src + "\nreturn { cpuTimes, load, meminfo, df, rate, " + "hwmonMap, lanAddr, cpuModel, du, band, worst, isIPv4, parsePubip, formatPubip, " + "ipCheck, due, push, bytes, speed, procs, sockets, netProcs };")(); @@ -62,24 +62,6 @@ 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); diff --git a/desktop/modules/perf/test_vram.sh b/desktop/modules/perf/test_vram.sh new file mode 100755 index 0000000..7ef4ade --- /dev/null +++ b/desktop/modules/perf/test_vram.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# +# Copyright (C) 2026 Danilo M. +# +# 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. + +# vram.sh: summed once per xe client id, units honoured, other drivers +# ignored; nothing at all when no xe client is found. +set -e +dir=$(dirname "$0") +tmp=$(mktemp) +trap 'rm -f "$tmp"' EXIT + +printf '%s\n' \ + "/proc/1/fdinfo/7:drm-driver: xe" \ + "/proc/1/fdinfo/7:drm-client-id: 38" \ + "/proc/1/fdinfo/7:drm-resident-vram0: 1024 KiB" \ + "/proc/1/fdinfo/10:drm-driver: xe" \ + "/proc/1/fdinfo/10:drm-client-id: 38" \ + "/proc/1/fdinfo/10:drm-resident-vram0: 1024 KiB" \ + "/proc/2/fdinfo/5:drm-driver: xe" \ + "/proc/2/fdinfo/5:drm-client-id: 40" \ + "/proc/2/fdinfo/5:drm-resident-vram0: 2 MiB" \ + "/proc/3/fdinfo/4:drm-driver: amdgpu" \ + "/proc/3/fdinfo/4:drm-client-id: 1" \ + "/proc/3/fdinfo/4:drm-resident-vram0: 9 GiB" > "$tmp" +got=$(sh "$dir/vram.sh" "$tmp") +[ "$got" = $((1024 * 1024 + 2 * 1024 * 1024)) ] || { echo "vram: got '$got'"; exit 1; } + +: > "$tmp" +got=$(sh "$dir/vram.sh" "$tmp") +[ -z "$got" ] || { echo "empty: got '$got'"; exit 1; } +echo ok diff --git a/desktop/modules/perf/vram.sh b/desktop/modules/perf/vram.sh new file mode 100755 index 0000000..8ba61fb --- /dev/null +++ b/desktop/modules/perf/vram.sh @@ -0,0 +1,54 @@ +#!/bin/sh +# +# Copyright (C) 2026 Danilo M. +# +# 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. + +# 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. Prints +# nothing when no xe client is found. With a file argument it reads that +# instead of scanning, for test_vram.sh. +# +# Summed here rather than in JS: a process holding thousands of DRM fds +# makes the scan a few hundred KB, and parsing that in QML every 2 s drove +# Qt's unmanaged-heap counter to underflow, after which the V4 engine ran a +# GC on every allocation and the shell sat at 100% CPU (AGENTS.md). +# +# Fds of a repeated client id with different values: the first one awk's +# for-in reaches counts, where Perf.js took the first in scan order. They +# share one client, so the values agree in practice. + +scan() { + if [ -n "$1" ]; then cat "$1" + else grep -sH -e '^drm-driver' -e '^drm-client-id' -e '^drm-resident-vram0' \ + /proc/[0-9]*/fdinfo/* 2>/dev/null + fi +} + +scan "$1" | awk -F: ' + NF >= 3 && $2 ~ /^drm-/ { + v = $3 + for (i = 4; i <= NF; i++) v = v ":" $i + sub(/^[ \t]+/, "", v) + d[$1, $2] = v + fd[$1] + } + END { + for (p in fd) { + id = d[p, "drm-client-id"] + if (d[p, "drm-driver"] != "xe" || id == "" || (id in seen)) continue + if (split(d[p, "drm-resident-vram0"], n, " ") < 1 || n[1] !~ /^[0-9]+$/) continue + seen[id] + u = n[2] == "KiB" ? 1024 : n[2] == "MiB" ? 1048576 : n[2] == "GiB" ? 1073741824 : 1 + total += n[1] * u + any = 1 + } + if (any) printf "%.0f\n", total + }' -- cgit v1.2.3