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,139 @@
1
+ # Performance Tracking
2
+
3
+ The Dashcam CLI now includes comprehensive performance tracking during recordings. This feature monitors CPU and memory usage throughout the recording session and includes the data in the log files.
4
+
5
+ ## What's Tracked
6
+
7
+ ### 1. Dashcam Process Metrics
8
+ - **CPU Usage**: Percentage of CPU used by the Dashcam process
9
+ - **Memory Usage**: Memory consumed by the Dashcam process in bytes and MB
10
+ - **Process Info**: PID, parent PID, CPU time, elapsed time
11
+
12
+ ### 2. System-Wide Metrics
13
+ - **Total Memory**: System total and free memory
14
+ - **Memory Usage Percentage**: Overall system memory utilization
15
+ - **CPU Count**: Number of CPU cores available
16
+
17
+ ### 3. Top Processes
18
+ - **Top 10 by CPU**: The most CPU-intensive processes running on the system
19
+ - **Top 10 by Memory**: The most memory-intensive processes running on the system
20
+ - **Process Details**: For each top process, tracks:
21
+ - Process name
22
+ - PID
23
+ - CPU usage percentage
24
+ - Memory usage in bytes
25
+ - Parent process ID
26
+ - CPU time and elapsed time
27
+
28
+ ## Sampling Frequency
29
+
30
+ Performance metrics are sampled every **1 second** (1000ms) during the recording session.
31
+
32
+ ## Output Format
33
+
34
+ Performance data is included in the recording result with the following structure:
35
+
36
+ ```javascript
37
+ {
38
+ performance: {
39
+ samples: [
40
+ {
41
+ timestamp: 1700000000000,
42
+ elapsedMs: 1000,
43
+ system: {
44
+ totalMemory: 17179869184,
45
+ freeMemory: 8589934592,
46
+ usedMemory: 8589934592,
47
+ memoryUsagePercent: 50.0,
48
+ cpuCount: 8
49
+ },
50
+ process: {
51
+ cpu: 15.5,
52
+ memory: 134217728,
53
+ pid: 12345,
54
+ ppid: 1,
55
+ ctime: 5000,
56
+ elapsed: 10000
57
+ },
58
+ topProcesses: [
59
+ {
60
+ pid: 54321,
61
+ name: "ffmpeg",
62
+ cpu: 85.2,
63
+ memory: 268435456,
64
+ ppid: 12345,
65
+ ctime: 8500,
66
+ elapsed: 10000
67
+ },
68
+ // ... up to 10 processes
69
+ ],
70
+ totalProcesses: 342
71
+ }
72
+ // ... one sample per second
73
+ ],
74
+ summary: {
75
+ durationMs: 10000,
76
+ sampleCount: 10,
77
+ monitorInterval: 1000,
78
+ avgProcessCPU: 12.3,
79
+ maxProcessCPU: 18.7,
80
+ avgProcessMemoryBytes: 134217728,
81
+ avgProcessMemoryMB: 128.0,
82
+ maxProcessMemoryBytes: 201326592,
83
+ maxProcessMemoryMB: 192.0,
84
+ avgSystemMemoryUsagePercent: 52.5,
85
+ maxSystemMemoryUsagePercent: 55.3,
86
+ totalSystemMemoryBytes: 17179869184,
87
+ totalSystemMemoryGB: 16.0
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ ## Summary Statistics
94
+
95
+ The performance tracker calculates summary statistics including:
96
+ - **Average and Max Process CPU**: How much CPU the Dashcam process used
97
+ - **Average and Max Process Memory**: Memory consumed by the Dashcam process
98
+ - **Average and Max System Memory Usage**: Overall system memory pressure
99
+ - **Total Duration**: How long the tracking ran
100
+ - **Sample Count**: Number of samples collected
101
+
102
+ ## Logging
103
+
104
+ Performance data is logged to the standard Dashcam log files:
105
+ - `~/.dashcam/logs/combined.log` - All log levels including performance samples
106
+ - `~/.dashcam/logs/debug.log` - Debug-level information
107
+ - Console output when running with `--verbose` flag
108
+
109
+ ## Testing
110
+
111
+ Run the performance tracking test:
112
+
113
+ ```bash
114
+ node test-performance-tracking.js
115
+ ```
116
+
117
+ This will:
118
+ 1. Start a 10-second recording
119
+ 2. Collect performance samples every second
120
+ 3. Display detailed performance statistics
121
+ 4. Show the top 10 processes at the end of the recording
122
+
123
+ ## Use Cases
124
+
125
+ Performance tracking helps with:
126
+ - **Debugging Performance Issues**: Identify when recordings are resource-intensive
127
+ - **Optimization**: Track the impact of code changes on resource usage
128
+ - **System Monitoring**: Understand what other processes are running during recordings
129
+ - **Troubleshooting**: Correlate performance issues with specific applications or system states
130
+ - **Capacity Planning**: Understand resource requirements for different recording scenarios
131
+
132
+ ## Implementation Details
133
+
134
+ The performance tracker uses:
135
+ - `pidusage` - For detailed per-process CPU and memory statistics
136
+ - `ps-list` - For listing all system processes
137
+ - `os` module - For system-wide memory and CPU information
138
+
139
+ The tracker runs in parallel with the recording and has minimal overhead (~1% CPU on average).
@@ -161,37 +161,64 @@ async function runBackgroundRecording() {
161
161
  }
162
162
  isShuttingDown = true;
163
163
 
164
- logger.info(`Received ${signal}, cleaning up child processes`);
165
- console.log('[Background] Received stop signal, cleaning up...');
164
+ logger.info(`Received ${signal}, stopping recording and collecting data`);
165
+ console.log('[Background] Received stop signal, stopping recording...');
166
166
 
167
- // Kill any child processes (ffmpeg, etc.)
168
167
  try {
169
- // Get all child processes and kill them
170
- const { exec } = await import('child_process');
171
- const platform = process.platform;
168
+ // Stop the recording properly to collect all tracking data
169
+ const { stopRecording } = await import('../lib/recorder.js');
170
+ const result = await stopRecording();
172
171
 
173
- if (platform === 'darwin' || platform === 'linux') {
174
- // On Unix, kill the entire process group
175
- exec(`pkill -P ${process.pid}`, (error) => {
176
- if (error && error.code !== 1) { // code 1 means no processes found
177
- logger.warn('Failed to kill child processes', { error: error.message });
178
- }
179
- logger.info('Child processes killed');
180
- });
181
- } else if (platform === 'win32') {
182
- // On Windows, use taskkill
183
- exec(`taskkill /F /T /PID ${process.pid}`, (error) => {
184
- if (error) {
185
- logger.warn('Failed to kill child processes on Windows', { error: error.message });
186
- }
187
- logger.info('Child processes killed');
188
- });
189
- }
172
+ logger.info('Recording stopped successfully', {
173
+ outputPath: result.outputPath,
174
+ duration: result.duration,
175
+ performanceSamples: result.performance?.summary?.sampleCount || 0
176
+ });
177
+
178
+ // Write the complete result to file for the stop command to read
179
+ const RECORDING_RESULT_FILE = path.join(PROCESS_DIR, 'recording-result.json');
180
+ fs.writeFileSync(RECORDING_RESULT_FILE, JSON.stringify({
181
+ ...result,
182
+ timestamp: Date.now()
183
+ }, null, 2));
184
+
185
+ logger.info('Saved recording result to file', {
186
+ path: RECORDING_RESULT_FILE,
187
+ performanceSamples: result.performance?.summary?.sampleCount || 0
188
+ });
189
+
190
+ // Update status to indicate recording is complete
191
+ writeStatus({
192
+ isRecording: false,
193
+ completedAt: Date.now(),
194
+ outputPath: result.outputPath,
195
+ duration: result.duration
196
+ });
190
197
 
191
- // Give it a moment to clean up
192
- await new Promise(resolve => setTimeout(resolve, 500));
193
198
  } catch (error) {
194
- logger.error('Error during cleanup', { error: error.message });
199
+ logger.error('Error stopping recording during shutdown', { error: error.message });
200
+
201
+ // Still try to kill child processes
202
+ try {
203
+ const { exec } = await import('child_process');
204
+ const platform = process.platform;
205
+
206
+ if (platform === 'darwin' || platform === 'linux') {
207
+ exec(`pkill -P ${process.pid}`, (error) => {
208
+ if (error && error.code !== 1) {
209
+ logger.warn('Failed to kill child processes', { error: error.message });
210
+ }
211
+ });
212
+ } else if (platform === 'win32') {
213
+ exec(`taskkill /F /T /PID ${process.pid}`, (error) => {
214
+ if (error) {
215
+ logger.warn('Failed to kill child processes on Windows', { error: error.message });
216
+ }
217
+ });
218
+ }
219
+ } catch (cleanupError) {
220
+ logger.error('Error during cleanup', { error: cleanupError.message });
221
+ }
195
222
  }
196
223
 
197
224
  logger.info('Background process exiting');
package/bin/dashcam.js CHANGED
@@ -25,6 +25,57 @@ if (!fs.existsSync(APP.recordingsDir)) {
25
25
  fs.mkdirSync(APP.recordingsDir, { recursive: true });
26
26
  }
27
27
 
28
+ // Handle graceful shutdown on terminal close or process termination
29
+ // This prevents orphaned FFmpeg processes and ensures clean exit
30
+ let isShuttingDown = false;
31
+
32
+ async function gracefulShutdown(signal) {
33
+ if (isShuttingDown) {
34
+ logger.debug('Shutdown already in progress, ignoring additional signal', { signal });
35
+ return;
36
+ }
37
+
38
+ isShuttingDown = true;
39
+ logger.info('Received shutdown signal, cleaning up...', { signal });
40
+
41
+ try {
42
+ // Check if there's an active recording and stop it
43
+ if (processManager.isRecordingActive()) {
44
+ logger.info('Stopping active recording before exit...');
45
+ await processManager.stopActiveRecording().catch(err => {
46
+ logger.warn('Failed to stop recording during shutdown', { error: err.message });
47
+ });
48
+ }
49
+
50
+ logger.info('Cleanup complete, exiting gracefully');
51
+ process.exit(0);
52
+ } catch (error) {
53
+ logger.error('Error during graceful shutdown', { error: error.message });
54
+ process.exit(1);
55
+ }
56
+ }
57
+
58
+ // Register signal handlers for graceful shutdown
59
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
60
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
61
+ process.on('SIGHUP', () => gracefulShutdown('SIGHUP'));
62
+
63
+ // Handle uncaught errors to prevent crashes
64
+ process.on('uncaughtException', (error) => {
65
+ logger.error('Uncaught exception', {
66
+ error: error.message,
67
+ stack: error.stack
68
+ });
69
+ gracefulShutdown('uncaughtException');
70
+ });
71
+
72
+ process.on('unhandledRejection', (reason, promise) => {
73
+ logger.error('Unhandled promise rejection', {
74
+ reason: reason instanceof Error ? reason.message : reason,
75
+ stack: reason instanceof Error ? reason.stack : undefined
76
+ });
77
+ });
78
+
28
79
  program
29
80
  .name('dashcam')
30
81
  .description('Capture the steps to reproduce every bug.')
@@ -469,6 +520,24 @@ program
469
520
  console.log('Recording stopped successfully');
470
521
  logger.debug('Stop result:', result);
471
522
 
523
+ // Try to read the recording result saved by the background process
524
+ // This includes performance data and other tracking information
525
+ const RECORDING_RESULT_FILE = path.join(os.homedir(), '.dashcam-cli', 'recording-result.json');
526
+ let backgroundResult = null;
527
+ if (fs.existsSync(RECORDING_RESULT_FILE)) {
528
+ try {
529
+ const resultData = fs.readFileSync(RECORDING_RESULT_FILE, 'utf8');
530
+ backgroundResult = JSON.parse(resultData);
531
+ logger.info('Loaded recording result from background process', {
532
+ outputPath: backgroundResult.outputPath,
533
+ duration: backgroundResult.duration,
534
+ performanceSamples: backgroundResult.performance?.summary?.sampleCount || 0
535
+ });
536
+ } catch (error) {
537
+ logger.warn('Failed to read background recording result', { error: error.message });
538
+ }
539
+ }
540
+
472
541
  // Reconstruct recording data from status and fix video with FFmpeg
473
542
  console.log('Processing recording...');
474
543
  logger.debug('Reconstructing recording data from status file');
@@ -660,6 +729,87 @@ program
660
729
  logger.warn('Failed to collect log tracking results', { error: error.message });
661
730
  }
662
731
 
732
+ // Read performance data from file if available
733
+ let performanceData = null;
734
+ try {
735
+ const performanceFile = path.join(recordingDir, 'performance.jsonl');
736
+ if (fs.existsSync(performanceFile)) {
737
+ logger.info('Found performance file, reading data', { file: performanceFile });
738
+
739
+ // Read the file directly and parse samples
740
+ const fileContent = fs.readFileSync(performanceFile, 'utf8');
741
+ const lines = fileContent.trim().split('\n').filter(line => line.length > 0);
742
+ const samples = lines.map(line => JSON.parse(line));
743
+
744
+ logger.info('Parsed performance samples from file', { sampleCount: samples.length });
745
+
746
+ // Calculate summary statistics
747
+ if (samples.length > 0) {
748
+ const firstSample = samples[0];
749
+ const lastSample = samples[samples.length - 1];
750
+
751
+ let totalProcessCPU = 0;
752
+ let totalProcessMemory = 0;
753
+ let totalSystemMemoryUsage = 0;
754
+ let maxProcessCPU = 0;
755
+ let maxProcessMemory = 0;
756
+ let maxSystemMemoryUsage = 0;
757
+
758
+ samples.forEach(sample => {
759
+ const processCPU = sample.process?.cpu || 0;
760
+ const processMemory = sample.process?.memory || 0;
761
+ const systemMemoryUsage = sample.system?.memoryUsagePercent || 0;
762
+
763
+ totalProcessCPU += processCPU;
764
+ totalProcessMemory += processMemory;
765
+ totalSystemMemoryUsage += systemMemoryUsage;
766
+
767
+ maxProcessCPU = Math.max(maxProcessCPU, processCPU);
768
+ maxProcessMemory = Math.max(maxProcessMemory, processMemory);
769
+ maxSystemMemoryUsage = Math.max(maxSystemMemoryUsage, systemMemoryUsage);
770
+ });
771
+
772
+ const count = samples.length;
773
+ const finalSample = samples[samples.length - 1];
774
+ const totalBytesReceived = finalSample.network?.bytesReceived || 0;
775
+ const totalBytesSent = finalSample.network?.bytesSent || 0;
776
+
777
+ const summary = {
778
+ durationMs: lastSample.timestamp - firstSample.timestamp,
779
+ sampleCount: count,
780
+ monitorInterval: 5000,
781
+ avgProcessCPU: totalProcessCPU / count,
782
+ maxProcessCPU,
783
+ avgProcessMemoryBytes: totalProcessMemory / count,
784
+ avgProcessMemoryMB: (totalProcessMemory / count) / (1024 * 1024),
785
+ maxProcessMemoryBytes: maxProcessMemory,
786
+ maxProcessMemoryMB: maxProcessMemory / (1024 * 1024),
787
+ avgSystemMemoryUsagePercent: totalSystemMemoryUsage / count,
788
+ maxSystemMemoryUsagePercent: maxSystemMemoryUsage,
789
+ totalSystemMemoryBytes: firstSample.system?.totalMemory || 0,
790
+ totalSystemMemoryGB: (firstSample.system?.totalMemory || 0) / (1024 * 1024 * 1024),
791
+ totalBytesReceived,
792
+ totalBytesSent,
793
+ totalMBReceived: totalBytesReceived / (1024 * 1024),
794
+ totalMBSent: totalBytesSent / (1024 * 1024)
795
+ };
796
+
797
+ performanceData = { samples, summary };
798
+
799
+ logger.info('Calculated performance summary', {
800
+ sampleCount: summary.sampleCount,
801
+ avgCPU: summary.avgProcessCPU.toFixed(1),
802
+ maxCPU: summary.maxProcessCPU.toFixed(1),
803
+ avgMemoryMB: summary.avgProcessMemoryMB.toFixed(1)
804
+ });
805
+ }
806
+ } else {
807
+ logger.debug('No performance file found', { expectedPath: performanceFile });
808
+ }
809
+ } catch (error) {
810
+ logger.warn('Failed to load performance data', { error: error.message, stack: error.stack });
811
+ }
812
+
663
813
  // The recording is on disk and can be uploaded with full metadata
664
814
  const recordingResult = {
665
815
  outputPath,
@@ -668,14 +818,22 @@ program
668
818
  duration: result.duration,
669
819
  fileSize: fs.statSync(outputPath).size,
670
820
  clientStartDate: activeStatus.startTime,
671
- apps: appTrackingResults.apps,
672
- icons: appTrackingResults.icons,
673
- logs: logTrackingResults,
821
+ apps: backgroundResult?.apps || appTrackingResults.apps,
822
+ icons: backgroundResult?.icons || appTrackingResults.icons,
823
+ logs: backgroundResult?.logs || logTrackingResults,
824
+ performance: performanceData || backgroundResult?.performance || null, // Prefer file data, fallback to background result
674
825
  title: activeStatus?.options?.title,
675
826
  description: activeStatus?.options?.description,
676
827
  project: activeStatus?.options?.project
677
828
  };
678
829
 
830
+ logger.info('Recording result prepared for upload', {
831
+ hasPerformanceData: !!recordingResult.performance,
832
+ performanceSamples: recordingResult.performance?.summary?.sampleCount || 0,
833
+ avgCPU: recordingResult.performance?.summary?.avgProcessCPU?.toFixed(1) || 'N/A',
834
+ maxCPU: recordingResult.performance?.summary?.maxProcessCPU?.toFixed(1) || 'N/A'
835
+ });
836
+
679
837
  if (!recordingResult || !recordingResult.outputPath) {
680
838
  console.error('Failed to process recording');
681
839
  logger.error('No recording result', { recordingResult });
@@ -695,6 +853,7 @@ program
695
853
  apps: recordingResult.apps,
696
854
  icons: recordingResult.icons,
697
855
  logs: recordingResult.logs,
856
+ performance: recordingResult.performance, // Include performance data
698
857
  gifPath: recordingResult.gifPath,
699
858
  snapshotPath: recordingResult.snapshotPath
700
859
  });
@@ -702,8 +861,19 @@ program
702
861
  console.log('Watch your recording:', uploadResult.shareLink);
703
862
  logger.info('Upload succeeded');
704
863
 
705
- // Clean up the result file
864
+ // Clean up the result file and recording result file
706
865
  processManager.cleanup();
866
+
867
+ // Also clean up recording result file from background process
868
+ const RECORDING_RESULT_FILE = path.join(os.homedir(), '.dashcam-cli', 'recording-result.json');
869
+ if (fs.existsSync(RECORDING_RESULT_FILE)) {
870
+ try {
871
+ fs.unlinkSync(RECORDING_RESULT_FILE);
872
+ logger.debug('Cleaned up recording result file');
873
+ } catch (error) {
874
+ logger.warn('Failed to clean up recording result file', { error: error.message });
875
+ }
876
+ }
707
877
  } catch (uploadError) {
708
878
  console.error('Upload failed:', uploadError.message);
709
879
  logger.error('Upload error details:', {
package/lib/auth.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import got from 'got';
2
- import { auth0Config } from './config.js';
2
+ import { auth0Config, API_ENDPOINT } from './config.js';
3
3
  import { logger, logFunctionCall } from './logger.js';
4
4
  import { Store } from './store.js';
5
5
 
@@ -14,11 +14,12 @@ const auth = {
14
14
  logger.info('Authenticating with API key');
15
15
  logger.verbose('Starting API key exchange', {
16
16
  apiKeyLength: apiKey?.length,
17
- hasApiKey: !!apiKey
17
+ hasApiKey: !!apiKey,
18
+ apiEndpoint: API_ENDPOINT
18
19
  });
19
20
 
20
21
  // Exchange API key for token
21
- const { token } = await got.post('https://testdriver-api.onrender.com/auth/exchange-api-key', {
22
+ const { token } = await got.post(`${API_ENDPOINT}/auth/exchange-api-key`, {
22
23
  json: { apiKey },
23
24
  timeout: 30000 // 30 second timeout
24
25
  }).json();
@@ -34,7 +35,7 @@ const auth = {
34
35
 
35
36
  // Get user info to verify the token works
36
37
  logger.debug('Fetching user information to validate token...');
37
- const user = await got.get('https://testdriver-api.onrender.com/api/v1/whoami', {
38
+ const user = await got.get(`${API_ENDPOINT}/api/v1/whoami`, {
38
39
  headers: {
39
40
  Authorization: `Bearer ${token}`
40
41
  },
@@ -105,7 +106,7 @@ const auth = {
105
106
  const token = await this.getToken();
106
107
 
107
108
  try {
108
- const response = await got.get('https://testdriver-api.onrender.com/api/v1/projects', {
109
+ const response = await got.get(`${API_ENDPOINT}/api/v1/projects`, {
109
110
  headers: {
110
111
  Authorization: `Bearer ${token}`
111
112
  },
@@ -160,7 +161,7 @@ const auth = {
160
161
  requestBody.project = replayData.project;
161
162
  }
162
163
 
163
- const response = await got.post('https://testdriver-api.onrender.com/api/v1/replay/upload', {
164
+ const response = await got.post(`${API_ENDPOINT}/api/v1/replay/upload`, {
164
165
  headers: {
165
166
  Authorization: `Bearer ${token}`
166
167
  },
@@ -188,7 +189,7 @@ const auth = {
188
189
  const token = await this.getToken();
189
190
 
190
191
  try {
191
- const response = await got.post('https://testdriver-api.onrender.com/api/v1/logs', {
192
+ const response = await got.post(`${API_ENDPOINT}/api/v1/logs`, {
192
193
  headers: {
193
194
  Authorization: `Bearer ${token}`
194
195
  },
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
- export const API_ENDPOINT = apiEndpoints[ENV];
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 = {