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.
@@ -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 VP9 codec for better quality and compression
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', '2M', // Target bitrate
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 = 1 second (every fps frames) - ensures frequent keyframes
338
- '-force_key_frames', `expr:gte(t,n_forced*1)`, // Force keyframe every 1 second
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', '9999' // Large queue to prevent frame drops
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 clientStartDate', {
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
  }