magnax 1.0.0__py3-none-any.whl

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.
Files changed (87) hide show
  1. magnax/__init__.py +3 -0
  2. magnax/__main__.py +25 -0
  3. magnax/debug.py +65 -0
  4. magnax/public/__init__.py +1 -0
  5. magnax/public/adb/linux/adb +0 -0
  6. magnax/public/adb/linux_arm/adb +0 -0
  7. magnax/public/adb/mac/adb +0 -0
  8. magnax/public/adb/windows/AdbWinApi.dll +0 -0
  9. magnax/public/adb/windows/AdbWinUsbApi.dll +0 -0
  10. magnax/public/adb/windows/adb.exe +0 -0
  11. magnax/public/adb.py +96 -0
  12. magnax/public/android_fps.py +750 -0
  13. magnax/public/apm.py +1306 -0
  14. magnax/public/apm_pk.py +184 -0
  15. magnax/public/common.py +1598 -0
  16. magnax/public/config.json +1 -0
  17. magnax/public/ios_perf_adapter.py +790 -0
  18. magnax/public/report_template/android.html +526 -0
  19. magnax/public/report_template/ios.html +482 -0
  20. magnax/public/scrcpy/scrcpy-win32-v2.4/AdbWinApi.dll +0 -0
  21. magnax/public/scrcpy/scrcpy-win32-v2.4/AdbWinUsbApi.dll +0 -0
  22. magnax/public/scrcpy/scrcpy-win32-v2.4/SDL2.dll +0 -0
  23. magnax/public/scrcpy/scrcpy-win32-v2.4/adb.exe +0 -0
  24. magnax/public/scrcpy/scrcpy-win32-v2.4/avcodec-60.dll +0 -0
  25. magnax/public/scrcpy/scrcpy-win32-v2.4/avformat-60.dll +0 -0
  26. magnax/public/scrcpy/scrcpy-win32-v2.4/avutil-58.dll +0 -0
  27. magnax/public/scrcpy/scrcpy-win32-v2.4/icon.png +0 -0
  28. magnax/public/scrcpy/scrcpy-win32-v2.4/libusb-1.0.dll +0 -0
  29. magnax/public/scrcpy/scrcpy-win32-v2.4/open_a_terminal_here.bat +1 -0
  30. magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy-console.bat +2 -0
  31. magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy-noconsole.vbs +7 -0
  32. magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy-server +0 -0
  33. magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy.exe +0 -0
  34. magnax/public/scrcpy/scrcpy-win32-v2.4/swresample-4.dll +0 -0
  35. magnax/public/scrcpy/scrcpy-win64-v2.4/AdbWinApi.dll +0 -0
  36. magnax/public/scrcpy/scrcpy-win64-v2.4/AdbWinUsbApi.dll +0 -0
  37. magnax/public/scrcpy/scrcpy-win64-v2.4/SDL2.dll +0 -0
  38. magnax/public/scrcpy/scrcpy-win64-v2.4/avformat-60.dll +0 -0
  39. magnax/public/scrcpy/scrcpy-win64-v2.4/avutil-58.dll +0 -0
  40. magnax/public/scrcpy/scrcpy-win64-v2.4/open_a_terminal_here.bat +1 -0
  41. magnax/public/scrcpy/scrcpy-win64-v2.4/scrcpy-noconsole.vbs +7 -0
  42. magnax/public/scrcpy/scrcpy-win64-v2.4/scrcpy-server +0 -0
  43. magnax/public/scrcpy/scrcpy-win64-v2.4/scrcpy.exe +0 -0
  44. magnax/public/scrcpy/scrcpy-win64-v2.4/swresample-4.dll +0 -0
  45. magnax/static/css/highlight.min.css +9 -0
  46. magnax/static/css/magnax-dark-theme.css +1237 -0
  47. magnax/static/css/select2-bootstrap-5-theme.min.css +3 -0
  48. magnax/static/css/select2-bootstrap-5-theme.rtl.min.css +3 -0
  49. magnax/static/css/select2.min.css +1 -0
  50. magnax/static/css/sweetalert2.min.css +1 -0
  51. magnax/static/css/tabler.demo.min.css +9 -0
  52. magnax/static/css/tabler.min.css +14 -0
  53. magnax/static/image/500.png +0 -0
  54. magnax/static/image/avatar.png +0 -0
  55. magnax/static/image/empty.png +0 -0
  56. magnax/static/image/readme/home.png +0 -0
  57. magnax/static/image/readme/pk.png +0 -0
  58. magnax/static/js/apexcharts.js +14 -0
  59. magnax/static/js/gray.js +16 -0
  60. magnax/static/js/highlight.min.js +1173 -0
  61. magnax/static/js/highstock.js +803 -0
  62. magnax/static/js/html2canvas.min.js +20 -0
  63. magnax/static/js/jquery.min.js +2 -0
  64. magnax/static/js/magnax-chart-theme.js +492 -0
  65. magnax/static/js/select2.min.js +2 -0
  66. magnax/static/js/sweetalert2.min.js +1 -0
  67. magnax/static/js/tabler.demo.min.js +9 -0
  68. magnax/static/js/tabler.min.js +9 -0
  69. magnax/static/logo/logo.png +0 -0
  70. magnax/templates/404.html +30 -0
  71. magnax/templates/analysis.html +1375 -0
  72. magnax/templates/analysis_compare.html +600 -0
  73. magnax/templates/analysis_pk.html +680 -0
  74. magnax/templates/base.html +365 -0
  75. magnax/templates/index.html +2471 -0
  76. magnax/templates/pk.html +743 -0
  77. magnax/templates/report.html +416 -0
  78. magnax/view/__init__.py +1 -0
  79. magnax/view/apis.py +952 -0
  80. magnax/view/pages.py +146 -0
  81. magnax/web.py +345 -0
  82. magnax-1.0.0.dist-info/METADATA +242 -0
  83. magnax-1.0.0.dist-info/RECORD +87 -0
  84. magnax-1.0.0.dist-info/WHEEL +5 -0
  85. magnax-1.0.0.dist-info/entry_points.txt +2 -0
  86. magnax-1.0.0.dist-info/licenses/LICENSE +21 -0
  87. magnax-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,750 @@
1
+ # -*- coding: utf-8 -*-
2
+ import datetime
3
+ import queue
4
+ import re
5
+ import threading
6
+ import time
7
+ import traceback
8
+ from loguru import logger
9
+ from magnax.public.adb import adb
10
+ from magnax.public.common import Devices
11
+
12
+ d = Devices()
13
+
14
+ collect_fps = 0
15
+ collect_jank = 0
16
+
17
+
18
+ class SurfaceStatsCollector(object):
19
+ def __init__(self, device, frequency, package_name, fps_queue, jank_threshold, surfaceview, use_legacy=False):
20
+ self.device = device
21
+ self.frequency = frequency
22
+ self.package_name = package_name
23
+ self.jank_threshold = jank_threshold / 1000.0
24
+ self.use_legacy_method = use_legacy
25
+ self.use_gfxinfo_method = False # 新增gfxinfo备用方案
26
+ self.surface_before = 0
27
+ self.last_timestamp = 0
28
+ self.gfxinfo_before = None # 存储之前的gfxinfo数据
29
+ self.data_queue = queue.Queue()
30
+ self.stop_event = threading.Event()
31
+ self.focus_window = None
32
+ self.surfaceview = surfaceview
33
+ self.fps_queue = fps_queue
34
+
35
+ def start(self, start_time):
36
+ if not self.use_legacy_method:
37
+ try:
38
+ self.focus_window = self.get_focus_activity()
39
+ if self.focus_window:
40
+ if self.focus_window.find('$') != -1:
41
+ self.focus_window = self.focus_window.replace('$', '\$')
42
+ logger.debug(f'[FPS] Using focus window: {self.focus_window}')
43
+ else:
44
+ logger.warning('[FPS] Failed to get focus window, switching to legacy method')
45
+ self.use_legacy_method = True
46
+ except Exception as e:
47
+ logger.warning(f'[FPS] Unable to get focus activity: {str(e)}, switching to legacy method')
48
+ self.use_legacy_method = True
49
+
50
+ if self.use_legacy_method:
51
+ logger.info("[FPS] Using legacy method for FPS detection")
52
+ try:
53
+ self.surface_before = self._get_surface_stats_legacy()
54
+ if not self.surface_before:
55
+ logger.error("[FPS] Legacy method failed to get initial surface stats")
56
+ logger.warning("[FPS] Switching to gfxinfo fallback method")
57
+ # 切换到gfxinfo方法
58
+ self.use_legacy_method = False
59
+ self.use_gfxinfo_method = True
60
+ # 初始化时获取基准gfxinfo数据(不重置)
61
+ self.gfxinfo_before = self._get_gfxinfo_stats(reset_stats=False)
62
+ if not self.gfxinfo_before:
63
+ logger.error("[FPS] All FPS detection methods failed")
64
+ # 使用默认值作为最后的回退
65
+ self.surface_before = {'page_flip_count': 0, 'timestamp': datetime.datetime.now()}
66
+ self.use_legacy_method = True # 回退到legacy方法避免后续代码出错
67
+ self.use_gfxinfo_method = False
68
+ else:
69
+ logger.info("[FPS] Successfully initialized gfxinfo method")
70
+ except Exception as e:
71
+ logger.error(f"[FPS] Legacy method initialization failed: {str(e)}")
72
+ logger.warning("[FPS] Trying gfxinfo fallback method")
73
+ self.use_legacy_method = False
74
+ self.use_gfxinfo_method = True
75
+ try:
76
+ self.gfxinfo_before = self._get_gfxinfo_stats(reset_stats=False)
77
+ if not self.gfxinfo_before:
78
+ logger.error("[FPS] All FPS detection methods failed")
79
+ self.surface_before = {'page_flip_count': 0, 'timestamp': datetime.datetime.now()}
80
+ self.use_legacy_method = True
81
+ self.use_gfxinfo_method = False
82
+ else:
83
+ logger.info("[FPS] Successfully initialized gfxinfo method")
84
+ except Exception as gfx_e:
85
+ logger.error(f"[FPS] gfxinfo method also failed: {str(gfx_e)}")
86
+ self.surface_before = {'page_flip_count': 0, 'timestamp': datetime.datetime.now()}
87
+ self.use_legacy_method = True
88
+ self.use_gfxinfo_method = False
89
+
90
+ self.collector_thread = threading.Thread(target=self._collector_thread)
91
+ self.collector_thread.start()
92
+ self.calculator_thread = threading.Thread(target=self._calculator_thread, args=(start_time,))
93
+ self.calculator_thread.start()
94
+
95
+ def stop(self):
96
+ if self.collector_thread:
97
+ self.stop_event.set()
98
+ self.collector_thread.join()
99
+ self.collector_thread = None
100
+ if self.fps_queue:
101
+ self.fps_queue.task_done()
102
+
103
+ def get_surfaceview(self):
104
+ activity_name = ''
105
+ activity_line = ''
106
+ try:
107
+ dumpsys_result = adb.shell(cmd='dumpsys SurfaceFlinger --list | {} {}'.format(d.filterType(), self.package_name), deviceId=self.device)
108
+ dumpsys_result_list = dumpsys_result.split('\n')
109
+ for line in dumpsys_result_list:
110
+ if line.startswith('SurfaceView') and line.find(self.package_name) != -1:
111
+ activity_line = line.strip()
112
+ return activity_line
113
+
114
+ activity_name = dumpsys_result_list[len(dumpsys_result_list) - 1]
115
+ if not activity_name.__contains__(self.package_name):
116
+ logger.error('get activity name failed, Please provide SurfaceFlinger --list information to the author')
117
+ logger.info('dumpsys SurfaceFlinger --list info: {}'.format(dumpsys_result))
118
+ except Exception:
119
+ traceback.print_exc()
120
+ logger.error('get activity name failed, Please provide SurfaceFlinger --list information to the author')
121
+ logger.info('dumpsys SurfaceFlinger --list info: {}'.format(dumpsys_result))
122
+ return activity_name
123
+
124
+ def get_surfaceview_activity(self):
125
+ activity_name = ''
126
+ activity_line = ''
127
+ try:
128
+ dumpsys_result = adb.shell(cmd='dumpsys SurfaceFlinger --list | {} {}'.format(d.filterType(), self.package_name), deviceId=self.device)
129
+ dumpsys_result_list = dumpsys_result.split('\n')
130
+ for line in dumpsys_result_list:
131
+ if line.startswith('SurfaceView') and line.find(self.package_name) != -1:
132
+ activity_line = line.strip()
133
+ break
134
+ if activity_line:
135
+ if activity_line.find(' ') != -1:
136
+ activity_name = activity_line.split(' ')[2]
137
+ else:
138
+ activity_name = activity_line.replace('SurfaceView','').replace('[','').replace(']','').replace('-','').strip()
139
+ else:
140
+ activity_name = dumpsys_result_list[len(dumpsys_result_list) - 1]
141
+ if not activity_name.__contains__(self.package_name):
142
+ logger.error('get activity name failed, Please provide SurfaceFlinger --list information to the author')
143
+ logger.info('dumpsys SurfaceFlinger --list info: {}'.format(dumpsys_result))
144
+ except Exception:
145
+ traceback.print_exc()
146
+ logger.error('get activity name failed, Please provide SurfaceFlinger --list information to the author')
147
+ logger.info('dumpsys SurfaceFlinger --list info: {}'.format(dumpsys_result))
148
+ return activity_name
149
+
150
+ def get_focus_activity(self):
151
+ activity_name = ''
152
+ activity_line = ''
153
+ dumpsys_result = adb.shell(cmd='dumpsys window windows', deviceId=self.device)
154
+ dumpsys_result_list = dumpsys_result.split('\n')
155
+ for line in dumpsys_result_list:
156
+ if line.find('mCurrentFocus') != -1:
157
+ activity_line = line.strip()
158
+ if activity_line:
159
+ activity_line_split = activity_line.split(' ')
160
+ else:
161
+ return activity_name
162
+ if len(activity_line_split) > 1:
163
+ if activity_line_split[1] == 'u0':
164
+ activity_name = activity_line_split[2].rstrip('}')
165
+ else:
166
+ activity_name = activity_line_split[1]
167
+ if not activity_name:
168
+ activity_name = self.get_surfaceview_activity()
169
+ return activity_name
170
+
171
+ def get_foreground_process(self):
172
+ focus_activity = self.get_focus_activity()
173
+ if focus_activity:
174
+ return focus_activity.split("/")[0]
175
+ else:
176
+ return ""
177
+
178
+ def _calculate_results(self, refresh_period, timestamps):
179
+ frame_count = len(timestamps)
180
+ if frame_count == 0:
181
+ fps = 0
182
+ jank = 0
183
+ elif frame_count == 1:
184
+ fps = 1
185
+ jank = 0
186
+ else:
187
+ seconds = timestamps[-1][1] - timestamps[0][1]
188
+ if seconds > 0:
189
+ fps = int(round((frame_count - 1) / seconds))
190
+ jank = self._calculate_janky(timestamps)
191
+ else:
192
+ fps = 1
193
+ jank = 0
194
+ return fps, jank
195
+
196
+ def _calculate_results_new(self, refresh_period, timestamps):
197
+ frame_count = len(timestamps)
198
+ if frame_count == 0:
199
+ fps = 0
200
+ jank = 0
201
+ elif frame_count == 1:
202
+ fps = 1
203
+ jank = 0
204
+ elif frame_count == 2 or frame_count == 3 or frame_count == 4:
205
+ seconds = timestamps[-1][1] - timestamps[0][1]
206
+ if seconds > 0:
207
+ fps = int(round((frame_count - 1) / seconds))
208
+ jank = self._calculate_janky(timestamps)
209
+ else:
210
+ fps = 1
211
+ jank = 0
212
+ else:
213
+ seconds = timestamps[-1][1] - timestamps[0][1]
214
+ if seconds > 0:
215
+ fps = int(round((frame_count - 1) / seconds))
216
+ jank = self._calculate_jankey_new(timestamps)
217
+ else:
218
+ fps = 1
219
+ jank = 0
220
+ return fps, jank
221
+
222
+ def _calculate_jankey_new(self, timestamps):
223
+ twofilmstamp = 83.3 / 1000.0
224
+ tempstamp = 0
225
+ jank = 0
226
+ for index, timestamp in enumerate(timestamps):
227
+ if (index == 0) or (index == 1) or (index == 2) or (index == 3):
228
+ if tempstamp == 0:
229
+ tempstamp = timestamp[1]
230
+ continue
231
+ costtime = timestamp[1] - tempstamp
232
+ if costtime > self.jank_threshold:
233
+ jank = jank + 1
234
+ tempstamp = timestamp[1]
235
+ elif index > 3:
236
+ currentstamp = timestamps[index][1]
237
+ lastonestamp = timestamps[index - 1][1]
238
+ lasttwostamp = timestamps[index - 2][1]
239
+ lastthreestamp = timestamps[index - 3][1]
240
+ lastfourstamp = timestamps[index - 4][1]
241
+ tempframetime = ((lastthreestamp - lastfourstamp) + (lasttwostamp - lastthreestamp) + (
242
+ lastonestamp - lasttwostamp)) / 3 * 2
243
+ currentframetime = currentstamp - lastonestamp
244
+ if (currentframetime > tempframetime) and (currentframetime > twofilmstamp):
245
+ jank = jank + 1
246
+ return jank
247
+
248
+ def _calculate_janky(self, timestamps):
249
+ tempstamp = 0
250
+ jank = 0
251
+ for timestamp in timestamps:
252
+ if tempstamp == 0:
253
+ tempstamp = timestamp[1]
254
+ continue
255
+ costtime = timestamp[1] - tempstamp
256
+ if costtime > self.jank_threshold:
257
+ jank = jank + 1
258
+ tempstamp = timestamp[1]
259
+ return jank
260
+
261
+ def _calculator_thread(self, start_time):
262
+ global collect_fps
263
+ global collect_jank
264
+ while True:
265
+ try:
266
+ data = self.data_queue.get()
267
+ if isinstance(data, str) and data == 'Stop':
268
+ break
269
+ before = time.time()
270
+
271
+ # 处理gfxinfo数据
272
+ if isinstance(data, tuple) and len(data) == 3 and data[0] == 'gfxinfo':
273
+ gfxinfo_data = data[1]
274
+ if self.gfxinfo_before is not None:
275
+ # 计算FPS - 基于前后两次gfxinfo数据的差值
276
+ td = gfxinfo_data['timestamp'] - self.gfxinfo_before['timestamp']
277
+ seconds = td.total_seconds()
278
+ frame_diff = gfxinfo_data['total_frames'] - self.gfxinfo_before['total_frames']
279
+ jank_diff = gfxinfo_data['janky_frames'] - self.gfxinfo_before['janky_frames']
280
+
281
+ if seconds > 0 and frame_diff >= 0:
282
+ fps = int(round(frame_diff / seconds)) if frame_diff > 0 else 0
283
+ if fps > 60: # 限制最大FPS
284
+ fps = 60
285
+ jank = jank_diff
286
+ else:
287
+ fps = 0
288
+ jank = 0
289
+
290
+ logger.debug(f'[FPS] gfxinfo calculated: fps={fps}, jank={jank}, frame_diff={frame_diff}, seconds={seconds:.2f}')
291
+ collect_fps = fps
292
+ collect_jank = jank
293
+ else:
294
+ # 第一次采样,记录基准数据
295
+ collect_fps = 0
296
+ collect_jank = 0
297
+ logger.debug("[FPS] gfxinfo: First sample, establishing baseline")
298
+
299
+ self.gfxinfo_before = gfxinfo_data
300
+
301
+ elif self.use_legacy_method:
302
+ td = data['timestamp'] - self.surface_before['timestamp']
303
+ seconds = td.seconds + td.microseconds / 1e6
304
+ frame_count = (data['page_flip_count'] -
305
+ self.surface_before['page_flip_count'])
306
+ fps = int(round(frame_count / seconds))
307
+ if fps > 60:
308
+ fps = 60
309
+ self.surface_before = data
310
+ # logger.debug('FPS:%2s'%fps)
311
+ collect_fps = fps
312
+ else:
313
+ refresh_period = data[0]
314
+ timestamps = data[1]
315
+ collect_time = data[2]
316
+ # fps,jank = self._calculate_results(refresh_period, timestamps)
317
+ fps, jank = self._calculate_results_new(refresh_period, timestamps)
318
+ # logger.debug('FPS:%2s Jank:%s'%(fps,jank))
319
+ collect_fps = fps
320
+ collect_jank = jank
321
+ time_consume = time.time() - before
322
+ delta_inter = self.frequency - time_consume
323
+ if delta_inter > 0:
324
+ time.sleep(delta_inter)
325
+ except:
326
+ logger.error("an exception hanpend in fps _calculator_thread ,reason unkown!")
327
+ s = traceback.format_exc()
328
+ logger.debug(s)
329
+ if self.fps_queue:
330
+ self.fps_queue.task_done()
331
+
332
+ def _collector_thread(self):
333
+ is_first = True
334
+ consecutive_failures = 0
335
+ max_failures = 5 # 最大连续失败次数
336
+
337
+ while not self.stop_event.is_set():
338
+ try:
339
+ before = time.time()
340
+ data_collected = False
341
+
342
+ if self.use_gfxinfo_method:
343
+ try:
344
+ # 获取当前gfxinfo统计数据
345
+ gfxinfo_state = self._get_gfxinfo_stats(reset_stats=False)
346
+ if gfxinfo_state:
347
+ self.data_queue.put(('gfxinfo', gfxinfo_state, time.time()))
348
+ data_collected = True
349
+ consecutive_failures = 0
350
+ else:
351
+ consecutive_failures += 1
352
+ if consecutive_failures <= 3:
353
+ logger.warning(f"[FPS] gfxinfo method failed to get data (attempt {consecutive_failures})")
354
+ elif consecutive_failures == max_failures:
355
+ logger.error(f"[FPS] gfxinfo method consistently failing, device may not support FPS detection")
356
+ except Exception as e:
357
+ consecutive_failures += 1
358
+ logger.error(f"[FPS] gfxinfo method exception (attempt {consecutive_failures}): {str(e)}")
359
+ elif self.use_legacy_method:
360
+ try:
361
+ surface_state = self._get_surface_stats_legacy()
362
+ if surface_state:
363
+ self.data_queue.put(surface_state)
364
+ data_collected = True
365
+ consecutive_failures = 0
366
+ else:
367
+ consecutive_failures += 1
368
+ if consecutive_failures <= 3: # 前3次失败只显示警告
369
+ logger.warning(f"[FPS] Legacy method failed to get data (attempt {consecutive_failures})")
370
+ elif consecutive_failures == max_failures:
371
+ logger.error(f"[FPS] Legacy method consistently failing, device may not support FPS detection")
372
+ except Exception as e:
373
+ consecutive_failures += 1
374
+ logger.error(f"[FPS] Legacy method exception (attempt {consecutive_failures}): {str(e)}")
375
+ else:
376
+ timestamps = []
377
+ refresh_period, new_timestamps = self._get_surfaceflinger_frame_data()
378
+
379
+ if refresh_period is None or new_timestamps is None:
380
+ consecutive_failures += 1
381
+ logger.warning(f"[FPS] SurfaceFlinger data is None (attempt {consecutive_failures})")
382
+
383
+ # 如果连续失败次数过多,尝试切换到legacy方法
384
+ if consecutive_failures >= max_failures and not self.use_legacy_method:
385
+ logger.warning("[FPS] Too many failures, switching to legacy method")
386
+ self.use_legacy_method = True
387
+ self.surface_before = self._get_surface_stats_legacy()
388
+ else:
389
+ # 尝试重新获取焦点窗口
390
+ self.focus_window = self.get_focus_activity()
391
+ continue
392
+
393
+ timestamps += [timestamp for timestamp in new_timestamps
394
+ if timestamp[1] > self.last_timestamp]
395
+ if len(timestamps):
396
+ first_timestamp = [[0, self.last_timestamp, 0]]
397
+ if not is_first:
398
+ timestamps = first_timestamp + timestamps
399
+ self.last_timestamp = timestamps[-1][1]
400
+ is_first = False
401
+ data_collected = True
402
+ consecutive_failures = 0
403
+ else:
404
+ is_first = True
405
+ cur_focus_window = self.get_focus_activity()
406
+ if self.focus_window != cur_focus_window:
407
+ logger.debug(f"[FPS] Focus window changed: {self.focus_window} -> {cur_focus_window}")
408
+ self.focus_window = cur_focus_window
409
+ continue
410
+
411
+ self.data_queue.put((refresh_period, timestamps, time.time()))
412
+
413
+ # 如果成功收集数据,控制采样频率
414
+ if data_collected:
415
+ time_consume = time.time() - before
416
+ # gfxinfo方法可以使用正常的采样间隔
417
+ if self.use_gfxinfo_method:
418
+ sample_interval = max(self.frequency, 1) # 至少1秒间隔
419
+ else:
420
+ sample_interval = self.frequency
421
+
422
+ delta_inter = sample_interval - time_consume
423
+ if delta_inter > 0:
424
+ time.sleep(delta_inter)
425
+
426
+ # 如果失败次数过多,增加等待时间避免过于频繁的重试
427
+ if consecutive_failures >= max_failures:
428
+ time.sleep(2) # 等待2秒再重试
429
+
430
+ except Exception as e:
431
+ consecutive_failures += 1
432
+ logger.error(f"[FPS] Exception in collector thread (attempt {consecutive_failures}): {str(e)}")
433
+ logger.debug(traceback.format_exc())
434
+ if self.fps_queue:
435
+ self.fps_queue.task_done()
436
+
437
+ # 如果异常次数过多,等待更长时间
438
+ if consecutive_failures >= max_failures:
439
+ time.sleep(5)
440
+
441
+ self.data_queue.put(u'Stop')
442
+
443
+ def _clear_surfaceflinger_latency_data(self):
444
+ """Clears the SurfaceFlinger latency data.
445
+
446
+ Returns:
447
+ True if SurfaceFlinger latency is supported by the device, otherwise
448
+ False.
449
+ """
450
+ # The command returns nothing if it is supported, otherwise returns many
451
+ # lines of result just like 'dumpsys SurfaceFlinger'.
452
+ if self.focus_window is None:
453
+ results = adb.shell(cmd='dumpsys SurfaceFlinger --latency-clear', deviceId=self.device)
454
+ else:
455
+ results = adb.shell(cmd='dumpsys SurfaceFlinger --latency-clear %s' % self.focus_window,
456
+ deviceId=self.device)
457
+ return not len(results)
458
+
459
+ def get_sdk_version(self):
460
+ sdk_version = int(adb.shell(cmd='getprop ro.build.version.sdk', deviceId=self.device))
461
+ return sdk_version
462
+
463
+ def _get_surfaceflinger_frame_data(self):
464
+ """Returns collected SurfaceFlinger frame timing data.
465
+ return:(16.6,[[t1,t2,t3],[t4,t5,t6]])
466
+ Returns:
467
+ A tuple containing:
468
+ - The display's nominal refresh period in seconds.
469
+ - A list of timestamps signifying frame presentation times in seconds.
470
+ The return value may be (None, None) if there was no data collected (for
471
+ example, if the app was closed before the collector thread has finished).
472
+ """
473
+ # shell dumpsys SurfaceFlinger --latency <window name>
474
+ # prints some information about the last 128 frames displayed in
475
+ # that window.
476
+ # The data returned looks like this:
477
+ # 16954612
478
+ # 7657467895508 7657482691352 7657493499756
479
+ # 7657484466553 7657499645964 7657511077881
480
+ # 7657500793457 7657516600576 7657527404785
481
+ # (...)
482
+ #
483
+ # The first line is the refresh period (here 16.95 ms), it is followed
484
+ # by 128 lines w/ 3 timestamps in nanosecond each:
485
+ # A) when the app started to draw
486
+ # B) the vsync immediately preceding SF submitting the frame to the h/w
487
+ # C) timestamp immediately after SF submitted that frame to the h/w
488
+ #
489
+ # The difference between the 1st and 3rd timestamp is the frame-latency.
490
+ # An interesting data is when the frame latency crosses a refresh period
491
+ # boundary, this can be calculated this way:
492
+ #
493
+ # ceil((C - A) / refresh-period)
494
+ #
495
+ # (each time the number above changes, we have a "jank").
496
+ # If this happens a lot during an animation, the animation appears
497
+ # janky, even if it runs at 60 fps in average.
498
+ #
499
+
500
+ # Google Pixel 2 android8.0 dumpsys SurfaceFlinger --latency结果
501
+ # 16666666
502
+ # 0 0 0
503
+ # 0 0 0
504
+ # 0 0 0
505
+ # 0 0 0
506
+ # 但华为 荣耀9 android8.0 dumpsys SurfaceFlinger --latency结果是正常的 但数据更新很慢 也不能用来计算fps
507
+ # 16666666
508
+ # 9223372036854775807 3618832932780 9223372036854775807
509
+ # 9223372036854775807 3618849592155 9223372036854775807
510
+ # 9223372036854775807 3618866251530 9223372036854775807
511
+
512
+ refresh_period = None
513
+ timestamps = []
514
+ nanoseconds_per_second = 1e9
515
+ pending_fence_timestamp = (1 << 63) - 1
516
+ if self.surfaceview is not True:
517
+ results = adb.shell(
518
+ cmd='dumpsys SurfaceFlinger --latency %s' % self.focus_window, deviceId=self.device)
519
+ results = results.replace("\r\n", "\n").splitlines()
520
+ refresh_period = int(results[0]) / nanoseconds_per_second
521
+ results = adb.shell(cmd='dumpsys gfxinfo %s framestats' % self.package_name, deviceId=self.device)
522
+ results = results.replace("\r\n", "\n").splitlines()
523
+ if not len(results):
524
+ return (None, None)
525
+ isHaveFoundWindow = False
526
+ PROFILEDATA_line = 0
527
+ activity = self.focus_window
528
+ if self.focus_window.__contains__('#'):
529
+ activity = activity.split('#')[0]
530
+ for line in results:
531
+ if not isHaveFoundWindow:
532
+ if "Window" in line and activity in line:
533
+ isHaveFoundWindow = True
534
+ if not isHaveFoundWindow:
535
+ continue
536
+ if "PROFILEDATA" in line:
537
+ PROFILEDATA_line += 1
538
+ fields = []
539
+ fields = line.split(",")
540
+ if fields and '0' == fields[0]:
541
+ # https://www.cnblogs.com/zhengna/p/10032078.html
542
+ # 1 INTENDED_VSYNC
543
+ # 2 VSYNC
544
+ # 13 FRAME_COMPLETED
545
+ timestamp = [int(fields[1]), int(fields[2]), int(fields[13])]
546
+ if timestamp[1] == pending_fence_timestamp:
547
+ continue
548
+ timestamp = [_timestamp / nanoseconds_per_second for _timestamp in timestamp]
549
+ timestamps.append(timestamp)
550
+ if 2 == PROFILEDATA_line:
551
+ break
552
+ else:
553
+ # self.focus_window = self.get_surfaceview_activity()
554
+ self.focus_window = self.get_surfaceview()
555
+ results = adb.shell(
556
+ cmd='dumpsys SurfaceFlinger --latency \\"%s\\"' % self.focus_window, deviceId=self.device)
557
+ results = results.replace("\r\n", "\n").splitlines()
558
+ if len(results) <= 1 or int(results[-2].split()[0]) ==0:
559
+ self.focus_window = self.get_surfaceview_activity()
560
+ results = adb.shell(
561
+ cmd='dumpsys SurfaceFlinger --latency \\"%s\\"' % self.focus_window, deviceId=self.device)
562
+ results = results.replace("\r\n", "\n").splitlines()
563
+ if not len(results):
564
+ return (None, None)
565
+ if not results[0].isdigit():
566
+ return (None, None)
567
+ try:
568
+ refresh_period = int(results[0]) / nanoseconds_per_second
569
+ except Exception as e:
570
+ logger.exception(e)
571
+ return (None, None)
572
+ # If a fence associated with a frame is still pending when we query the
573
+ # latency data, SurfaceFlinger gives the frame a timestamp of INT64_MAX.
574
+ # Since we only care about completed frames, we will ignore any timestamps
575
+ # with this value.
576
+
577
+ for line in results[2:]:
578
+ fields = line.split()
579
+ if len(fields) != 3:
580
+ continue
581
+ timestamp = [int(fields[0]), int(fields[1]), int(fields[2])]
582
+ if timestamp[1] == pending_fence_timestamp:
583
+ continue
584
+ timestamp = [_timestamp / nanoseconds_per_second for _timestamp in timestamp]
585
+ timestamps.append(timestamp)
586
+ return (refresh_period, timestamps)
587
+
588
+ def _get_surface_stats_legacy(self):
589
+ """Legacy method (before JellyBean), returns the current Surface index
590
+ and timestamp.
591
+
592
+ Calculate FPS by measuring the difference of Surface index returned by
593
+ SurfaceFlinger in a period of time.
594
+
595
+ Returns:
596
+ Dict of {page_flip_count (or 0 if there was an error), timestamp}.
597
+ """
598
+ cur_surface = None
599
+ timestamp = datetime.datetime.now()
600
+ ret = adb.shell(cmd="service call SurfaceFlinger 1013", deviceId=self.device)
601
+ if not ret:
602
+ logger.warning("[FPS] Legacy method: No response from SurfaceFlinger service")
603
+ return None
604
+
605
+ # 检查是否包含错误信息
606
+ if "Error:" in ret or "Permission denied" in ret or "Operation not permitted" in ret:
607
+ logger.error(f"[FPS] Legacy method failed: {ret.strip()}")
608
+ return None
609
+
610
+ match = re.search('^Result: Parcel\((\w+)', ret)
611
+ if match:
612
+ hex_str = match.group(1)
613
+ # 验证是否为有效的十六进制字符串
614
+ try:
615
+ # 检查是否包含非十六进制字符
616
+ if not all(c in '0123456789abcdefABCDEF' for c in hex_str):
617
+ logger.error(f"[FPS] Legacy method: Invalid hex string '{hex_str}' in response: {ret.strip()}")
618
+ return None
619
+ cur_surface = int(hex_str, 16)
620
+ logger.debug(f"[FPS] Legacy method: Successfully got surface count {cur_surface}")
621
+ return {'page_flip_count': cur_surface, 'timestamp': timestamp}
622
+ except ValueError as e:
623
+ logger.error(f"[FPS] Legacy method: Failed to parse hex '{hex_str}': {str(e)}")
624
+ return None
625
+ else:
626
+ logger.error(f"[FPS] Legacy method: Unexpected response format: {ret.strip()}")
627
+ return None
628
+
629
+ def _get_gfxinfo_stats(self, reset_stats=False):
630
+ """Gfxinfo fallback method for FPS detection.
631
+
632
+ Uses dumpsys gfxinfo to get frame statistics.
633
+
634
+ Args:
635
+ reset_stats: Whether to reset the statistics first for fresh data
636
+
637
+ Returns:
638
+ Dict containing frame statistics or None if failed.
639
+ """
640
+ try:
641
+ # 如果需要重置统计数据,先清除累积数据
642
+ if reset_stats:
643
+ reset_cmd = f'dumpsys gfxinfo {self.package_name} reset'
644
+ adb.shell(cmd=reset_cmd, deviceId=self.device)
645
+ time.sleep(0.1) # 短暂等待重置完成
646
+
647
+ timestamp = datetime.datetime.now()
648
+ cmd = f'dumpsys gfxinfo {self.package_name}'
649
+ result = adb.shell(cmd=cmd, deviceId=self.device)
650
+
651
+ if not result or "No process found" in result:
652
+ logger.warning(f"[FPS] gfxinfo: No process found for {self.package_name}")
653
+ return None
654
+
655
+ # 解析gfxinfo数据
656
+ lines = result.split('\n')
657
+ total_frames = 0
658
+ janky_frames = 0
659
+
660
+ for line in lines:
661
+ line = line.strip()
662
+ if "Total frames rendered:" in line:
663
+ try:
664
+ total_frames = int(line.split(':')[1].strip())
665
+ except (ValueError, IndexError):
666
+ pass
667
+ elif "Janky frames:" in line:
668
+ try:
669
+ # 解析 "Janky frames: 7 (6.36%)" 格式
670
+ janky_part = line.split(':')[1].strip()
671
+ janky_frames = int(janky_part.split(' ')[0])
672
+ except (ValueError, IndexError):
673
+ pass
674
+
675
+ if total_frames >= 0: # 允许0帧的情况,表示没有新帧
676
+ logger.debug(f"[FPS] gfxinfo: total_frames={total_frames}, janky_frames={janky_frames}")
677
+ return {
678
+ 'total_frames': total_frames,
679
+ 'janky_frames': janky_frames,
680
+ 'timestamp': timestamp
681
+ }
682
+ else:
683
+ logger.warning("[FPS] gfxinfo: No valid frame data found")
684
+ return None
685
+
686
+ except Exception as e:
687
+ logger.error(f"[FPS] gfxinfo method failed: {str(e)}")
688
+ return None
689
+
690
+
691
+ class Monitor(object):
692
+ def __init__(self, **kwargs):
693
+ self.config = kwargs
694
+ self.matched_data = {}
695
+
696
+ def start(self):
697
+ logger.warn("请在%s类中实现start方法" % type(self))
698
+
699
+ def clear(self):
700
+ self.matched_data = {}
701
+
702
+ def stop(self):
703
+ logger.warning("请在%s类中实现stop方法" % type(self))
704
+
705
+ def save(self):
706
+ logger.warning("请在%s类中实现save方法" % type(self))
707
+
708
+
709
+ class TimeUtils(object):
710
+ UnderLineFormatter = "%Y_%m_%d_%H_%M_%S"
711
+ NormalFormatter = "%Y-%m-%d %H-%M-%S"
712
+ ColonFormatter = "%Y-%m-%d %H:%M:%S"
713
+
714
+ @staticmethod
715
+ def getCurrentTimeUnderline():
716
+ return time.strftime(TimeUtils.UnderLineFormatter, time.localtime(time.time()))
717
+
718
+
719
+ class FPSMonitor(Monitor):
720
+ def __init__(self, device_id, package_name=None, frequency=1.0, timeout=24 * 60 * 60, fps_queue=None,
721
+ jank_threshold=166, use_legacy=False, surfaceview=True, start_time=None, **kwargs):
722
+ super().__init__(**kwargs)
723
+ self.start_time = start_time
724
+ self.use_legacy = use_legacy
725
+ self.frequency = frequency # 取样频率
726
+ self.jank_threshold = jank_threshold
727
+ self.device = device_id
728
+ self.timeout = timeout
729
+ self.surfaceview = surfaceview
730
+ self.package = package_name
731
+ self.fpscollector = SurfaceStatsCollector(self.device, self.frequency, package_name, fps_queue,
732
+ self.jank_threshold, self.surfaceview, self.use_legacy)
733
+
734
+ def start(self):
735
+ self.fpscollector.start(self.start_time)
736
+
737
+ def stop(self):
738
+ global collect_fps
739
+ global collect_jank
740
+ self.fpscollector.stop()
741
+ return collect_fps, collect_jank
742
+
743
+ def save(self):
744
+ pass
745
+
746
+ def parse(self, file_path):
747
+ pass
748
+
749
+ def get_fps_collector(self):
750
+ return self.fpscollector