dashcam 1.4.2-beta → 1.4.5-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/PERFORMANCE_TRACKING.md +139 -0
- package/bin/dashcam-background.js +59 -4
- package/bin/dashcam.js +321 -7
- package/lib/auth.js +8 -7
- package/lib/config.js +2 -1
- package/lib/extension-logs/helpers.js +7 -1
- package/lib/logs/index.js +5 -1
- package/lib/performanceTracker.js +487 -0
- package/lib/processManager.js +31 -12
- package/lib/recorder.js +40 -19
- package/lib/topProcesses.js +128 -0
- package/lib/uploader.js +18 -6
- package/lib/utilities/jsonl.js +11 -2
- package/lib/websocket/server.js +19 -9
- package/package.json +2 -1
- package/test-perf-tracker.js +52 -0
- package/test-performance-tracking.js +108 -0
- package/test-top-processes.js +23 -0
- package/test_workflow.sh +22 -5
package/lib/config.js
CHANGED
|
@@ -26,7 +26,8 @@ export const apiEndpoints = {
|
|
|
26
26
|
production: 'https://testdriver-api.onrender.com'
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// Allow TD_API_ROOT to override the endpoint
|
|
30
|
+
export const API_ENDPOINT = process.env.TD_API_ROOT || apiEndpoints[ENV];
|
|
30
31
|
|
|
31
32
|
// App configuration
|
|
32
33
|
export const APP = {
|
|
@@ -84,7 +84,13 @@ function filterWebEvents(
|
|
|
84
84
|
event => event.type === 'INITIAL_TABS' || event.payload.tabId
|
|
85
85
|
);
|
|
86
86
|
const patterns = groupLogsStatuses
|
|
87
|
-
.map((status) =>
|
|
87
|
+
.map((status) => {
|
|
88
|
+
// Handle cases where items might not be set (e.g., during upload)
|
|
89
|
+
if (!status.items || !Array.isArray(status.items)) {
|
|
90
|
+
return status.patterns || [];
|
|
91
|
+
}
|
|
92
|
+
return status.items.map((item) => item.item);
|
|
93
|
+
})
|
|
88
94
|
.flat();
|
|
89
95
|
|
|
90
96
|
const newEvents = [];
|
package/lib/logs/index.js
CHANGED
|
@@ -123,7 +123,11 @@ async function trimLogs(groupLogStatuses, startMS, endMS, clientStartDate, clipI
|
|
|
123
123
|
filteredEvents
|
|
124
124
|
);
|
|
125
125
|
} catch (error) {
|
|
126
|
-
logger.error('Error trimming log file', {
|
|
126
|
+
logger.error('Error trimming log file', {
|
|
127
|
+
file: status.fileLocation,
|
|
128
|
+
error: error.message,
|
|
129
|
+
stack: error.stack
|
|
130
|
+
});
|
|
127
131
|
}
|
|
128
132
|
});
|
|
129
133
|
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
import pidusage from 'pidusage';
|
|
4
|
+
import { getTopProcesses } from './topProcesses.js';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Tracks CPU and memory usage during recording
|
|
10
|
+
*/
|
|
11
|
+
class PerformanceTracker {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.interval = null;
|
|
14
|
+
this.samples = [];
|
|
15
|
+
this.startTime = null;
|
|
16
|
+
this.pid = process.pid;
|
|
17
|
+
this.monitorInterval = 5000; // Sample every 5 seconds
|
|
18
|
+
this.lastNetworkStats = null; // Track previous network stats for delta calculation
|
|
19
|
+
this.performanceFile = null; // Path to performance data file
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get network I/O statistics
|
|
24
|
+
*/
|
|
25
|
+
async getNetworkMetrics() {
|
|
26
|
+
try {
|
|
27
|
+
const networkInterfaces = os.networkInterfaces();
|
|
28
|
+
|
|
29
|
+
// Get network stats using os module (basic approach)
|
|
30
|
+
// On macOS/Linux we can read from /proc/net/dev or use system commands
|
|
31
|
+
let totalBytesReceived = 0;
|
|
32
|
+
let totalBytesSent = 0;
|
|
33
|
+
|
|
34
|
+
if (process.platform === 'darwin') {
|
|
35
|
+
// macOS - use netstat command
|
|
36
|
+
const { execSync } = await import('child_process');
|
|
37
|
+
try {
|
|
38
|
+
const output = execSync('netstat -ib', { encoding: 'utf8', timeout: 1000 });
|
|
39
|
+
const lines = output.split('\n');
|
|
40
|
+
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
const parts = line.trim().split(/\s+/);
|
|
43
|
+
if (parts.length >= 7 && parts[0] !== 'Name') {
|
|
44
|
+
const ibytes = parseInt(parts[6]) || 0;
|
|
45
|
+
const obytes = parseInt(parts[9]) || 0;
|
|
46
|
+
totalBytesReceived += ibytes;
|
|
47
|
+
totalBytesSent += obytes;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger.debug('Failed to get network stats from netstat', { error: error.message });
|
|
52
|
+
}
|
|
53
|
+
} else if (process.platform === 'linux') {
|
|
54
|
+
// Linux - read from /proc/net/dev
|
|
55
|
+
const fs = await import('fs');
|
|
56
|
+
try {
|
|
57
|
+
const netDev = fs.readFileSync('/proc/net/dev', 'utf8');
|
|
58
|
+
const lines = netDev.split('\n');
|
|
59
|
+
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
if (line.includes(':')) {
|
|
62
|
+
const parts = line.split(':')[1].trim().split(/\s+/);
|
|
63
|
+
if (parts.length >= 9) {
|
|
64
|
+
totalBytesReceived += parseInt(parts[0]) || 0;
|
|
65
|
+
totalBytesSent += parseInt(parts[8]) || 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.debug('Failed to read /proc/net/dev', { error: error.message });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const currentStats = {
|
|
75
|
+
bytesReceived: totalBytesReceived,
|
|
76
|
+
bytesSent: totalBytesSent,
|
|
77
|
+
timestamp: Date.now()
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Calculate deltas (bytes per second)
|
|
81
|
+
let bytesReceivedPerSec = 0;
|
|
82
|
+
let bytesSentPerSec = 0;
|
|
83
|
+
|
|
84
|
+
if (this.lastNetworkStats) {
|
|
85
|
+
const timeDelta = (currentStats.timestamp - this.lastNetworkStats.timestamp) / 1000; // seconds
|
|
86
|
+
if (timeDelta > 0) {
|
|
87
|
+
bytesReceivedPerSec = (currentStats.bytesReceived - this.lastNetworkStats.bytesReceived) / timeDelta;
|
|
88
|
+
bytesSentPerSec = (currentStats.bytesSent - this.lastNetworkStats.bytesSent) / timeDelta;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.lastNetworkStats = currentStats;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
network: {
|
|
96
|
+
bytesReceived: currentStats.bytesReceived,
|
|
97
|
+
bytesSent: currentStats.bytesSent,
|
|
98
|
+
bytesReceivedPerSec,
|
|
99
|
+
bytesSentPerSec,
|
|
100
|
+
mbReceivedPerSec: bytesReceivedPerSec / (1024 * 1024),
|
|
101
|
+
mbSentPerSec: bytesSentPerSec / (1024 * 1024)
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
logger.warn('Failed to get network metrics', { error: error.message });
|
|
106
|
+
return {
|
|
107
|
+
network: {
|
|
108
|
+
bytesReceived: 0,
|
|
109
|
+
bytesSent: 0,
|
|
110
|
+
bytesReceivedPerSec: 0,
|
|
111
|
+
bytesSentPerSec: 0,
|
|
112
|
+
mbReceivedPerSec: 0,
|
|
113
|
+
mbSentPerSec: 0
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get current system-wide CPU and memory metrics
|
|
121
|
+
*/
|
|
122
|
+
async getSystemMetrics() {
|
|
123
|
+
const totalMem = os.totalmem();
|
|
124
|
+
const freeMem = os.freemem();
|
|
125
|
+
const usedMem = totalMem - freeMem;
|
|
126
|
+
|
|
127
|
+
// Get CPU info
|
|
128
|
+
const cpus = os.cpus();
|
|
129
|
+
|
|
130
|
+
// Calculate average CPU usage across all cores
|
|
131
|
+
let totalIdle = 0;
|
|
132
|
+
let totalTick = 0;
|
|
133
|
+
|
|
134
|
+
cpus.forEach(cpu => {
|
|
135
|
+
for (const type in cpu.times) {
|
|
136
|
+
totalTick += cpu.times[type];
|
|
137
|
+
}
|
|
138
|
+
totalIdle += cpu.times.idle;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
system: {
|
|
143
|
+
totalMemory: totalMem,
|
|
144
|
+
freeMemory: freeMem,
|
|
145
|
+
usedMemory: usedMem,
|
|
146
|
+
memoryUsagePercent: (usedMem / totalMem) * 100,
|
|
147
|
+
cpuCount: cpus.length,
|
|
148
|
+
totalIdle,
|
|
149
|
+
totalTick
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get process-specific CPU and memory metrics
|
|
156
|
+
*/
|
|
157
|
+
async getProcessMetrics() {
|
|
158
|
+
try {
|
|
159
|
+
const stats = await pidusage(this.pid);
|
|
160
|
+
return {
|
|
161
|
+
process: {
|
|
162
|
+
cpu: stats.cpu, // CPU usage percentage
|
|
163
|
+
memory: stats.memory, // Memory in bytes
|
|
164
|
+
ppid: stats.ppid,
|
|
165
|
+
pid: stats.pid,
|
|
166
|
+
ctime: stats.ctime,
|
|
167
|
+
elapsed: stats.elapsed,
|
|
168
|
+
timestamp: stats.timestamp
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
} catch (error) {
|
|
172
|
+
logger.warn('Failed to get process metrics', { error: error.message });
|
|
173
|
+
return {
|
|
174
|
+
process: {
|
|
175
|
+
cpu: 0,
|
|
176
|
+
memory: 0,
|
|
177
|
+
pid: this.pid
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get top processes by CPU and memory usage
|
|
185
|
+
*/
|
|
186
|
+
async getTopProcessesData() {
|
|
187
|
+
try {
|
|
188
|
+
// Get top 10 processes using cross-platform implementation
|
|
189
|
+
const topProcs = await getTopProcesses(10);
|
|
190
|
+
|
|
191
|
+
if (!topProcs || topProcs.length === 0) {
|
|
192
|
+
return {
|
|
193
|
+
topProcesses: [],
|
|
194
|
+
totalProcesses: 0
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Get detailed stats using pidusage for each process
|
|
199
|
+
const detailedStats = [];
|
|
200
|
+
for (const proc of topProcs) {
|
|
201
|
+
try {
|
|
202
|
+
const stats = await pidusage(proc.pid);
|
|
203
|
+
detailedStats.push({
|
|
204
|
+
pid: proc.pid,
|
|
205
|
+
name: proc.name,
|
|
206
|
+
cpu: stats.cpu,
|
|
207
|
+
memory: stats.memory,
|
|
208
|
+
ppid: stats.ppid,
|
|
209
|
+
ctime: stats.ctime,
|
|
210
|
+
elapsed: stats.elapsed
|
|
211
|
+
});
|
|
212
|
+
} catch (error) {
|
|
213
|
+
// Process might have exited, use basic data from ps/PowerShell
|
|
214
|
+
logger.debug('Failed to get detailed stats for process, using basic data', {
|
|
215
|
+
pid: proc.pid,
|
|
216
|
+
error: error.message
|
|
217
|
+
});
|
|
218
|
+
detailedStats.push({
|
|
219
|
+
pid: proc.pid,
|
|
220
|
+
name: proc.name,
|
|
221
|
+
cpu: proc.cpu || 0,
|
|
222
|
+
memory: proc.memBytes || (proc.mem || 0) * 1024 * 1024, // Convert % to rough bytes or use WS
|
|
223
|
+
ppid: 0,
|
|
224
|
+
ctime: 0,
|
|
225
|
+
elapsed: 0
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Already sorted by CPU from getTopProcesses
|
|
231
|
+
return {
|
|
232
|
+
topProcesses: detailedStats,
|
|
233
|
+
totalProcesses: topProcs.length
|
|
234
|
+
};
|
|
235
|
+
} catch (error) {
|
|
236
|
+
logger.warn('Failed to get top processes', { error: error.message });
|
|
237
|
+
return {
|
|
238
|
+
topProcesses: [],
|
|
239
|
+
totalProcesses: 0
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Collect a performance sample (with top processes)
|
|
246
|
+
*/
|
|
247
|
+
async collectSample() {
|
|
248
|
+
const timestamp = Date.now();
|
|
249
|
+
const elapsedMs = this.startTime ? timestamp - this.startTime : 0;
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
// Collect all metrics including top processes
|
|
253
|
+
const [systemMetrics, processMetrics, networkMetrics, topProcessesData] = await Promise.all([
|
|
254
|
+
this.getSystemMetrics(),
|
|
255
|
+
this.getProcessMetrics(),
|
|
256
|
+
this.getNetworkMetrics(),
|
|
257
|
+
this.getTopProcessesData()
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
const sample = {
|
|
261
|
+
timestamp,
|
|
262
|
+
elapsedMs,
|
|
263
|
+
...systemMetrics,
|
|
264
|
+
...processMetrics,
|
|
265
|
+
...networkMetrics,
|
|
266
|
+
...topProcessesData
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
this.samples.push(sample);
|
|
270
|
+
|
|
271
|
+
// Save sample to file immediately
|
|
272
|
+
if (this.performanceFile) {
|
|
273
|
+
try {
|
|
274
|
+
fs.appendFileSync(this.performanceFile, JSON.stringify(sample) + '\n');
|
|
275
|
+
} catch (error) {
|
|
276
|
+
logger.warn('Failed to write performance sample to file', { error: error.message });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Log sample in verbose mode
|
|
281
|
+
logger.verbose('Performance sample collected', {
|
|
282
|
+
elapsedSeconds: (elapsedMs / 1000).toFixed(1),
|
|
283
|
+
systemMemoryUsage: `${sample.system.memoryUsagePercent.toFixed(1)}%`,
|
|
284
|
+
processMemoryMB: (sample.process.memory / (1024 * 1024)).toFixed(1),
|
|
285
|
+
processCPU: `${sample.process.cpu.toFixed(1)}%`,
|
|
286
|
+
networkIn: `${sample.network.mbReceivedPerSec.toFixed(2)} MB/s`,
|
|
287
|
+
networkOut: `${sample.network.mbSentPerSec.toFixed(2)} MB/s`,
|
|
288
|
+
topProcessesCount: sample.topProcesses?.length || 0
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
} catch (error) {
|
|
292
|
+
logger.warn('Failed to collect performance sample', { error: error.message });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Start tracking performance - lightweight version with system metrics only
|
|
298
|
+
*/
|
|
299
|
+
start(outputDir = null) {
|
|
300
|
+
if (this.interval) {
|
|
301
|
+
logger.warn('Performance tracking already started');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
this.startTime = Date.now();
|
|
306
|
+
this.samples = [];
|
|
307
|
+
|
|
308
|
+
// Set up performance data file
|
|
309
|
+
if (outputDir) {
|
|
310
|
+
this.performanceFile = path.join(outputDir, 'performance.jsonl');
|
|
311
|
+
// Clear any existing file
|
|
312
|
+
try {
|
|
313
|
+
if (fs.existsSync(this.performanceFile)) {
|
|
314
|
+
fs.unlinkSync(this.performanceFile);
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
logger.warn('Failed to clear performance file', { error: error.message });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
logger.info('Starting performance tracking (with top processes)', {
|
|
322
|
+
pid: this.pid,
|
|
323
|
+
monitorInterval: this.monitorInterval,
|
|
324
|
+
performanceFile: this.performanceFile
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Collect initial sample
|
|
328
|
+
this.collectSample();
|
|
329
|
+
|
|
330
|
+
// Start periodic collection every 5 seconds
|
|
331
|
+
this.interval = setInterval(() => {
|
|
332
|
+
this.collectSample();
|
|
333
|
+
}, this.monitorInterval);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Stop tracking and return summary
|
|
338
|
+
*/
|
|
339
|
+
stop() {
|
|
340
|
+
if (this.interval) {
|
|
341
|
+
clearInterval(this.interval);
|
|
342
|
+
this.interval = null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// If we have a performance file, read all samples from it
|
|
346
|
+
if (this.performanceFile && fs.existsSync(this.performanceFile)) {
|
|
347
|
+
try {
|
|
348
|
+
const fileContent = fs.readFileSync(this.performanceFile, 'utf8');
|
|
349
|
+
const lines = fileContent.trim().split('\n').filter(line => line.length > 0);
|
|
350
|
+
this.samples = lines.map(line => JSON.parse(line));
|
|
351
|
+
logger.info('Loaded performance samples from file', {
|
|
352
|
+
sampleCount: this.samples.length,
|
|
353
|
+
file: this.performanceFile
|
|
354
|
+
});
|
|
355
|
+
} catch (error) {
|
|
356
|
+
logger.warn('Failed to read performance samples from file', { error: error.message });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (this.samples.length === 0) {
|
|
361
|
+
logger.warn('No performance samples collected');
|
|
362
|
+
return {
|
|
363
|
+
samples: [],
|
|
364
|
+
summary: null
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Calculate summary statistics
|
|
369
|
+
const summary = this.calculateSummary();
|
|
370
|
+
|
|
371
|
+
logger.info('Performance tracking stopped', {
|
|
372
|
+
totalSamples: this.samples.length,
|
|
373
|
+
duration: summary.durationMs,
|
|
374
|
+
avgProcessCPU: `${summary.avgProcessCPU.toFixed(1)}%`,
|
|
375
|
+
avgProcessMemoryMB: summary.avgProcessMemoryMB.toFixed(1),
|
|
376
|
+
maxProcessCPU: `${summary.maxProcessCPU.toFixed(1)}%`,
|
|
377
|
+
maxProcessMemoryMB: summary.maxProcessMemoryMB.toFixed(1)
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const result = {
|
|
381
|
+
samples: this.samples,
|
|
382
|
+
summary
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// DON'T delete the performance file - keep it for the stop command to read
|
|
386
|
+
// The stop command or uploader will clean it up after reading
|
|
387
|
+
logger.debug('Keeping performance file for upload', { file: this.performanceFile });
|
|
388
|
+
|
|
389
|
+
// Reset in-memory state but keep file path for cleanup later
|
|
390
|
+
this.samples = [];
|
|
391
|
+
this.startTime = null;
|
|
392
|
+
// Don't reset this.performanceFile - caller may need it
|
|
393
|
+
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Calculate summary statistics from samples
|
|
399
|
+
*/
|
|
400
|
+
calculateSummary() {
|
|
401
|
+
if (this.samples.length === 0) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const firstSample = this.samples[0];
|
|
406
|
+
const lastSample = this.samples[this.samples.length - 1];
|
|
407
|
+
|
|
408
|
+
// Calculate averages and max values
|
|
409
|
+
let totalProcessCPU = 0;
|
|
410
|
+
let totalProcessMemory = 0;
|
|
411
|
+
let totalSystemMemoryUsage = 0;
|
|
412
|
+
let maxProcessCPU = 0;
|
|
413
|
+
let maxProcessMemory = 0;
|
|
414
|
+
let maxSystemMemoryUsage = 0;
|
|
415
|
+
|
|
416
|
+
this.samples.forEach(sample => {
|
|
417
|
+
const processCPU = sample.process.cpu || 0;
|
|
418
|
+
const processMemory = sample.process.memory || 0;
|
|
419
|
+
const systemMemoryUsage = sample.system.memoryUsagePercent || 0;
|
|
420
|
+
|
|
421
|
+
totalProcessCPU += processCPU;
|
|
422
|
+
totalProcessMemory += processMemory;
|
|
423
|
+
totalSystemMemoryUsage += systemMemoryUsage;
|
|
424
|
+
|
|
425
|
+
maxProcessCPU = Math.max(maxProcessCPU, processCPU);
|
|
426
|
+
maxProcessMemory = Math.max(maxProcessMemory, processMemory);
|
|
427
|
+
maxSystemMemoryUsage = Math.max(maxSystemMemoryUsage, systemMemoryUsage);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const count = this.samples.length;
|
|
431
|
+
|
|
432
|
+
// Calculate network totals from last sample
|
|
433
|
+
const finalSample = this.samples[this.samples.length - 1];
|
|
434
|
+
const totalBytesReceived = finalSample.network?.bytesReceived || 0;
|
|
435
|
+
const totalBytesSent = finalSample.network?.bytesSent || 0;
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
durationMs: lastSample.timestamp - firstSample.timestamp,
|
|
439
|
+
sampleCount: count,
|
|
440
|
+
monitorInterval: this.monitorInterval,
|
|
441
|
+
// Process metrics
|
|
442
|
+
avgProcessCPU: totalProcessCPU / count,
|
|
443
|
+
maxProcessCPU,
|
|
444
|
+
avgProcessMemoryBytes: totalProcessMemory / count,
|
|
445
|
+
avgProcessMemoryMB: (totalProcessMemory / count) / (1024 * 1024),
|
|
446
|
+
maxProcessMemoryBytes: maxProcessMemory,
|
|
447
|
+
maxProcessMemoryMB: maxProcessMemory / (1024 * 1024),
|
|
448
|
+
// System metrics
|
|
449
|
+
avgSystemMemoryUsagePercent: totalSystemMemoryUsage / count,
|
|
450
|
+
maxSystemMemoryUsagePercent: maxSystemMemoryUsage,
|
|
451
|
+
totalSystemMemoryBytes: firstSample.system.totalMemory,
|
|
452
|
+
totalSystemMemoryGB: firstSample.system.totalMemory / (1024 * 1024 * 1024),
|
|
453
|
+
// Network metrics
|
|
454
|
+
totalBytesReceived,
|
|
455
|
+
totalBytesSent,
|
|
456
|
+
totalMBReceived: totalBytesReceived / (1024 * 1024),
|
|
457
|
+
totalMBSent: totalBytesSent / (1024 * 1024)
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Check if tracking is active
|
|
463
|
+
*/
|
|
464
|
+
isTracking() {
|
|
465
|
+
return this.interval !== null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Cleanup performance file (call after upload)
|
|
470
|
+
*/
|
|
471
|
+
cleanup() {
|
|
472
|
+
if (this.performanceFile && fs.existsSync(this.performanceFile)) {
|
|
473
|
+
try {
|
|
474
|
+
fs.unlinkSync(this.performanceFile);
|
|
475
|
+
logger.debug('Cleaned up performance file', { file: this.performanceFile });
|
|
476
|
+
} catch (error) {
|
|
477
|
+
logger.warn('Failed to cleanup performance file', { error: error.message });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
this.performanceFile = null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Create singleton instance
|
|
485
|
+
const performanceTracker = new PerformanceTracker();
|
|
486
|
+
|
|
487
|
+
export { performanceTracker, PerformanceTracker };
|
package/lib/processManager.js
CHANGED
|
@@ -283,31 +283,50 @@ class ProcessManager {
|
|
|
283
283
|
|
|
284
284
|
logger.info('Killing background process', { pid });
|
|
285
285
|
|
|
286
|
-
//
|
|
287
|
-
//
|
|
286
|
+
// Try graceful shutdown first with SIGTERM (allows cleanup handlers to run)
|
|
287
|
+
// Then use SIGKILL if process doesn't exit
|
|
288
288
|
try {
|
|
289
|
-
process.kill(pid, '
|
|
290
|
-
logger.info('Sent
|
|
289
|
+
process.kill(pid, 'SIGTERM');
|
|
290
|
+
logger.info('Sent SIGTERM to background process');
|
|
291
291
|
} catch (error) {
|
|
292
|
-
logger.error('Failed to
|
|
292
|
+
logger.error('Failed to send SIGTERM to background process', { error });
|
|
293
293
|
throw new Error('Failed to stop background recording process');
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
// Wait
|
|
297
|
-
logger.debug('Waiting for background process to exit...');
|
|
298
|
-
const
|
|
296
|
+
// Wait for graceful shutdown
|
|
297
|
+
logger.debug('Waiting for background process to exit gracefully...');
|
|
298
|
+
const maxGracefulWait = 3000; // 3 seconds for graceful shutdown
|
|
299
299
|
const startWait = Date.now();
|
|
300
300
|
|
|
301
|
-
while (this.isProcessRunning(pid) && (Date.now() - startWait) <
|
|
301
|
+
while (this.isProcessRunning(pid) && (Date.now() - startWait) < maxGracefulWait) {
|
|
302
302
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
// If still running, force kill with SIGKILL
|
|
305
306
|
if (this.isProcessRunning(pid)) {
|
|
306
|
-
logger.
|
|
307
|
-
|
|
307
|
+
logger.warn('Background process did not exit gracefully, sending SIGKILL');
|
|
308
|
+
try {
|
|
309
|
+
process.kill(pid, 'SIGKILL');
|
|
310
|
+
logger.info('Sent SIGKILL to background process');
|
|
311
|
+
|
|
312
|
+
// Wait for SIGKILL to take effect
|
|
313
|
+
const maxKillWait = 2000; // 2 seconds should be plenty for SIGKILL
|
|
314
|
+
const killStart = Date.now();
|
|
315
|
+
while (this.isProcessRunning(pid) && (Date.now() - killStart) < maxKillWait) {
|
|
316
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (this.isProcessRunning(pid)) {
|
|
320
|
+
logger.error('Background process did not exit even after SIGKILL');
|
|
321
|
+
throw new Error('Failed to kill background process');
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
logger.error('Failed to SIGKILL background process', { error });
|
|
325
|
+
throw new Error('Failed to force kill background process');
|
|
326
|
+
}
|
|
308
327
|
}
|
|
309
328
|
|
|
310
|
-
logger.info('Background process
|
|
329
|
+
logger.info('Background process stopped');
|
|
311
330
|
|
|
312
331
|
// Mark status as completed
|
|
313
332
|
this.markStatusCompleted({
|