momodebug 1.0.0 → 1.4.2
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/package.json +1 -1
- package/public/css/style.css +87 -2
- package/public/js/dashboard.js +146 -9
- package/src/adbObserver.js +3 -1
- package/src/index.js +5 -6
- package/src/iosDeviceObserver.js +53 -4
- package/src/iosObserver.js +57 -6
- package/src/views/dashboard.ejs +56 -12
- package/src/websocket.js +4 -31
package/package.json
CHANGED
package/public/css/style.css
CHANGED
|
@@ -94,10 +94,57 @@ nav {
|
|
|
94
94
|
|
|
95
95
|
/* Dashboard UI */
|
|
96
96
|
.dashboard-header {
|
|
97
|
+
display: flex;
|
|
98
|
+
justify-content: space-between;
|
|
99
|
+
align-items: flex-start;
|
|
100
|
+
gap: 1rem;
|
|
101
|
+
margin-bottom: 2.5rem;
|
|
102
|
+
flex-wrap: wrap;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.header-left {
|
|
97
106
|
display: flex;
|
|
98
107
|
flex-direction: column;
|
|
99
108
|
gap: 0.5rem;
|
|
100
|
-
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.header-right {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
gap: 0.75rem;
|
|
115
|
+
flex-wrap: wrap;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.device-dropdown {
|
|
119
|
+
background: var(--card-bg);
|
|
120
|
+
color: var(--text-primary);
|
|
121
|
+
border: 1px solid var(--card-border);
|
|
122
|
+
border-radius: 8px;
|
|
123
|
+
padding: 0.6rem 1rem;
|
|
124
|
+
font-size: 0.9rem;
|
|
125
|
+
font-weight: 500;
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
min-width: 180px;
|
|
128
|
+
transition: all 0.3s ease;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.device-dropdown:hover {
|
|
132
|
+
border-color: var(--accent-purple);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.device-dropdown:focus {
|
|
136
|
+
outline: none;
|
|
137
|
+
border-color: var(--accent-purple);
|
|
138
|
+
box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.2);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.device-badge {
|
|
142
|
+
background: rgba(34, 197, 94, 0.15);
|
|
143
|
+
color: #4ade80;
|
|
144
|
+
padding: 0.4rem 0.8rem;
|
|
145
|
+
border-radius: 20px;
|
|
146
|
+
font-size: 0.75rem;
|
|
147
|
+
font-weight: 700;
|
|
101
148
|
}
|
|
102
149
|
|
|
103
150
|
.live-status {
|
|
@@ -153,7 +200,7 @@ nav {
|
|
|
153
200
|
padding: 1.75rem;
|
|
154
201
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
155
202
|
position: relative;
|
|
156
|
-
overflow:
|
|
203
|
+
overflow: visible;
|
|
157
204
|
display: flex;
|
|
158
205
|
flex-direction: column;
|
|
159
206
|
}
|
|
@@ -181,6 +228,44 @@ nav {
|
|
|
181
228
|
margin-bottom: 1rem;
|
|
182
229
|
}
|
|
183
230
|
|
|
231
|
+
.info-icon {
|
|
232
|
+
cursor: help;
|
|
233
|
+
opacity: 0.5;
|
|
234
|
+
font-size: 0.9rem;
|
|
235
|
+
position: relative;
|
|
236
|
+
transition: opacity 0.2s ease;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.info-icon:hover {
|
|
240
|
+
opacity: 1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.info-icon::after {
|
|
244
|
+
content: attr(data-tooltip);
|
|
245
|
+
position: absolute;
|
|
246
|
+
bottom: 125%;
|
|
247
|
+
right: 0;
|
|
248
|
+
background: rgba(0, 0, 0, 0.9);
|
|
249
|
+
color: #fff;
|
|
250
|
+
padding: 0.75rem 1rem;
|
|
251
|
+
border-radius: 8px;
|
|
252
|
+
font-size: 0.8rem;
|
|
253
|
+
font-weight: 400;
|
|
254
|
+
white-space: nowrap;
|
|
255
|
+
max-width: 280px;
|
|
256
|
+
white-space: normal;
|
|
257
|
+
opacity: 0;
|
|
258
|
+
visibility: hidden;
|
|
259
|
+
transition: all 0.2s ease;
|
|
260
|
+
z-index: 9999;
|
|
261
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.info-icon:hover::after {
|
|
265
|
+
opacity: 1;
|
|
266
|
+
visibility: visible;
|
|
267
|
+
}
|
|
268
|
+
|
|
184
269
|
.metric-title {
|
|
185
270
|
color: var(--text-secondary);
|
|
186
271
|
font-size: 0.9rem;
|
package/public/js/dashboard.js
CHANGED
|
@@ -2,20 +2,29 @@ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
|
2
2
|
const ws = new WebSocket(`${protocol}//${window.location.host}?dashboard=true`);
|
|
3
3
|
|
|
4
4
|
const charts = {};
|
|
5
|
+
const deviceLastSeen = {}; // Track last activity time for each device
|
|
6
|
+
let selectedDevice = null; // null means no device selected yet
|
|
7
|
+
|
|
8
|
+
const DEVICE_TIMEOUT_MS = 5000; // Consider device disconnected after 5 seconds of inactivity
|
|
5
9
|
|
|
6
10
|
const metricConfigs = {
|
|
7
|
-
'
|
|
8
|
-
'memory_mb': { label: 'Memory (PSS)', color: '#3b82f6' },
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
11
|
+
'cpu_usage': { label: 'CPU Usage', color: '#a855f7', desc: 'Phần trăm CPU đang được sử dụng bởi app' },
|
|
12
|
+
'memory_mb': { label: 'Memory (PSS)', color: '#3b82f6', desc: 'Proportional Set Size - tổng bộ nhớ app chiếm dụng (bao gồm shared memory)' },
|
|
13
|
+
'heap_mb': { label: 'Heap Memory', color: '#8b5cf6', desc: 'Bộ nhớ Java/Kotlin Heap được cấp phát (chỉ Android)' },
|
|
14
|
+
'frame_ms': { label: 'Frame Time', color: '#06b6d4', desc: 'Thời gian render mỗi frame, càng thấp càng mượt' },
|
|
15
|
+
'fps': { label: 'FPS', color: '#22c55e', desc: 'Số frame hiển thị mỗi giây, mục tiêu 60fps' },
|
|
16
|
+
'jank_percent': { label: 'Jank', color: '#f97316', desc: 'Phần trăm frame bị drop/giật (chỉ Android)' },
|
|
17
|
+
'disk_read_kbps': { label: 'Disk Read', color: '#f59e0b', desc: 'Tốc độ đọc từ ổ đĩa' },
|
|
18
|
+
'disk_write_kbps': { label: 'Disk Write', color: '#ef4444', desc: 'Tốc độ ghi vào ổ đĩa' },
|
|
19
|
+
'network_total_mb': { label: 'Net Total', color: '#6366f1', desc: 'Tổng dữ liệu mạng đã truyền/nhận' }
|
|
14
20
|
};
|
|
15
21
|
|
|
16
22
|
// Initialize charts
|
|
17
23
|
Object.entries(metricConfigs).forEach(([key, config]) => {
|
|
18
|
-
const
|
|
24
|
+
const canvas = document.getElementById(`chart-${key}`);
|
|
25
|
+
if (!canvas) return;
|
|
26
|
+
|
|
27
|
+
const ctx = canvas.getContext('2d');
|
|
19
28
|
charts[key] = new Chart(ctx, {
|
|
20
29
|
type: 'line',
|
|
21
30
|
data: {
|
|
@@ -25,6 +34,8 @@ Object.entries(metricConfigs).forEach(([key, config]) => {
|
|
|
25
34
|
borderColor: config.color,
|
|
26
35
|
borderWidth: 2,
|
|
27
36
|
pointRadius: 0,
|
|
37
|
+
pointHoverRadius: 6,
|
|
38
|
+
pointHoverBackgroundColor: config.color,
|
|
28
39
|
fill: true,
|
|
29
40
|
backgroundColor: `${config.color}20`,
|
|
30
41
|
tension: 0.4
|
|
@@ -33,7 +44,25 @@ Object.entries(metricConfigs).forEach(([key, config]) => {
|
|
|
33
44
|
options: {
|
|
34
45
|
responsive: true,
|
|
35
46
|
maintainAspectRatio: false,
|
|
36
|
-
|
|
47
|
+
interaction: {
|
|
48
|
+
intersect: false,
|
|
49
|
+
mode: 'index'
|
|
50
|
+
},
|
|
51
|
+
plugins: {
|
|
52
|
+
legend: { display: false },
|
|
53
|
+
tooltip: {
|
|
54
|
+
enabled: true,
|
|
55
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
56
|
+
titleFont: { size: 12 },
|
|
57
|
+
bodyFont: { size: 14, weight: 'bold' },
|
|
58
|
+
padding: 10,
|
|
59
|
+
displayColors: false,
|
|
60
|
+
callbacks: {
|
|
61
|
+
title: () => config.label,
|
|
62
|
+
label: (context) => context.raw.toFixed(2)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
37
66
|
scales: {
|
|
38
67
|
x: { display: false },
|
|
39
68
|
y: {
|
|
@@ -46,6 +75,105 @@ Object.entries(metricConfigs).forEach(([key, config]) => {
|
|
|
46
75
|
});
|
|
47
76
|
});
|
|
48
77
|
|
|
78
|
+
// Device selector change handler
|
|
79
|
+
document.getElementById('device-selector').addEventListener('change', (e) => {
|
|
80
|
+
selectedDevice = e.target.value;
|
|
81
|
+
console.log(`📱 Watching device: ${selectedDevice}`);
|
|
82
|
+
resetCharts();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function resetCharts() {
|
|
86
|
+
// Reset all chart data and display values
|
|
87
|
+
Object.keys(metricConfigs).forEach(key => {
|
|
88
|
+
const chart = charts[key];
|
|
89
|
+
if (chart) {
|
|
90
|
+
chart.data.datasets[0].data = Array(20).fill(0);
|
|
91
|
+
chart.update('none');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const valElement = document.getElementById(`val-${key}`);
|
|
95
|
+
if (valElement) {
|
|
96
|
+
valElement.innerText = '--';
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function updateDeviceList(deviceClass) {
|
|
102
|
+
deviceLastSeen[deviceClass] = Date.now();
|
|
103
|
+
|
|
104
|
+
// Check if device is new
|
|
105
|
+
const selector = document.getElementById('device-selector');
|
|
106
|
+
const existingOption = Array.from(selector.options).find(opt => opt.value === deviceClass);
|
|
107
|
+
|
|
108
|
+
if (!existingOption) {
|
|
109
|
+
const option = document.createElement('option');
|
|
110
|
+
option.value = deviceClass;
|
|
111
|
+
option.textContent = deviceClass;
|
|
112
|
+
selector.appendChild(option);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Auto-select first device if no device is currently selected
|
|
116
|
+
if (selectedDevice === null || selectedDevice === '') {
|
|
117
|
+
selectedDevice = deviceClass;
|
|
118
|
+
selector.value = deviceClass;
|
|
119
|
+
console.log(`📱 Auto-selected device: ${deviceClass}`);
|
|
120
|
+
resetCharts();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
updateDeviceCount();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function updateDeviceCount() {
|
|
127
|
+
const activeDevices = Object.keys(deviceLastSeen).length;
|
|
128
|
+
document.getElementById('device-count').textContent = `${activeDevices} connected`;
|
|
129
|
+
|
|
130
|
+
// Show/hide awaiting message based on connected devices
|
|
131
|
+
const awaitingMessage = document.getElementById('awaiting-message');
|
|
132
|
+
if (awaitingMessage) {
|
|
133
|
+
awaitingMessage.style.display = activeDevices > 0 ? 'none' : 'block';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function checkInactiveDevices() {
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
const selector = document.getElementById('device-selector');
|
|
140
|
+
|
|
141
|
+
Object.keys(deviceLastSeen).forEach(deviceClass => {
|
|
142
|
+
if (now - deviceLastSeen[deviceClass] > DEVICE_TIMEOUT_MS) {
|
|
143
|
+
// Remove inactive device
|
|
144
|
+
delete deviceLastSeen[deviceClass];
|
|
145
|
+
|
|
146
|
+
// Remove from dropdown
|
|
147
|
+
const option = Array.from(selector.options).find(opt => opt.value === deviceClass);
|
|
148
|
+
if (option) {
|
|
149
|
+
option.remove();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// If the removed device was selected, select another available device
|
|
153
|
+
if (selectedDevice === deviceClass) {
|
|
154
|
+
const remainingDevices = Object.keys(deviceLastSeen);
|
|
155
|
+
if (remainingDevices.length > 0) {
|
|
156
|
+
selectedDevice = remainingDevices[0];
|
|
157
|
+
selector.value = selectedDevice;
|
|
158
|
+
console.log(`📱 Switched to device: ${selectedDevice}`);
|
|
159
|
+
} else {
|
|
160
|
+
selectedDevice = null;
|
|
161
|
+
selector.value = '';
|
|
162
|
+
console.log(`📱 No devices available`);
|
|
163
|
+
}
|
|
164
|
+
resetCharts();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(`❌ Device disconnected: ${deviceClass}`);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
updateDeviceCount();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check for inactive devices every 2 seconds
|
|
175
|
+
setInterval(checkInactiveDevices, 2000);
|
|
176
|
+
|
|
49
177
|
ws.onopen = () => {
|
|
50
178
|
console.log('✅ Connected to metrics server');
|
|
51
179
|
};
|
|
@@ -55,6 +183,15 @@ ws.onmessage = (event) => {
|
|
|
55
183
|
|
|
56
184
|
if (message.type === 'live_metric') {
|
|
57
185
|
const sample = message.data;
|
|
186
|
+
|
|
187
|
+
// Track connected devices
|
|
188
|
+
updateDeviceList(sample.deviceClass);
|
|
189
|
+
|
|
190
|
+
// Filter by selected device (only show data for the selected device)
|
|
191
|
+
if (selectedDevice === null || sample.deviceClass !== selectedDevice) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
58
195
|
const valElement = document.getElementById(`val-${sample.metric}`);
|
|
59
196
|
|
|
60
197
|
if (valElement) {
|
package/src/adbObserver.js
CHANGED
|
@@ -18,8 +18,10 @@ export function startAdbObserver() {
|
|
|
18
18
|
const mappings = {
|
|
19
19
|
'fps': 'fps',
|
|
20
20
|
'frameTimeMs': 'frame_ms',
|
|
21
|
-
'cpuUsagePercent': '
|
|
21
|
+
'cpuUsagePercent': 'cpu_usage',
|
|
22
22
|
'memoryPssMb': 'memory_mb',
|
|
23
|
+
'memoryHeapMb': 'heap_mb',
|
|
24
|
+
'jankPercent': 'jank_percent',
|
|
23
25
|
'diskReadKbps': 'disk_read_kbps',
|
|
24
26
|
'diskWriteKbps': 'disk_write_kbps',
|
|
25
27
|
'networkTotalMb': 'network_total_mb'
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
2
|
import express from 'express';
|
|
3
3
|
import { createServer } from 'http';
|
|
4
4
|
import { exec } from 'child_process';
|
|
@@ -73,15 +73,14 @@ setupWebSocket(httpServer);
|
|
|
73
73
|
// Seed initial data
|
|
74
74
|
seedMockData();
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
startIosDeviceObserver(); // iOS Real Device (idevicesyslog)
|
|
76
|
+
startAdbObserver();
|
|
77
|
+
startIosObserver();
|
|
78
|
+
startIosDeviceObserver();
|
|
80
79
|
|
|
81
80
|
// Start server
|
|
82
81
|
httpServer.listen(PORT, () => {
|
|
83
82
|
console.log(`\n🚀 Unified Performance Server running at http://localhost:${PORT}`);
|
|
84
|
-
|
|
83
|
+
|
|
85
84
|
|
|
86
85
|
// Auto-open browser
|
|
87
86
|
exec(`open http://localhost:${PORT}`);
|
package/src/iosDeviceObserver.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { broadcastToDashboards } from './websocket.js';
|
|
3
3
|
|
|
4
|
+
let currentLogProcess = null;
|
|
5
|
+
let lastMetricTime = Date.now();
|
|
6
|
+
let heartbeatInterval = null;
|
|
7
|
+
const HEARTBEAT_TIMEOUT_MS = 15000; // Restart stream if no data for 15 seconds
|
|
8
|
+
const HEARTBEAT_CHECK_MS = 5000; // Check every 5 seconds
|
|
9
|
+
|
|
4
10
|
export function startIosDeviceObserver() {
|
|
5
11
|
console.log('📱 Starting iOS Real Device Observer (idevicesyslog)...');
|
|
6
12
|
|
|
@@ -15,14 +21,49 @@ export function startIosDeviceObserver() {
|
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
startDeviceSyslog();
|
|
24
|
+
startHeartbeat();
|
|
18
25
|
});
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
function startHeartbeat() {
|
|
29
|
+
// Clear existing heartbeat
|
|
30
|
+
if (heartbeatInterval) {
|
|
31
|
+
clearInterval(heartbeatInterval);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
heartbeatInterval = setInterval(() => {
|
|
35
|
+
const timeSinceLastMetric = Date.now() - lastMetricTime;
|
|
36
|
+
|
|
37
|
+
// If we haven't received metrics for a while, restart idevicesyslog
|
|
38
|
+
// This handles cases where app was closed and reopened
|
|
39
|
+
if (timeSinceLastMetric > HEARTBEAT_TIMEOUT_MS && currentLogProcess) {
|
|
40
|
+
console.log('⏰ No iOS device metrics received for 15s, restarting idevicesyslog to capture new app session...');
|
|
41
|
+
restartDeviceSyslog();
|
|
42
|
+
}
|
|
43
|
+
}, HEARTBEAT_CHECK_MS);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function restartDeviceSyslog() {
|
|
47
|
+
if (currentLogProcess) {
|
|
48
|
+
currentLogProcess.kill('SIGTERM');
|
|
49
|
+
currentLogProcess = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Small delay before restarting
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
startDeviceSyslog();
|
|
55
|
+
}, 500);
|
|
56
|
+
}
|
|
57
|
+
|
|
21
58
|
function startDeviceSyslog() {
|
|
22
59
|
// Stream logs from connected iOS device
|
|
23
|
-
// Filter
|
|
60
|
+
// Filter by process name
|
|
24
61
|
const deviceLog = spawn('idevicesyslog', ['--process', 'ExampleApp']);
|
|
25
62
|
|
|
63
|
+
currentLogProcess = deviceLog;
|
|
64
|
+
// Reset last metric time when starting new stream
|
|
65
|
+
lastMetricTime = Date.now();
|
|
66
|
+
|
|
26
67
|
deviceLog.stdout.on('data', (data) => {
|
|
27
68
|
const output = data.toString();
|
|
28
69
|
const lines = output.split('\n');
|
|
@@ -37,11 +78,14 @@ function startDeviceSyslog() {
|
|
|
37
78
|
const jsonStr = match[1];
|
|
38
79
|
const rawData = JSON.parse(jsonStr);
|
|
39
80
|
|
|
81
|
+
// Update heartbeat timestamp
|
|
82
|
+
lastMetricTime = Date.now();
|
|
83
|
+
|
|
40
84
|
// Map fields (same as simulator)
|
|
41
85
|
const mappings = {
|
|
42
86
|
'fps': 'fps',
|
|
43
87
|
'frameTimeMs': 'frame_ms',
|
|
44
|
-
'cpuUsagePercent': '
|
|
88
|
+
'cpuUsagePercent': 'cpu_usage',
|
|
45
89
|
'memoryPssMb': 'memory_mb',
|
|
46
90
|
'diskReadKbps': 'disk_read_kbps',
|
|
47
91
|
'diskWriteKbps': 'disk_write_kbps',
|
|
@@ -77,7 +121,12 @@ function startDeviceSyslog() {
|
|
|
77
121
|
});
|
|
78
122
|
|
|
79
123
|
deviceLog.on('close', (code) => {
|
|
80
|
-
|
|
81
|
-
|
|
124
|
+
currentLogProcess = null;
|
|
125
|
+
|
|
126
|
+
if (code !== null) {
|
|
127
|
+
// Only restart if it wasn't intentionally killed
|
|
128
|
+
console.log(`📱 idevicesyslog exited with code ${code}. Restarting in 2s...`);
|
|
129
|
+
setTimeout(startDeviceSyslog, 2000);
|
|
130
|
+
}
|
|
82
131
|
});
|
|
83
132
|
}
|
package/src/iosObserver.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { spawn, exec } from 'child_process';
|
|
2
2
|
import { broadcastToDashboards } from './websocket.js';
|
|
3
3
|
|
|
4
|
+
let currentLogProcess = null;
|
|
5
|
+
let lastMetricTime = Date.now();
|
|
6
|
+
let heartbeatInterval = null;
|
|
7
|
+
const HEARTBEAT_TIMEOUT_MS = 15000; // Restart stream if no data for 15 seconds
|
|
8
|
+
const HEARTBEAT_CHECK_MS = 5000; // Check every 5 seconds
|
|
9
|
+
|
|
4
10
|
export function startIosObserver() {
|
|
5
11
|
console.log('🍎 Starting iOS Simulator Observer (PerfObserverSDK)...');
|
|
6
12
|
|
|
@@ -15,12 +21,43 @@ export function startIosObserver() {
|
|
|
15
21
|
|
|
16
22
|
console.log('✅ Found booted simulator, starting log stream...');
|
|
17
23
|
startLogStream();
|
|
24
|
+
startHeartbeat();
|
|
18
25
|
});
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
function startHeartbeat() {
|
|
29
|
+
// Clear existing heartbeat
|
|
30
|
+
if (heartbeatInterval) {
|
|
31
|
+
clearInterval(heartbeatInterval);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
heartbeatInterval = setInterval(() => {
|
|
35
|
+
const timeSinceLastMetric = Date.now() - lastMetricTime;
|
|
36
|
+
|
|
37
|
+
// If we haven't received metrics for a while, restart the log stream
|
|
38
|
+
// This handles cases where app was closed and reopened
|
|
39
|
+
if (timeSinceLastMetric > HEARTBEAT_TIMEOUT_MS && currentLogProcess) {
|
|
40
|
+
console.log('⏰ No iOS metrics received for 15s, restarting log stream to capture new app session...');
|
|
41
|
+
restartLogStream();
|
|
42
|
+
}
|
|
43
|
+
}, HEARTBEAT_CHECK_MS);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function restartLogStream() {
|
|
47
|
+
if (currentLogProcess) {
|
|
48
|
+
currentLogProcess.kill('SIGTERM');
|
|
49
|
+
currentLogProcess = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Small delay before restarting
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
startLogStream();
|
|
55
|
+
}, 500);
|
|
56
|
+
}
|
|
57
|
+
|
|
21
58
|
function startLogStream() {
|
|
22
59
|
// Command to stream logs from the booted simulator
|
|
23
|
-
// Filter by
|
|
60
|
+
// Filter by message containing METRIC_UPDATE (for NSLog output)
|
|
24
61
|
const iosLog = spawn('xcrun', [
|
|
25
62
|
'simctl',
|
|
26
63
|
'spawn',
|
|
@@ -28,9 +65,13 @@ function startLogStream() {
|
|
|
28
65
|
'log',
|
|
29
66
|
'stream',
|
|
30
67
|
'--predicate',
|
|
31
|
-
'
|
|
68
|
+
'eventMessage CONTAINS "METRIC_UPDATE"'
|
|
32
69
|
]);
|
|
33
70
|
|
|
71
|
+
currentLogProcess = iosLog;
|
|
72
|
+
// Reset last metric time when starting new stream
|
|
73
|
+
lastMetricTime = Date.now();
|
|
74
|
+
|
|
34
75
|
iosLog.stdout.on('data', (data) => {
|
|
35
76
|
const output = data.toString();
|
|
36
77
|
const lines = output.split('\n');
|
|
@@ -41,11 +82,14 @@ function startLogStream() {
|
|
|
41
82
|
const jsonStr = line.split('METRIC_UPDATE:')[1].trim();
|
|
42
83
|
const rawData = JSON.parse(jsonStr);
|
|
43
84
|
|
|
85
|
+
// Update heartbeat timestamp
|
|
86
|
+
lastMetricTime = Date.now();
|
|
87
|
+
|
|
44
88
|
// Map fields (same as Android for consistency)
|
|
45
89
|
const mappings = {
|
|
46
90
|
'fps': 'fps',
|
|
47
91
|
'frameTimeMs': 'frame_ms',
|
|
48
|
-
'cpuUsagePercent': '
|
|
92
|
+
'cpuUsagePercent': 'cpu_usage',
|
|
49
93
|
'memoryPssMb': 'memory_mb',
|
|
50
94
|
'diskReadKbps': 'disk_read_kbps',
|
|
51
95
|
'diskWriteKbps': 'disk_write_kbps',
|
|
@@ -81,12 +125,19 @@ function startLogStream() {
|
|
|
81
125
|
});
|
|
82
126
|
|
|
83
127
|
iosLog.on('close', (code) => {
|
|
128
|
+
currentLogProcess = null;
|
|
129
|
+
|
|
84
130
|
if (code === 148 || code === 1) {
|
|
85
131
|
console.log('⚠️ Simulator stopped or not available. Waiting for simulator...');
|
|
132
|
+
if (heartbeatInterval) {
|
|
133
|
+
clearInterval(heartbeatInterval);
|
|
134
|
+
heartbeatInterval = null;
|
|
135
|
+
}
|
|
86
136
|
setTimeout(startIosObserver, 30000);
|
|
87
|
-
} else {
|
|
88
|
-
|
|
89
|
-
|
|
137
|
+
} else if (code !== null) {
|
|
138
|
+
// Only restart if it wasn't intentionally killed
|
|
139
|
+
console.log(`📡 iOS log process exited with code ${code}. Restarting in 2s...`);
|
|
140
|
+
setTimeout(startLogStream, 2000);
|
|
90
141
|
}
|
|
91
142
|
});
|
|
92
143
|
|
package/src/views/dashboard.ejs
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
1
|
<div class="dashboard-header">
|
|
2
|
-
<div class="
|
|
3
|
-
<div class="
|
|
4
|
-
|
|
2
|
+
<div class="header-left">
|
|
3
|
+
<div class="live-status">
|
|
4
|
+
<div class="dot"></div>
|
|
5
|
+
<span>Live Streaming Active</span>
|
|
6
|
+
</div>
|
|
7
|
+
<p id="awaiting-message" style="color: var(--text-secondary); margin-top: 0.5rem;">Awaiting connections from
|
|
8
|
+
Android/iOS SDK...</p>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="header-right">
|
|
11
|
+
<label for="device-selector" style="color: var(--text-secondary); margin-right: 0.5rem;">Device:</label>
|
|
12
|
+
<select id="device-selector" class="device-dropdown">
|
|
13
|
+
<option value="" disabled selected>No Device</option>
|
|
14
|
+
</select>
|
|
15
|
+
<span id="device-count" class="device-badge">0 connected</span>
|
|
5
16
|
</div>
|
|
6
|
-
<p style="color: var(--text-secondary); margin-top: 0.5rem;">Awaiting connections from Android SDK...</p>
|
|
7
17
|
</div>
|
|
8
18
|
|
|
9
19
|
<div class="metrics-grid">
|
|
10
20
|
<!-- CPU Card -->
|
|
11
|
-
<div class="metric-card glass" id="card-
|
|
21
|
+
<div class="metric-card glass" id="card-cpu_usage">
|
|
12
22
|
<div class="metric-header">
|
|
13
|
-
<span class="metric-title">⚡ CPU
|
|
14
|
-
<
|
|
23
|
+
<span class="metric-title">⚡ CPU Usage</span>
|
|
24
|
+
<span class="info-icon" data-tooltip="Phần trăm CPU đang được sử dụng bởi app">ℹ️</span>
|
|
15
25
|
</div>
|
|
16
26
|
<div class="metric-value-container">
|
|
17
|
-
<span id="val-
|
|
18
|
-
<span class="metric-unit"
|
|
27
|
+
<span id="val-cpu_usage" class="metric-value">--</span>
|
|
28
|
+
<span class="metric-unit">%</span>
|
|
19
29
|
</div>
|
|
20
30
|
<div class="chart-container">
|
|
21
|
-
<canvas id="chart-
|
|
31
|
+
<canvas id="chart-cpu_usage"></canvas>
|
|
22
32
|
</div>
|
|
23
33
|
</div>
|
|
24
34
|
|
|
@@ -26,7 +36,7 @@
|
|
|
26
36
|
<div class="metric-card glass" id="card-memory_mb">
|
|
27
37
|
<div class="metric-header">
|
|
28
38
|
<span class="metric-title">💾 Memory (PSS)</span>
|
|
29
|
-
<
|
|
39
|
+
<span class="info-icon" data-tooltip="Proportional Set Size - tổng bộ nhớ app chiếm dụng">ℹ️</span>
|
|
30
40
|
</div>
|
|
31
41
|
<div class="metric-value-container">
|
|
32
42
|
<span id="val-memory_mb" class="metric-value">--</span>
|
|
@@ -37,11 +47,26 @@
|
|
|
37
47
|
</div>
|
|
38
48
|
</div>
|
|
39
49
|
|
|
50
|
+
<!-- Heap Memory Card -->
|
|
51
|
+
<div class="metric-card glass" id="card-heap_mb">
|
|
52
|
+
<div class="metric-header">
|
|
53
|
+
<span class="metric-title">🧠 Heap Memory</span>
|
|
54
|
+
<span class="info-icon" data-tooltip="Bộ nhớ Java/Kotlin Heap (chỉ Android)">ℹ️</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="metric-value-container">
|
|
57
|
+
<span id="val-heap_mb" class="metric-value">--</span>
|
|
58
|
+
<span class="metric-unit">MB</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="chart-container">
|
|
61
|
+
<canvas id="chart-heap_mb"></canvas>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
40
65
|
<!-- Frame Card -->
|
|
41
66
|
<div class="metric-card glass" id="card-frame_ms">
|
|
42
67
|
<div class="metric-header">
|
|
43
68
|
<span class="metric-title">🎮 Frame Time</span>
|
|
44
|
-
<
|
|
69
|
+
<span class="info-icon" data-tooltip="Thời gian render mỗi frame, càng thấp càng mượt">ℹ️</span>
|
|
45
70
|
</div>
|
|
46
71
|
<div class="metric-value-container">
|
|
47
72
|
<span id="val-frame_ms" class="metric-value">--</span>
|
|
@@ -56,6 +81,7 @@
|
|
|
56
81
|
<div class="metric-card glass" id="card-fps">
|
|
57
82
|
<div class="metric-header">
|
|
58
83
|
<span class="metric-title">🚀 Realtime FPS</span>
|
|
84
|
+
<span class="info-icon" data-tooltip="Số frame mỗi giây, mục tiêu 60fps">ℹ️</span>
|
|
59
85
|
</div>
|
|
60
86
|
<div class="metric-value-container">
|
|
61
87
|
<span id="val-fps" class="metric-value">--</span>
|
|
@@ -66,10 +92,26 @@
|
|
|
66
92
|
</div>
|
|
67
93
|
</div>
|
|
68
94
|
|
|
95
|
+
<!-- Jank Card -->
|
|
96
|
+
<div class="metric-card glass" id="card-jank_percent">
|
|
97
|
+
<div class="metric-header">
|
|
98
|
+
<span class="metric-title">⚠️ Jank</span>
|
|
99
|
+
<span class="info-icon" data-tooltip="Phần trăm frame bị drop/giật (chỉ Android)">ℹ️</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="metric-value-container">
|
|
102
|
+
<span id="val-jank_percent" class="metric-value">--</span>
|
|
103
|
+
<span class="metric-unit">%</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="chart-container">
|
|
106
|
+
<canvas id="chart-jank_percent"></canvas>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
69
110
|
<!-- Disk Read Card -->
|
|
70
111
|
<div class="metric-card glass" id="card-disk_read_kbps">
|
|
71
112
|
<div class="metric-header">
|
|
72
113
|
<span class="metric-title">📥 Disk Read</span>
|
|
114
|
+
<span class="info-icon" data-tooltip="Tốc độ đọc từ ổ đĩa">ℹ️</span>
|
|
73
115
|
</div>
|
|
74
116
|
<div class="metric-value-container">
|
|
75
117
|
<span id="val-disk_read_kbps" class="metric-value">--</span>
|
|
@@ -84,6 +126,7 @@
|
|
|
84
126
|
<div class="metric-card glass" id="card-disk_write_kbps">
|
|
85
127
|
<div class="metric-header">
|
|
86
128
|
<span class="metric-title">📤 Disk Write</span>
|
|
129
|
+
<span class="info-icon" data-tooltip="Tốc độ ghi vào ổ đĩa">ℹ️</span>
|
|
87
130
|
</div>
|
|
88
131
|
<div class="metric-value-container">
|
|
89
132
|
<span id="val-disk_write_kbps" class="metric-value">--</span>
|
|
@@ -98,6 +141,7 @@
|
|
|
98
141
|
<div class="metric-card glass" id="card-network_total_mb">
|
|
99
142
|
<div class="metric-header">
|
|
100
143
|
<span class="metric-title">🌐 Net Total</span>
|
|
144
|
+
<span class="info-icon" data-tooltip="Tổng dữ liệu mạng đã truyền/nhận">ℹ️</span>
|
|
101
145
|
</div>
|
|
102
146
|
<div class="metric-value-container">
|
|
103
147
|
<span id="val-network_total_mb" class="metric-value">--</span>
|
package/src/websocket.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// WebSocket handler for metrics ingestion and real-time dashboard updates
|
|
2
1
|
import { WebSocketServer } from 'ws';
|
|
3
2
|
import { storage } from './storage.js';
|
|
4
3
|
|
|
@@ -14,8 +13,6 @@ export function broadcastToDashboards(data) {
|
|
|
14
13
|
dashboardClients.forEach(client => {
|
|
15
14
|
if (client.readyState === 1) {
|
|
16
15
|
client.send(update);
|
|
17
|
-
} else {
|
|
18
|
-
console.log('❌ broadcastToDashboards failed');
|
|
19
16
|
}
|
|
20
17
|
});
|
|
21
18
|
return enrichedSample;
|
|
@@ -25,36 +22,12 @@ export function setupWebSocket(server) {
|
|
|
25
22
|
const wss = new WebSocketServer({ server });
|
|
26
23
|
|
|
27
24
|
wss.on('connection', (ws, req) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (isDashboard) {
|
|
31
|
-
dashboardClients.add(ws);
|
|
32
|
-
console.log('🆕 Browser dashboard connected');
|
|
33
|
-
} else {
|
|
34
|
-
console.log('📱 Android device connected');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
ws.on('message', (message) => {
|
|
38
|
-
try {
|
|
39
|
-
const event = JSON.parse(message);
|
|
40
|
-
if (event.type === 'metric') {
|
|
41
|
-
// Broadcast to all connected dashboards
|
|
42
|
-
broadcastToDashboards(event.data);
|
|
43
|
-
} else if (event.type === 'ping') {
|
|
44
|
-
ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
|
|
45
|
-
}
|
|
46
|
-
} catch (error) {
|
|
47
|
-
console.error('WebSocket message error:', error);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
25
|
+
dashboardClients.add(ws);
|
|
26
|
+
console.log('🆕 Browser dashboard connected');
|
|
50
27
|
|
|
51
28
|
ws.on('close', () => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
console.log('❌ Browser dashboard disconnected');
|
|
55
|
-
} else {
|
|
56
|
-
console.log('❌ Android device disconnected');
|
|
57
|
-
}
|
|
29
|
+
dashboardClients.delete(ws);
|
|
30
|
+
console.log('❌ Browser dashboard disconnected');
|
|
58
31
|
});
|
|
59
32
|
});
|
|
60
33
|
|