@yourfam/yf-vitals 0.1.0 → 0.2.0
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 +4 -2
- package/package.json +1 -1
- package/src/args.js +3 -2
- package/src/bar.js +17 -2
- package/src/color.js +1 -0
- package/src/disk.js +30 -9
- package/src/format.js +7 -3
- package/src/history.js +12 -23
- package/src/render.js +77 -16
- package/src/sample.js +76 -66
package/README.md
CHANGED
|
@@ -37,12 +37,14 @@ 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.
|
|
44
44
|
|
|
45
|
-
Each percent row has a 20-cell bar and a sparkline of the last 60 ticks.
|
|
45
|
+
Each percent row has a 20-cell bar and a sparkline of the last 60 ticks. Unused spark slots use the lowest tick (`▁` / `_`), not blank space. Bar fill stays the row color below 50%, turns **yellow at 50%**, **red at 80%**. Sparklines stay the row color.
|
|
46
|
+
|
|
47
|
+
Disk and net each show **two** sparks (`R`/`W`, `↑`/`↓`) scaled to the **max of that pair** in the buffer (rates have no 100% ceiling). CPU / RAM / GPU sparks are percent 0–100.
|
|
46
48
|
|
|
47
49
|
First disk/net tick may show `—` while counters settle.
|
|
48
50
|
|
package/package.json
CHANGED
package/src/args.js
CHANGED
|
@@ -98,7 +98,8 @@ 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
|
|
102
|
-
Disk / net
|
|
101
|
+
Disk rates are KiB/s–GiB/s; network rates are kbps/Mbps/Gbps.
|
|
102
|
+
Disk / net sparks are split (R/W, ↑/↓) and scale to the pair max in the last 60 ticks.
|
|
103
|
+
Bar fill turns yellow ≥50% and red ≥80%. Unused spark slots use the lowest tick.
|
|
103
104
|
Needs a terminal (no pipes). Node 20+.`;
|
|
104
105
|
}
|
package/src/bar.js
CHANGED
|
@@ -55,8 +55,9 @@ export function sparkline(values, width, opts = {}) {
|
|
|
55
55
|
if (w === 0) return "";
|
|
56
56
|
const charset = opts.ascii ? ASC_SPARK : UNI_SPARK;
|
|
57
57
|
const list = Array.isArray(values) ? values : [];
|
|
58
|
+
const floor = charset[0];
|
|
58
59
|
if (list.length === 0) {
|
|
59
|
-
return
|
|
60
|
+
return floor.repeat(w);
|
|
60
61
|
}
|
|
61
62
|
const slice = list.slice(-w);
|
|
62
63
|
const pad = w - slice.length;
|
|
@@ -64,7 +65,21 @@ export function sparkline(values, width, opts = {}) {
|
|
|
64
65
|
const dataMax = slice.reduce((m, v) => (v > m ? v : m), 0);
|
|
65
66
|
const max = explicitMax != null ? explicitMax : dataMax;
|
|
66
67
|
const chars = slice.map((v) => sparkChar(v, max, charset));
|
|
67
|
-
return
|
|
68
|
+
return floor.repeat(pad) + chars.join("");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const HEAT_WARM = 50;
|
|
72
|
+
export const HEAT_HOT = 80;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {number} percent
|
|
76
|
+
* @returns {"ok" | "warm" | "hot"}
|
|
77
|
+
*/
|
|
78
|
+
export function heatLevel(percent) {
|
|
79
|
+
const n = Number(percent);
|
|
80
|
+
if (!Number.isFinite(n) || n < HEAT_WARM) return "ok";
|
|
81
|
+
if (n < HEAT_HOT) return "warm";
|
|
82
|
+
return "hot";
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
/**
|
package/src/color.js
CHANGED
|
@@ -12,6 +12,7 @@ export function createColor(opts) {
|
|
|
12
12
|
magenta: (value) => pc.magenta(String(value)),
|
|
13
13
|
green: (value) => pc.green(String(value)),
|
|
14
14
|
yellow: (value) => pc.yellow(String(value)),
|
|
15
|
+
red: (value) => pc.red(String(value)),
|
|
15
16
|
dim: (value) => pc.dim(String(value)),
|
|
16
17
|
};
|
|
17
18
|
}
|
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/history.js
CHANGED
|
@@ -5,12 +5,14 @@ import { HISTORY_SIZE } from "./bar.js";
|
|
|
5
5
|
* @property {number[]} cpu
|
|
6
6
|
* @property {number[]} ram
|
|
7
7
|
* @property {number[]} gpu
|
|
8
|
-
* @property {number[]}
|
|
9
|
-
* @property {number[]}
|
|
8
|
+
* @property {number[]} dskR
|
|
9
|
+
* @property {number[]} dskW
|
|
10
|
+
* @property {number[]} netUp
|
|
11
|
+
* @property {number[]} netDn
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
export function createHistory() {
|
|
13
|
-
return { cpu: [], ram: [], gpu: [],
|
|
15
|
+
return { cpu: [], ram: [], gpu: [], dskR: [], dskW: [], netUp: [], netDn: [] };
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -33,26 +35,13 @@ export function pushSample(buf, value, size = HISTORY_SIZE) {
|
|
|
33
35
|
* @returns {History}
|
|
34
36
|
*/
|
|
35
37
|
export function appendHistory(history, snap) {
|
|
36
|
-
const cpu = snap.cpu?.percent;
|
|
37
|
-
const ram = snap.ram?.percent;
|
|
38
|
-
const gpu = snap.gpu?.percent;
|
|
39
|
-
const read = snap.disk?.readBps;
|
|
40
|
-
const write = snap.disk?.writeBps;
|
|
41
|
-
const tx = snap.net?.txBps;
|
|
42
|
-
const rx = snap.net?.rxBps;
|
|
43
|
-
let dsk;
|
|
44
|
-
if (read != null || write != null) {
|
|
45
|
-
dsk = (read ?? 0) + (write ?? 0);
|
|
46
|
-
}
|
|
47
|
-
let net;
|
|
48
|
-
if (tx != null || rx != null) {
|
|
49
|
-
net = (tx ?? 0) + (rx ?? 0);
|
|
50
|
-
}
|
|
51
38
|
return {
|
|
52
|
-
cpu: pushSample(history.cpu, cpu),
|
|
53
|
-
ram: pushSample(history.ram, ram),
|
|
54
|
-
gpu: pushSample(history.gpu, gpu),
|
|
55
|
-
|
|
56
|
-
|
|
39
|
+
cpu: pushSample(history.cpu, snap.cpu?.percent),
|
|
40
|
+
ram: pushSample(history.ram, snap.ram?.percent),
|
|
41
|
+
gpu: pushSample(history.gpu, snap.gpu?.percent),
|
|
42
|
+
dskR: pushSample(history.dskR, snap.disk?.readBps),
|
|
43
|
+
dskW: pushSample(history.dskW, snap.disk?.writeBps),
|
|
44
|
+
netUp: pushSample(history.netUp, snap.net?.txBps),
|
|
45
|
+
netDn: pushSample(history.netDn, snap.net?.rxBps),
|
|
57
46
|
};
|
|
58
47
|
}
|
package/src/render.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { useAscii } from "./ascii.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BAR_WIDTH,
|
|
4
|
+
bar,
|
|
5
|
+
barCharset,
|
|
6
|
+
heatLevel,
|
|
7
|
+
sparkWidth,
|
|
8
|
+
sparkline,
|
|
9
|
+
} from "./bar.js";
|
|
3
10
|
import { createColor, stripAnsi } from "./color.js";
|
|
4
11
|
import {
|
|
5
12
|
formatBitRate,
|
|
@@ -39,6 +46,58 @@ export function fitLine(line, columns) {
|
|
|
39
46
|
return out;
|
|
40
47
|
}
|
|
41
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @param {ReturnType<typeof createColor>} color
|
|
51
|
+
* @param {number} percent
|
|
52
|
+
* @param {"cyan" | "magenta" | "green"} row
|
|
53
|
+
*/
|
|
54
|
+
export function heatPaint(color, percent, row) {
|
|
55
|
+
const level = heatLevel(percent);
|
|
56
|
+
if (level === "hot") return color.red;
|
|
57
|
+
if (level === "warm") return color.yellow;
|
|
58
|
+
return color[row];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Percent bar: fill takes heat color, empty stays dim. Sparkline stays the row color.
|
|
63
|
+
*
|
|
64
|
+
* @param {number} percent
|
|
65
|
+
* @param {ReturnType<typeof createColor>} color
|
|
66
|
+
* @param {"cyan" | "magenta" | "green"} row
|
|
67
|
+
* @param {boolean} ascii
|
|
68
|
+
*/
|
|
69
|
+
export function paintPercentBar(percent, color, row, ascii) {
|
|
70
|
+
const cs = barCharset(ascii);
|
|
71
|
+
const raw = bar(percent, BAR_WIDTH, cs);
|
|
72
|
+
const filled = raw.replaceAll(cs.empty, "").length;
|
|
73
|
+
const fill = raw.slice(0, filled);
|
|
74
|
+
const empty = raw.slice(filled);
|
|
75
|
+
const paint = heatPaint(color, percent, row);
|
|
76
|
+
return `[${paint(fill)}${color.dim(empty)}]`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Two labeled sparks on one line, sharing a max so the pair is comparable.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} leftLabel
|
|
83
|
+
* @param {number[]} leftVals
|
|
84
|
+
* @param {string} rightLabel
|
|
85
|
+
* @param {number[]} rightVals
|
|
86
|
+
* @param {number} sparkW
|
|
87
|
+
* @param {boolean} ascii
|
|
88
|
+
*/
|
|
89
|
+
export function splitSparkLine(leftLabel, leftVals, rightLabel, rightVals, sparkW, ascii) {
|
|
90
|
+
const leftTag = `${leftLabel} `;
|
|
91
|
+
const mid = ` ${rightLabel} `;
|
|
92
|
+
const overhead = leftTag.length + mid.length;
|
|
93
|
+
const inner = Math.max(overhead + 8, Number(sparkW) || 16);
|
|
94
|
+
const each = Math.max(4, Math.floor((inner - overhead) / 2));
|
|
95
|
+
const pairMax = Math.max(0, ...leftVals, ...rightVals);
|
|
96
|
+
const left = sparkline(leftVals, each, { ascii, max: pairMax || undefined });
|
|
97
|
+
const right = sparkline(rightVals, each, { ascii, max: pairMax || undefined });
|
|
98
|
+
return `${leftTag}${left}${mid}${right}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
42
101
|
/**
|
|
43
102
|
* @param {import("./sample.js").GpuSample | null | undefined} gpu
|
|
44
103
|
* @param {{ history: number[], ascii: boolean, color: ReturnType<typeof createColor>, sparkW: number }} opts
|
|
@@ -47,18 +106,15 @@ export function fitLine(line, columns) {
|
|
|
47
106
|
export function buildGpuRow(gpu, opts) {
|
|
48
107
|
if (gpu == null) return null;
|
|
49
108
|
const { ascii, color, sparkW, history } = opts;
|
|
50
|
-
const cs = barCharset(ascii);
|
|
51
109
|
const pct = gpu.percent;
|
|
52
|
-
const barStr =
|
|
53
|
-
pct == null ? null : bar(pct, BAR_WIDTH, cs);
|
|
54
110
|
const pctStr = pct == null ? "n/a" : formatPercent(pct);
|
|
55
111
|
const mem =
|
|
56
112
|
gpu.used != null && gpu.total != null ? formatBytePair(gpu.used, gpu.total) : "";
|
|
57
113
|
const label = color.green("GPU");
|
|
58
114
|
const head =
|
|
59
|
-
|
|
115
|
+
pct == null
|
|
60
116
|
? `${label} ${pctStr}${mem ? ` ${mem}` : ""}`
|
|
61
|
-
: `${label}
|
|
117
|
+
: `${label} ${paintPercentBar(pct, color, "green", ascii)} ${pctStr}${mem ? ` ${mem}` : ""}`;
|
|
62
118
|
const spark = color.green(sparkline(history, sparkW, { ascii, max: 100 }));
|
|
63
119
|
return [head, ` ${spark}`];
|
|
64
120
|
}
|
|
@@ -74,7 +130,6 @@ export function renderFrame(snap, history, opts = {}) {
|
|
|
74
130
|
const ascii = useAscii(env);
|
|
75
131
|
const color = createColor({ isTTY: Boolean(opts.isTTY), env });
|
|
76
132
|
const sparkW = sparkWidth(columns);
|
|
77
|
-
const cs = barCharset(ascii);
|
|
78
133
|
const interval = formatInterval(opts.intervalSec ?? 1);
|
|
79
134
|
const cores = formatCores(snap.physical, snap.logical);
|
|
80
135
|
const ramTot = formatBytes(snap.ramTotal);
|
|
@@ -91,10 +146,9 @@ export function renderFrame(snap, history, opts = {}) {
|
|
|
91
146
|
const lines = [header, ""];
|
|
92
147
|
|
|
93
148
|
if (snap.cpu) {
|
|
94
|
-
const b = color.cyan(bar(snap.cpu.percent, BAR_WIDTH, cs));
|
|
95
149
|
const detail = formatCoresDetail(snap.physical, snap.logical);
|
|
96
150
|
lines.push(
|
|
97
|
-
`${color.cyan("CPU")}
|
|
151
|
+
`${color.cyan("CPU")} ${paintPercentBar(snap.cpu.percent, color, "cyan", ascii)} ${formatPercent(snap.cpu.percent)} ${detail}`,
|
|
98
152
|
);
|
|
99
153
|
lines.push(` ${color.cyan(sparkline(history.cpu, sparkW, { ascii, max: 100 }))}`);
|
|
100
154
|
} else {
|
|
@@ -104,9 +158,8 @@ export function renderFrame(snap, history, opts = {}) {
|
|
|
104
158
|
lines.push("");
|
|
105
159
|
|
|
106
160
|
if (snap.ram) {
|
|
107
|
-
const b = color.magenta(bar(snap.ram.percent, BAR_WIDTH, cs));
|
|
108
161
|
lines.push(
|
|
109
|
-
`${color.magenta("RAM")}
|
|
162
|
+
`${color.magenta("RAM")} ${paintPercentBar(snap.ram.percent, color, "magenta", ascii)} ${formatPercent(snap.ram.percent)} ${formatBytePair(snap.ram.used, snap.ram.total)}`,
|
|
110
163
|
);
|
|
111
164
|
lines.push(` ${color.magenta(sparkline(history.ram, sparkW, { ascii, max: 100 }))}`);
|
|
112
165
|
} else {
|
|
@@ -130,21 +183,29 @@ export function renderFrame(snap, history, opts = {}) {
|
|
|
130
183
|
const r = formatByteRate(snap.disk.readBps);
|
|
131
184
|
const w = formatByteRate(snap.disk.writeBps);
|
|
132
185
|
lines.push(`${color.green("DSK")} R ${r.padStart(11)} W ${w.padStart(11)}`);
|
|
133
|
-
lines.push(
|
|
186
|
+
lines.push(
|
|
187
|
+
` ${color.green(splitSparkLine("R", history.dskR, "W", history.dskW, sparkW, ascii))}`,
|
|
188
|
+
);
|
|
134
189
|
} else {
|
|
135
190
|
lines.push(`${color.green("DSK")} n/a`);
|
|
136
|
-
lines.push(
|
|
191
|
+
lines.push(
|
|
192
|
+
` ${color.green(splitSparkLine("R", history.dskR, "W", history.dskW, sparkW, ascii))}`,
|
|
193
|
+
);
|
|
137
194
|
}
|
|
138
195
|
lines.push("");
|
|
139
196
|
|
|
140
197
|
if (snap.net) {
|
|
141
198
|
const up = formatBitRate(snap.net.txBps);
|
|
142
199
|
const down = formatBitRate(snap.net.rxBps);
|
|
143
|
-
lines.push(`${color.yellow("NET")} ↑ ${up.padStart(
|
|
144
|
-
lines.push(
|
|
200
|
+
lines.push(`${color.yellow("NET")} ↑ ${up.padStart(12)} ↓ ${down.padStart(12)}`);
|
|
201
|
+
lines.push(
|
|
202
|
+
` ${color.yellow(splitSparkLine("↑", history.netUp, "↓", history.netDn, sparkW, ascii))}`,
|
|
203
|
+
);
|
|
145
204
|
} else {
|
|
146
205
|
lines.push(`${color.yellow("NET")} n/a`);
|
|
147
|
-
lines.push(
|
|
206
|
+
lines.push(
|
|
207
|
+
` ${color.yellow(splitSparkLine("↑", history.netUp, "↓", history.netDn, sparkW, ascii))}`,
|
|
208
|
+
);
|
|
148
209
|
}
|
|
149
210
|
lines.push("");
|
|
150
211
|
lines.push("q quit");
|
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;
|