dashcam 1.4.5-beta.0 → 1.4.9-beta
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/LINUX_PERF_FIX.md +184 -0
- package/lib/performanceTracker.js +34 -5
- package/lib/topProcesses.js +222 -30
- package/package.json +1 -1
- package/test-perf-linux.js +208 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Linux Performance Tracking Fixes
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
Performance data from dashcam-cli-minimal was not working on Ubuntu Linux.
|
|
5
|
+
|
|
6
|
+
## Root Causes Identified
|
|
7
|
+
|
|
8
|
+
### 1. **ps command compatibility**
|
|
9
|
+
- Some Linux distributions (especially minimal/container images) use BusyBox `ps` which doesn't support the `--sort` option
|
|
10
|
+
- The code was failing when trying to use `ps --sort=-pcpu`
|
|
11
|
+
|
|
12
|
+
### 2. **Missing error handling**
|
|
13
|
+
- Insufficient logging made it hard to diagnose where the performance tracking was failing
|
|
14
|
+
- No fallback mechanisms when certain system calls failed
|
|
15
|
+
|
|
16
|
+
### 3. **Network metrics parsing**
|
|
17
|
+
- The macOS netstat parsing had an off-by-one error in the parts length check
|
|
18
|
+
- Could cause network metrics to fail silently
|
|
19
|
+
|
|
20
|
+
### 4. **Import optimization**
|
|
21
|
+
- The Linux network code was using `await import('fs')` dynamically when `fs` was already imported at the top
|
|
22
|
+
|
|
23
|
+
## Fixes Applied
|
|
24
|
+
|
|
25
|
+
### topProcesses.js
|
|
26
|
+
1. **Added fallback for ps --sort**:
|
|
27
|
+
```javascript
|
|
28
|
+
try {
|
|
29
|
+
// Try with --sort first
|
|
30
|
+
const result = await execFileAsync('ps', ['-eo', 'pid,pcpu,pmem,comm', '--sort=-pcpu'], ...);
|
|
31
|
+
} catch (sortError) {
|
|
32
|
+
// Fallback to unsorted and sort in JavaScript
|
|
33
|
+
const result = await execFileAsync('ps', ['-eo', 'pid,pcpu,pmem,comm'], ...);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
2. **Improved parsing with filtering**:
|
|
38
|
+
- Now filters out invalid entries (pid <= 0)
|
|
39
|
+
- Handles missing/undefined CPU and memory values
|
|
40
|
+
- Always sorts by CPU in JavaScript as a backup
|
|
41
|
+
|
|
42
|
+
3. **Added timeouts**:
|
|
43
|
+
- All process listings now have 5-10 second timeouts to prevent hanging
|
|
44
|
+
|
|
45
|
+
### performanceTracker.js
|
|
46
|
+
1. **Fixed network metrics**:
|
|
47
|
+
- Corrected macOS netstat parsing (parts.length >= 10 instead of >= 7)
|
|
48
|
+
- Removed redundant `await import('fs')` on Linux
|
|
49
|
+
- Already using `fs` from the top-level import
|
|
50
|
+
|
|
51
|
+
2. **Enhanced error handling**:
|
|
52
|
+
- All metric collection methods now have comprehensive try/catch blocks
|
|
53
|
+
- Graceful degradation - if one metric fails, others continue
|
|
54
|
+
- Better logging with platform and error context
|
|
55
|
+
|
|
56
|
+
3. **Improved logging**:
|
|
57
|
+
- Added debug logs for successful operations
|
|
58
|
+
- More detailed error messages including platform info
|
|
59
|
+
- Stack traces for debugging
|
|
60
|
+
|
|
61
|
+
## Testing
|
|
62
|
+
|
|
63
|
+
### Quick Test (10 seconds)
|
|
64
|
+
```bash
|
|
65
|
+
cd dashcam-cli-minimal
|
|
66
|
+
node test-perf-linux.js
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This will:
|
|
70
|
+
- Test `pidusage` library
|
|
71
|
+
- Test `getTopProcesses` function
|
|
72
|
+
- Test network metrics reading
|
|
73
|
+
- Test system metrics
|
|
74
|
+
- Run full performance tracker for 10 seconds
|
|
75
|
+
- Show detailed results and identify any failures
|
|
76
|
+
|
|
77
|
+
### Full Recording Test
|
|
78
|
+
```bash
|
|
79
|
+
# Start a recording with performance tracking
|
|
80
|
+
dashcam start
|
|
81
|
+
|
|
82
|
+
# Do some work for 10-15 seconds
|
|
83
|
+
# ...
|
|
84
|
+
|
|
85
|
+
# Stop recording
|
|
86
|
+
dashcam stop
|
|
87
|
+
|
|
88
|
+
# Check the output for performance data
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Expected Output
|
|
92
|
+
The test should show:
|
|
93
|
+
```
|
|
94
|
+
✓ pidusage: PASS
|
|
95
|
+
✓ topProcesses: PASS
|
|
96
|
+
✓ networkMetrics: PASS
|
|
97
|
+
✓ systemMetrics: PASS
|
|
98
|
+
✓ performanceTracker: PASS
|
|
99
|
+
|
|
100
|
+
Overall: ✓ ALL TESTS PASSED
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Platform-Specific Notes
|
|
104
|
+
|
|
105
|
+
### Ubuntu/Debian
|
|
106
|
+
- Should work on all versions
|
|
107
|
+
- Uses `/proc/net/dev` for network stats
|
|
108
|
+
- Falls back if `ps --sort` not available
|
|
109
|
+
|
|
110
|
+
### Alpine Linux / Docker
|
|
111
|
+
- BusyBox `ps` detected automatically
|
|
112
|
+
- Sorts processes in JavaScript instead
|
|
113
|
+
- May have limited network stats in containers
|
|
114
|
+
|
|
115
|
+
### CentOS/RHEL
|
|
116
|
+
- Full GNU ps support
|
|
117
|
+
- Should use `--sort` option
|
|
118
|
+
- Full network stats available
|
|
119
|
+
|
|
120
|
+
## Troubleshooting
|
|
121
|
+
|
|
122
|
+
### If performance data is still empty:
|
|
123
|
+
|
|
124
|
+
1. **Check pidusage works**:
|
|
125
|
+
```bash
|
|
126
|
+
node -e "import('pidusage').then(m => m.default(process.pid).then(console.log))"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
2. **Check ps command**:
|
|
130
|
+
```bash
|
|
131
|
+
ps -eo pid,pcpu,pmem,comm --sort=-pcpu | head -5
|
|
132
|
+
# If this fails, try:
|
|
133
|
+
ps -eo pid,pcpu,pmem,comm | head -5
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
3. **Check /proc/net/dev exists**:
|
|
137
|
+
```bash
|
|
138
|
+
cat /proc/net/dev
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
4. **Run with verbose logging**:
|
|
142
|
+
```bash
|
|
143
|
+
dashcam start --verbose
|
|
144
|
+
# ... do work ...
|
|
145
|
+
dashcam stop --verbose
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
5. **Check the performance file directly**:
|
|
149
|
+
```bash
|
|
150
|
+
# Look for performance.jsonl in the output directory
|
|
151
|
+
cat ~/.dashcam/recordings/*/performance.jsonl | head -1 | jq
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## What Gets Tracked
|
|
155
|
+
|
|
156
|
+
Even with the fixes, the following is tracked every 5 seconds:
|
|
157
|
+
|
|
158
|
+
- ✅ Process CPU usage (dashcam process)
|
|
159
|
+
- ✅ Process memory usage (dashcam process)
|
|
160
|
+
- ✅ System-wide memory usage
|
|
161
|
+
- ✅ System CPU core count
|
|
162
|
+
- ✅ Top 10 processes by CPU
|
|
163
|
+
- ✅ Network I/O (where available)
|
|
164
|
+
|
|
165
|
+
## API Upload
|
|
166
|
+
|
|
167
|
+
The performance data is uploaded to the API with the recording:
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
{
|
|
171
|
+
performance: {
|
|
172
|
+
samples: [...], // Array of samples taken during recording
|
|
173
|
+
summary: { // Aggregated statistics
|
|
174
|
+
avgProcessCPU: 12.3,
|
|
175
|
+
maxProcessCPU: 18.7,
|
|
176
|
+
avgProcessMemoryMB: 128.0,
|
|
177
|
+
maxProcessMemoryMB: 192.0,
|
|
178
|
+
// ...
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
This data is then displayed in the web UI under the "Performance" tab.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
2
|
import { logger } from './logger.js';
|
|
3
3
|
import pidusage from 'pidusage';
|
|
4
|
-
import { getTopProcesses } from './topProcesses.js';
|
|
4
|
+
import { getTopProcesses, cleanupPowerShell } from './topProcesses.js';
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
|
|
@@ -40,7 +40,7 @@ class PerformanceTracker {
|
|
|
40
40
|
|
|
41
41
|
for (const line of lines) {
|
|
42
42
|
const parts = line.trim().split(/\s+/);
|
|
43
|
-
if (parts.length >=
|
|
43
|
+
if (parts.length >= 10 && parts[0] !== 'Name') {
|
|
44
44
|
const ibytes = parseInt(parts[6]) || 0;
|
|
45
45
|
const obytes = parseInt(parts[9]) || 0;
|
|
46
46
|
totalBytesReceived += ibytes;
|
|
@@ -52,7 +52,6 @@ class PerformanceTracker {
|
|
|
52
52
|
}
|
|
53
53
|
} else if (process.platform === 'linux') {
|
|
54
54
|
// Linux - read from /proc/net/dev
|
|
55
|
-
const fs = await import('fs');
|
|
56
55
|
try {
|
|
57
56
|
const netDev = fs.readFileSync('/proc/net/dev', 'utf8');
|
|
58
57
|
const lines = netDev.split('\n');
|
|
@@ -157,6 +156,13 @@ class PerformanceTracker {
|
|
|
157
156
|
async getProcessMetrics() {
|
|
158
157
|
try {
|
|
159
158
|
const stats = await pidusage(this.pid);
|
|
159
|
+
|
|
160
|
+
logger.debug('Process metrics collected', {
|
|
161
|
+
cpu: stats.cpu?.toFixed(2),
|
|
162
|
+
memoryMB: (stats.memory / (1024 * 1024)).toFixed(1),
|
|
163
|
+
pid: stats.pid
|
|
164
|
+
});
|
|
165
|
+
|
|
160
166
|
return {
|
|
161
167
|
process: {
|
|
162
168
|
cpu: stats.cpu, // CPU usage percentage
|
|
@@ -169,7 +175,11 @@ class PerformanceTracker {
|
|
|
169
175
|
}
|
|
170
176
|
};
|
|
171
177
|
} catch (error) {
|
|
172
|
-
logger.warn('Failed to get process metrics', {
|
|
178
|
+
logger.warn('Failed to get process metrics', {
|
|
179
|
+
error: error.message,
|
|
180
|
+
pid: this.pid,
|
|
181
|
+
platform: process.platform
|
|
182
|
+
});
|
|
173
183
|
return {
|
|
174
184
|
process: {
|
|
175
185
|
cpu: 0,
|
|
@@ -189,12 +199,21 @@ class PerformanceTracker {
|
|
|
189
199
|
const topProcs = await getTopProcesses(10);
|
|
190
200
|
|
|
191
201
|
if (!topProcs || topProcs.length === 0) {
|
|
202
|
+
logger.debug('No top processes returned', {
|
|
203
|
+
platform: process.platform
|
|
204
|
+
});
|
|
192
205
|
return {
|
|
193
206
|
topProcesses: [],
|
|
194
207
|
totalProcesses: 0
|
|
195
208
|
};
|
|
196
209
|
}
|
|
197
210
|
|
|
211
|
+
logger.debug('Top processes fetched', {
|
|
212
|
+
count: topProcs.length,
|
|
213
|
+
platform: process.platform,
|
|
214
|
+
firstProcess: topProcs[0]?.name
|
|
215
|
+
});
|
|
216
|
+
|
|
198
217
|
// Get detailed stats using pidusage for each process
|
|
199
218
|
const detailedStats = [];
|
|
200
219
|
for (const proc of topProcs) {
|
|
@@ -213,6 +232,7 @@ class PerformanceTracker {
|
|
|
213
232
|
// Process might have exited, use basic data from ps/PowerShell
|
|
214
233
|
logger.debug('Failed to get detailed stats for process, using basic data', {
|
|
215
234
|
pid: proc.pid,
|
|
235
|
+
name: proc.name,
|
|
216
236
|
error: error.message
|
|
217
237
|
});
|
|
218
238
|
detailedStats.push({
|
|
@@ -233,7 +253,11 @@ class PerformanceTracker {
|
|
|
233
253
|
totalProcesses: topProcs.length
|
|
234
254
|
};
|
|
235
255
|
} catch (error) {
|
|
236
|
-
logger.warn('Failed to get top processes', {
|
|
256
|
+
logger.warn('Failed to get top processes', {
|
|
257
|
+
error: error.message,
|
|
258
|
+
platform: process.platform,
|
|
259
|
+
stack: error.stack
|
|
260
|
+
});
|
|
237
261
|
return {
|
|
238
262
|
topProcesses: [],
|
|
239
263
|
totalProcesses: 0
|
|
@@ -478,6 +502,11 @@ class PerformanceTracker {
|
|
|
478
502
|
}
|
|
479
503
|
}
|
|
480
504
|
this.performanceFile = null;
|
|
505
|
+
|
|
506
|
+
// Cleanup PowerShell instance on Windows
|
|
507
|
+
if (process.platform === 'win32') {
|
|
508
|
+
cleanupPowerShell();
|
|
509
|
+
}
|
|
481
510
|
}
|
|
482
511
|
}
|
|
483
512
|
|
package/lib/topProcesses.js
CHANGED
|
@@ -1,25 +1,186 @@
|
|
|
1
|
-
import { execFile } from 'child_process';
|
|
1
|
+
import { execFile, spawn } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { logger } from './logger.js';
|
|
5
5
|
|
|
6
6
|
const execFileAsync = promisify(execFile);
|
|
7
7
|
|
|
8
|
+
// Persistent PowerShell instance for Windows
|
|
9
|
+
let persistentPowerShell = null;
|
|
10
|
+
let psCommandQueue = [];
|
|
11
|
+
let psProcessing = false;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Initialize a persistent PowerShell instance for Windows
|
|
15
|
+
*/
|
|
16
|
+
function initPersistentPowerShell() {
|
|
17
|
+
if (persistentPowerShell) {
|
|
18
|
+
return persistentPowerShell;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
logger.debug('Initializing persistent PowerShell instance');
|
|
22
|
+
|
|
23
|
+
const ps = spawn('powershell.exe', [
|
|
24
|
+
'-NoLogo',
|
|
25
|
+
'-NoProfile',
|
|
26
|
+
'-NonInteractive',
|
|
27
|
+
'-WindowStyle', 'Hidden',
|
|
28
|
+
'-Command', '-'
|
|
29
|
+
], {
|
|
30
|
+
windowsHide: true,
|
|
31
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
let outputBuffer = '';
|
|
35
|
+
const DELIMITER = '---END-OF-COMMAND---';
|
|
36
|
+
|
|
37
|
+
ps.stdout.on('data', (data) => {
|
|
38
|
+
outputBuffer += data.toString();
|
|
39
|
+
|
|
40
|
+
// Check if we have a complete response
|
|
41
|
+
const delimiterIndex = outputBuffer.indexOf(DELIMITER);
|
|
42
|
+
if (delimiterIndex !== -1) {
|
|
43
|
+
const output = outputBuffer.substring(0, delimiterIndex);
|
|
44
|
+
outputBuffer = outputBuffer.substring(delimiterIndex + DELIMITER.length);
|
|
45
|
+
|
|
46
|
+
// Resolve the pending command
|
|
47
|
+
if (psCommandQueue.length > 0) {
|
|
48
|
+
const { resolve } = psCommandQueue.shift();
|
|
49
|
+
resolve(output);
|
|
50
|
+
psProcessing = false;
|
|
51
|
+
processNextCommand();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
ps.stderr.on('data', (data) => {
|
|
57
|
+
logger.debug('PowerShell stderr:', data.toString());
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
ps.on('close', (code) => {
|
|
61
|
+
logger.debug('PowerShell process closed', { code });
|
|
62
|
+
persistentPowerShell = null;
|
|
63
|
+
// Reject all pending commands
|
|
64
|
+
while (psCommandQueue.length > 0) {
|
|
65
|
+
const { reject } = psCommandQueue.shift();
|
|
66
|
+
reject(new Error('PowerShell process closed'));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
ps.on('error', (error) => {
|
|
71
|
+
logger.warn('PowerShell process error', { error: error.message });
|
|
72
|
+
persistentPowerShell = null;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
persistentPowerShell = ps;
|
|
76
|
+
persistentPowerShell.delimiter = DELIMITER;
|
|
77
|
+
|
|
78
|
+
return ps;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Process the next command in the queue
|
|
83
|
+
*/
|
|
84
|
+
function processNextCommand() {
|
|
85
|
+
if (psProcessing || psCommandQueue.length === 0) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
psProcessing = true;
|
|
90
|
+
const { command } = psCommandQueue[0];
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
persistentPowerShell.stdin.write(command + '\n');
|
|
94
|
+
persistentPowerShell.stdin.write(`Write-Host '${persistentPowerShell.delimiter}'\n`);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
logger.warn('Failed to write to PowerShell stdin', { error: error.message });
|
|
97
|
+
const { reject } = psCommandQueue.shift();
|
|
98
|
+
reject(error);
|
|
99
|
+
psProcessing = false;
|
|
100
|
+
processNextCommand();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Execute a command in the persistent PowerShell instance
|
|
106
|
+
*/
|
|
107
|
+
function execPowerShellCommand(command, timeout = 10000) {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const ps = initPersistentPowerShell();
|
|
110
|
+
|
|
111
|
+
const timeoutId = setTimeout(() => {
|
|
112
|
+
reject(new Error('PowerShell command timeout'));
|
|
113
|
+
}, timeout);
|
|
114
|
+
|
|
115
|
+
psCommandQueue.push({
|
|
116
|
+
command,
|
|
117
|
+
resolve: (output) => {
|
|
118
|
+
clearTimeout(timeoutId);
|
|
119
|
+
resolve(output);
|
|
120
|
+
},
|
|
121
|
+
reject: (error) => {
|
|
122
|
+
clearTimeout(timeoutId);
|
|
123
|
+
reject(error);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
processNextCommand();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Cleanup the persistent PowerShell instance
|
|
133
|
+
*/
|
|
134
|
+
export function cleanupPowerShell() {
|
|
135
|
+
if (persistentPowerShell) {
|
|
136
|
+
logger.debug('Cleaning up persistent PowerShell instance');
|
|
137
|
+
try {
|
|
138
|
+
persistentPowerShell.stdin.end();
|
|
139
|
+
persistentPowerShell.kill();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
logger.debug('Error cleaning up PowerShell', { error: error.message });
|
|
142
|
+
}
|
|
143
|
+
persistentPowerShell = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
8
147
|
/**
|
|
9
148
|
* Parse ps output format: " PID %CPU %MEM COMMAND"
|
|
10
149
|
*/
|
|
11
150
|
function parsePsOutput(stdout, limit) {
|
|
151
|
+
logger.debug('Parsing ps output', {
|
|
152
|
+
outputLength: stdout.length,
|
|
153
|
+
firstLine: stdout.split('\n')[0],
|
|
154
|
+
lineCount: stdout.split('\n').length
|
|
155
|
+
});
|
|
156
|
+
|
|
12
157
|
const lines = stdout.trim().split('\n');
|
|
158
|
+
|
|
159
|
+
if (lines.length === 0) {
|
|
160
|
+
logger.warn('ps output is empty');
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
13
164
|
// Skip header line
|
|
14
|
-
|
|
165
|
+
const processes = lines.slice(1).map(line => {
|
|
15
166
|
const parts = line.trim().split(/\s+/, 4);
|
|
16
167
|
return {
|
|
17
168
|
pid: Number(parts[0]),
|
|
18
|
-
cpu: Number(parts[1]),
|
|
19
|
-
mem: Number(parts[2]),
|
|
169
|
+
cpu: Number(parts[1]) || 0,
|
|
170
|
+
mem: Number(parts[2]) || 0,
|
|
20
171
|
name: parts[3] || ''
|
|
21
172
|
};
|
|
173
|
+
}).filter(proc => proc.pid > 0); // Filter out invalid entries
|
|
174
|
+
|
|
175
|
+
logger.debug('Parsed processes', {
|
|
176
|
+
count: processes.length,
|
|
177
|
+
sample: processes.slice(0, 3)
|
|
22
178
|
});
|
|
179
|
+
|
|
180
|
+
// Sort by CPU descending (in case ps doesn't support --sort)
|
|
181
|
+
processes.sort((a, b) => b.cpu - a.cpu);
|
|
182
|
+
|
|
183
|
+
return processes.slice(0, limit);
|
|
23
184
|
}
|
|
24
185
|
|
|
25
186
|
/**
|
|
@@ -32,24 +193,67 @@ async function getTopProcessesUnix(limit = 10) {
|
|
|
32
193
|
if (os.platform() === 'darwin') {
|
|
33
194
|
// macOS uses BSD ps - different syntax, no --sort option
|
|
34
195
|
// Use -r flag to sort by CPU usage
|
|
196
|
+
console.log('[topProcesses] Running macOS ps command');
|
|
35
197
|
const result = await execFileAsync('ps', [
|
|
36
198
|
'-Arco',
|
|
37
199
|
'pid,pcpu,pmem,comm'
|
|
38
|
-
], { encoding: 'utf8' });
|
|
200
|
+
], { encoding: 'utf8', timeout: 5000 });
|
|
39
201
|
stdout = result.stdout;
|
|
202
|
+
console.log('[topProcesses] macOS ps succeeded, output length:', stdout.length);
|
|
40
203
|
} else {
|
|
41
|
-
// Linux
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
'pid,pcpu,pmem,comm',
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
204
|
+
// Linux - try different variations in order of preference
|
|
205
|
+
const psVariations = [
|
|
206
|
+
// Standard GNU ps with --sort
|
|
207
|
+
{ args: ['-eo', 'pid,pcpu,pmem,comm', '--sort=-pcpu'], name: 'GNU ps with --sort' },
|
|
208
|
+
// GNU ps without --sort (we'll sort in JS)
|
|
209
|
+
{ args: ['-eo', 'pid,pcpu,pmem,comm'], name: 'GNU ps without --sort' },
|
|
210
|
+
// BusyBox ps (minimal options)
|
|
211
|
+
{ args: ['-o', 'pid,pcpu,pmem,comm'], name: 'BusyBox ps' },
|
|
212
|
+
// Most basic ps command
|
|
213
|
+
{ args: ['aux'], name: 'basic ps aux' }
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
let psSuccess = false;
|
|
217
|
+
for (const variation of psVariations) {
|
|
218
|
+
try {
|
|
219
|
+
console.log(`[topProcesses] Trying: ${variation.name}`);
|
|
220
|
+
const result = await execFileAsync('ps', variation.args, {
|
|
221
|
+
encoding: 'utf8',
|
|
222
|
+
timeout: 5000
|
|
223
|
+
});
|
|
224
|
+
stdout = result.stdout;
|
|
225
|
+
console.log(`[topProcesses] ${variation.name} succeeded, output length:`, stdout.length);
|
|
226
|
+
logger.debug(`ps command succeeded: ${variation.name}`, { outputLength: stdout.length });
|
|
227
|
+
psSuccess = true;
|
|
228
|
+
break;
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.log(`[topProcesses] ${variation.name} failed:`, err.message);
|
|
231
|
+
// Try next variation
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!psSuccess) {
|
|
236
|
+
throw new Error('All ps command variations failed');
|
|
237
|
+
}
|
|
48
238
|
}
|
|
49
239
|
|
|
50
|
-
|
|
240
|
+
console.log('[topProcesses] Parsing ps output...');
|
|
241
|
+
const processes = parsePsOutput(stdout, limit);
|
|
242
|
+
console.log('[topProcesses] Parsed', processes.length, 'processes');
|
|
243
|
+
logger.debug('Parsed processes from ps output', {
|
|
244
|
+
processCount: processes.length,
|
|
245
|
+
firstProcess: processes[0]
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return processes;
|
|
51
249
|
} catch (error) {
|
|
52
|
-
|
|
250
|
+
console.error('[topProcesses] FATAL ERROR getting top processes:', error.message);
|
|
251
|
+
console.error('[topProcesses] Error stack:', error.stack);
|
|
252
|
+
logger.warn('Failed to get top processes on Unix', {
|
|
253
|
+
error: error.message,
|
|
254
|
+
stack: error.stack,
|
|
255
|
+
platform: os.platform()
|
|
256
|
+
});
|
|
53
257
|
return [];
|
|
54
258
|
}
|
|
55
259
|
}
|
|
@@ -86,22 +290,10 @@ function parsePsWinJson(stdout, limit) {
|
|
|
86
290
|
*/
|
|
87
291
|
async function getTopProcessesWindows(limit = 10) {
|
|
88
292
|
try {
|
|
89
|
-
// Use PowerShell
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
'Select-Object Id,CPU,WS,ProcessName | ',
|
|
94
|
-
'ConvertTo-Json'
|
|
95
|
-
].join('');
|
|
96
|
-
|
|
97
|
-
const { stdout } = await execFileAsync('powershell.exe', [
|
|
98
|
-
'-NoLogo',
|
|
99
|
-
'-NoProfile',
|
|
100
|
-
'-NonInteractive',
|
|
101
|
-
'-Command',
|
|
102
|
-
psCmd
|
|
103
|
-
], { encoding: 'utf8' });
|
|
104
|
-
|
|
293
|
+
// Use persistent PowerShell instance
|
|
294
|
+
const psCmd = "Get-Process | Select-Object Id,CPU,WS,ProcessName | ConvertTo-Json";
|
|
295
|
+
|
|
296
|
+
const stdout = await execPowerShellCommand(psCmd, 10000);
|
|
105
297
|
return parsePsWinJson(stdout, limit);
|
|
106
298
|
} catch (error) {
|
|
107
299
|
logger.warn('Failed to get top processes on Windows', { error: error.message });
|
package/package.json
CHANGED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Linux-specific performance tracking test
|
|
5
|
+
* Tests each component individually to identify issues
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { performanceTracker } from './lib/performanceTracker.js';
|
|
9
|
+
import { getTopProcesses } from './lib/topProcesses.js';
|
|
10
|
+
import { logger, setVerbose } from './lib/logger.js';
|
|
11
|
+
import pidusage from 'pidusage';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
|
|
15
|
+
// Enable verbose logging
|
|
16
|
+
setVerbose(true);
|
|
17
|
+
|
|
18
|
+
async function testPidUsage() {
|
|
19
|
+
logger.info('=== Testing pidusage ===');
|
|
20
|
+
try {
|
|
21
|
+
const stats = await pidusage(process.pid);
|
|
22
|
+
logger.info('pidusage SUCCESS', {
|
|
23
|
+
cpu: stats.cpu?.toFixed(2),
|
|
24
|
+
memory: (stats.memory / (1024 * 1024)).toFixed(1) + ' MB',
|
|
25
|
+
pid: stats.pid,
|
|
26
|
+
ppid: stats.ppid
|
|
27
|
+
});
|
|
28
|
+
return true;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
logger.error('pidusage FAILED', { error: error.message, stack: error.stack });
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function testTopProcesses() {
|
|
36
|
+
logger.info('=== Testing getTopProcesses ===');
|
|
37
|
+
try {
|
|
38
|
+
const topProcs = await getTopProcesses(5);
|
|
39
|
+
logger.info('getTopProcesses SUCCESS', {
|
|
40
|
+
count: topProcs.length,
|
|
41
|
+
processes: topProcs.map(p => `${p.name} (PID: ${p.pid}, CPU: ${p.cpu}%)`)
|
|
42
|
+
});
|
|
43
|
+
return topProcs.length > 0;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
logger.error('getTopProcesses FAILED', { error: error.message, stack: error.stack });
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function testNetworkMetrics() {
|
|
51
|
+
logger.info('=== Testing Network Metrics ===');
|
|
52
|
+
try {
|
|
53
|
+
if (process.platform === 'linux') {
|
|
54
|
+
// Test /proc/net/dev reading
|
|
55
|
+
if (fs.existsSync('/proc/net/dev')) {
|
|
56
|
+
const content = fs.readFileSync('/proc/net/dev', 'utf8');
|
|
57
|
+
const lines = content.split('\n');
|
|
58
|
+
let totalRx = 0;
|
|
59
|
+
let totalTx = 0;
|
|
60
|
+
|
|
61
|
+
logger.info('/proc/net/dev file found');
|
|
62
|
+
logger.debug('First few lines:', lines.slice(0, 5));
|
|
63
|
+
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
if (line.includes(':')) {
|
|
66
|
+
const parts = line.split(':')[1].trim().split(/\s+/);
|
|
67
|
+
if (parts.length >= 9) {
|
|
68
|
+
const rx = parseInt(parts[0]) || 0;
|
|
69
|
+
const tx = parseInt(parts[8]) || 0;
|
|
70
|
+
totalRx += rx;
|
|
71
|
+
totalTx += tx;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
logger.info('Network stats parsed', {
|
|
77
|
+
totalRx: (totalRx / (1024 * 1024)).toFixed(2) + ' MB',
|
|
78
|
+
totalTx: (totalTx / (1024 * 1024)).toFixed(2) + ' MB'
|
|
79
|
+
});
|
|
80
|
+
return true;
|
|
81
|
+
} else {
|
|
82
|
+
logger.warn('/proc/net/dev not found - running in container?');
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
logger.info('Skipping network test (not Linux)');
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logger.error('Network metrics FAILED', { error: error.message, stack: error.stack });
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function testSystemMetrics() {
|
|
96
|
+
logger.info('=== Testing System Metrics ===');
|
|
97
|
+
try {
|
|
98
|
+
const totalMem = os.totalmem();
|
|
99
|
+
const freeMem = os.freemem();
|
|
100
|
+
const usedMem = totalMem - freeMem;
|
|
101
|
+
const cpus = os.cpus();
|
|
102
|
+
|
|
103
|
+
logger.info('System metrics SUCCESS', {
|
|
104
|
+
totalMemory: (totalMem / (1024 * 1024 * 1024)).toFixed(2) + ' GB',
|
|
105
|
+
freeMemory: (freeMem / (1024 * 1024 * 1024)).toFixed(2) + ' GB',
|
|
106
|
+
usedMemory: (usedMem / (1024 * 1024 * 1024)).toFixed(2) + ' GB',
|
|
107
|
+
memoryUsagePercent: ((usedMem / totalMem) * 100).toFixed(1) + '%',
|
|
108
|
+
cpuCount: cpus.length,
|
|
109
|
+
platform: os.platform(),
|
|
110
|
+
arch: os.arch()
|
|
111
|
+
});
|
|
112
|
+
return true;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
logger.error('System metrics FAILED', { error: error.message });
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function testPerformanceTracker() {
|
|
120
|
+
logger.info('=== Testing PerformanceTracker ===');
|
|
121
|
+
try {
|
|
122
|
+
logger.info('Starting performance tracker for 10 seconds...');
|
|
123
|
+
|
|
124
|
+
// Use temp directory for performance file
|
|
125
|
+
const tmpDir = os.tmpdir();
|
|
126
|
+
performanceTracker.start(tmpDir);
|
|
127
|
+
|
|
128
|
+
// Wait 10 seconds
|
|
129
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
130
|
+
|
|
131
|
+
// Stop tracking
|
|
132
|
+
const result = performanceTracker.stop();
|
|
133
|
+
|
|
134
|
+
logger.info('PerformanceTracker stopped', {
|
|
135
|
+
sampleCount: result.samples?.length || 0,
|
|
136
|
+
hasSummary: !!result.summary
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (result.summary) {
|
|
140
|
+
logger.info('Performance Summary', {
|
|
141
|
+
duration: (result.summary.durationMs / 1000).toFixed(1) + 's',
|
|
142
|
+
avgCPU: result.summary.avgProcessCPU?.toFixed(1) + '%',
|
|
143
|
+
maxCPU: result.summary.maxProcessCPU?.toFixed(1) + '%',
|
|
144
|
+
avgMemory: result.summary.avgProcessMemoryMB?.toFixed(1) + ' MB',
|
|
145
|
+
maxMemory: result.summary.maxProcessMemoryMB?.toFixed(1) + ' MB'
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (result.samples && result.samples.length > 0) {
|
|
150
|
+
const lastSample = result.samples[result.samples.length - 1];
|
|
151
|
+
logger.info('Last sample data', {
|
|
152
|
+
hasSystem: !!lastSample.system,
|
|
153
|
+
hasProcess: !!lastSample.process,
|
|
154
|
+
hasNetwork: !!lastSample.network,
|
|
155
|
+
hasTopProcesses: !!lastSample.topProcesses,
|
|
156
|
+
topProcessesCount: lastSample.topProcesses?.length || 0
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (lastSample.topProcesses && lastSample.topProcesses.length > 0) {
|
|
160
|
+
logger.info('Top 3 processes from last sample:');
|
|
161
|
+
lastSample.topProcesses.slice(0, 3).forEach((proc, i) => {
|
|
162
|
+
logger.info(` ${i + 1}. ${proc.name} - CPU: ${proc.cpu?.toFixed(1)}%, Mem: ${(proc.memory / (1024 * 1024)).toFixed(1)} MB`);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Clean up
|
|
168
|
+
performanceTracker.cleanup();
|
|
169
|
+
|
|
170
|
+
return result.samples && result.samples.length > 0;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
logger.error('PerformanceTracker FAILED', { error: error.message, stack: error.stack });
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function runAllTests() {
|
|
178
|
+
logger.info('Starting Linux Performance Tracking Tests');
|
|
179
|
+
logger.info('Platform:', os.platform());
|
|
180
|
+
logger.info('Architecture:', os.arch());
|
|
181
|
+
logger.info('Node version:', process.version);
|
|
182
|
+
logger.info('');
|
|
183
|
+
|
|
184
|
+
const results = {
|
|
185
|
+
pidusage: await testPidUsage(),
|
|
186
|
+
topProcesses: await testTopProcesses(),
|
|
187
|
+
networkMetrics: await testNetworkMetrics(),
|
|
188
|
+
systemMetrics: await testSystemMetrics(),
|
|
189
|
+
performanceTracker: await testPerformanceTracker()
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
logger.info('');
|
|
193
|
+
logger.info('=== Test Results Summary ===');
|
|
194
|
+
Object.entries(results).forEach(([test, passed]) => {
|
|
195
|
+
logger.info(`${test}: ${passed ? '✓ PASS' : '✗ FAIL'}`);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const allPassed = Object.values(results).every(r => r);
|
|
199
|
+
logger.info('');
|
|
200
|
+
logger.info(`Overall: ${allPassed ? '✓ ALL TESTS PASSED' : '✗ SOME TESTS FAILED'}`);
|
|
201
|
+
|
|
202
|
+
process.exit(allPassed ? 0 : 1);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
runAllTests().catch(error => {
|
|
206
|
+
logger.error('Test suite failed', { error: error.message, stack: error.stack });
|
|
207
|
+
process.exit(1);
|
|
208
|
+
});
|