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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "momodebug",
3
- "version": "1.0.0",
3
+ "version": "1.4.2",
4
4
  "description": "Real-time performance monitoring dashboard for Android and iOS apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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
- margin-bottom: 2.5rem;
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: hidden;
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;
@@ -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
- 'cpu_ms': { label: 'CPU Time', color: '#a855f7' },
8
- 'memory_mb': { label: 'Memory (PSS)', color: '#3b82f6' },
9
- 'frame_ms': { label: 'Frame Time', color: '#06b6d4' },
10
- 'fps': { label: 'FPS', color: '#22c55e' },
11
- 'disk_read_kbps': { label: 'Disk Read', color: '#f59e0b' },
12
- 'disk_write_kbps': { label: 'Disk Write', color: '#ef4444' },
13
- 'network_total_mb': { label: 'Net Total', color: '#6366f1' }
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 ctx = document.getElementById(`chart-${key}`).getContext('2d');
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
- plugins: { legend: { display: false } },
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) {
@@ -18,8 +18,10 @@ export function startAdbObserver() {
18
18
  const mappings = {
19
19
  'fps': 'fps',
20
20
  'frameTimeMs': 'frame_ms',
21
- 'cpuUsagePercent': 'cpu_ms',
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
- // Main unified server with Express and WebSocket
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
- // Start observers
77
- startAdbObserver(); // Android (adb logcat)
78
- startIosObserver(); // iOS Simulator (simctl log stream)
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
- console.log(`📡 WebSocket listening for metrics from SDK\n`);
83
+
85
84
 
86
85
  // Auto-open browser
87
86
  exec(`open http://localhost:${PORT}`);
@@ -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 for PerfObserverSDK subsystem
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': 'cpu_ms',
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
- console.log(`📱 idevicesyslog exited with code ${code}. Restarting in 10s...`);
81
- setTimeout(startDeviceSyslog, 10000);
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
  }
@@ -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 subsystem (set in os_log) to capture SDK logs
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
- 'subsystem == "PerfObserverSDK"'
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': 'cpu_ms',
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
- console.log(`📡 iOS log process exited with code ${code}. Restarting in 10s...`);
89
- setTimeout(startIosObserver, 10000);
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
 
@@ -1,24 +1,34 @@
1
1
  <div class="dashboard-header">
2
- <div class="live-status">
3
- <div class="dot"></div>
4
- <span>Live Streaming Active</span>
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-cpu_ms">
21
+ <div class="metric-card glass" id="card-cpu_usage">
12
22
  <div class="metric-header">
13
- <span class="metric-title">⚡ CPU Time</span>
14
- <div class="delta" id="delta-cpu_ms"></div>
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-cpu_ms" class="metric-value">--</span>
18
- <span class="metric-unit">ms</span>
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-cpu_ms"></canvas>
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
- <div class="delta" id="delta-memory_mb"></div>
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
- <div class="delta" id="delta-frame_ms"></div>
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
- const isDashboard = req.url.includes('dashboard');
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
- if (isDashboard) {
53
- dashboardClients.delete(ws);
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