dashcam 1.4.3-beta → 1.4.5-beta.0
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,128 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { logger } from './logger.js';
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse ps output format: " PID %CPU %MEM COMMAND"
|
|
10
|
+
*/
|
|
11
|
+
function parsePsOutput(stdout, limit) {
|
|
12
|
+
const lines = stdout.trim().split('\n');
|
|
13
|
+
// Skip header line
|
|
14
|
+
return lines.slice(1, 1 + limit).map(line => {
|
|
15
|
+
const parts = line.trim().split(/\s+/, 4);
|
|
16
|
+
return {
|
|
17
|
+
pid: Number(parts[0]),
|
|
18
|
+
cpu: Number(parts[1]),
|
|
19
|
+
mem: Number(parts[2]),
|
|
20
|
+
name: parts[3] || ''
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get top processes on Unix-like systems (Linux, macOS)
|
|
27
|
+
*/
|
|
28
|
+
async function getTopProcessesUnix(limit = 10) {
|
|
29
|
+
try {
|
|
30
|
+
let stdout;
|
|
31
|
+
|
|
32
|
+
if (os.platform() === 'darwin') {
|
|
33
|
+
// macOS uses BSD ps - different syntax, no --sort option
|
|
34
|
+
// Use -r flag to sort by CPU usage
|
|
35
|
+
const result = await execFileAsync('ps', [
|
|
36
|
+
'-Arco',
|
|
37
|
+
'pid,pcpu,pmem,comm'
|
|
38
|
+
], { encoding: 'utf8' });
|
|
39
|
+
stdout = result.stdout;
|
|
40
|
+
} else {
|
|
41
|
+
// Linux uses GNU ps - supports --sort
|
|
42
|
+
const result = await execFileAsync('ps', [
|
|
43
|
+
'-eo',
|
|
44
|
+
'pid,pcpu,pmem,comm',
|
|
45
|
+
'--sort=-pcpu'
|
|
46
|
+
], { encoding: 'utf8' });
|
|
47
|
+
stdout = result.stdout;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return parsePsOutput(stdout, limit);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
logger.warn('Failed to get top processes on Unix', { error: error.message });
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse PowerShell JSON output for Windows processes
|
|
59
|
+
*/
|
|
60
|
+
function parsePsWinJson(stdout, limit) {
|
|
61
|
+
let arr;
|
|
62
|
+
try {
|
|
63
|
+
arr = JSON.parse(stdout);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
logger.warn('Failed to parse PowerShell JSON output', { error: e.message });
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!Array.isArray(arr)) {
|
|
70
|
+
arr = [arr];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Some fields may be undefined if CPU hasn't updated yet
|
|
74
|
+
arr.sort((a, b) => (b.CPU || 0) - (a.CPU || 0));
|
|
75
|
+
|
|
76
|
+
return arr.slice(0, limit).map(p => ({
|
|
77
|
+
pid: p.Id,
|
|
78
|
+
cpu: p.CPU || 0, // total CPU seconds
|
|
79
|
+
memBytes: p.WS || 0, // working set in bytes
|
|
80
|
+
name: p.ProcessName || ''
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get top processes on Windows using PowerShell
|
|
86
|
+
*/
|
|
87
|
+
async function getTopProcessesWindows(limit = 10) {
|
|
88
|
+
try {
|
|
89
|
+
// Use PowerShell to get process info and convert to JSON
|
|
90
|
+
// NOTE: PowerShell startup is slower but more reliable than WMI
|
|
91
|
+
const psCmd = [
|
|
92
|
+
'Get-Process | ',
|
|
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
|
+
|
|
105
|
+
return parsePsWinJson(stdout, limit);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
logger.warn('Failed to get top processes on Windows', { error: error.message });
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get top processes by CPU usage (cross-platform)
|
|
114
|
+
* @param {number} limit - Number of top processes to return (default: 10)
|
|
115
|
+
* @returns {Promise<Array>} Array of process objects with pid, cpu, mem/memBytes, and name
|
|
116
|
+
*/
|
|
117
|
+
export async function getTopProcesses(limit = 10) {
|
|
118
|
+
const platform = os.platform();
|
|
119
|
+
|
|
120
|
+
logger.debug('Getting top processes', { platform, limit });
|
|
121
|
+
|
|
122
|
+
if (platform === 'win32') {
|
|
123
|
+
return getTopProcessesWindows(limit);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Linux, macOS, and other Unix-like systems
|
|
127
|
+
return getTopProcessesUnix(limit);
|
|
128
|
+
}
|
package/lib/uploader.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'path';
|
|
|
6
6
|
import { auth } from './auth.js';
|
|
7
7
|
import got from 'got';
|
|
8
8
|
import { getSystemInfo } from './systemInfo.js';
|
|
9
|
+
import { API_ENDPOINT } from './config.js';
|
|
9
10
|
|
|
10
11
|
class Uploader {
|
|
11
12
|
constructor() {
|
|
@@ -250,7 +251,8 @@ export async function upload(filePath, metadata = {}) {
|
|
|
250
251
|
apps: metadata.apps && metadata.apps.length > 0 ? metadata.apps : ['Screen Recording'], // Use tracked apps or fallback
|
|
251
252
|
title: metadata.title || defaultTitle,
|
|
252
253
|
system: systemInfo, // Include system information
|
|
253
|
-
clientStartDate: metadata.clientStartDate || Date.now() // Use actual recording start time
|
|
254
|
+
clientStartDate: metadata.clientStartDate || Date.now(), // Use actual recording start time
|
|
255
|
+
performance: metadata.performance // Include performance tracking data
|
|
254
256
|
};
|
|
255
257
|
|
|
256
258
|
// Add project if we have one
|
|
@@ -262,18 +264,28 @@ export async function upload(filePath, metadata = {}) {
|
|
|
262
264
|
replayConfig.description = metadata.description;
|
|
263
265
|
}
|
|
264
266
|
|
|
265
|
-
logger.verbose('Creating replay with config',
|
|
267
|
+
logger.verbose('Creating replay with config', {
|
|
268
|
+
...replayConfig,
|
|
269
|
+
performanceSamples: replayConfig.performance?.samples?.length || 0,
|
|
270
|
+
performanceSummary: replayConfig.performance?.summary ? 'included' : 'none'
|
|
271
|
+
});
|
|
266
272
|
|
|
267
273
|
console.log('Creating replay on server...');
|
|
268
|
-
logger.info('Creating replay',
|
|
274
|
+
logger.info('Creating replay', {
|
|
275
|
+
title: replayConfig.title,
|
|
276
|
+
duration: replayConfig.duration,
|
|
277
|
+
apps: replayConfig.apps,
|
|
278
|
+
hasPerformanceData: !!replayConfig.performance,
|
|
279
|
+
performanceSamples: replayConfig.performance?.samples?.length || 0
|
|
280
|
+
});
|
|
269
281
|
|
|
270
282
|
// Create the replay first
|
|
271
283
|
const token = await auth.getToken();
|
|
272
284
|
|
|
273
285
|
let newReplay;
|
|
274
286
|
try {
|
|
275
|
-
logger.debug('Sending replay creation request...');
|
|
276
|
-
newReplay = await got.post(
|
|
287
|
+
logger.debug('Sending replay creation request...', { apiEndpoint: API_ENDPOINT });
|
|
288
|
+
newReplay = await got.post(`${API_ENDPOINT}/api/v1/replay`, {
|
|
277
289
|
headers: {
|
|
278
290
|
Authorization: `Bearer ${token}`
|
|
279
291
|
},
|
|
@@ -459,7 +471,7 @@ export async function upload(filePath, metadata = {}) {
|
|
|
459
471
|
console.log('Publishing replay...');
|
|
460
472
|
|
|
461
473
|
try {
|
|
462
|
-
await got.post(
|
|
474
|
+
await got.post(`${API_ENDPOINT}/api/v1/replay/publish`, {
|
|
463
475
|
headers: {
|
|
464
476
|
Authorization: `Bearer ${token}`
|
|
465
477
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dashcam",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.5-beta.0",
|
|
4
4
|
"description": "Minimal CLI version of Dashcam desktop app",
|
|
5
5
|
"main": "bin/dashcam.js",
|
|
6
6
|
"bin": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"mask-sensitive-data": "^0.11.5",
|
|
48
48
|
"node-fetch": "^2.7.0",
|
|
49
49
|
"open": "^9.1.0",
|
|
50
|
+
"pidusage": "^3.0.2",
|
|
50
51
|
"systeminformation": "^5.18.15",
|
|
51
52
|
"tail": "^2.2.6",
|
|
52
53
|
"winston": "^3.11.0",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { PerformanceTracker } from './lib/performanceTracker.js';
|
|
4
|
+
import { logger, setVerbose } from './lib/logger.js';
|
|
5
|
+
|
|
6
|
+
setVerbose(true);
|
|
7
|
+
|
|
8
|
+
async function testPerformanceTracking() {
|
|
9
|
+
console.log('Testing performance tracking with top processes...\n');
|
|
10
|
+
|
|
11
|
+
const tracker = new PerformanceTracker();
|
|
12
|
+
|
|
13
|
+
// Start tracking
|
|
14
|
+
tracker.start();
|
|
15
|
+
console.log('Started tracking...');
|
|
16
|
+
|
|
17
|
+
// Wait for 6 seconds to get at least one sample
|
|
18
|
+
console.log('Waiting 6 seconds for sample...');
|
|
19
|
+
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
20
|
+
|
|
21
|
+
// Stop tracking
|
|
22
|
+
const result = tracker.stop();
|
|
23
|
+
|
|
24
|
+
console.log('\n=== Performance Tracking Results ===');
|
|
25
|
+
console.log(`Total samples: ${result.samples.length}`);
|
|
26
|
+
|
|
27
|
+
if (result.samples.length > 0) {
|
|
28
|
+
const firstSample = result.samples[0];
|
|
29
|
+
console.log('\nFirst sample keys:', Object.keys(firstSample));
|
|
30
|
+
console.log('Has topProcesses:', !!firstSample.topProcesses);
|
|
31
|
+
console.log('topProcesses count:', firstSample.topProcesses?.length || 0);
|
|
32
|
+
|
|
33
|
+
if (firstSample.topProcesses && firstSample.topProcesses.length > 0) {
|
|
34
|
+
console.log('\nTop 3 processes:');
|
|
35
|
+
firstSample.topProcesses.slice(0, 3).forEach((proc, i) => {
|
|
36
|
+
console.log(`${i + 1}. ${proc.name} (PID: ${proc.pid})`);
|
|
37
|
+
console.log(` CPU: ${proc.cpu.toFixed(1)}%`);
|
|
38
|
+
console.log(` Memory: ${(proc.memory / (1024 * 1024)).toFixed(1)} MB`);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (result.summary) {
|
|
44
|
+
console.log('\nSummary:');
|
|
45
|
+
console.log(` Duration: ${(result.summary.durationMs / 1000).toFixed(1)}s`);
|
|
46
|
+
console.log(` Sample count: ${result.summary.sampleCount}`);
|
|
47
|
+
console.log(` Avg CPU: ${result.summary.avgProcessCPU.toFixed(1)}%`);
|
|
48
|
+
console.log(` Max CPU: ${result.summary.maxProcessCPU.toFixed(1)}%`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
testPerformanceTracking().catch(console.error);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test script to verify performance tracking during recording
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { startRecording, stopRecording } from './lib/recorder.js';
|
|
8
|
+
import { logger, setVerbose } from './lib/logger.js';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
|
|
11
|
+
// Enable verbose logging
|
|
12
|
+
setVerbose(true);
|
|
13
|
+
|
|
14
|
+
async function testPerformanceTracking() {
|
|
15
|
+
try {
|
|
16
|
+
logger.info('Starting performance tracking test...');
|
|
17
|
+
|
|
18
|
+
// Start recording
|
|
19
|
+
logger.info('Starting recording...');
|
|
20
|
+
const startResult = await startRecording({
|
|
21
|
+
fps: 10,
|
|
22
|
+
includeAudio: false
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
logger.info('Recording started', {
|
|
26
|
+
outputPath: startResult.outputPath,
|
|
27
|
+
startTime: startResult.startTime
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Record for 10 seconds
|
|
31
|
+
logger.info('Recording for 10 seconds...');
|
|
32
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
33
|
+
|
|
34
|
+
// Stop recording
|
|
35
|
+
logger.info('Stopping recording...');
|
|
36
|
+
const result = await stopRecording();
|
|
37
|
+
|
|
38
|
+
logger.info('Recording stopped successfully');
|
|
39
|
+
|
|
40
|
+
// Display performance results
|
|
41
|
+
if (result.performance) {
|
|
42
|
+
logger.info('=== Performance Tracking Results ===');
|
|
43
|
+
|
|
44
|
+
if (result.performance.summary) {
|
|
45
|
+
const summary = result.performance.summary;
|
|
46
|
+
logger.info('Summary Statistics:');
|
|
47
|
+
logger.info(` Duration: ${(summary.durationMs / 1000).toFixed(1)}s`);
|
|
48
|
+
logger.info(` Sample Count: ${summary.sampleCount}`);
|
|
49
|
+
logger.info(` Monitor Interval: ${summary.monitorInterval}ms`);
|
|
50
|
+
logger.info('');
|
|
51
|
+
logger.info('Process Metrics:');
|
|
52
|
+
logger.info(` Average CPU: ${summary.avgProcessCPU.toFixed(1)}%`);
|
|
53
|
+
logger.info(` Max CPU: ${summary.maxProcessCPU.toFixed(1)}%`);
|
|
54
|
+
logger.info(` Average Memory: ${summary.avgProcessMemoryMB.toFixed(1)} MB`);
|
|
55
|
+
logger.info(` Max Memory: ${summary.maxProcessMemoryMB.toFixed(1)} MB`);
|
|
56
|
+
logger.info('');
|
|
57
|
+
logger.info('System Metrics:');
|
|
58
|
+
logger.info(` Average System Memory Usage: ${summary.avgSystemMemoryUsagePercent.toFixed(1)}%`);
|
|
59
|
+
logger.info(` Max System Memory Usage: ${summary.maxSystemMemoryUsagePercent.toFixed(1)}%`);
|
|
60
|
+
logger.info(` Total System Memory: ${summary.totalSystemMemoryGB.toFixed(2)} GB`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (result.performance.samples && result.performance.samples.length > 0) {
|
|
64
|
+
logger.info('');
|
|
65
|
+
logger.info(`Collected ${result.performance.samples.length} performance samples`);
|
|
66
|
+
|
|
67
|
+
// Show top processes from the last sample
|
|
68
|
+
const lastSample = result.performance.samples[result.performance.samples.length - 1];
|
|
69
|
+
if (lastSample.topProcesses && lastSample.topProcesses.length > 0) {
|
|
70
|
+
logger.info('');
|
|
71
|
+
logger.info('Top 10 Processes (from last sample):');
|
|
72
|
+
lastSample.topProcesses.forEach((proc, index) => {
|
|
73
|
+
logger.info(` ${index + 1}. ${proc.name} (PID: ${proc.pid})`);
|
|
74
|
+
logger.info(` CPU: ${proc.cpu.toFixed(1)}%, Memory: ${(proc.memory / (1024 * 1024)).toFixed(1)} MB`);
|
|
75
|
+
});
|
|
76
|
+
logger.info(`Total processes on system: ${lastSample.totalProcesses}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
logger.warn('No performance data in result');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Display other results
|
|
84
|
+
logger.info('');
|
|
85
|
+
logger.info('=== Recording Results ===');
|
|
86
|
+
logger.info(`Output: ${result.outputPath}`);
|
|
87
|
+
logger.info(`Duration: ${result.duration}ms`);
|
|
88
|
+
logger.info(`File Size: ${(result.fileSize / 1024 / 1024).toFixed(2)} MB`);
|
|
89
|
+
logger.info(`Apps Tracked: ${result.apps?.length || 0}`);
|
|
90
|
+
logger.info(`Log Events: ${result.logs?.reduce((sum, log) => sum + log.count, 0) || 0}`);
|
|
91
|
+
|
|
92
|
+
// Verify file exists
|
|
93
|
+
if (fs.existsSync(result.outputPath)) {
|
|
94
|
+
logger.info('Recording file created successfully');
|
|
95
|
+
} else {
|
|
96
|
+
logger.error('Recording file not found!');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
logger.info('Test completed successfully!');
|
|
100
|
+
|
|
101
|
+
} catch (error) {
|
|
102
|
+
logger.error('Test failed:', error);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Run the test
|
|
108
|
+
testPerformanceTracking();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { getTopProcesses } from './lib/topProcesses.js';
|
|
4
|
+
import { logger, setVerbose } from './lib/logger.js';
|
|
5
|
+
|
|
6
|
+
setVerbose(true);
|
|
7
|
+
|
|
8
|
+
async function test() {
|
|
9
|
+
console.log('Testing getTopProcesses...\n');
|
|
10
|
+
|
|
11
|
+
const processes = await getTopProcesses(10);
|
|
12
|
+
|
|
13
|
+
console.log(`Found ${processes.length} processes:\n`);
|
|
14
|
+
|
|
15
|
+
processes.forEach((proc, index) => {
|
|
16
|
+
console.log(`${index + 1}. ${proc.name} (PID: ${proc.pid})`);
|
|
17
|
+
console.log(` CPU: ${proc.cpu}%`);
|
|
18
|
+
console.log(` Memory: ${proc.mem || proc.memBytes}${proc.memBytes ? ' bytes' : '%'}`);
|
|
19
|
+
console.log();
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test().catch(console.error);
|
package/test_workflow.sh
CHANGED
|
@@ -115,8 +115,15 @@ sleep 2
|
|
|
115
115
|
echo ""
|
|
116
116
|
echo "6. Stopping recording and uploading..."
|
|
117
117
|
# Check if recording is still active
|
|
118
|
+
SHARE_LINK=""
|
|
118
119
|
if ./bin/dashcam.js status | grep -q "Recording in progress"; then
|
|
119
|
-
|
|
120
|
+
# Capture the output to extract the share link
|
|
121
|
+
STOP_OUTPUT=$(./bin/dashcam.js stop 2>&1)
|
|
122
|
+
echo "$STOP_OUTPUT"
|
|
123
|
+
|
|
124
|
+
# Extract the share link from the output
|
|
125
|
+
SHARE_LINK=$(echo "$STOP_OUTPUT" | grep -oE "https?://[^\s]+" | head -1)
|
|
126
|
+
|
|
120
127
|
echo "✅ Recording stopped and uploaded"
|
|
121
128
|
else
|
|
122
129
|
echo "⚠️ Recording already completed (this is expected with background mode)"
|
|
@@ -134,6 +141,14 @@ echo ""
|
|
|
134
141
|
echo "📊 Final Status:"
|
|
135
142
|
./bin/dashcam.js status
|
|
136
143
|
|
|
144
|
+
# Open the recording in browser if we got a share link
|
|
145
|
+
if [ -n "$SHARE_LINK" ]; then
|
|
146
|
+
echo ""
|
|
147
|
+
echo "🌐 Opening recording in browser..."
|
|
148
|
+
echo "Link: $SHARE_LINK"
|
|
149
|
+
open "$SHARE_LINK"
|
|
150
|
+
fi
|
|
151
|
+
|
|
137
152
|
echo ""
|
|
138
153
|
echo "╔════════════════════════════════════════════════════════════════╗"
|
|
139
154
|
echo "║ SYNC VERIFICATION GUIDE ║"
|