systemlens 1.0.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/.env.example +5 -0
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/bin/systemlens.js +127 -0
- package/package.json +73 -0
- package/server.js +106 -0
- package/src/analyzers/cpu.analyzer.js +124 -0
- package/src/analyzers/memory.analyzer.js +107 -0
- package/src/analyzers/process.analyzer.js +136 -0
- package/src/analyzers/spike.detector.js +135 -0
- package/src/classifiers/process.classifier.js +127 -0
- package/src/collectors/cpu.collector.js +48 -0
- package/src/collectors/disk.collector.js +41 -0
- package/src/collectors/memory.collector.js +41 -0
- package/src/collectors/process.collector.js +65 -0
- package/src/engines/ai.engine.js +105 -0
- package/src/engines/explanation.engine.js +348 -0
- package/src/engines/suggestion.engine.js +242 -0
- package/src/history/history.tracker.js +114 -0
- package/src/index.js +130 -0
- package/src/monitor/realtime.monitor.js +145 -0
- package/src/renderer/cli.renderer.js +318 -0
- package/src/utils/constants.js +80 -0
- package/src/utils/helpers.js +110 -0
- package/web/app.js +352 -0
- package/web/index.html +209 -0
- package/web/style.css +886 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// ─── Memory Analyzer ─────────────────────────────────────────────
|
|
2
|
+
import { THRESHOLDS, SEVERITY } from '../utils/constants.js';
|
|
3
|
+
import { formatBytes } from '../utils/helpers.js';
|
|
4
|
+
|
|
5
|
+
export class MemoryAnalyzer {
|
|
6
|
+
/**
|
|
7
|
+
* Analyze memory data and return insights
|
|
8
|
+
* @param {Object} memData - From MemoryCollector
|
|
9
|
+
* @param {Object|null} previousData - Previous snapshot for comparison
|
|
10
|
+
* @returns {Object} Analysis results
|
|
11
|
+
*/
|
|
12
|
+
analyze(memData, previousData = null) {
|
|
13
|
+
const usedPercent = memData.ram.usedPercent;
|
|
14
|
+
const issues = [];
|
|
15
|
+
const insights = [];
|
|
16
|
+
|
|
17
|
+
// ── Severity classification ──
|
|
18
|
+
let severity;
|
|
19
|
+
if (usedPercent >= THRESHOLDS.MEMORY_HIGH) {
|
|
20
|
+
severity = SEVERITY.CRITICAL;
|
|
21
|
+
issues.push({
|
|
22
|
+
type: 'memory_high',
|
|
23
|
+
severity: 'critical',
|
|
24
|
+
message: `Memory usage is critically high at ${usedPercent}% (${memData.ram.formatted.used} of ${memData.ram.formatted.total})`,
|
|
25
|
+
value: usedPercent,
|
|
26
|
+
});
|
|
27
|
+
} else if (usedPercent >= THRESHOLDS.MEMORY_WARNING) {
|
|
28
|
+
severity = SEVERITY.WARNING;
|
|
29
|
+
issues.push({
|
|
30
|
+
type: 'memory_warning',
|
|
31
|
+
severity: 'warning',
|
|
32
|
+
message: `Memory usage is elevated at ${usedPercent}% (${memData.ram.formatted.used} of ${memData.ram.formatted.total})`,
|
|
33
|
+
value: usedPercent,
|
|
34
|
+
});
|
|
35
|
+
} else if (usedPercent >= THRESHOLDS.MEMORY_MODERATE) {
|
|
36
|
+
severity = SEVERITY.INFO;
|
|
37
|
+
insights.push({
|
|
38
|
+
type: 'memory_moderate',
|
|
39
|
+
message: `Memory usage is moderate at ${usedPercent}%`,
|
|
40
|
+
value: usedPercent,
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
severity = SEVERITY.OK;
|
|
44
|
+
insights.push({
|
|
45
|
+
type: 'memory_ok',
|
|
46
|
+
message: `Memory usage is healthy at ${usedPercent}% — plenty of free RAM available`,
|
|
47
|
+
value: usedPercent,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Available memory warning ──
|
|
52
|
+
const availableGB = memData.ram.available / (1024 * 1024 * 1024);
|
|
53
|
+
if (availableGB < 1 && usedPercent > 50) {
|
|
54
|
+
issues.push({
|
|
55
|
+
type: 'low_available_memory',
|
|
56
|
+
severity: 'warning',
|
|
57
|
+
message: `Only ${formatBytes(memData.ram.available)} of RAM is available — system may start swapping soon`,
|
|
58
|
+
value: availableGB,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Swap usage ──
|
|
63
|
+
if (memData.swap.usedPercent > 20) {
|
|
64
|
+
issues.push({
|
|
65
|
+
type: 'swap_active',
|
|
66
|
+
severity: memData.swap.usedPercent > 50 ? 'critical' : 'warning',
|
|
67
|
+
message: `Swap is being used (${memData.swap.usedPercent}% — ${memData.swap.formatted.used}). This means RAM has been exhausted and the system is using slower disk storage as memory, causing significant slowdown.`,
|
|
68
|
+
value: memData.swap.usedPercent,
|
|
69
|
+
});
|
|
70
|
+
} else if (memData.swap.usedPercent > 0 && memData.swap.total > 0) {
|
|
71
|
+
insights.push({
|
|
72
|
+
type: 'swap_minor',
|
|
73
|
+
message: `Minimal swap usage detected (${memData.swap.usedPercent}%) — generally not a concern`,
|
|
74
|
+
value: memData.swap.usedPercent,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Memory spike ──
|
|
79
|
+
if (previousData) {
|
|
80
|
+
const prevUsed = previousData.ram.usedPercent;
|
|
81
|
+
const delta = usedPercent - prevUsed;
|
|
82
|
+
|
|
83
|
+
if (delta >= THRESHOLDS.SPIKE_THRESHOLD) {
|
|
84
|
+
issues.push({
|
|
85
|
+
type: 'memory_spike',
|
|
86
|
+
severity: 'warning',
|
|
87
|
+
message: `Memory usage spiked by ${delta.toFixed(1)}% (from ${prevUsed}% to ${usedPercent}%) — something just allocated a large chunk of memory`,
|
|
88
|
+
value: delta,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
usedPercent,
|
|
95
|
+
severity,
|
|
96
|
+
issues,
|
|
97
|
+
insights,
|
|
98
|
+
details: {
|
|
99
|
+
total: memData.ram.total,
|
|
100
|
+
used: memData.ram.active,
|
|
101
|
+
available: memData.ram.available,
|
|
102
|
+
swapUsed: memData.swap.usedPercent,
|
|
103
|
+
formatted: memData.ram.formatted,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// ─── Process Analyzer ────────────────────────────────────────────
|
|
2
|
+
// Identifies responsibility and patterns in process behavior
|
|
3
|
+
import { THRESHOLDS, SEVERITY } from '../utils/constants.js';
|
|
4
|
+
|
|
5
|
+
export class ProcessAnalyzer {
|
|
6
|
+
/**
|
|
7
|
+
* Analyze processes and determine what's causing load
|
|
8
|
+
* @param {Array} classifiedProcesses - Processes with classification
|
|
9
|
+
* @param {Object} cpuAnalysis - Results from CpuAnalyzer
|
|
10
|
+
* @param {Object} memAnalysis - Results from MemoryAnalyzer
|
|
11
|
+
* @returns {Object} Process-level insights
|
|
12
|
+
*/
|
|
13
|
+
analyze(classifiedProcesses, cpuAnalysis, memAnalysis) {
|
|
14
|
+
const issues = [];
|
|
15
|
+
const insights = [];
|
|
16
|
+
const responsibilities = [];
|
|
17
|
+
|
|
18
|
+
// ── Identify CPU hogs ──
|
|
19
|
+
const cpuHogs = classifiedProcesses.filter(p => p.cpu >= THRESHOLDS.PROCESS_CPU_HIGH);
|
|
20
|
+
const cpuWarners = classifiedProcesses.filter(
|
|
21
|
+
p => p.cpu >= THRESHOLDS.PROCESS_CPU_WARNING && p.cpu < THRESHOLDS.PROCESS_CPU_HIGH
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (cpuHogs.length === 1) {
|
|
25
|
+
const hog = cpuHogs[0];
|
|
26
|
+
responsibilities.push({
|
|
27
|
+
type: 'single_cpu_dominant',
|
|
28
|
+
process: hog,
|
|
29
|
+
message: `"${hog.name}" is the primary CPU consumer at ${hog.cpu}%`,
|
|
30
|
+
severity: 'critical',
|
|
31
|
+
});
|
|
32
|
+
} else if (cpuHogs.length > 1) {
|
|
33
|
+
responsibilities.push({
|
|
34
|
+
type: 'multiple_cpu_hogs',
|
|
35
|
+
processes: cpuHogs,
|
|
36
|
+
message: `${cpuHogs.length} processes are competing for CPU: ${cpuHogs.map(p => `${p.name} (${p.cpu}%)`).join(', ')}`,
|
|
37
|
+
severity: 'critical',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Identify memory hogs ──
|
|
42
|
+
const memHogs = classifiedProcesses.filter(p => p.mem >= THRESHOLDS.PROCESS_MEM_HIGH);
|
|
43
|
+
|
|
44
|
+
if (memHogs.length > 0) {
|
|
45
|
+
for (const hog of memHogs) {
|
|
46
|
+
responsibilities.push({
|
|
47
|
+
type: 'memory_hog',
|
|
48
|
+
process: hog,
|
|
49
|
+
message: `"${hog.name}" is consuming ${hog.mem}% of memory (${hog.memRssFormatted})`,
|
|
50
|
+
severity: hog.mem >= 30 ? 'critical' : 'warning',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Detect "death by a thousand cuts" (many small processes adding up) ──
|
|
56
|
+
const smallProcs = classifiedProcesses.filter(
|
|
57
|
+
p => p.cpu >= 2 && p.cpu < THRESHOLDS.PROCESS_CPU_WARNING
|
|
58
|
+
);
|
|
59
|
+
const smallProcTotalCpu = smallProcs.reduce((sum, p) => sum + p.cpu, 0);
|
|
60
|
+
|
|
61
|
+
if (smallProcs.length >= 5 && smallProcTotalCpu > 30) {
|
|
62
|
+
issues.push({
|
|
63
|
+
type: 'distributed_load',
|
|
64
|
+
severity: 'warning',
|
|
65
|
+
message: `${smallProcs.length} background processes are collectively using ${smallProcTotalCpu.toFixed(1)}% CPU — no single culprit, but the combined load is significant`,
|
|
66
|
+
processes: smallProcs,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Browser analysis ──
|
|
71
|
+
const browserProcs = classifiedProcesses.filter(p => p.classification.isBrowser);
|
|
72
|
+
if (browserProcs.length > 3) {
|
|
73
|
+
const totalBrowserCpu = browserProcs.reduce((sum, p) => sum + p.cpu, 0);
|
|
74
|
+
const totalBrowserMem = browserProcs.reduce((sum, p) => sum + p.mem, 0);
|
|
75
|
+
// Browsers create ~2-3 OS processes per tab (renderer, GPU, extensions, etc.)
|
|
76
|
+
// So process count ≠ tab count — estimate actual tabs
|
|
77
|
+
const estimatedTabs = Math.max(1, Math.round(browserProcs.length / 2));
|
|
78
|
+
|
|
79
|
+
if (totalBrowserCpu > 20 || totalBrowserMem > 20) {
|
|
80
|
+
issues.push({
|
|
81
|
+
type: 'browser_heavy',
|
|
82
|
+
severity: totalBrowserCpu > 40 ? 'critical' : 'warning',
|
|
83
|
+
message: `Browser is using ${totalBrowserCpu.toFixed(1)}% CPU and ${totalBrowserMem.toFixed(1)}% memory across ~${estimatedTabs} tabs (${browserProcs.length} internal processes)`,
|
|
84
|
+
processes: browserProcs,
|
|
85
|
+
estimatedTabs,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Dev server detection ──
|
|
91
|
+
const devProcs = classifiedProcesses.filter(p => p.classification.isDev);
|
|
92
|
+
const highCpuDev = devProcs.filter(p => p.cpu >= THRESHOLDS.PROCESS_CPU_WARNING);
|
|
93
|
+
|
|
94
|
+
if (highCpuDev.length > 0) {
|
|
95
|
+
for (const devProc of highCpuDev) {
|
|
96
|
+
const isHotReload = (devProc.command || '').match(/--watch|hmr|hot|dev|vite|next/i);
|
|
97
|
+
const isPossibleLoop = devProc.cpu >= 80;
|
|
98
|
+
|
|
99
|
+
insights.push({
|
|
100
|
+
type: 'dev_process_high_cpu',
|
|
101
|
+
message: isPossibleLoop
|
|
102
|
+
? `🔄 "${devProc.name}" is using ${devProc.cpu}% CPU — possible infinite loop or expensive computation in your code`
|
|
103
|
+
: isHotReload
|
|
104
|
+
? `🔄 "${devProc.name}" (${devProc.cpu}% CPU) — likely running with hot reload which can intensify CPU usage during rebuilds`
|
|
105
|
+
: `🛠️ "${devProc.name}" is using ${devProc.cpu}% CPU during development`,
|
|
106
|
+
process: devProc,
|
|
107
|
+
severity: isPossibleLoop ? 'critical' : 'warning',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Determine overall responsibility ──
|
|
113
|
+
let primaryCause = 'balanced';
|
|
114
|
+
if (cpuHogs.length === 1 && cpuHogs[0].cpu > 50) {
|
|
115
|
+
primaryCause = 'single_process';
|
|
116
|
+
} else if (cpuHogs.length > 1) {
|
|
117
|
+
primaryCause = 'multiple_processes';
|
|
118
|
+
} else if (smallProcs.length >= 5 && smallProcTotalCpu > 30) {
|
|
119
|
+
primaryCause = 'distributed';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
primaryCause,
|
|
124
|
+
responsibilities,
|
|
125
|
+
issues,
|
|
126
|
+
insights,
|
|
127
|
+
summary: {
|
|
128
|
+
cpuHogCount: cpuHogs.length,
|
|
129
|
+
memHogCount: memHogs.length,
|
|
130
|
+
totalProcesses: classifiedProcesses.length,
|
|
131
|
+
browserProcessCount: browserProcs.length,
|
|
132
|
+
devProcessCount: devProcs.length,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// ─── Spike Detector ──────────────────────────────────────────────
|
|
2
|
+
// Tracks system metrics over time to detect patterns and sustained issues
|
|
3
|
+
import { THRESHOLDS } from '../utils/constants.js';
|
|
4
|
+
|
|
5
|
+
export class SpikeDetector {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.cpuHistory = [];
|
|
8
|
+
this.memHistory = [];
|
|
9
|
+
this.maxHistory = 60; // ~3 minutes at 3s intervals
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Record a new data point and detect patterns
|
|
14
|
+
* @param {Object} cpuAnalysis - Current CPU analysis results
|
|
15
|
+
* @param {Object} memAnalysis - Current Memory analysis results
|
|
16
|
+
* @returns {Object} Pattern detection results
|
|
17
|
+
*/
|
|
18
|
+
record(cpuAnalysis, memAnalysis) {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
|
|
21
|
+
this.cpuHistory.push({ value: cpuAnalysis.overall, time: now });
|
|
22
|
+
this.memHistory.push({ value: memAnalysis.usedPercent, time: now });
|
|
23
|
+
|
|
24
|
+
// Trim to max history size
|
|
25
|
+
if (this.cpuHistory.length > this.maxHistory) this.cpuHistory.shift();
|
|
26
|
+
if (this.memHistory.length > this.maxHistory) this.memHistory.shift();
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
cpu: this._analyzeHistory(this.cpuHistory, 'CPU'),
|
|
30
|
+
memory: this._analyzeHistory(this.memHistory, 'Memory'),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Analyze a metric history for patterns
|
|
36
|
+
*/
|
|
37
|
+
_analyzeHistory(history, label) {
|
|
38
|
+
if (history.length < 3) {
|
|
39
|
+
return { trend: 'insufficient_data', patterns: [] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const patterns = [];
|
|
43
|
+
const values = history.map(h => h.value);
|
|
44
|
+
const recent = values.slice(-5);
|
|
45
|
+
const older = values.slice(-15, -5);
|
|
46
|
+
|
|
47
|
+
// ── Trend detection ──
|
|
48
|
+
const recentAvg = recent.reduce((s, v) => s + v, 0) / recent.length;
|
|
49
|
+
const olderAvg = older.length > 0
|
|
50
|
+
? older.reduce((s, v) => s + v, 0) / older.length
|
|
51
|
+
: recentAvg;
|
|
52
|
+
|
|
53
|
+
let trend = 'stable';
|
|
54
|
+
if (recentAvg > olderAvg + 10) trend = 'rising';
|
|
55
|
+
else if (recentAvg < olderAvg - 10) trend = 'falling';
|
|
56
|
+
|
|
57
|
+
// ── Sustained high detection ──
|
|
58
|
+
const highThreshold = label === 'CPU' ? THRESHOLDS.CPU_HIGH : THRESHOLDS.MEMORY_HIGH;
|
|
59
|
+
const sustainedHigh = this._detectSustained(history, highThreshold);
|
|
60
|
+
|
|
61
|
+
if (sustainedHigh) {
|
|
62
|
+
patterns.push({
|
|
63
|
+
type: 'sustained_high',
|
|
64
|
+
message: `${label} has been above ${highThreshold}% for ${sustainedHigh.durationLabel}`,
|
|
65
|
+
duration: sustainedHigh.duration,
|
|
66
|
+
severity: sustainedHigh.duration > 120000 ? 'critical' : 'warning',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Oscillation detection (usage bouncing up and down) ──
|
|
71
|
+
if (values.length >= 6) {
|
|
72
|
+
const diffs = [];
|
|
73
|
+
for (let i = 1; i < values.length; i++) {
|
|
74
|
+
diffs.push(values[i] - values[i - 1]);
|
|
75
|
+
}
|
|
76
|
+
const directionChanges = diffs.filter((d, i) =>
|
|
77
|
+
i > 0 && Math.sign(d) !== Math.sign(diffs[i - 1]) && Math.abs(d) > 5
|
|
78
|
+
).length;
|
|
79
|
+
|
|
80
|
+
if (directionChanges >= 4) {
|
|
81
|
+
patterns.push({
|
|
82
|
+
type: 'oscillating',
|
|
83
|
+
message: `${label} usage is oscillating — frequently jumping up and down, possibly due to periodic tasks or unstable workloads`,
|
|
84
|
+
severity: 'info',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
trend,
|
|
91
|
+
recentAvg: parseFloat(recentAvg.toFixed(1)),
|
|
92
|
+
patterns,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Detect sustained high usage
|
|
98
|
+
*/
|
|
99
|
+
_detectSustained(history, threshold) {
|
|
100
|
+
if (history.length < 2) return null;
|
|
101
|
+
|
|
102
|
+
let startIdx = -1;
|
|
103
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
104
|
+
if (history[i].value < threshold) {
|
|
105
|
+
startIdx = i + 1;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (startIdx === -1) startIdx = 0;
|
|
111
|
+
if (startIdx >= history.length) return null;
|
|
112
|
+
|
|
113
|
+
const duration = Date.now() - history[startIdx].time;
|
|
114
|
+
if (duration < 6000) return null; // Need at least 6 seconds
|
|
115
|
+
|
|
116
|
+
const seconds = Math.round(duration / 1000);
|
|
117
|
+
let durationLabel;
|
|
118
|
+
if (seconds < 60) durationLabel = `${seconds} seconds`;
|
|
119
|
+
else if (seconds < 3600) durationLabel = `${Math.round(seconds / 60)} minutes`;
|
|
120
|
+
else durationLabel = `${(seconds / 3600).toFixed(1)} hours`;
|
|
121
|
+
|
|
122
|
+
return { duration, durationLabel };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get current pattern summary
|
|
127
|
+
*/
|
|
128
|
+
getSummary() {
|
|
129
|
+
return {
|
|
130
|
+
dataPoints: this.cpuHistory.length,
|
|
131
|
+
cpuHistory: this.cpuHistory.map(h => h.value),
|
|
132
|
+
memHistory: this.memHistory.map(h => h.value),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// ─── Process Classifier ─────────────────────────────────────────
|
|
2
|
+
// Classifies processes into meaningful categories for human understanding
|
|
3
|
+
import { PROCESS_CATEGORIES } from '../utils/constants.js';
|
|
4
|
+
|
|
5
|
+
export class ProcessClassifier {
|
|
6
|
+
constructor() {
|
|
7
|
+
// Pre-compile patterns for fast matching
|
|
8
|
+
this.compiledPatterns = {};
|
|
9
|
+
for (const [key, category] of Object.entries(PROCESS_CATEGORIES)) {
|
|
10
|
+
if (key === 'UNKNOWN') continue;
|
|
11
|
+
this.compiledPatterns[key] = {
|
|
12
|
+
...category,
|
|
13
|
+
regex: new RegExp(
|
|
14
|
+
category.patterns.map(p => `(^|[/\\\\])${p}(\\d*|[-_.]|$)`).join('|'),
|
|
15
|
+
'i'
|
|
16
|
+
),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Classify a single process
|
|
23
|
+
* @param {Object} process - Process data { name, command, path }
|
|
24
|
+
* @returns {Object} Category info
|
|
25
|
+
*/
|
|
26
|
+
classify(process) {
|
|
27
|
+
const searchStr = `${process.name} ${process.command || ''} ${process.path || ''}`.toLowerCase();
|
|
28
|
+
|
|
29
|
+
for (const [key, compiled] of Object.entries(this.compiledPatterns)) {
|
|
30
|
+
if (compiled.regex.test(searchStr)) {
|
|
31
|
+
return {
|
|
32
|
+
category: key,
|
|
33
|
+
label: compiled.label,
|
|
34
|
+
color: compiled.color,
|
|
35
|
+
isSystem: key === 'SYSTEM' || key === 'BACKGROUND',
|
|
36
|
+
isDev: key === 'DEV_TOOLS' || key === 'EDITORS',
|
|
37
|
+
isBrowser: key === 'BROWSER',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
category: 'UNKNOWN',
|
|
44
|
+
label: PROCESS_CATEGORIES.UNKNOWN.label,
|
|
45
|
+
color: PROCESS_CATEGORIES.UNKNOWN.color,
|
|
46
|
+
isSystem: false,
|
|
47
|
+
isDev: false,
|
|
48
|
+
isBrowser: false,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Classify all processes and return enriched list
|
|
54
|
+
* @param {Array} processes - Array of process objects
|
|
55
|
+
* @returns {Array} Processes with classification added
|
|
56
|
+
*/
|
|
57
|
+
classifyAll(processes) {
|
|
58
|
+
return processes.map(proc => ({
|
|
59
|
+
...proc,
|
|
60
|
+
classification: this.classify(proc),
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Group processes by category
|
|
66
|
+
* @param {Array} classifiedProcesses - Array of classified process objects
|
|
67
|
+
* @returns {Object} Processes grouped by category key
|
|
68
|
+
*/
|
|
69
|
+
groupByCategory(classifiedProcesses) {
|
|
70
|
+
const groups = {};
|
|
71
|
+
|
|
72
|
+
for (const proc of classifiedProcesses) {
|
|
73
|
+
const cat = proc.classification.category;
|
|
74
|
+
if (!groups[cat]) {
|
|
75
|
+
groups[cat] = {
|
|
76
|
+
...PROCESS_CATEGORIES[cat],
|
|
77
|
+
processes: [],
|
|
78
|
+
totalCpu: 0,
|
|
79
|
+
totalMem: 0,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
groups[cat].processes.push(proc);
|
|
83
|
+
groups[cat].totalCpu += proc.cpu || 0;
|
|
84
|
+
groups[cat].totalMem += proc.mem || 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Round totals
|
|
88
|
+
for (const group of Object.values(groups)) {
|
|
89
|
+
group.totalCpu = parseFloat(group.totalCpu.toFixed(1));
|
|
90
|
+
group.totalMem = parseFloat(group.totalMem.toFixed(1));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return groups;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Detect if there are development-related processes running
|
|
98
|
+
* @param {Array} classifiedProcesses
|
|
99
|
+
* @returns {Object} Dev environment summary
|
|
100
|
+
*/
|
|
101
|
+
detectDevEnvironment(classifiedProcesses) {
|
|
102
|
+
const devProcs = classifiedProcesses.filter(p => p.classification.isDev);
|
|
103
|
+
const editorProcs = classifiedProcesses.filter(p => p.classification.category === 'EDITORS');
|
|
104
|
+
|
|
105
|
+
const hasNodeServer = devProcs.some(p =>
|
|
106
|
+
/node\b/.test(p.name) && (p.command || '').match(/server|dev|start|next|vite|webpack/)
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const hasHotReload = devProcs.some(p =>
|
|
110
|
+
(p.command || '').match(/--watch|hmr|hot|dev/)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const hasTests = devProcs.some(p =>
|
|
114
|
+
(p.command || '').match(/jest|vitest|mocha|test|spec/)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
isDevActive: devProcs.length > 0,
|
|
119
|
+
devProcessCount: devProcs.length,
|
|
120
|
+
editorActive: editorProcs.length > 0,
|
|
121
|
+
hasNodeServer,
|
|
122
|
+
hasHotReload,
|
|
123
|
+
hasTests,
|
|
124
|
+
devProcesses: devProcs,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// ─── CPU Data Collector ──────────────────────────────────────────
|
|
2
|
+
import si from 'systeminformation';
|
|
3
|
+
|
|
4
|
+
export class CpuCollector {
|
|
5
|
+
/**
|
|
6
|
+
* Collect comprehensive CPU data
|
|
7
|
+
* @returns {Object} CPU info + current load + temperature
|
|
8
|
+
*/
|
|
9
|
+
async collect() {
|
|
10
|
+
const [cpuInfo, load, speed, temp] = await Promise.all([
|
|
11
|
+
si.cpu(),
|
|
12
|
+
si.currentLoad(),
|
|
13
|
+
si.cpuCurrentSpeed(),
|
|
14
|
+
si.cpuTemperature().catch(() => null),
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
info: {
|
|
19
|
+
manufacturer: cpuInfo.manufacturer,
|
|
20
|
+
brand: cpuInfo.brand,
|
|
21
|
+
cores: cpuInfo.physicalCores,
|
|
22
|
+
threads: cpuInfo.cores,
|
|
23
|
+
speed: cpuInfo.speed,
|
|
24
|
+
},
|
|
25
|
+
load: {
|
|
26
|
+
overall: parseFloat(load.currentLoad?.toFixed(1) || 0),
|
|
27
|
+
user: parseFloat(load.currentLoadUser?.toFixed(1) || 0),
|
|
28
|
+
system: parseFloat(load.currentLoadSystem?.toFixed(1) || 0),
|
|
29
|
+
idle: parseFloat(load.currentLoadIdle?.toFixed(1) || 0),
|
|
30
|
+
perCore: (load.cpus || []).map((c, i) => ({
|
|
31
|
+
core: i,
|
|
32
|
+
load: parseFloat(c.load?.toFixed(1) || 0),
|
|
33
|
+
})),
|
|
34
|
+
},
|
|
35
|
+
speed: {
|
|
36
|
+
current: speed.avg,
|
|
37
|
+
min: speed.min,
|
|
38
|
+
max: speed.max,
|
|
39
|
+
},
|
|
40
|
+
temperature: temp ? {
|
|
41
|
+
main: temp.main,
|
|
42
|
+
max: temp.max,
|
|
43
|
+
cores: temp.cores,
|
|
44
|
+
} : null,
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ─── Disk Data Collector ─────────────────────────────────────────
|
|
2
|
+
import si from 'systeminformation';
|
|
3
|
+
import { formatBytes, percentage } from '../utils/helpers.js';
|
|
4
|
+
|
|
5
|
+
export class DiskCollector {
|
|
6
|
+
/**
|
|
7
|
+
* Collect disk usage and I/O data
|
|
8
|
+
* @returns {Object} Filesystem usage + disk I/O
|
|
9
|
+
*/
|
|
10
|
+
async collect() {
|
|
11
|
+
const [fsSize, fsStats] = await Promise.all([
|
|
12
|
+
si.fsSize(),
|
|
13
|
+
si.disksIO().catch(() => null),
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const filesystems = fsSize.map(fs => ({
|
|
17
|
+
mount: fs.mount,
|
|
18
|
+
type: fs.type,
|
|
19
|
+
size: fs.size,
|
|
20
|
+
used: fs.used,
|
|
21
|
+
available: fs.available,
|
|
22
|
+
usedPercent: parseFloat(fs.use?.toFixed(1) || 0),
|
|
23
|
+
formatted: {
|
|
24
|
+
size: formatBytes(fs.size),
|
|
25
|
+
used: formatBytes(fs.used),
|
|
26
|
+
available: formatBytes(fs.available),
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
filesystems,
|
|
32
|
+
io: fsStats ? {
|
|
33
|
+
readPerSecond: fsStats.rIO_sec || 0,
|
|
34
|
+
writePerSecond: fsStats.wIO_sec || 0,
|
|
35
|
+
totalReadBytes: fsStats.rIO || 0,
|
|
36
|
+
totalWriteBytes: fsStats.wIO || 0,
|
|
37
|
+
} : null,
|
|
38
|
+
timestamp: Date.now(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ─── Memory Data Collector ───────────────────────────────────────
|
|
2
|
+
import si from 'systeminformation';
|
|
3
|
+
import { formatBytes, percentage } from '../utils/helpers.js';
|
|
4
|
+
|
|
5
|
+
export class MemoryCollector {
|
|
6
|
+
/**
|
|
7
|
+
* Collect comprehensive memory data
|
|
8
|
+
* @returns {Object} RAM + swap info
|
|
9
|
+
*/
|
|
10
|
+
async collect() {
|
|
11
|
+
const mem = await si.mem();
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
ram: {
|
|
15
|
+
total: mem.total,
|
|
16
|
+
used: mem.used,
|
|
17
|
+
free: mem.free,
|
|
18
|
+
active: mem.active,
|
|
19
|
+
available: mem.available,
|
|
20
|
+
usedPercent: percentage(mem.active, mem.total),
|
|
21
|
+
formatted: {
|
|
22
|
+
total: formatBytes(mem.total),
|
|
23
|
+
used: formatBytes(mem.active),
|
|
24
|
+
free: formatBytes(mem.available),
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
swap: {
|
|
28
|
+
total: mem.swaptotal,
|
|
29
|
+
used: mem.swapused,
|
|
30
|
+
free: mem.swapfree,
|
|
31
|
+
usedPercent: mem.swaptotal > 0 ? percentage(mem.swapused, mem.swaptotal) : 0,
|
|
32
|
+
formatted: {
|
|
33
|
+
total: formatBytes(mem.swaptotal),
|
|
34
|
+
used: formatBytes(mem.swapused),
|
|
35
|
+
free: formatBytes(mem.swapfree),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
timestamp: Date.now(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|