dashcam 1.4.3-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 +53 -26
- package/bin/dashcam.js +174 -4
- package/lib/auth.js +8 -7
- package/lib/config.js +2 -1
- package/lib/performanceTracker.js +487 -0
- package/lib/recorder.js +37 -9
- package/lib/topProcesses.js +128 -0
- package/lib/uploader.js +18 -6
- 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 +16 -1
|
@@ -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/recorder.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createGif, createSnapshot } from './ffmpeg.js';
|
|
|
4
4
|
import { applicationTracker } from './applicationTracker.js';
|
|
5
5
|
import { logsTrackerManager, trimLogs } from './logs/index.js';
|
|
6
6
|
import { getFfmpegPath, getFfprobePath } from './binaries.js';
|
|
7
|
+
import { performanceTracker } from './performanceTracker.js';
|
|
7
8
|
import path from 'path';
|
|
8
9
|
import os from 'os';
|
|
9
10
|
import fs from 'fs';
|
|
@@ -328,14 +329,14 @@ export async function startRecording({
|
|
|
328
329
|
const outputArgs = [
|
|
329
330
|
// Convert pixel format first to handle transparency issues on Windows
|
|
330
331
|
'-pix_fmt', 'yuv420p', // Force YUV420P format (no alpha channel)
|
|
331
|
-
'-c:v', 'libvpx', // Use
|
|
332
|
+
'-c:v', 'libvpx', // Use VP8 codec (not VP9) for lower CPU usage
|
|
332
333
|
'-quality', 'realtime', // Use realtime quality preset for faster encoding
|
|
333
334
|
'-cpu-used', '8', // Maximum speed (0-8, higher = faster but lower quality)
|
|
334
335
|
'-deadline', 'realtime',// Realtime encoding mode for lowest latency
|
|
335
|
-
'-b:v', '
|
|
336
|
+
'-b:v', '1M', // Reduced bitrate to 1M (from 2M) to lower CPU usage
|
|
336
337
|
'-r', fps.toString(), // Ensure output framerate matches input
|
|
337
|
-
'-g', fps.toString(), // Keyframe interval =
|
|
338
|
-
'-
|
|
338
|
+
'-g', (fps * 2).toString(), // Keyframe interval = 2 seconds (reduced frequency)
|
|
339
|
+
'-threads', '2', // Limit thread count to reduce CPU usage
|
|
339
340
|
'-auto-alt-ref', '0', // Disable auto alternate reference frames (fixes transparency encoding error)
|
|
340
341
|
// WebM options for more frequent disk writes and proper stream handling
|
|
341
342
|
'-f', 'webm', // Force WebM container format
|
|
@@ -343,7 +344,7 @@ export async function startRecording({
|
|
|
343
344
|
'-fflags', '+genpts', // Generate presentation timestamps
|
|
344
345
|
'-avoid_negative_ts', 'make_zero', // Avoid negative timestamps
|
|
345
346
|
'-vsync', '1', // Ensure every frame is encoded (CFR - constant frame rate)
|
|
346
|
-
'-max_muxing_queue_size', '
|
|
347
|
+
'-max_muxing_queue_size', '1024' // Reduced queue size to lower memory usage
|
|
347
348
|
];
|
|
348
349
|
|
|
349
350
|
if (includeAudio) {
|
|
@@ -443,6 +444,11 @@ export async function startRecording({
|
|
|
443
444
|
logger.debug('Starting application tracking...');
|
|
444
445
|
applicationTracker.start();
|
|
445
446
|
|
|
447
|
+
// Start performance tracking - pass output directory for file storage
|
|
448
|
+
logger.debug('Starting performance tracking...');
|
|
449
|
+
const outputDir = path.dirname(outputPath);
|
|
450
|
+
performanceTracker.start(outputDir);
|
|
451
|
+
|
|
446
452
|
// Start log tracking for this recording (don't await to avoid blocking)
|
|
447
453
|
const recorderId = path.basename(outputPath).replace('.webm', '');
|
|
448
454
|
logger.debug('Starting log tracking...', { recorderId });
|
|
@@ -717,7 +723,8 @@ export async function stopRecording() {
|
|
|
717
723
|
clientStartDate: recordingStartTime,
|
|
718
724
|
apps: applicationTracker.stop().apps,
|
|
719
725
|
icons: applicationTracker.stop().icons,
|
|
720
|
-
logs: await logsTrackerManager.stop({ recorderId: path.basename(outputPath).replace('.webm', ''), screenId: '1' })
|
|
726
|
+
logs: await logsTrackerManager.stop({ recorderId: path.basename(outputPath).replace('.webm', ''), screenId: '1' }),
|
|
727
|
+
performance: performanceTracker.stop()
|
|
721
728
|
};
|
|
722
729
|
|
|
723
730
|
currentRecording = null;
|
|
@@ -792,6 +799,10 @@ export async function stopRecording() {
|
|
|
792
799
|
logger.debug('Stopping application tracking...');
|
|
793
800
|
const appTrackingResults = applicationTracker.stop();
|
|
794
801
|
|
|
802
|
+
// Stop performance tracking and get results
|
|
803
|
+
logger.debug('Stopping performance tracking...');
|
|
804
|
+
const performanceResults = performanceTracker.stop();
|
|
805
|
+
|
|
795
806
|
// Stop log tracking and get results
|
|
796
807
|
const recorderId = path.basename(outputPath).replace('.webm', '');
|
|
797
808
|
logger.debug('Stopping log tracking...', { recorderId });
|
|
@@ -809,6 +820,11 @@ export async function stopRecording() {
|
|
|
809
820
|
logResults: {
|
|
810
821
|
trackers: logTrackingResults.length,
|
|
811
822
|
totalEvents: logTrackingResults.reduce((sum, result) => sum + result.count, 0)
|
|
823
|
+
},
|
|
824
|
+
performanceResults: {
|
|
825
|
+
sampleCount: performanceResults.summary?.sampleCount || 0,
|
|
826
|
+
avgProcessCPU: performanceResults.summary?.avgProcessCPU?.toFixed(1) || 0,
|
|
827
|
+
avgProcessMemoryMB: performanceResults.summary?.avgProcessMemoryMB?.toFixed(1) || 0
|
|
812
828
|
}
|
|
813
829
|
});
|
|
814
830
|
|
|
@@ -840,13 +856,19 @@ export async function stopRecording() {
|
|
|
840
856
|
clientStartDate: recordingStartTime, // Include the recording start timestamp
|
|
841
857
|
apps: appTrackingResults.apps, // Include tracked applications
|
|
842
858
|
icons: appTrackingResults.icons, // Include application icons metadata
|
|
843
|
-
logs: logTrackingResults // Include log tracking results
|
|
859
|
+
logs: logTrackingResults, // Include log tracking results
|
|
860
|
+
performance: performanceResults // Include performance tracking results
|
|
844
861
|
};
|
|
845
862
|
|
|
846
|
-
logger.info('Recording stopped with
|
|
863
|
+
logger.info('Recording stopped with performance data', {
|
|
847
864
|
clientStartDate: recordingStartTime,
|
|
848
865
|
clientStartDateReadable: new Date(recordingStartTime).toISOString(),
|
|
849
|
-
duration: result.duration
|
|
866
|
+
duration: result.duration,
|
|
867
|
+
performanceSamples: performanceResults.summary?.sampleCount || 0,
|
|
868
|
+
avgCPU: performanceResults.summary?.avgProcessCPU?.toFixed(1) || 0,
|
|
869
|
+
maxCPU: performanceResults.summary?.maxProcessCPU?.toFixed(1) || 0,
|
|
870
|
+
avgMemoryMB: performanceResults.summary?.avgProcessMemoryMB?.toFixed(1) || 0,
|
|
871
|
+
maxMemoryMB: performanceResults.summary?.maxProcessMemoryMB?.toFixed(1) || 0
|
|
850
872
|
});
|
|
851
873
|
|
|
852
874
|
currentRecording = null;
|
|
@@ -881,6 +903,12 @@ export async function stopRecording() {
|
|
|
881
903
|
|
|
882
904
|
// Stop application tracking on error
|
|
883
905
|
applicationTracker.stop();
|
|
906
|
+
|
|
907
|
+
// Stop performance tracking on error
|
|
908
|
+
if (performanceTracker.isTracking()) {
|
|
909
|
+
performanceTracker.stop();
|
|
910
|
+
}
|
|
911
|
+
|
|
884
912
|
throw error;
|
|
885
913
|
}
|
|
886
914
|
}
|