metwatch 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 +6 -6
- package/index.ts +63 -65
- package/package.json +5 -5
- package/src/core/process-manager.ts +10 -6
- package/src/core/runtime-manager.ts +16 -6
- package/src/services/network.service.ts +37 -23
- package/src/services/process.service.ts +41 -5
- package/src/ui/App.tsx +165 -0
- package/src/ui/screen.ts +10 -39
- package/src/ui/widgets/cpu.widget.tsx +71 -0
- package/src/ui/widgets/disk.widget.tsx +85 -0
- package/src/ui/widgets/logs.widget.tsx +126 -0
- package/src/ui/widgets/memory.widget.tsx +60 -0
- package/src/ui/widgets/network.widget.tsx +125 -0
- package/src/ui/widgets/process-table.widget.tsx +262 -0
- package/src/ui/widgets/runtime.widget.tsx +82 -0
- package/src/ui/widgets/sparkline.tsx +107 -0
- package/src/utils/formatters.ts +13 -9
- package/src/types/blessed.d.ts +0 -332
- package/src/ui/layout.ts +0 -318
- package/src/ui/widgets/cpu.widget.ts +0 -98
- package/src/ui/widgets/disk.widget.ts +0 -134
- package/src/ui/widgets/logs.widget.ts +0 -168
- package/src/ui/widgets/memory.widget.ts +0 -94
- package/src/ui/widgets/network.widget.ts +0 -185
- package/src/ui/widgets/process-table.widget.ts +0 -293
- package/src/ui/widgets/runtime.widget.ts +0 -119
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
- **CPU panel** — overall usage percentage + per-core bars, color-coded by load
|
|
18
18
|
- **Memory panel** — RAM used / free / cached / total with visual gauge
|
|
19
19
|
- **Disk panel** — per-mount usage bars with read/write IO rates
|
|
20
|
-
- **Network panel** — realtime RX/TX throughput
|
|
21
|
-
- **Process table** — all system processes sorted by CPU or memory; dual-mode (All / Watched)
|
|
20
|
+
- **Network panel** — realtime RX/TX throughput sparkline (last 60 s) + per-interface stats
|
|
21
|
+
- **Process table** — all system processes sorted by CPU or memory; dual-mode (All / Watched); viewport-capped (no terminal scroll)
|
|
22
22
|
- **Managed processes** — launch scripts with `mw start`, get auto-restart with exponential back-off
|
|
23
23
|
- **Runtime metrics** — Node.js / Bun heap, RSS, event-loop lag and GC stats via Chrome DevTools Protocol
|
|
24
24
|
- **Log streaming** — stdout/stderr of managed processes buffered and displayed in the Logs panel
|
|
@@ -252,7 +252,7 @@ MetWatch reads `metwatch.config.json` from the current working directory at star
|
|
|
252
252
|
| **CPU** | always on | Overall CPU% + per-core bars colored by load |
|
|
253
253
|
| **Memory** | always on | RAM used / free / cached / total with gauge |
|
|
254
254
|
| **Disk** | `d` | Per-mount usage bars + read/write IO rates (MB/s) |
|
|
255
|
-
| **Network** | `n` | Per-interface RX/TX rates +
|
|
255
|
+
| **Network** | `n` | Per-interface RX/TX rates + 60-second throughput sparkline |
|
|
256
256
|
| **Runtime** | `R` | Heap, RSS, event-loop lag, GC stats for managed processes |
|
|
257
257
|
| **Processes** | `p` | Scrollable process table — All or Watched mode |
|
|
258
258
|
| **Logs** | `l` (focus) | Buffered stdout/stderr of managed processes |
|
|
@@ -311,9 +311,9 @@ Contributions are welcome! Here is how to get started:
|
|
|
311
311
|
### Code conventions (summary)
|
|
312
312
|
|
|
313
313
|
- **TypeScript strict mode** — no `any`, no `@ts-ignore` without a comment
|
|
314
|
-
- **Factory functions** over classes — use `create*()` returning plain objects
|
|
315
|
-
- **
|
|
316
|
-
- **
|
|
314
|
+
- **Factory functions** over classes — managers use `create*()` returning plain objects
|
|
315
|
+
- **React components** — widgets are `.tsx` files using `useState`/`useEffect` hooks; `useInput` for keyboard handling
|
|
316
|
+
- **Event bus** — every `bus.on()` must store and call its unsubscribe in the `useEffect` cleanup
|
|
317
317
|
- **Layer rules** — widgets never import from `services/` directly; all data via bus + state
|
|
318
318
|
- **No `.then()` chains** — use `async/await` everywhere
|
|
319
319
|
|
package/index.ts
CHANGED
|
@@ -3,40 +3,38 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Initialization order:
|
|
5
5
|
// 1. Load config
|
|
6
|
-
// 2. Create
|
|
7
|
-
// 3.
|
|
8
|
-
// 4.
|
|
9
|
-
// 5.
|
|
10
|
-
// 6.
|
|
11
|
-
// 7.
|
|
12
|
-
// 8. Register global keybindings + signal handlers
|
|
13
|
-
// 9. First screen render
|
|
6
|
+
// 2. Create launcher + log-manager + runtime-manager (if managed procs)
|
|
7
|
+
// 3. Wire launcher → runtime-manager (register inspector on start)
|
|
8
|
+
// 4. Start metrics + process managers
|
|
9
|
+
// 5. Launcher starts all managed processes
|
|
10
|
+
// 6. Render ink/React app
|
|
11
|
+
// 7. Register signal handlers
|
|
14
12
|
//
|
|
15
13
|
// Teardown (quit):
|
|
16
14
|
// 1. Stop polling managers + runtime-manager
|
|
17
15
|
// 2. Stop all managed processes
|
|
18
|
-
// 3.
|
|
19
|
-
// 4.
|
|
20
|
-
// 5. process.exit(0)
|
|
16
|
+
// 3. Unmount ink app
|
|
17
|
+
// 4. process.exit(0)
|
|
21
18
|
// ---------------------------------------------------------------------------
|
|
22
19
|
|
|
23
20
|
import { readFileSync, existsSync } from 'fs';
|
|
24
21
|
import { resolve } from 'path';
|
|
25
|
-
|
|
26
|
-
import
|
|
27
|
-
|
|
28
|
-
import { createMetricsManager }
|
|
29
|
-
import { createProcessManager }
|
|
30
|
-
import { createLauncher }
|
|
31
|
-
import { createLogManager }
|
|
32
|
-
import { createRuntimeManager }
|
|
33
|
-
import { bus }
|
|
22
|
+
import { render } from 'ink';
|
|
23
|
+
import React from 'react';
|
|
24
|
+
|
|
25
|
+
import { createMetricsManager } from './src/core/metrics-manager.ts';
|
|
26
|
+
import { createProcessManager } from './src/core/process-manager.ts';
|
|
27
|
+
import { createLauncher } from './src/core/launcher.ts';
|
|
28
|
+
import { createLogManager } from './src/core/log-manager.ts';
|
|
29
|
+
import { createRuntimeManager } from './src/core/runtime-manager.ts';
|
|
30
|
+
import { bus } from './src/core/event-bus.ts';
|
|
34
31
|
import {
|
|
35
32
|
DEFAULT_CONFIG,
|
|
36
33
|
type MetWatchConfig,
|
|
37
34
|
type ResolvedConfig,
|
|
38
35
|
} from './src/types/config.types.ts';
|
|
39
|
-
import type { ManagedProcessDef }
|
|
36
|
+
import type { ManagedProcessDef } from './src/types/managed-process.types.ts';
|
|
37
|
+
import { App } from './src/ui/App.tsx';
|
|
40
38
|
|
|
41
39
|
// ── Config loading ──────────────────────────────────────────────────────────
|
|
42
40
|
|
|
@@ -84,53 +82,60 @@ export async function main(extraDefs: ManagedProcessDef[] = []): Promise<void> {
|
|
|
84
82
|
const logManager = hasManaged ? createLogManager(config.logScrollback ?? 500) : null;
|
|
85
83
|
const runtimeManager = hasManaged ? createRuntimeManager(config.refreshInterval) : null;
|
|
86
84
|
|
|
87
|
-
// Wire launcher events to runtime-manager
|
|
88
|
-
// automatically whenever a managed process starts or stops.
|
|
89
|
-
const unsubStarted = hasManaged ? bus.on('managed:started', ({ id, pid }) => {
|
|
90
|
-
// The launcher appends --inspect=0; the WS URL is reported on stderr.
|
|
91
|
-
// We receive it via the log:line event below.
|
|
92
|
-
void id; void pid;
|
|
93
|
-
}) : () => undefined;
|
|
94
|
-
|
|
95
|
-
// Parse inspector URL from log lines (Node/Bun print it to stderr on start)
|
|
85
|
+
// Wire launcher events to runtime-manager
|
|
96
86
|
const inspectorUrls = new Map<string, string>();
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
106
|
-
|
|
87
|
+
const managedCount = mergedDefs.length;
|
|
88
|
+
let unsubLogLine: (() => void) | null = null;
|
|
89
|
+
|
|
90
|
+
if (hasManaged) {
|
|
91
|
+
unsubLogLine = bus.on('log:line', ({ id, stream, line }) => {
|
|
92
|
+
if (stream !== 'stderr') return;
|
|
93
|
+
if (inspectorUrls.has(id)) return;
|
|
94
|
+
const m = line.match(/Debugger listening on (ws:\/\/[^\s]+)/);
|
|
95
|
+
if (m && m[1]) {
|
|
96
|
+
inspectorUrls.set(id, m[1]);
|
|
97
|
+
const proc = launcher?.get(id);
|
|
98
|
+
if (proc?.pid) {
|
|
99
|
+
runtimeManager?.register(id, proc.pid, m[1]);
|
|
100
|
+
}
|
|
101
|
+
if (inspectorUrls.size >= managedCount && unsubLogLine) {
|
|
102
|
+
unsubLogLine();
|
|
103
|
+
unsubLogLine = null;
|
|
104
|
+
}
|
|
107
105
|
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
110
108
|
|
|
111
|
-
const unsubStopped
|
|
109
|
+
const unsubStopped = hasManaged ? bus.on('managed:stopped', ({ id }) => {
|
|
112
110
|
runtimeManager?.unregister(id);
|
|
113
111
|
inspectorUrls.delete(id);
|
|
114
112
|
}) : () => undefined;
|
|
115
113
|
|
|
116
|
-
const unsubCrashed
|
|
114
|
+
const unsubCrashed = hasManaged ? bus.on('managed:crashed', ({ id }) => {
|
|
117
115
|
runtimeManager?.unregister(id);
|
|
118
116
|
inspectorUrls.delete(id);
|
|
119
117
|
}) : () => undefined;
|
|
120
118
|
|
|
121
|
-
const screen = createScreen();
|
|
122
|
-
const layout = buildLayout({ screen, config, launcher, logManager });
|
|
123
|
-
|
|
124
119
|
const metricsManager = createMetricsManager({ intervalMs: config.refreshInterval });
|
|
125
120
|
const processManager = createProcessManager(config);
|
|
126
121
|
|
|
127
122
|
metricsManager.start();
|
|
128
123
|
processManager.start();
|
|
129
124
|
|
|
130
|
-
// Start managed processes after widgets are ready
|
|
131
125
|
launcher?.startAll();
|
|
132
126
|
|
|
133
|
-
// ──
|
|
127
|
+
// ── Error handling ────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
const unsubError = bus.on('app:error', ({ source, error }) => {
|
|
130
|
+
process.stderr.write(`[MetWatch] error [${source}]: ${error.message}\n`);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
process.on('unhandledRejection', (reason) => {
|
|
134
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
135
|
+
process.stderr.write(`[MetWatch] unhandledRejection: ${msg}\n`);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ── Teardown ──────────────────────────────────────────────────────────────
|
|
134
139
|
|
|
135
140
|
function quit(): void {
|
|
136
141
|
bus.emit('ui:quit', undefined);
|
|
@@ -139,33 +144,26 @@ export async function main(extraDefs: ManagedProcessDef[] = []): Promise<void> {
|
|
|
139
144
|
runtimeManager?.stop();
|
|
140
145
|
launcher?.stopAll();
|
|
141
146
|
logManager?.destroy();
|
|
142
|
-
|
|
143
|
-
unsubLogLine();
|
|
147
|
+
unsubLogLine?.();
|
|
144
148
|
unsubStopped();
|
|
145
149
|
unsubCrashed();
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
unsubError();
|
|
151
|
+
unmount();
|
|
148
152
|
process.exit(0);
|
|
149
153
|
}
|
|
150
154
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// ── Error handling ────────────────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
bus.on('app:error', ({ source, error }) => {
|
|
156
|
-
void source;
|
|
157
|
-
void error;
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// ── First render ──────────────────────────────────────────────────────────
|
|
155
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
161
156
|
|
|
162
|
-
|
|
157
|
+
const { unmount } = render(
|
|
158
|
+
React.createElement(App, { config, launcher, logManager, onQuit: quit }),
|
|
159
|
+
{ exitOnCtrlC: false }
|
|
160
|
+
);
|
|
163
161
|
|
|
164
162
|
// ── Signal handling ───────────────────────────────────────────────────────
|
|
165
163
|
|
|
166
164
|
process.on('SIGTERM', quit);
|
|
167
165
|
process.on('uncaughtException', (err) => {
|
|
168
|
-
|
|
166
|
+
unmount();
|
|
169
167
|
console.error('[MetWatch] Uncaught exception:', err);
|
|
170
168
|
process.exit(1);
|
|
171
169
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metwatch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Terminal process monitoring & management tool — like htop + PM2 in your terminal",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -35,16 +35,16 @@
|
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/bun": "latest",
|
|
38
|
-
"@types/node": "^25.8.0"
|
|
38
|
+
"@types/node": "^25.8.0",
|
|
39
|
+
"@types/react": "^18.3.23"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
42
|
"typescript": "^5"
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
44
|
-
"
|
|
45
|
-
"blessed-contrib": "^4.11.0",
|
|
46
|
-
"chalk": "^5.6.2",
|
|
45
|
+
"ink": "^5.1.0",
|
|
47
46
|
"pidusage": "^4.0.1",
|
|
47
|
+
"react": "^18.3.1",
|
|
48
48
|
"systeminformation": "^5.31.6"
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -22,6 +22,7 @@ interface ProcessManagerHandle {
|
|
|
22
22
|
|
|
23
23
|
export function createProcessManager(config: ResolvedConfig): ProcessManagerHandle {
|
|
24
24
|
let timer: ReturnType<typeof setInterval> | null = null;
|
|
25
|
+
let startTimer: ReturnType<typeof setTimeout> | null = null;
|
|
25
26
|
let running = false;
|
|
26
27
|
|
|
27
28
|
// Listen for kill requests from the UI
|
|
@@ -61,18 +62,21 @@ export function createProcessManager(config: ResolvedConfig): ProcessManagerHand
|
|
|
61
62
|
function start(): void {
|
|
62
63
|
if (running) return;
|
|
63
64
|
running = true;
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
// Stagger 200ms behind metrics-manager to avoid simultaneous event-loop saturation.
|
|
66
|
+
// Use 2× the metrics interval so process polls never coincide with metrics polls.
|
|
67
|
+
startTimer = setTimeout(() => {
|
|
68
|
+
startTimer = null;
|
|
69
|
+
void tick();
|
|
70
|
+
timer = setInterval(() => void tick(), config.refreshInterval * 2);
|
|
71
|
+
}, 200);
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
function stop(): void {
|
|
69
75
|
if (!running) return;
|
|
70
76
|
running = false;
|
|
71
77
|
unsubKill();
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
timer = null;
|
|
75
|
-
}
|
|
78
|
+
if (startTimer !== null) { clearTimeout(startTimer); startTimer = null; }
|
|
79
|
+
if (timer !== null) { clearInterval(timer); timer = null; }
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
return { start, stop };
|
|
@@ -119,13 +119,21 @@ function cdpCall<T>(
|
|
|
119
119
|
): Promise<T> {
|
|
120
120
|
return new Promise((resolve, reject) => {
|
|
121
121
|
const id = session.id++;
|
|
122
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
122
123
|
session.pending.set(id, {
|
|
123
|
-
resolve:
|
|
124
|
-
|
|
124
|
+
resolve: (v) => {
|
|
125
|
+
if (timeoutHandle !== null) { clearTimeout(timeoutHandle); timeoutHandle = null; }
|
|
126
|
+
(resolve as (v: unknown) => void)(v);
|
|
127
|
+
},
|
|
128
|
+
reject: (e) => {
|
|
129
|
+
if (timeoutHandle !== null) { clearTimeout(timeoutHandle); timeoutHandle = null; }
|
|
130
|
+
reject(e);
|
|
131
|
+
},
|
|
125
132
|
});
|
|
126
133
|
session.ws.send(JSON.stringify({ id, method, params }));
|
|
127
134
|
// Timeout individual calls to avoid hanging
|
|
128
|
-
setTimeout(() => {
|
|
135
|
+
timeoutHandle = setTimeout(() => {
|
|
136
|
+
timeoutHandle = null;
|
|
129
137
|
if (session.pending.has(id)) {
|
|
130
138
|
session.pending.delete(id);
|
|
131
139
|
reject(new Error(`CDP timeout: ${method}`));
|
|
@@ -228,6 +236,7 @@ async function pollProcess(
|
|
|
228
236
|
export function createRuntimeManager(intervalMs: number): RuntimeManagerHandle {
|
|
229
237
|
const sessions = new Map<string, ProcessSession>();
|
|
230
238
|
let timer: ReturnType<typeof setInterval> | null = null;
|
|
239
|
+
let warmupTimer: ReturnType<typeof setTimeout> | null = null;
|
|
231
240
|
|
|
232
241
|
async function tick(): Promise<void> {
|
|
233
242
|
for (const ps of sessions.values()) {
|
|
@@ -241,9 +250,9 @@ export function createRuntimeManager(intervalMs: number): RuntimeManagerHandle {
|
|
|
241
250
|
}
|
|
242
251
|
|
|
243
252
|
function startTimer(): void {
|
|
244
|
-
if (timer !== null) return;
|
|
245
|
-
|
|
246
|
-
|
|
253
|
+
if (timer !== null || warmupTimer !== null) return;
|
|
254
|
+
warmupTimer = setTimeout(() => {
|
|
255
|
+
warmupTimer = null;
|
|
247
256
|
void tick();
|
|
248
257
|
timer = setInterval(() => void tick(), intervalMs);
|
|
249
258
|
}, 1500);
|
|
@@ -281,6 +290,7 @@ export function createRuntimeManager(intervalMs: number): RuntimeManagerHandle {
|
|
|
281
290
|
}
|
|
282
291
|
|
|
283
292
|
function stop(): void {
|
|
293
|
+
if (warmupTimer !== null) { clearTimeout(warmupTimer); warmupTimer = null; }
|
|
284
294
|
if (timer !== null) { clearInterval(timer); timer = null; }
|
|
285
295
|
for (const ps of sessions.values()) {
|
|
286
296
|
try { ps.session?.ws.close(); } catch { /* ignore */ }
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Wraps systeminformation network APIs to produce normalized NetworkMetrics.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
// si.networkInterfaces()
|
|
8
|
-
//
|
|
6
|
+
// Per-tick call: si.networkStats() — cumulative byte/packet counters (delta-based)
|
|
7
|
+
// Cached call: si.networkInterfaces() — static info refreshed every 30 s or on
|
|
8
|
+
// the first call. IP addresses and operstate almost never change.
|
|
9
9
|
//
|
|
10
10
|
// Rates are calculated as deltas between successive calls — same approach as
|
|
11
11
|
// disk.service.ts. First call returns zero rates.
|
|
@@ -30,6 +30,28 @@ interface IfaceSnapshot {
|
|
|
30
30
|
|
|
31
31
|
const _prev = new Map<string, IfaceSnapshot>();
|
|
32
32
|
|
|
33
|
+
// ── Static interface cache ────────────────────────────────────────────────────
|
|
34
|
+
// networkInterfaces() enumerates all NICs including virtual ones — expensive.
|
|
35
|
+
// Cache for 30 s; refresh on expiry or when a previously-unknown iface appears.
|
|
36
|
+
|
|
37
|
+
const IFACE_CACHE_TTL = 30_000;
|
|
38
|
+
let _ifaceCache: Map<string, { ip4: string; ip6: string; operstate: string }> = new Map();
|
|
39
|
+
let _ifaceCacheTs = 0;
|
|
40
|
+
|
|
41
|
+
async function getStaticIfaceMap(): Promise<Map<string, { ip4: string; ip6: string; operstate: string }>> {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
if (now - _ifaceCacheTs < IFACE_CACHE_TTL) return _ifaceCache;
|
|
44
|
+
|
|
45
|
+
const raw = await si.networkInterfaces();
|
|
46
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
47
|
+
_ifaceCache = new Map(arr.map(i => [
|
|
48
|
+
i.iface,
|
|
49
|
+
{ ip4: i.ip4 ?? '', ip6: i.ip6 ?? '', operstate: i.operstate ?? '' },
|
|
50
|
+
]));
|
|
51
|
+
_ifaceCacheTs = now;
|
|
52
|
+
return _ifaceCache;
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
34
56
|
|
|
35
57
|
function operstate(s: string | undefined): 'up' | 'down' | 'unknown' {
|
|
@@ -38,20 +60,20 @@ function operstate(s: string | undefined): 'up' | 'down' | 'unknown' {
|
|
|
38
60
|
return 'unknown';
|
|
39
61
|
}
|
|
40
62
|
|
|
63
|
+
// ── Collator for sorting (avoids constructing one inside the comparator) ──────
|
|
64
|
+
const _collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
|
65
|
+
|
|
41
66
|
// ── Fetch ─────────────────────────────────────────────────────────────────────
|
|
42
67
|
|
|
43
68
|
export async function fetchNetworkMetrics(): Promise<NetworkMetrics> {
|
|
44
69
|
const now = Date.now();
|
|
45
70
|
|
|
46
|
-
|
|
47
|
-
|
|
71
|
+
// Stats every tick; static info from cache (refreshed every 30 s)
|
|
72
|
+
const [stats, staticInfo] = await Promise.all([
|
|
48
73
|
si.networkStats('*'),
|
|
74
|
+
getStaticIfaceMap(),
|
|
49
75
|
]);
|
|
50
76
|
|
|
51
|
-
// Build a map of static info keyed by iface name
|
|
52
|
-
const ifaceArray = Array.isArray(ifaces) ? ifaces : [ifaces];
|
|
53
|
-
const staticInfo = new Map(ifaceArray.map(i => [i.iface, i]));
|
|
54
|
-
|
|
55
77
|
const statsArray = Array.isArray(stats) ? stats : [stats];
|
|
56
78
|
|
|
57
79
|
const interfaces: NetworkInterface[] = [];
|
|
@@ -65,18 +87,10 @@ export async function fetchNetworkMetrics(): Promise<NetworkMetrics> {
|
|
|
65
87
|
const prev = _prev.get(iface);
|
|
66
88
|
const dtSec = prev ? Math.max(0.001, (now - prev.ts) / 1000) : 1;
|
|
67
89
|
|
|
68
|
-
const rxBPS = prev
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
? Math.max(0, ((s.tx_bytes ?? 0) - prev.tx_bytes) / dtSec)
|
|
73
|
-
: 0;
|
|
74
|
-
const rxPPS = prev
|
|
75
|
-
? Math.max(0, ((s.rx_sec ?? 0) - prev.rx_sec) / dtSec)
|
|
76
|
-
: 0;
|
|
77
|
-
const txPPS = prev
|
|
78
|
-
? Math.max(0, ((s.tx_sec ?? 0) - prev.tx_sec) / dtSec)
|
|
79
|
-
: 0;
|
|
90
|
+
const rxBPS = prev ? Math.max(0, ((s.rx_bytes ?? 0) - prev.rx_bytes) / dtSec) : 0;
|
|
91
|
+
const txBPS = prev ? Math.max(0, ((s.tx_bytes ?? 0) - prev.tx_bytes) / dtSec) : 0;
|
|
92
|
+
const rxPPS = prev ? Math.max(0, ((s.rx_sec ?? 0) - prev.rx_sec) / dtSec) : 0;
|
|
93
|
+
const txPPS = prev ? Math.max(0, ((s.tx_sec ?? 0) - prev.tx_sec) / dtSec) : 0;
|
|
80
94
|
|
|
81
95
|
_prev.set(iface, {
|
|
82
96
|
rx_bytes: s.rx_bytes ?? 0,
|
|
@@ -114,12 +128,12 @@ export async function fetchNetworkMetrics(): Promise<NetworkMetrics> {
|
|
|
114
128
|
});
|
|
115
129
|
}
|
|
116
130
|
|
|
117
|
-
// Sort: up interfaces first, then by name
|
|
131
|
+
// Sort: up interfaces first, then by name using cached collator
|
|
118
132
|
interfaces.sort((a, b) => {
|
|
119
133
|
if (a.operstate !== b.operstate) {
|
|
120
134
|
return a.operstate === 'up' ? -1 : 1;
|
|
121
135
|
}
|
|
122
|
-
return a.iface
|
|
136
|
+
return _collator.compare(a.iface, b.iface);
|
|
123
137
|
});
|
|
124
138
|
|
|
125
139
|
return {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import si from 'systeminformation';
|
|
10
10
|
import type { ProcessInfo, ProcessList, ProcessStatus } from '../types/process.types.ts';
|
|
11
|
+
import { getMemoryMetrics } from '../core/state-manager.ts';
|
|
11
12
|
|
|
12
13
|
function mapStatus(raw: string): ProcessStatus {
|
|
13
14
|
switch (raw.toLowerCase()) {
|
|
@@ -20,14 +21,46 @@ function mapStatus(raw: string): ProcessStatus {
|
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Partial selection sort — O(n * limit) but avoids copying the entire list
|
|
26
|
+
* and sorting it when limit << n. For limit=50, n=1000 → 50k comparisons vs
|
|
27
|
+
* 10k for a full sort but saves the O(n) spread allocation.
|
|
28
|
+
* For larger limits a min-heap approach would be O(n log limit); given our
|
|
29
|
+
* default maxProcesses=50 the simple selection is fast enough and GC-friendly.
|
|
30
|
+
*/
|
|
31
|
+
function topKByCpu<T extends { cpu: number }>(list: T[], k: number): T[] {
|
|
32
|
+
const n = list.length;
|
|
33
|
+
const count = Math.min(k, n);
|
|
34
|
+
// Work on indices to avoid object allocations
|
|
35
|
+
const result: T[] = [];
|
|
36
|
+
|
|
37
|
+
// Track which indices have already been picked
|
|
38
|
+
const used = new Uint8Array(n);
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < count; i++) {
|
|
41
|
+
let bestIdx = -1;
|
|
42
|
+
let bestCpu = -1;
|
|
43
|
+
for (let j = 0; j < n; j++) {
|
|
44
|
+
if (!used[j] && list[j]!.cpu > bestCpu) {
|
|
45
|
+
bestCpu = list[j]!.cpu;
|
|
46
|
+
bestIdx = j;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (bestIdx === -1) break;
|
|
50
|
+
used[bestIdx] = 1;
|
|
51
|
+
result.push(list[bestIdx]!);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
23
57
|
export async function fetchProcessList(limit = 100): Promise<ProcessList> {
|
|
24
58
|
const { list } = await si.processes();
|
|
25
59
|
|
|
26
|
-
//
|
|
27
|
-
const
|
|
28
|
-
const top = sorted.slice(0, limit);
|
|
60
|
+
// Top-K by CPU — avoids full O(n log n) sort + O(n) spread for large process lists.
|
|
61
|
+
const top = topKByCpu(list, limit);
|
|
29
62
|
|
|
30
|
-
const totalMem = (await si.mem()).total;
|
|
63
|
+
const totalMem = getMemoryMetrics()?.total ?? (await si.mem()).total;
|
|
31
64
|
|
|
32
65
|
return top.map((p): ProcessInfo => ({
|
|
33
66
|
pid: p.pid,
|
|
@@ -39,7 +72,10 @@ export async function fetchProcessList(limit = 100): Promise<ProcessList> {
|
|
|
39
72
|
? Math.round(((p.memRss ?? 0) / totalMem) * 1000) / 10
|
|
40
73
|
: 0,
|
|
41
74
|
status: mapStatus(p.state ?? ''),
|
|
42
|
-
|
|
75
|
+
// Avoid Date construction: use numeric check first, then Date.parse as fallback.
|
|
76
|
+
startedAt: p.started
|
|
77
|
+
? (typeof p.started === 'number' ? p.started : Date.parse(p.started as string) || null)
|
|
78
|
+
: null,
|
|
43
79
|
ppid: p.parentPid ?? null,
|
|
44
80
|
// ── Extended (Phase 2) ──────────────────────────────────────────────────
|
|
45
81
|
user: (p as unknown as { user?: string }).user ?? '',
|