@yourfam/yf-vitals 0.1.0 → 0.1.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/README.md +1 -1
- package/package.json +1 -1
- package/src/args.js +1 -1
- package/src/disk.js +30 -9
- package/src/format.js +7 -3
- package/src/render.js +1 -1
- package/src/sample.js +76 -66
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ One live dashboard. Refresh every **1.0s** (or `--interval`, or **2.0s** with `-
|
|
|
37
37
|
| **CPU** | Overall utilization 0–100%, plus physical / logical core counts |
|
|
38
38
|
| **RAM** | Percent used, plus used / total (`GiB` / `MiB`) |
|
|
39
39
|
| **DSK** | Read and write **rates** (KiB/s, MiB/s, GiB/s) |
|
|
40
|
-
| **NET** | Send ↑ and receive ↓ in **Mbps** (1 Mbps = 1e6 bit/s) |
|
|
40
|
+
| **NET** | Send ↑ and receive ↓ in **kbps / Mbps / Gbps** (decimal bits; 1 Mbps = 1e6 bit/s) |
|
|
41
41
|
| **GPU** | Utilization and VRAM when telemetry exists; **the row is omitted** when it does not |
|
|
42
42
|
|
|
43
43
|
Header: `yf-vitals` · hostname · OS · `NP/NL` cores · RAM total · tick interval.
|
package/package.json
CHANGED
package/src/args.js
CHANGED
|
@@ -98,7 +98,7 @@ Usage:
|
|
|
98
98
|
--version, -V Package version
|
|
99
99
|
|
|
100
100
|
q / Q / Ctrl+C quit. GPU row is omitted when the OS has no GPU telemetry.
|
|
101
|
-
Disk rates are
|
|
101
|
+
Disk rates are KiB/s–GiB/s; network rates are kbps/Mbps/Gbps.
|
|
102
102
|
Disk / net sparklines scale to the max in the last 60 ticks (no 100% ceiling).
|
|
103
103
|
Needs a terminal (no pipes). Node 20+.`;
|
|
104
104
|
}
|
package/src/disk.js
CHANGED
|
@@ -1,23 +1,43 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const PS_COUNTERS = `
|
|
4
4
|
$ErrorActionPreference = 'SilentlyContinue'
|
|
5
5
|
while (($line = [Console]::In.ReadLine()) -ne $null) {
|
|
6
6
|
if ($line -eq 'q') { break }
|
|
7
|
+
$dr = [int64]0
|
|
8
|
+
$dw = [int64]0
|
|
9
|
+
$rx = [int64]0
|
|
10
|
+
$tx = [int64]0
|
|
7
11
|
$d = Get-CimInstance -ClassName Win32_PerfRawData_PerfDisk_PhysicalDisk -Filter "Name='_Total'"
|
|
8
|
-
if ($d) {
|
|
9
|
-
|
|
12
|
+
if ($d) {
|
|
13
|
+
$dr = [int64]$d.DiskReadBytesPersec
|
|
14
|
+
$dw = [int64]$d.DiskWriteBytesPersec
|
|
15
|
+
}
|
|
16
|
+
Get-NetAdapterStatistics | ForEach-Object {
|
|
17
|
+
$name = [string]$_.Name
|
|
18
|
+
if ($name -match 'vEthernet|Loopback|Bluetooth|WSL|Hyper-V|Virtual|Pseudo|isatap|Teredo') { return }
|
|
19
|
+
$rx += [int64]$_.ReceivedBytes
|
|
20
|
+
$tx += [int64]$_.SentBytes
|
|
21
|
+
}
|
|
22
|
+
Write-Output ("{0} {1} {2} {3}" -f $dr, $dw, $rx, $tx)
|
|
10
23
|
}
|
|
11
24
|
`.trim();
|
|
12
25
|
|
|
13
26
|
/**
|
|
14
27
|
* @param {string} line
|
|
15
|
-
* @returns {{ rx: number, wx: number } | null}
|
|
28
|
+
* @returns {{ rx: number, wx: number, netRx: number, netTx: number } | null}
|
|
16
29
|
*/
|
|
17
30
|
export function parseDiskCounterLine(line) {
|
|
18
|
-
const m = String(line || "")
|
|
31
|
+
const m = String(line || "")
|
|
32
|
+
.trim()
|
|
33
|
+
.match(/^(\d+)\s+(\d+)(?:\s+(\d+)\s+(\d+))?$/);
|
|
19
34
|
if (!m) return null;
|
|
20
|
-
return {
|
|
35
|
+
return {
|
|
36
|
+
rx: Number(m[1]),
|
|
37
|
+
wx: Number(m[2]),
|
|
38
|
+
netRx: m[3] != null ? Number(m[3]) : 0,
|
|
39
|
+
netTx: m[4] != null ? Number(m[4]) : 0,
|
|
40
|
+
};
|
|
21
41
|
}
|
|
22
42
|
|
|
23
43
|
function encodedCommand(script) {
|
|
@@ -26,11 +46,12 @@ function encodedCommand(script) {
|
|
|
26
46
|
|
|
27
47
|
/**
|
|
28
48
|
* Long-lived PowerShell so WMI stays warm (~10ms/tick after first query).
|
|
49
|
+
* One round-trip: disk bytes + physical NIC byte counters.
|
|
29
50
|
*/
|
|
30
51
|
export function createWindowsDiskReader() {
|
|
31
52
|
let child = null;
|
|
32
53
|
let buf = "";
|
|
33
|
-
/** @type {{ resolve: (v:
|
|
54
|
+
/** @type {{ resolve: (v: ReturnType<typeof parseDiskCounterLine>) => void }[]} */
|
|
34
55
|
let pending = [];
|
|
35
56
|
let closed = false;
|
|
36
57
|
|
|
@@ -43,7 +64,7 @@ export function createWindowsDiskReader() {
|
|
|
43
64
|
if (child || closed) return;
|
|
44
65
|
child = spawn(
|
|
45
66
|
"powershell.exe",
|
|
46
|
-
["-NoProfile", "-NonInteractive", "-EncodedCommand", encodedCommand(
|
|
67
|
+
["-NoProfile", "-NonInteractive", "-EncodedCommand", encodedCommand(PS_COUNTERS)],
|
|
47
68
|
{ windowsHide: true, stdio: ["pipe", "pipe", "ignore"] },
|
|
48
69
|
);
|
|
49
70
|
child.stdout.setEncoding("utf8");
|
|
@@ -67,7 +88,7 @@ export function createWindowsDiskReader() {
|
|
|
67
88
|
|
|
68
89
|
return {
|
|
69
90
|
/**
|
|
70
|
-
* @returns {Promise<
|
|
91
|
+
* @returns {Promise<ReturnType<typeof parseDiskCounterLine>>}
|
|
71
92
|
*/
|
|
72
93
|
read() {
|
|
73
94
|
if (closed) return Promise.resolve(null);
|
package/src/format.js
CHANGED
|
@@ -39,15 +39,19 @@ export function formatByteRate(bytesPerSec) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Network throughput in decimal
|
|
42
|
+
* Network throughput in decimal bit/s, auto-scaled (kbps / Mbps / Gbps).
|
|
43
|
+
* 1 kbps = 1e3 bit/s, 1 Mbps = 1e6 bit/s.
|
|
43
44
|
* @param {number | null | undefined} bytesPerSec
|
|
44
45
|
*/
|
|
45
46
|
export function formatBitRate(bytesPerSec) {
|
|
46
47
|
if (bytesPerSec == null) return "—";
|
|
47
48
|
const n = Number(bytesPerSec);
|
|
48
49
|
if (!Number.isFinite(n) || n < 0) return "—";
|
|
49
|
-
const
|
|
50
|
-
return `${
|
|
50
|
+
const bits = n * 8;
|
|
51
|
+
if (bits >= 1e9) return `${(bits / 1e9).toFixed(2)} Gbps`;
|
|
52
|
+
if (bits >= 1e6) return `${(bits / 1e6).toFixed(1)} Mbps`;
|
|
53
|
+
if (bits >= 1e3) return `${(bits / 1e3).toFixed(1)} kbps`;
|
|
54
|
+
return `${bits.toFixed(0)} bps`;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
/**
|
package/src/render.js
CHANGED
|
@@ -140,7 +140,7 @@ export function renderFrame(snap, history, opts = {}) {
|
|
|
140
140
|
if (snap.net) {
|
|
141
141
|
const up = formatBitRate(snap.net.txBps);
|
|
142
142
|
const down = formatBitRate(snap.net.rxBps);
|
|
143
|
-
lines.push(`${color.yellow("NET")} ↑ ${up.padStart(
|
|
143
|
+
lines.push(`${color.yellow("NET")} ↑ ${up.padStart(12)} ↓ ${down.padStart(12)}`);
|
|
144
144
|
lines.push(` ${color.yellow(sparkline(history.net, sparkW, { ascii }))}`);
|
|
145
145
|
} else {
|
|
146
146
|
lines.push(`${color.yellow("NET")} n/a`);
|
package/src/sample.js
CHANGED
|
@@ -100,23 +100,22 @@ export function gpuBytes(n, vramMb) {
|
|
|
100
100
|
*/
|
|
101
101
|
export function pickGpu(controllers) {
|
|
102
102
|
if (!Array.isArray(controllers) || controllers.length === 0) return null;
|
|
103
|
-
|
|
103
|
+
/** @type {GpuSample[]} */
|
|
104
|
+
const usable = [];
|
|
105
|
+
for (const c of controllers) {
|
|
104
106
|
const util = num(c.utilizationGpu);
|
|
105
|
-
const total = gpuBytes(num(c.memoryTotal), num(c.vram));
|
|
106
107
|
const used = gpuBytes(num(c.memoryUsed), null);
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
const total = gpuBytes(num(c.memoryTotal), null);
|
|
109
|
+
if (util == null && (used == null || total == null)) continue;
|
|
110
|
+
usable.push({
|
|
111
|
+
percent: util ?? 0,
|
|
112
|
+
used,
|
|
113
|
+
total,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
109
116
|
if (usable.length === 0) return null;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const percent = num(c.utilizationGpu);
|
|
113
|
-
const total = gpuBytes(num(c.memoryTotal), num(c.vram));
|
|
114
|
-
const used = gpuBytes(num(c.memoryUsed), null);
|
|
115
|
-
return {
|
|
116
|
-
percent,
|
|
117
|
-
used,
|
|
118
|
-
total,
|
|
119
|
-
};
|
|
117
|
+
usable.sort((a, b) => (b.total ?? 0) - (a.total ?? 0));
|
|
118
|
+
return usable[0];
|
|
120
119
|
}
|
|
121
120
|
|
|
122
121
|
/**
|
|
@@ -174,15 +173,16 @@ function mergePair(next, last, a, b) {
|
|
|
174
173
|
/**
|
|
175
174
|
* @param {os.CpuInfo[]} cpus
|
|
176
175
|
*/
|
|
177
|
-
export function cpuIdleTotal(cpus) {
|
|
176
|
+
export function cpuIdleTotal(cpus, windows = process.platform === "win32") {
|
|
178
177
|
let idle = 0;
|
|
179
178
|
let total = 0;
|
|
180
179
|
for (const c of cpus) {
|
|
181
180
|
const t = c.times;
|
|
181
|
+
const irq = t.irq || 0;
|
|
182
|
+
const sys = windows ? Math.max(0, (t.sys || 0) - irq) : t.sys || 0;
|
|
182
183
|
const i = t.idle || 0;
|
|
183
|
-
const sum = (t.user || 0) + (t.nice || 0) + (t.sys || 0) + (t.irq || 0) + i;
|
|
184
184
|
idle += i;
|
|
185
|
-
total +=
|
|
185
|
+
total += (t.user || 0) + (t.nice || 0) + sys + irq + i;
|
|
186
186
|
}
|
|
187
187
|
return { idle, total };
|
|
188
188
|
}
|
|
@@ -204,6 +204,16 @@ export function cpuPercentFromDelta(prev, curr) {
|
|
|
204
204
|
/**
|
|
205
205
|
* @param {Array<{ iface?: string, rx_bytes?: number, tx_bytes?: number, operstate?: string, internal?: boolean }>} stats
|
|
206
206
|
*/
|
|
207
|
+
const VIRTUAL_IFACE =
|
|
208
|
+
/^(lo|lo0)$|Loopback|vEthernet|Hyper-V|WSL|Bluetooth|Virtual|VPN|TAP|TUN|Tailscale|Pseudo|isatap|Teredo/i;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @param {string} name
|
|
212
|
+
*/
|
|
213
|
+
export function isVirtualIface(name) {
|
|
214
|
+
return VIRTUAL_IFACE.test(String(name || ""));
|
|
215
|
+
}
|
|
216
|
+
|
|
207
217
|
export function sumNetBytes(stats) {
|
|
208
218
|
let rx = 0;
|
|
209
219
|
let tx = 0;
|
|
@@ -211,12 +221,8 @@ export function sumNetBytes(stats) {
|
|
|
211
221
|
if (!Array.isArray(stats)) return { rx: null, tx: null };
|
|
212
222
|
for (const n of stats) {
|
|
213
223
|
const name = String(n.iface || "");
|
|
214
|
-
if (n.internal ||
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
if (n.operstate && n.operstate !== "up" && n.operstate !== "unknown") {
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
224
|
+
if (n.internal || isVirtualIface(name)) continue;
|
|
225
|
+
if (n.operstate && n.operstate !== "up") continue;
|
|
220
226
|
const r = num(n.rx_bytes);
|
|
221
227
|
const t = num(n.tx_bytes);
|
|
222
228
|
if (r == null && t == null) continue;
|
|
@@ -282,34 +288,28 @@ export function createSampler(si) {
|
|
|
282
288
|
}
|
|
283
289
|
|
|
284
290
|
async function sampleCpu(lib) {
|
|
285
|
-
const curr = cpuIdleTotal(os.cpus());
|
|
286
|
-
if (!prevCpu) {
|
|
287
|
-
prevCpu = curr;
|
|
288
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
289
|
-
const curr2 = cpuIdleTotal(os.cpus());
|
|
290
|
-
const pctFast = cpuPercentFromDelta(prevCpu, curr2);
|
|
291
|
-
prevCpu = curr2;
|
|
292
|
-
if (pctFast != null) return { percent: pctFast };
|
|
293
|
-
} else {
|
|
294
|
-
const pct = cpuPercentFromDelta(prevCpu, curr);
|
|
295
|
-
prevCpu = curr;
|
|
296
|
-
if (pct != null) return { percent: pct };
|
|
297
|
-
}
|
|
298
291
|
try {
|
|
299
292
|
const load = await lib.currentLoad();
|
|
300
293
|
const pct = num(load?.currentLoad);
|
|
301
294
|
if (pct != null) return { percent: Math.min(100, Math.max(0, pct)) };
|
|
302
295
|
} catch {
|
|
303
|
-
//
|
|
296
|
+
// fall through to os.cpus() idle-delta
|
|
304
297
|
}
|
|
305
|
-
|
|
298
|
+
const curr = cpuIdleTotal(os.cpus());
|
|
299
|
+
const pct = cpuPercentFromDelta(prevCpu, curr);
|
|
300
|
+
prevCpu = curr;
|
|
301
|
+
if (pct == null) return null;
|
|
302
|
+
return { percent: pct };
|
|
306
303
|
}
|
|
307
304
|
|
|
308
305
|
async function sampleRam(lib, ramTotal) {
|
|
309
306
|
try {
|
|
310
307
|
const mem = await lib.mem();
|
|
311
308
|
const total = num(mem?.total) ?? ramTotal;
|
|
312
|
-
const
|
|
309
|
+
const available = num(mem?.available) ?? num(mem?.free);
|
|
310
|
+
let used = null;
|
|
311
|
+
if (total != null && available != null) used = Math.max(0, total - available);
|
|
312
|
+
else used = num(mem?.used) ?? num(mem?.active);
|
|
313
313
|
if (used == null || total == null || total <= 0) return null;
|
|
314
314
|
return {
|
|
315
315
|
percent: Math.min(100, Math.max(0, (used / total) * 100)),
|
|
@@ -329,21 +329,20 @@ export function createSampler(si) {
|
|
|
329
329
|
}
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
wx = w.wx;
|
|
332
|
+
/**
|
|
333
|
+
* @param {number} ts
|
|
334
|
+
* @param {{ rx: number, wx: number } | null} win
|
|
335
|
+
*/
|
|
336
|
+
async function sampleDisk(lib, ts, win) {
|
|
337
|
+
let rx = win ? num(win.rx) : null;
|
|
338
|
+
let wx = win ? num(win.wx) : null;
|
|
339
|
+
if (rx == null && wx == null) {
|
|
340
|
+
try {
|
|
341
|
+
const fs = await lib.fsStats();
|
|
342
|
+
rx = num(fs?.rx);
|
|
343
|
+
wx = num(fs?.wx);
|
|
344
|
+
} catch {
|
|
345
|
+
// Windows fsStats is always null
|
|
347
346
|
}
|
|
348
347
|
}
|
|
349
348
|
if (rx == null && wx == null) return { readBps: null, writeBps: null };
|
|
@@ -353,17 +352,27 @@ export function createSampler(si) {
|
|
|
353
352
|
return { readBps, writeBps };
|
|
354
353
|
}
|
|
355
354
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
355
|
+
/**
|
|
356
|
+
* @param {number} ts
|
|
357
|
+
* @param {{ netRx?: number, netTx?: number } | null} win
|
|
358
|
+
*/
|
|
359
|
+
async function sampleNet(lib, ts, win) {
|
|
360
|
+
let rx = win ? num(win.netRx) : null;
|
|
361
|
+
let tx = win ? num(win.netTx) : null;
|
|
362
|
+
if (rx == null && tx == null) {
|
|
363
|
+
try {
|
|
364
|
+
const stats = await lib.networkStats("*");
|
|
365
|
+
const summed = sumNetBytes(Array.isArray(stats) ? stats : stats ? [stats] : []);
|
|
366
|
+
rx = summed.rx;
|
|
367
|
+
tx = summed.tx;
|
|
368
|
+
} catch {
|
|
369
|
+
return { txBps: null, rxBps: null };
|
|
370
|
+
}
|
|
366
371
|
}
|
|
372
|
+
const rxBps = rateFromCounters(prevNet?.rx, rx, prevTs, ts);
|
|
373
|
+
const txBps = rateFromCounters(prevNet?.tx, tx, prevTs, ts);
|
|
374
|
+
if (rx != null && tx != null) prevNet = { rx, tx };
|
|
375
|
+
return { txBps, rxBps };
|
|
367
376
|
}
|
|
368
377
|
|
|
369
378
|
async function sampleGpu(lib) {
|
|
@@ -383,12 +392,13 @@ export function createSampler(si) {
|
|
|
383
392
|
const lib = await loadSi();
|
|
384
393
|
if (!staticP) staticP = loadStatic();
|
|
385
394
|
const meta = await staticP;
|
|
395
|
+
const win = winDisk ? await winDisk.read().catch(() => null) : null;
|
|
386
396
|
const ts = Date.now();
|
|
387
397
|
const [cpu, ram, disk, net, gpu] = await Promise.all([
|
|
388
398
|
sampleCpu(lib).catch(() => last?.cpu ?? null),
|
|
389
399
|
sampleRam(lib, meta.ramTotal).catch(() => last?.ram ?? null),
|
|
390
|
-
sampleDisk(lib, ts).catch(() => last?.disk ?? { readBps: null, writeBps: null }),
|
|
391
|
-
sampleNet(lib, ts).catch(() => last?.net ?? { txBps: null, rxBps: null }),
|
|
400
|
+
sampleDisk(lib, ts, win).catch(() => last?.disk ?? { readBps: null, writeBps: null }),
|
|
401
|
+
sampleNet(lib, ts, win).catch(() => last?.net ?? { txBps: null, rxBps: null }),
|
|
392
402
|
sampleGpu(lib).catch(() => last?.gpu ?? null),
|
|
393
403
|
]);
|
|
394
404
|
prevTs = ts;
|