@xynogen/pix-runtime 0.17.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/sysstat.ts +56 -105
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-runtime",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "Pix shared runtime — versioned pix.json config, atomic persistence, typed change events",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"@ast-grep/napi": "^0.45.0"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
+
"systeminformation": "5.33.19",
|
|
72
73
|
"xxhash-wasm": "1.1.0"
|
|
73
74
|
},
|
|
74
75
|
"peerDependenciesMeta": {
|
package/src/sysstat.ts
CHANGED
|
@@ -1,118 +1,69 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* CPU and RAM come from `node:os`, so they work on every OS. GPU busy % and temperature
|
|
5
|
-
* are read from Linux sysfs (`/sys/class/drm`, `/sys/class/hwmon`, `/sys/class/thermal`);
|
|
6
|
-
* elsewhere, or when the driver exposes nothing, those fields are undefined.
|
|
7
|
-
* ponytail: no NVIDIA support (its driver has no sysfs busy file). Add an `nvidia-smi`
|
|
8
|
-
* catalog binary and a slow poll here if that is needed.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
12
|
-
import { cpus, freemem, totalmem } from "node:os";
|
|
13
|
-
import { join } from "node:path";
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import si from "systeminformation";
|
|
14
3
|
|
|
15
4
|
export interface SystemStats {
|
|
16
|
-
/** Busy CPU since the previous sample, 0–100. Undefined on the first sample. */
|
|
17
5
|
cpu?: number;
|
|
18
|
-
|
|
19
|
-
ram?: number;
|
|
20
|
-
/** GPU busy, 0–100 (Linux amdgpu/i915/xe sysfs only). */
|
|
6
|
+
cpuTemp?: number;
|
|
21
7
|
gpu?: number;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Opaque CPU counter snapshot; pass the previous one to get a CPU percent. */
|
|
27
|
-
export type CpuTimes = { busy: number; total: number };
|
|
28
|
-
|
|
29
|
-
export function cpuTimes(): CpuTimes {
|
|
30
|
-
let busy = 0;
|
|
31
|
-
let total = 0;
|
|
32
|
-
for (const { times } of cpus()) {
|
|
33
|
-
const sum = times.user + times.nice + times.sys + times.idle + times.irq;
|
|
34
|
-
total += sum;
|
|
35
|
-
busy += sum - times.idle;
|
|
36
|
-
}
|
|
37
|
-
return { busy, total };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Busy share between two snapshots, 0–100, or undefined when no time passed. */
|
|
41
|
-
export function cpuPercent(prev: CpuTimes, next: CpuTimes): number | undefined {
|
|
42
|
-
const total = next.total - prev.total;
|
|
43
|
-
if (total <= 0) return undefined;
|
|
44
|
-
return clampPct(((next.busy - prev.busy) / total) * 100);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const clampPct = (n: number) => Math.min(100, Math.max(0, Math.round(n)));
|
|
48
|
-
|
|
49
|
-
function readNumber(path: string): number | undefined {
|
|
50
|
-
try {
|
|
51
|
-
const n = Number(readFileSync(path, "utf8").trim());
|
|
52
|
-
return Number.isFinite(n) ? n : undefined;
|
|
53
|
-
} catch {
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function list(dir: string): string[] {
|
|
59
|
-
try {
|
|
60
|
-
return readdirSync(dir);
|
|
61
|
-
} catch {
|
|
62
|
-
return [];
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Highest `gpu_busy_percent` across DRM cards (a laptop may report an idle iGPU too). */
|
|
67
|
-
export function readGpuBusy(sys = "/sys"): number | undefined {
|
|
68
|
-
let best: number | undefined;
|
|
69
|
-
for (const card of list(join(sys, "class/drm"))) {
|
|
70
|
-
if (!/^card\d+$/.test(card)) continue;
|
|
71
|
-
const n = readNumber(join(sys, "class/drm", card, "device/gpu_busy_percent"));
|
|
72
|
-
if (n !== undefined) best = Math.max(best ?? 0, clampPct(n));
|
|
73
|
-
}
|
|
74
|
-
return best;
|
|
8
|
+
gpuTemp?: number;
|
|
9
|
+
ram?: number;
|
|
75
10
|
}
|
|
76
11
|
|
|
77
|
-
|
|
78
|
-
const
|
|
12
|
+
/** Missing sensors, sentinel values and failed probes stay unavailable, never become zero. */
|
|
13
|
+
const reading = (value: unknown, max: number): number | undefined =>
|
|
14
|
+
typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= max
|
|
15
|
+
? Math.round(value)
|
|
16
|
+
: undefined;
|
|
79
17
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
for (const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
18
|
+
async function readAmdStats(): Promise<SystemStats> {
|
|
19
|
+
const stats: SystemStats = {};
|
|
20
|
+
const read = (path: string) => fs.readFile(path, "utf8").catch(() => "");
|
|
21
|
+
const cards = await fs.readdir("/sys/class/drm").catch(() => []);
|
|
22
|
+
for (const card of cards.filter((name) => /^card\d+$/.test(name))) {
|
|
23
|
+
const device = `/sys/class/drm/${card}/device`;
|
|
24
|
+
if ((await read(`${device}/vendor`)).trim() !== "0x1002") continue;
|
|
25
|
+
const busy = (await read(`${device}/gpu_busy_percent`)).trim();
|
|
26
|
+
const gpu = busy ? reading(Number(busy), 100) : undefined;
|
|
27
|
+
if (gpu !== undefined) stats.gpu = Math.max(stats.gpu ?? 0, gpu);
|
|
28
|
+
const sensors = await fs.readdir(`${device}/hwmon`).catch(() => []);
|
|
29
|
+
for (const sensor of sensors) {
|
|
30
|
+
const raw = (await read(`${device}/hwmon/${sensor}/temp1_input`)).trim();
|
|
31
|
+
const temperature = raw ? reading(Number(raw) / 1000, 150) : undefined;
|
|
32
|
+
if (temperature !== undefined) stats.gpuTemp = Math.max(stats.gpuTemp ?? 0, temperature);
|
|
33
|
+
}
|
|
88
34
|
}
|
|
89
|
-
|
|
90
|
-
const dir = byName.get(name);
|
|
91
|
-
const milli = dir && readNumber(join(hwmon, dir, "temp1_input"));
|
|
92
|
-
if (milli) return Math.round(milli / 1000);
|
|
93
|
-
}
|
|
94
|
-
const zone = readNumber(join(sys, "class/thermal/thermal_zone0/temp"));
|
|
95
|
-
return zone ? Math.round(zone / 1000) : undefined;
|
|
35
|
+
return stats;
|
|
96
36
|
}
|
|
97
37
|
|
|
98
|
-
/**
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
38
|
+
/** Read each source independently so an unsupported sensor cannot discard other readings. */
|
|
39
|
+
export async function readSystemStats(): Promise<SystemStats> {
|
|
40
|
+
const [cpu, temperature, graphics, memory] = await Promise.allSettled([
|
|
41
|
+
si.currentLoad(),
|
|
42
|
+
si.cpuTemperature(),
|
|
43
|
+
si.graphics(),
|
|
44
|
+
si.mem(),
|
|
45
|
+
]);
|
|
46
|
+
const controllers = graphics.status === "fulfilled" ? graphics.value.controllers : [];
|
|
47
|
+
const highest = (key: "utilizationGpu" | "temperatureGpu", max: number) => {
|
|
48
|
+
const values = controllers
|
|
49
|
+
.map((controller) => reading(controller[key], max))
|
|
50
|
+
.filter((value): value is number => value !== undefined);
|
|
51
|
+
return values.length ? Math.max(...values) : undefined;
|
|
52
|
+
};
|
|
53
|
+
const gpu = highest("utilizationGpu", 100);
|
|
54
|
+
const gpuTemp = highest("temperatureGpu", 150);
|
|
55
|
+
const amd =
|
|
56
|
+
process.platform === "linux" && (gpu === undefined || gpuTemp === undefined)
|
|
57
|
+
? await readAmdStats()
|
|
58
|
+
: {};
|
|
109
59
|
return {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
60
|
+
cpu: cpu.status === "fulfilled" ? reading(cpu.value.currentLoad, 100) : undefined,
|
|
61
|
+
cpuTemp: temperature.status === "fulfilled" ? reading(temperature.value.main, 150) : undefined,
|
|
62
|
+
gpu: gpu ?? amd.gpu,
|
|
63
|
+
gpuTemp: gpuTemp ?? amd.gpuTemp,
|
|
64
|
+
ram:
|
|
65
|
+
memory.status === "fulfilled" && memory.value.total > 0
|
|
66
|
+
? reading(((memory.value.total - memory.value.available) / memory.value.total) * 100, 100)
|
|
67
|
+
: undefined,
|
|
117
68
|
};
|
|
118
69
|
}
|