@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.
Files changed (2) hide show
  1. package/package.json +2 -1
  2. 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.0",
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
- * sysstat.ts — cheap host load sample for status bars: CPU %, RAM %, GPU %, CPU temperature.
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
- /** Used memory (total − available), 0–100. */
19
- ram?: number;
20
- /** GPU busy, 0–100 (Linux amdgpu/i915/xe sysfs only). */
6
+ cpuTemp?: number;
21
7
  gpu?: number;
22
- /** CPU package temperature in °C (Linux hwmon/thermal only). */
23
- temp?: number;
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
- // CPU sensor drivers, most specific first. coretemp = Intel, k10temp/zenpower = AMD.
78
- const CPU_HWMON = ["k10temp", "zenpower", "coretemp", "cpu_thermal", "acpitz"];
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
- /** CPU temperature in °C from hwmon (known CPU drivers), then thermal_zone0. */
81
- export function readCpuTemp(sys = "/sys"): number | undefined {
82
- const hwmon = join(sys, "class/hwmon");
83
- const byName = new Map<string, string>();
84
- for (const dir of list(hwmon)) {
85
- try {
86
- byName.set(readFileSync(join(hwmon, dir, "name"), "utf8").trim(), dir);
87
- } catch {}
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
- for (const name of CPU_HWMON) {
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
- * Take one sample. Keep the returned `times` and pass it back next call so CPU % covers
100
- * the interval between calls. Synchronous and cheap (a few small sysfs reads).
101
- */
102
- export function readSystemStats(
103
- prev?: CpuTimes,
104
- sys = "/sys",
105
- ): { stats: SystemStats; times: CpuTimes } {
106
- const times = cpuTimes();
107
- const total = totalmem();
108
- const linux = process.platform === "linux";
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
- times,
111
- stats: {
112
- cpu: prev ? cpuPercent(prev, times) : undefined,
113
- ram: total > 0 ? clampPct(((total - freemem()) / total) * 100) : undefined,
114
- gpu: linux ? readGpuBusy(sys) : undefined,
115
- temp: linux ? readCpuTemp(sys) : undefined,
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
  }