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.
- magnax/__init__.py +3 -0
- magnax/__main__.py +25 -0
- magnax/debug.py +65 -0
- magnax/public/__init__.py +1 -0
- magnax/public/adb/linux/adb +0 -0
- magnax/public/adb/linux_arm/adb +0 -0
- magnax/public/adb/mac/adb +0 -0
- magnax/public/adb/windows/AdbWinApi.dll +0 -0
- magnax/public/adb/windows/AdbWinUsbApi.dll +0 -0
- magnax/public/adb/windows/adb.exe +0 -0
- magnax/public/adb.py +96 -0
- magnax/public/android_fps.py +750 -0
- magnax/public/apm.py +1306 -0
- magnax/public/apm_pk.py +184 -0
- magnax/public/common.py +1598 -0
- magnax/public/config.json +1 -0
- magnax/public/ios_perf_adapter.py +790 -0
- magnax/public/report_template/android.html +526 -0
- magnax/public/report_template/ios.html +482 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/AdbWinApi.dll +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/AdbWinUsbApi.dll +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/SDL2.dll +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/adb.exe +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/avcodec-60.dll +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/avformat-60.dll +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/avutil-58.dll +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/icon.png +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/libusb-1.0.dll +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/open_a_terminal_here.bat +1 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy-console.bat +2 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy-noconsole.vbs +7 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy-server +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy.exe +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/swresample-4.dll +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/AdbWinApi.dll +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/AdbWinUsbApi.dll +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/SDL2.dll +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/avformat-60.dll +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/avutil-58.dll +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/open_a_terminal_here.bat +1 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/scrcpy-noconsole.vbs +7 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/scrcpy-server +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/scrcpy.exe +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/swresample-4.dll +0 -0
- magnax/static/css/highlight.min.css +9 -0
- magnax/static/css/magnax-dark-theme.css +1237 -0
- magnax/static/css/select2-bootstrap-5-theme.min.css +3 -0
- magnax/static/css/select2-bootstrap-5-theme.rtl.min.css +3 -0
- magnax/static/css/select2.min.css +1 -0
- magnax/static/css/sweetalert2.min.css +1 -0
- magnax/static/css/tabler.demo.min.css +9 -0
- magnax/static/css/tabler.min.css +14 -0
- magnax/static/image/500.png +0 -0
- magnax/static/image/avatar.png +0 -0
- magnax/static/image/empty.png +0 -0
- magnax/static/image/readme/home.png +0 -0
- magnax/static/image/readme/pk.png +0 -0
- magnax/static/js/apexcharts.js +14 -0
- magnax/static/js/gray.js +16 -0
- magnax/static/js/highlight.min.js +1173 -0
- magnax/static/js/highstock.js +803 -0
- magnax/static/js/html2canvas.min.js +20 -0
- magnax/static/js/jquery.min.js +2 -0
- magnax/static/js/magnax-chart-theme.js +492 -0
- magnax/static/js/select2.min.js +2 -0
- magnax/static/js/sweetalert2.min.js +1 -0
- magnax/static/js/tabler.demo.min.js +9 -0
- magnax/static/js/tabler.min.js +9 -0
- magnax/static/logo/logo.png +0 -0
- magnax/templates/404.html +30 -0
- magnax/templates/analysis.html +1375 -0
- magnax/templates/analysis_compare.html +600 -0
- magnax/templates/analysis_pk.html +680 -0
- magnax/templates/base.html +365 -0
- magnax/templates/index.html +2471 -0
- magnax/templates/pk.html +743 -0
- magnax/templates/report.html +416 -0
- magnax/view/__init__.py +1 -0
- magnax/view/apis.py +952 -0
- magnax/view/pages.py +146 -0
- magnax/web.py +345 -0
- magnax-1.0.0.dist-info/METADATA +242 -0
- magnax-1.0.0.dist-info/RECORD +87 -0
- magnax-1.0.0.dist-info/WHEEL +5 -0
- magnax-1.0.0.dist-info/entry_points.txt +2 -0
- magnax-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|