@yourfam/yf-vitals 0.1.1 → 0.3.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 -1
- package/package.json +1 -1
- package/src/args.js +3 -1
- package/src/bar.js +17 -2
- package/src/color.js +1 -0
- package/src/dashboard.js +12 -2
- package/src/disk.js +6 -2
- package/src/diskuse.js +103 -0
- package/src/format.js +4 -2
- package/src/history.js +14 -23
- package/src/render.js +89 -15
- package/src/sample.js +128 -65
package/README.md
CHANGED
|
@@ -36,13 +36,16 @@ One live dashboard. Refresh every **1.0s** (or `--interval`, or **2.0s** with `-
|
|
|
36
36
|
|---|---|
|
|
37
37
|
| **CPU** | Overall utilization 0–100%, plus physical / logical core counts |
|
|
38
38
|
| **RAM** | Percent used, plus used / total (`GiB` / `MiB`) |
|
|
39
|
+
| **USE** | Local hard-disk fill (system volume first). Skips Google Drive, iCloud, network, FAT/USB. Extra local disks as extra lines |
|
|
39
40
|
| **DSK** | Read and write **rates** (KiB/s, MiB/s, GiB/s) |
|
|
40
41
|
| **NET** | Send ↑ and receive ↓ in **kbps / Mbps / Gbps** (decimal bits; 1 Mbps = 1e6 bit/s) |
|
|
41
42
|
| **GPU** | Utilization and VRAM when telemetry exists; **the row is omitted** when it does not |
|
|
42
43
|
|
|
43
44
|
Header: `yf-vitals` · hostname · OS · `NP/NL` cores · RAM total · tick interval.
|
|
44
45
|
|
|
45
|
-
Each percent row has a 20-cell bar and a sparkline of the last 60 ticks.
|
|
46
|
+
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.
|
|
47
|
+
|
|
48
|
+
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
49
|
|
|
47
50
|
First disk/net tick may show `—` while counters settle.
|
|
48
51
|
|
package/package.json
CHANGED
package/src/args.js
CHANGED
|
@@ -98,7 +98,9 @@ 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
|
+
USE is local NTFS/APFS/HFS fill (no Google Drive, iCloud, or network shares).
|
|
101
102
|
Disk rates are KiB/s–GiB/s; network rates are kbps/Mbps/Gbps.
|
|
102
|
-
Disk / net
|
|
103
|
+
Disk / net sparks are split (R/W, ↑/↓) and scale to the pair max in the last 60 ticks.
|
|
104
|
+
Bar fill turns yellow ≥50% and red ≥80%. Unused spark slots use the lowest tick.
|
|
103
105
|
Needs a terminal (no pipes). Node 20+.`;
|
|
104
106
|
}
|
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/dashboard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendHistory, createHistory } from "./history.js";
|
|
2
2
|
import { renderFrame } from "./render.js";
|
|
3
|
-
import { createSampler, mergeLastGood } from "./sample.js";
|
|
3
|
+
import { createSampler, instantSnapshot, mergeLastGood } from "./sample.js";
|
|
4
4
|
import { CLEAR_HOME, enterTerminal, restoreTerminal } from "./tty.js";
|
|
5
5
|
|
|
6
6
|
const CTRL_C = 0x03;
|
|
@@ -61,6 +61,16 @@ export async function runDashboard(args, deps) {
|
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
enterTerminal(write, stdin);
|
|
64
|
+
const first = instantSnapshot();
|
|
65
|
+
write(
|
|
66
|
+
CLEAR_HOME +
|
|
67
|
+
renderFrame(first, createHistory(), {
|
|
68
|
+
columns: deps.columns(),
|
|
69
|
+
env: deps.env,
|
|
70
|
+
isTTY: true,
|
|
71
|
+
intervalSec: args.interval,
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
64
74
|
|
|
65
75
|
const onData = (chunk) => {
|
|
66
76
|
if (isQuitKey(chunk)) quit = true;
|
|
@@ -82,7 +92,7 @@ export async function runDashboard(args, deps) {
|
|
|
82
92
|
sampler = deps.sampler || createSampler();
|
|
83
93
|
let history = createHistory();
|
|
84
94
|
/** @type {import("./sample.js").Snapshot | null} */
|
|
85
|
-
let last =
|
|
95
|
+
let last = first;
|
|
86
96
|
|
|
87
97
|
while (!shouldStop()) {
|
|
88
98
|
let snap;
|
package/src/disk.js
CHANGED
|
@@ -87,10 +87,14 @@ export function createWindowsDiskReader() {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
return {
|
|
90
|
+
start() {
|
|
91
|
+
ensure();
|
|
92
|
+
},
|
|
90
93
|
/**
|
|
94
|
+
* @param {number} [timeoutMs]
|
|
91
95
|
* @returns {Promise<ReturnType<typeof parseDiskCounterLine>>}
|
|
92
96
|
*/
|
|
93
|
-
read() {
|
|
97
|
+
read(timeoutMs = 400) {
|
|
94
98
|
if (closed) return Promise.resolve(null);
|
|
95
99
|
ensure();
|
|
96
100
|
if (!child || !child.stdin.writable) return Promise.resolve(null);
|
|
@@ -99,7 +103,7 @@ export function createWindowsDiskReader() {
|
|
|
99
103
|
const i = pending.findIndex((p) => p.resolve === done);
|
|
100
104
|
if (i >= 0) pending.splice(i, 1);
|
|
101
105
|
resolve(null);
|
|
102
|
-
},
|
|
106
|
+
}, timeoutMs);
|
|
103
107
|
const done = (v) => {
|
|
104
108
|
clearTimeout(t);
|
|
105
109
|
resolve(v);
|
package/src/diskuse.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const MIN_BYTES = 8 * 1024 * 1024 * 1024;
|
|
2
|
+
|
|
3
|
+
const SKIP_FS =
|
|
4
|
+
/^(fat|fat12|fat16|fat32|msdos|nfs|smbfs|cifs|afp|webdav|fuse|sshfs|osxfuse|macfuse|tmpfs|devfs|autofs|iso9660|udf|overlay|9p)$/i;
|
|
5
|
+
|
|
6
|
+
const SKIP_NAME =
|
|
7
|
+
/google\s*drive|gdrive|icloud|onedrive|dropbox|cloudstorage|cloudmounter|box\.com/i;
|
|
8
|
+
|
|
9
|
+
const SKIP_MOUNT =
|
|
10
|
+
/^\/(System\/Volumes\/(Preboot|VM|Update|Recovery|iOS|hardware|xarts|cryptex)|\.MobileBackups|Volumes\/\.timemachine)/i;
|
|
11
|
+
|
|
12
|
+
const LOCAL_FS = /^(ntfs|refs|apfs|hfs|hfs\+|ext[234]|xfs|btrfs|zfs)$/i;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {{ fs?: string, type?: string, mount?: string, size?: number, used?: number }} row
|
|
16
|
+
*/
|
|
17
|
+
export function isLocalCapacityVolume(row) {
|
|
18
|
+
if (!row) return false;
|
|
19
|
+
const type = String(row.type || "");
|
|
20
|
+
const mount = String(row.mount || "");
|
|
21
|
+
const fs = String(row.fs || "");
|
|
22
|
+
const blob = `${type} ${mount} ${fs}`;
|
|
23
|
+
if (SKIP_NAME.test(blob)) return false;
|
|
24
|
+
if (SKIP_FS.test(type)) return false;
|
|
25
|
+
if (SKIP_MOUNT.test(mount)) return false;
|
|
26
|
+
const size = Number(row.size) || 0;
|
|
27
|
+
if (size < MIN_BYTES) return false;
|
|
28
|
+
if (LOCAL_FS.test(type)) return true;
|
|
29
|
+
if (/^[A-Z]:\\?$/i.test(mount || fs) && LOCAL_FS.test(type)) return true;
|
|
30
|
+
if (mount === "/" || mount === "/System/Volumes/Data") return true;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} mount
|
|
36
|
+
*/
|
|
37
|
+
export function shortMount(mount) {
|
|
38
|
+
const m = String(mount || "");
|
|
39
|
+
const drive = m.match(/^([A-Z]:)\\?$/i);
|
|
40
|
+
if (drive) return drive[1].toUpperCase();
|
|
41
|
+
if (m === "/System/Volumes/Data") return "Data";
|
|
42
|
+
if (m === "/") return "/";
|
|
43
|
+
const parts = m.split(/[/\\]/).filter(Boolean);
|
|
44
|
+
return parts[parts.length - 1] || m || "?";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {{ fs?: string, type?: string, mount?: string, size?: number, used?: number, use?: number }} row
|
|
49
|
+
*/
|
|
50
|
+
function systemScore(row) {
|
|
51
|
+
const m = String(row.mount || "");
|
|
52
|
+
if (/^C:\\?$/i.test(m)) return 3;
|
|
53
|
+
if (m === "/System/Volumes/Data") return 2;
|
|
54
|
+
if (m === "/") return 1;
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {Array<{ fs?: string, type?: string, mount?: string, size?: number, used?: number, use?: number }>} rows
|
|
60
|
+
* @returns {{ percent: number, used: number, total: number, mount: string, others: { percent: number, used: number, total: number, mount: string }[] } | null}
|
|
61
|
+
*/
|
|
62
|
+
export function summarizeLocalDisks(rows) {
|
|
63
|
+
const local = (Array.isArray(rows) ? rows : []).filter(isLocalCapacityVolume);
|
|
64
|
+
/** @type {typeof local} */
|
|
65
|
+
const uniq = [];
|
|
66
|
+
for (const r of local.slice().sort((a, b) => systemScore(b) - systemScore(a) || (b.size || 0) - (a.size || 0))) {
|
|
67
|
+
const dupIdx = uniq.findIndex(
|
|
68
|
+
(u) =>
|
|
69
|
+
Math.abs((u.size || 0) - (r.size || 0)) < 1024 * 1024 &&
|
|
70
|
+
Math.abs((u.used || 0) - (r.used || 0)) < 0.01 * (r.size || 1),
|
|
71
|
+
);
|
|
72
|
+
if (dupIdx >= 0) continue;
|
|
73
|
+
uniq.push(r);
|
|
74
|
+
}
|
|
75
|
+
if (!uniq.length) return null;
|
|
76
|
+
const primary = uniq[0];
|
|
77
|
+
const total = Number(primary.size) || 0;
|
|
78
|
+
const used = Number(primary.used) || 0;
|
|
79
|
+
const percent =
|
|
80
|
+
typeof primary.use === "number" && Number.isFinite(primary.use)
|
|
81
|
+
? primary.use
|
|
82
|
+
: total > 0
|
|
83
|
+
? (used / total) * 100
|
|
84
|
+
: 0;
|
|
85
|
+
return {
|
|
86
|
+
percent: Math.min(100, Math.max(0, percent)),
|
|
87
|
+
used,
|
|
88
|
+
total,
|
|
89
|
+
mount: shortMount(primary.mount || primary.fs || ""),
|
|
90
|
+
others: uniq.slice(1).map((r) => {
|
|
91
|
+
const t = Number(r.size) || 0;
|
|
92
|
+
const u = Number(r.used) || 0;
|
|
93
|
+
const p =
|
|
94
|
+
typeof r.use === "number" && Number.isFinite(r.use) ? r.use : t > 0 ? (u / t) * 100 : 0;
|
|
95
|
+
return {
|
|
96
|
+
percent: Math.min(100, Math.max(0, p)),
|
|
97
|
+
used: u,
|
|
98
|
+
total: t,
|
|
99
|
+
mount: shortMount(r.mount || r.fs || ""),
|
|
100
|
+
};
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
}
|
package/src/format.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const KIB = 1024;
|
|
2
2
|
const MIB = 1024 * 1024;
|
|
3
3
|
const GIB = 1024 * 1024 * 1024;
|
|
4
|
+
const TIB = 1024 * 1024 * 1024 * 1024;
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @param {number} bytes
|
|
@@ -8,6 +9,7 @@ const GIB = 1024 * 1024 * 1024;
|
|
|
8
9
|
export function formatBytes(bytes) {
|
|
9
10
|
const n = Number(bytes);
|
|
10
11
|
if (!Number.isFinite(n) || n < 0) return "n/a";
|
|
12
|
+
if (n >= TIB) return `${(n / TIB).toFixed(1)} TiB`;
|
|
11
13
|
if (n >= GIB) return `${(n / GIB).toFixed(1)} GiB`;
|
|
12
14
|
return `${(n / MIB).toFixed(1)} MiB`;
|
|
13
15
|
}
|
|
@@ -20,8 +22,8 @@ export function formatBytePair(used, total) {
|
|
|
20
22
|
const t = Number(total);
|
|
21
23
|
const u = Number(used);
|
|
22
24
|
if (!Number.isFinite(t) || t < 0 || !Number.isFinite(u) || u < 0) return "n/a";
|
|
23
|
-
const unit = t >= GIB ? "GiB" : "MiB";
|
|
24
|
-
const div = unit === "GiB" ? GIB : MIB;
|
|
25
|
+
const unit = t >= TIB ? "TiB" : t >= GIB ? "GiB" : "MiB";
|
|
26
|
+
const div = unit === "TiB" ? TIB : unit === "GiB" ? GIB : MIB;
|
|
25
27
|
return `${(u / div).toFixed(1)} / ${(t / div).toFixed(1)} ${unit}`;
|
|
26
28
|
}
|
|
27
29
|
|
package/src/history.js
CHANGED
|
@@ -5,12 +5,15 @@ 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
|
|
12
|
+
* @property {number[]} diskUse
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
15
|
export function createHistory() {
|
|
13
|
-
return { cpu: [], ram: [], gpu: [],
|
|
16
|
+
return { cpu: [], ram: [], gpu: [], dskR: [], dskW: [], netUp: [], netDn: [], diskUse: [] };
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
/**
|
|
@@ -33,26 +36,14 @@ export function pushSample(buf, value, size = HISTORY_SIZE) {
|
|
|
33
36
|
* @returns {History}
|
|
34
37
|
*/
|
|
35
38
|
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
39
|
return {
|
|
52
|
-
cpu: pushSample(history.cpu, cpu),
|
|
53
|
-
ram: pushSample(history.ram, ram),
|
|
54
|
-
gpu: pushSample(history.gpu, gpu),
|
|
55
|
-
|
|
56
|
-
|
|
40
|
+
cpu: pushSample(history.cpu, snap.cpu?.percent),
|
|
41
|
+
ram: pushSample(history.ram, snap.ram?.percent),
|
|
42
|
+
gpu: pushSample(history.gpu, snap.gpu?.percent),
|
|
43
|
+
dskR: pushSample(history.dskR, snap.disk?.readBps),
|
|
44
|
+
dskW: pushSample(history.dskW, snap.disk?.writeBps),
|
|
45
|
+
netUp: pushSample(history.netUp, snap.net?.txBps),
|
|
46
|
+
netDn: pushSample(history.netDn, snap.net?.rxBps),
|
|
47
|
+
diskUse: pushSample(history.diskUse, snap.diskUse?.percent),
|
|
57
48
|
};
|
|
58
49
|
}
|
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 {
|
|
@@ -115,6 +168,19 @@ export function renderFrame(snap, history, opts = {}) {
|
|
|
115
168
|
}
|
|
116
169
|
lines.push("");
|
|
117
170
|
|
|
171
|
+
if (snap.diskUse) {
|
|
172
|
+
lines.push(
|
|
173
|
+
`${color.green("USE")} ${paintPercentBar(snap.diskUse.percent, color, "green", ascii)} ${formatPercent(snap.diskUse.percent)} ${formatBytePair(snap.diskUse.used, snap.diskUse.total)} ${snap.diskUse.mount}`,
|
|
174
|
+
);
|
|
175
|
+
lines.push(` ${color.green(sparkline(history.diskUse, sparkW, { ascii, max: 100 }))}`);
|
|
176
|
+
for (const extra of snap.diskUse.others || []) {
|
|
177
|
+
lines.push(
|
|
178
|
+
` ${extra.mount} ${formatPercent(extra.percent).trim()} ${formatBytePair(extra.used, extra.total)}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (snap.diskUse) lines.push("");
|
|
183
|
+
|
|
118
184
|
const gpuLines = buildGpuRow(snap.gpu, {
|
|
119
185
|
history: history.gpu,
|
|
120
186
|
ascii,
|
|
@@ -130,10 +196,14 @@ export function renderFrame(snap, history, opts = {}) {
|
|
|
130
196
|
const r = formatByteRate(snap.disk.readBps);
|
|
131
197
|
const w = formatByteRate(snap.disk.writeBps);
|
|
132
198
|
lines.push(`${color.green("DSK")} R ${r.padStart(11)} W ${w.padStart(11)}`);
|
|
133
|
-
lines.push(
|
|
199
|
+
lines.push(
|
|
200
|
+
` ${color.green(splitSparkLine("R", history.dskR, "W", history.dskW, sparkW, ascii))}`,
|
|
201
|
+
);
|
|
134
202
|
} else {
|
|
135
203
|
lines.push(`${color.green("DSK")} n/a`);
|
|
136
|
-
lines.push(
|
|
204
|
+
lines.push(
|
|
205
|
+
` ${color.green(splitSparkLine("R", history.dskR, "W", history.dskW, sparkW, ascii))}`,
|
|
206
|
+
);
|
|
137
207
|
}
|
|
138
208
|
lines.push("");
|
|
139
209
|
|
|
@@ -141,10 +211,14 @@ export function renderFrame(snap, history, opts = {}) {
|
|
|
141
211
|
const up = formatBitRate(snap.net.txBps);
|
|
142
212
|
const down = formatBitRate(snap.net.rxBps);
|
|
143
213
|
lines.push(`${color.yellow("NET")} ↑ ${up.padStart(12)} ↓ ${down.padStart(12)}`);
|
|
144
|
-
lines.push(
|
|
214
|
+
lines.push(
|
|
215
|
+
` ${color.yellow(splitSparkLine("↑", history.netUp, "↓", history.netDn, sparkW, ascii))}`,
|
|
216
|
+
);
|
|
145
217
|
} else {
|
|
146
218
|
lines.push(`${color.yellow("NET")} n/a`);
|
|
147
|
-
lines.push(
|
|
219
|
+
lines.push(
|
|
220
|
+
` ${color.yellow(splitSparkLine("↑", history.netUp, "↓", history.netDn, sparkW, ascii))}`,
|
|
221
|
+
);
|
|
148
222
|
}
|
|
149
223
|
lines.push("");
|
|
150
224
|
lines.push("q quit");
|
package/src/sample.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import { createWindowsDiskReader } from "./disk.js";
|
|
3
|
+
import { summarizeLocalDisks } from "./diskuse.js";
|
|
3
4
|
import { rateFromCounters } from "./rates.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -36,6 +37,7 @@ import { rateFromCounters } from "./rates.js";
|
|
|
36
37
|
* @property {DiskSample | null} disk
|
|
37
38
|
* @property {NetSample | null} net
|
|
38
39
|
* @property {GpuSample | null} gpu
|
|
40
|
+
* @property {{ percent: number, used: number, total: number, mount: string, others: { percent: number, used: number, total: number, mount: string }[] } | null} diskUse
|
|
39
41
|
*/
|
|
40
42
|
|
|
41
43
|
/**
|
|
@@ -82,6 +84,43 @@ function windowsLabel(release) {
|
|
|
82
84
|
return "Windows";
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Sync snapshot from `os` only — first paint must not wait on PowerShell / nvidia-smi.
|
|
89
|
+
* @returns {Snapshot}
|
|
90
|
+
*/
|
|
91
|
+
export function instantSnapshot() {
|
|
92
|
+
const logical = Math.max(os.cpus().length, 1);
|
|
93
|
+
const total = os.totalmem();
|
|
94
|
+
const free = os.freemem();
|
|
95
|
+
const used = Math.max(0, total - free);
|
|
96
|
+
return {
|
|
97
|
+
ts: Date.now(),
|
|
98
|
+
hostname: os.hostname(),
|
|
99
|
+
osName: shortOsName(null),
|
|
100
|
+
physical: logical,
|
|
101
|
+
logical,
|
|
102
|
+
ramTotal: total,
|
|
103
|
+
cpu: null,
|
|
104
|
+
ram: total > 0 ? { percent: Math.min(100, (used / total) * 100), used, total } : null,
|
|
105
|
+
disk: { readBps: null, writeBps: null },
|
|
106
|
+
net: { txBps: null, rxBps: null },
|
|
107
|
+
gpu: null,
|
|
108
|
+
diskUse: null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function ramFromOs(ramTotal) {
|
|
113
|
+
const total = ramTotal || os.totalmem();
|
|
114
|
+
const free = os.freemem();
|
|
115
|
+
const used = Math.max(0, total - free);
|
|
116
|
+
if (total <= 0) return null;
|
|
117
|
+
return {
|
|
118
|
+
percent: Math.min(100, Math.max(0, (used / total) * 100)),
|
|
119
|
+
used,
|
|
120
|
+
total,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
85
124
|
/**
|
|
86
125
|
* GPU memory fields in systeminformation are sometimes MiB, sometimes bytes.
|
|
87
126
|
* @param {number | null} n
|
|
@@ -139,6 +178,7 @@ export function mergeLastGood(last, next) {
|
|
|
139
178
|
disk: null,
|
|
140
179
|
net: null,
|
|
141
180
|
gpu: null,
|
|
181
|
+
diskUse: null,
|
|
142
182
|
};
|
|
143
183
|
return {
|
|
144
184
|
ts: next.ts,
|
|
@@ -152,6 +192,7 @@ export function mergeLastGood(last, next) {
|
|
|
152
192
|
disk: mergePair(next.disk, base.disk, "readBps", "writeBps"),
|
|
153
193
|
net: mergePair(next.net, base.net, "txBps", "rxBps"),
|
|
154
194
|
gpu: next.gpu ?? base.gpu,
|
|
195
|
+
diskUse: next.diskUse ?? base.diskUse,
|
|
155
196
|
};
|
|
156
197
|
}
|
|
157
198
|
|
|
@@ -248,9 +289,29 @@ export function createSampler(si) {
|
|
|
248
289
|
let prevTs = null;
|
|
249
290
|
/** @type {Snapshot | null} */
|
|
250
291
|
let last = null;
|
|
251
|
-
/** @type {
|
|
252
|
-
let
|
|
253
|
-
|
|
292
|
+
/** @type {{ hostname: string, osName: string, physical: number, logical: number, ramTotal: number }} */
|
|
293
|
+
let meta = (() => {
|
|
294
|
+
const s = instantSnapshot();
|
|
295
|
+
return {
|
|
296
|
+
hostname: s.hostname,
|
|
297
|
+
osName: s.osName,
|
|
298
|
+
physical: s.physical,
|
|
299
|
+
logical: s.logical,
|
|
300
|
+
ramTotal: s.ramTotal,
|
|
301
|
+
};
|
|
302
|
+
})();
|
|
303
|
+
let staticStarted = false;
|
|
304
|
+
/** @type {GpuSample | null} */
|
|
305
|
+
let gpuCache = null;
|
|
306
|
+
let gpuStarted = false;
|
|
307
|
+
/** @type {Snapshot["diskUse"]} */
|
|
308
|
+
let diskUseCache = null;
|
|
309
|
+
let diskUseStarted = false;
|
|
310
|
+
const win32 = process.platform === "win32";
|
|
311
|
+
const winDisk = win32 ? createWindowsDiskReader() : null;
|
|
312
|
+
winDisk?.start();
|
|
313
|
+
let winReady = false;
|
|
314
|
+
prevCpu = cpuIdleTotal(os.cpus());
|
|
254
315
|
|
|
255
316
|
async function loadSi() {
|
|
256
317
|
if (si) return si;
|
|
@@ -258,43 +319,27 @@ export function createSampler(si) {
|
|
|
258
319
|
return mod.default ?? mod;
|
|
259
320
|
}
|
|
260
321
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
// keep os.cpus() counts
|
|
280
|
-
}
|
|
281
|
-
try {
|
|
282
|
-
const mem = await lib.mem();
|
|
283
|
-
if (mem?.total) ramTotal = mem.total;
|
|
284
|
-
} catch {
|
|
285
|
-
// keep os.totalmem
|
|
286
|
-
}
|
|
287
|
-
return { hostname, osName, physical, logical, ramTotal };
|
|
322
|
+
function kickStatic(lib) {
|
|
323
|
+
if (staticStarted) return;
|
|
324
|
+
staticStarted = true;
|
|
325
|
+
(async () => {
|
|
326
|
+
try {
|
|
327
|
+
const info = await lib.osInfo();
|
|
328
|
+
meta = { ...meta, osName: shortOsName(info) };
|
|
329
|
+
} catch {
|
|
330
|
+
// keep fallback
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const cpu = await lib.cpu();
|
|
334
|
+
if (cpu?.physicalCores) meta = { ...meta, physical: cpu.physicalCores };
|
|
335
|
+
if (cpu?.cores) meta = { ...meta, logical: cpu.cores };
|
|
336
|
+
} catch {
|
|
337
|
+
// keep os.cpus() counts
|
|
338
|
+
}
|
|
339
|
+
})();
|
|
288
340
|
}
|
|
289
341
|
|
|
290
|
-
async function sampleCpu(
|
|
291
|
-
try {
|
|
292
|
-
const load = await lib.currentLoad();
|
|
293
|
-
const pct = num(load?.currentLoad);
|
|
294
|
-
if (pct != null) return { percent: Math.min(100, Math.max(0, pct)) };
|
|
295
|
-
} catch {
|
|
296
|
-
// fall through to os.cpus() idle-delta
|
|
297
|
-
}
|
|
342
|
+
async function sampleCpu() {
|
|
298
343
|
const curr = cpuIdleTotal(os.cpus());
|
|
299
344
|
const pct = cpuPercentFromDelta(prevCpu, curr);
|
|
300
345
|
prevCpu = curr;
|
|
@@ -303,6 +348,7 @@ export function createSampler(si) {
|
|
|
303
348
|
}
|
|
304
349
|
|
|
305
350
|
async function sampleRam(lib, ramTotal) {
|
|
351
|
+
if (win32) return ramFromOs(ramTotal);
|
|
306
352
|
try {
|
|
307
353
|
const mem = await lib.mem();
|
|
308
354
|
const total = num(mem?.total) ?? ramTotal;
|
|
@@ -310,22 +356,14 @@ export function createSampler(si) {
|
|
|
310
356
|
let used = null;
|
|
311
357
|
if (total != null && available != null) used = Math.max(0, total - available);
|
|
312
358
|
else used = num(mem?.used) ?? num(mem?.active);
|
|
313
|
-
if (used == null || total == null || total <= 0) return
|
|
359
|
+
if (used == null || total == null || total <= 0) return ramFromOs(ramTotal);
|
|
314
360
|
return {
|
|
315
361
|
percent: Math.min(100, Math.max(0, (used / total) * 100)),
|
|
316
362
|
used,
|
|
317
363
|
total,
|
|
318
364
|
};
|
|
319
365
|
} catch {
|
|
320
|
-
|
|
321
|
-
const free = os.freemem();
|
|
322
|
-
const used = total - free;
|
|
323
|
-
if (total <= 0) return null;
|
|
324
|
-
return {
|
|
325
|
-
percent: Math.min(100, Math.max(0, (used / total) * 100)),
|
|
326
|
-
used,
|
|
327
|
-
total,
|
|
328
|
-
};
|
|
366
|
+
return ramFromOs(ramTotal);
|
|
329
367
|
}
|
|
330
368
|
}
|
|
331
369
|
|
|
@@ -336,13 +374,13 @@ export function createSampler(si) {
|
|
|
336
374
|
async function sampleDisk(lib, ts, win) {
|
|
337
375
|
let rx = win ? num(win.rx) : null;
|
|
338
376
|
let wx = win ? num(win.wx) : null;
|
|
339
|
-
if (rx == null && wx == null) {
|
|
377
|
+
if (rx == null && wx == null && !win32) {
|
|
340
378
|
try {
|
|
341
379
|
const fs = await lib.fsStats();
|
|
342
380
|
rx = num(fs?.rx);
|
|
343
381
|
wx = num(fs?.wx);
|
|
344
382
|
} catch {
|
|
345
|
-
//
|
|
383
|
+
// ignore
|
|
346
384
|
}
|
|
347
385
|
}
|
|
348
386
|
if (rx == null && wx == null) return { readBps: null, writeBps: null };
|
|
@@ -359,7 +397,7 @@ export function createSampler(si) {
|
|
|
359
397
|
async function sampleNet(lib, ts, win) {
|
|
360
398
|
let rx = win ? num(win.netRx) : null;
|
|
361
399
|
let tx = win ? num(win.netTx) : null;
|
|
362
|
-
if (rx == null && tx == null) {
|
|
400
|
+
if (rx == null && tx == null && !win32) {
|
|
363
401
|
try {
|
|
364
402
|
const stats = await lib.networkStats("*");
|
|
365
403
|
const summed = sumNetBytes(Array.isArray(stats) ? stats : stats ? [stats] : []);
|
|
@@ -375,13 +413,36 @@ export function createSampler(si) {
|
|
|
375
413
|
return { txBps, rxBps };
|
|
376
414
|
}
|
|
377
415
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
416
|
+
function kickGpu(lib) {
|
|
417
|
+
if (gpuStarted) return;
|
|
418
|
+
gpuStarted = true;
|
|
419
|
+
lib
|
|
420
|
+
.graphics()
|
|
421
|
+
.then((g) => {
|
|
422
|
+
gpuCache = pickGpu(g?.controllers || []);
|
|
423
|
+
})
|
|
424
|
+
.catch(() => {
|
|
425
|
+
gpuCache = null;
|
|
426
|
+
})
|
|
427
|
+
.finally(() => {
|
|
428
|
+
gpuStarted = false;
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function kickDiskUse(lib) {
|
|
433
|
+
if (diskUseStarted) return;
|
|
434
|
+
diskUseStarted = true;
|
|
435
|
+
lib
|
|
436
|
+
.fsSize()
|
|
437
|
+
.then((rows) => {
|
|
438
|
+
diskUseCache = summarizeLocalDisks(rows);
|
|
439
|
+
})
|
|
440
|
+
.catch(() => {})
|
|
441
|
+
.finally(() => {
|
|
442
|
+
setTimeout(() => {
|
|
443
|
+
diskUseStarted = false;
|
|
444
|
+
}, 15000);
|
|
445
|
+
});
|
|
385
446
|
}
|
|
386
447
|
|
|
387
448
|
return {
|
|
@@ -390,16 +451,17 @@ export function createSampler(si) {
|
|
|
390
451
|
*/
|
|
391
452
|
async sample() {
|
|
392
453
|
const lib = await loadSi();
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
454
|
+
kickStatic(lib);
|
|
455
|
+
kickGpu(lib);
|
|
456
|
+
kickDiskUse(lib);
|
|
457
|
+
const win = winDisk ? await winDisk.read(winReady ? 400 : 50).catch(() => null) : null;
|
|
458
|
+
if (win) winReady = true;
|
|
396
459
|
const ts = Date.now();
|
|
397
|
-
const [cpu, ram, disk, net
|
|
398
|
-
sampleCpu(
|
|
460
|
+
const [cpu, ram, disk, net] = await Promise.all([
|
|
461
|
+
sampleCpu().catch(() => last?.cpu ?? null),
|
|
399
462
|
sampleRam(lib, meta.ramTotal).catch(() => last?.ram ?? null),
|
|
400
463
|
sampleDisk(lib, ts, win).catch(() => last?.disk ?? { readBps: null, writeBps: null }),
|
|
401
464
|
sampleNet(lib, ts, win).catch(() => last?.net ?? { txBps: null, rxBps: null }),
|
|
402
|
-
sampleGpu(lib).catch(() => last?.gpu ?? null),
|
|
403
465
|
]);
|
|
404
466
|
prevTs = ts;
|
|
405
467
|
const snap = mergeLastGood(last, {
|
|
@@ -409,7 +471,8 @@ export function createSampler(si) {
|
|
|
409
471
|
ram,
|
|
410
472
|
disk,
|
|
411
473
|
net,
|
|
412
|
-
gpu,
|
|
474
|
+
gpu: gpuCache,
|
|
475
|
+
diskUse: diskUseCache,
|
|
413
476
|
});
|
|
414
477
|
last = snap;
|
|
415
478
|
return snap;
|