# 安卓
通过 fpsMonitor 类获取到 fps 计算器;
def getAndroidFps(self, noLog=False): | |
"""get Android Fps, unit:HZ""" | |
monitors = FPSMonitor(device_id=self.deviceId, package_name=self.pkgName, frequency=1, | |
surfaceview=self.surfaceview, start_time=TimeUtils.getCurrentTimeUnderline()) | |
monitors.start() | |
fps, jank = monitors.stop() | |
if noLog is False: | |
apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f') | |
f.add_log(os.path.join(f.report_dir,'fps.log'), apm_time, fps) | |
f.add_log(os.path.join(f.report_dir,'jank.log'), apm_time, jank) | |
return fps, jank |
在 FPSMonitor 中使用 SurfaceStatsCollector 来采集 FPS 数据
class FPSMonitor(Monitor): | |
def __init__(self, device_id, package_name=None, frequency=1.0, timeout=24 * 60 * 60, fps_queue=None, | |
jank_threshold=166, use_legacy=False, surfaceview=True, start_time=None, **kwargs): | |
super().__init__(**kwargs) | |
self.start_time = start_time | |
self.use_legacy = use_legacy | |
self.frequency = frequency # 取样频率 | |
self.jank_threshold = jank_threshold | |
self.device = device_id | |
self.timeout = timeout | |
self.surfaceview = surfaceview | |
if not package_name: | |
package_name = self.device.adb.get_foreground_process() | |
self.package = package_name | |
self.fpscollector = SurfaceStatsCollector(self.device, self.frequency, package_name, fps_queue, | |
self.jank_threshold, self.surfaceview, self.use_legacy) |
SurfaceStatsCollector 涵盖的功能有:
# 获取 surface 的 activity 名字
def get_surfaceview_activity(self): | |
activity_name = '' | |
activity_line = '' | |
try: | |
dumpsys_result = adb.shell(cmd='dumpsys SurfaceFlinger --list | {} {}'.format(d.filterType(), self.package_name), deviceId=self.device) | |
dumpsys_result_list = dumpsys_result.split('\n') | |
for line in dumpsys_result_list: | |
if line.startswith('SurfaceView') and line.find(self.package_name) != -1: | |
activity_line = line.strip() | |
break | |
if activity_line: | |
if activity_line.find(' ') != -1: | |
activity_name = activity_line.split(' ')[2] | |
else: | |
activity_name = activity_line.replace('SurfaceView','').replace('[','').replace(']','') | |
else: | |
activity_name = dumpsys_result_list[len(dumpsys_result_list) - 1] | |
if not activity_name.__contains__(self.package_name): | |
logger.error('get activity name failed, Please provide SurfaceFlinger --list information to the author') | |
logger.info('dumpsys SurfaceFlinger --list info: {}'.format(dumpsys_result)) | |
except Exception: | |
traceback.print_exc() | |
logger.error('get activity name failed, Please provide SurfaceFlinger --list information to the author') | |
logger.info('dumpsys SurfaceFlinger --list info: {}'.format(dumpsys_result)) | |
return activity_name |
# 获取聚焦的 activity 名字
def get_focus_activity(self): | |
activity_name = '' | |
activity_line = '' | |
dumpsys_result = adb.shell(cmd='dumpsys window windows', deviceId=self.device) | |
dumpsys_result_list = dumpsys_result.split('\n') | |
for line in dumpsys_result_list: | |
if line.find('mCurrentFocus') != -1: | |
activity_line = line.strip() | |
if activity_line: | |
activity_line_split = activity_line.split(' ') | |
else: | |
return activity_name | |
if len(activity_line_split) > 1: | |
if activity_line_split[1] == 'u0': | |
activity_name = activity_line_split[2].rstrip('}') | |
else: | |
activity_name = activity_line_split[1] | |
if not activity_name: | |
activity_name = self.get_surfaceview_activity() | |
return activity_name |
# 获取前台进程名字
def get_foreground_process(self): | |
focus_activity = self.get_focus_activity() | |
if focus_activity: | |
return focus_activity.split("/")[0] | |
else: | |
return "" |
# 获取 sdk 版本
def get_sdk_version(self): | |
sdk_version = int(adb.shell(cmd='getprop ro.build.version.sdk', deviceId=self.device)) | |
return sdk_version |
# 版本兼容
为了兼容不同版本的安卓系统,做了两套方法,在启动时进行区分:
def start(self, start_time): | |
if not self.use_legacy_method: | |
try: | |
self.focus_window = self.get_focus_activity() | |
if self.focus_window.find('$') != -1: | |
self.focus_window = self.focus_window.replace('$', '\$') | |
except Exception: | |
logger.warning(u'Unable to dynamically obtain the current activity name, using page_ Flip statistics full screen frame rate') | |
self.use_legacy_method = True | |
self.surface_before = self._get_surface_stats_legacy() | |
else: | |
logger.debug("dumpsys SurfaceFlinger --latency-clear is none") | |
self.use_legacy_method = True | |
self.surface_before = self._get_surface_stats_legacy() | |
self.collector_thread = threading.Thread(target=self._collector_thread) | |
self.collector_thread.start() | |
self.calculator_thread = threading.Thread(target=self._calculator_thread, args=(start_time,)) | |
self.calculator_thread.start() |
这里分别启动了采集线程和计算线程。
# 旧版本方法
获取当前的 surface 索引和时间戳
def _get_surface_stats_legacy(self): | |
"""Legacy method (before JellyBean), returns the current Surface index | |
and timestamp. | |
Calculate FPS by measuring the difference of Surface index returned by | |
SurfaceFlinger in a period of time. | |
Returns: | |
Dict of {page_flip_count (or 0 if there was an error), timestamp}. | |
""" | |
cur_surface = None | |
timestamp = datetime.datetime.now() | |
ret = adb.shell(cmd="service call SurfaceFlinger 1013", deviceId=self.device) | |
if not ret: | |
return None | |
match = re.search('^Result: Parcel\((\w+)', ret) | |
if match: | |
cur_surface = int(match.group(1), 16) | |
return {'page_flip_count': cur_surface, 'timestamp': timestamp} | |
return None |
# 新版本方法
# 首先获取到帧数据列表
def _get_surfaceflinger_frame_data(self): | |
"""Returns collected SurfaceFlinger frame timing data. | |
return:(16.6,[[t1,t2,t3],[t4,t5,t6]]) | |
Returns: | |
A tuple containing: | |
- The display's nominal refresh period in seconds. | |
- A list of timestamps signifying frame presentation times in seconds. | |
The return value may be (None, None) if there was no data collected (for | |
example, if the app was closed before the collector thread has finished). | |
""" | |
# shell dumpsys SurfaceFlinger --latency <window name> | |
# prints some information about the last 128 frames displayed in | |
# that window. | |
# The data returned looks like this: | |
# 16954612 | |
# 7657467895508 7657482691352 7657493499756 | |
# 7657484466553 7657499645964 7657511077881 | |
# 7657500793457 7657516600576 7657527404785 | |
# (...) | |
# | |
# The first line is the refresh period (here 16.95 ms), it is followed | |
# by 128 lines w/ 3 timestamps in nanosecond each: | |
# A) when the app started to draw | |
# B) the vsync immediately preceding SF submitting the frame to the h/w | |
# C) timestamp immediately after SF submitted that frame to the h/w | |
# | |
# The difference between the 1st and 3rd timestamp is the frame-latency. | |
# An interesting data is when the frame latency crosses a refresh period | |
# boundary, this can be calculated this way: | |
# | |
# ceil((C - A) / refresh-period) | |
# | |
# (each time the number above changes, we have a "jank"). | |
# If this happens a lot during an animation, the animation appears | |
# janky, even if it runs at 60 fps in average. | |
# | |
# Google Pixel 2 android8.0 dumpsys SurfaceFlinger --latency 结果 | |
# 16666666 | |
# 0 0 0 | |
# 0 0 0 | |
# 0 0 0 | |
# 0 0 0 | |
# 但华为 荣耀 9 android8.0 dumpsys SurfaceFlinger --latency 结果是正常的 但数据更新很慢 也不能用来计算 fps | |
# 16666666 | |
# 9223372036854775807 3618832932780 9223372036854775807 | |
# 9223372036854775807 3618849592155 9223372036854775807 | |
# 9223372036854775807 3618866251530 9223372036854775807 | |
refresh_period = None | |
timestamps = [] | |
nanoseconds_per_second = 1e9 | |
pending_fence_timestamp = (1 << 63) - 1 | |
if self.surfaceview is not True: | |
results = adb.shell( | |
cmd='dumpsys SurfaceFlinger --latency %s' % self.focus_window, deviceId=self.device) | |
results = results.replace("\r\n", "\n").splitlines() | |
refresh_period = int(results[0]) / nanoseconds_per_second | |
results = adb.shell(cmd='dumpsys gfxinfo %s framestats' % self.package_name, deviceId=self.device) | |
results = results.replace("\r\n", "\n").splitlines() | |
if not len(results): | |
return (None, None) | |
isHaveFoundWindow = False | |
PROFILEDATA_line = 0 | |
activity = self.focus_window | |
if self.focus_window.__contains__('#'): | |
activity = activity.split('#')[0] | |
for line in results: | |
if not isHaveFoundWindow: | |
if "Window" in line and activity in line: | |
isHaveFoundWindow = True | |
if not isHaveFoundWindow: | |
continue | |
if "PROFILEDATA" in line: | |
PROFILEDATA_line += 1 | |
fields = [] | |
fields = line.split(",") | |
if fields and '0' == fields[0]: | |
timestamp = [int(fields[1]), int(fields[2]), int(fields[13])] | |
if timestamp[1] == pending_fence_timestamp: | |
continue | |
timestamp = [_timestamp / nanoseconds_per_second for _timestamp in timestamp] | |
timestamps.append(timestamp) | |
if 2 == PROFILEDATA_line: | |
break | |
else: | |
self.focus_window = self.get_surfaceview_activity() | |
results = adb.shell( | |
cmd='dumpsys SurfaceFlinger --latency %s' % self.focus_window, deviceId=self.device) | |
results = results.replace("\r\n", "\n").splitlines() | |
if not len(results): | |
return (None, None) | |
if not results[0].isdigit(): | |
return (None, None) | |
try: | |
refresh_period = int(results[0]) / nanoseconds_per_second | |
except Exception as e: | |
logger.exception(e) | |
return (None, None) | |
# If a fence associated with a frame is still pending when we query the | |
# latency data, SurfaceFlinger gives the frame a timestamp of INT64_MAX. | |
# Since we only care about completed frames, we will ignore any timestamps | |
# with this value. | |
for line in results[1:]: | |
fields = line.split() | |
if len(fields) != 3: | |
continue | |
timestamp = [int(fields[0]), int(fields[1]), int(fields[2])] | |
if timestamp[1] == pending_fence_timestamp: | |
continue | |
timestamp = [_timestamp / nanoseconds_per_second for _timestamp in timestamp] | |
timestamps.append(timestamp) | |
return (refresh_period, timestamps) |
然后将数据插入到队列中
refresh_period, new_timestamps = self._get_surfaceflinger_frame_data() | |
if refresh_period is None or new_timestamps is None: | |
self.focus_window = self.get_focus_activity() | |
logger.warning("refresh_period is None or timestamps is None") | |
continue | |
timestamps += [timestamp for timestamp in new_timestamps | |
if timestamp[1] > self.last_timestamp] | |
if len(timestamps): | |
first_timestamp = [[0, self.last_timestamp, 0]] | |
if not is_first: | |
timestamps = first_timestamp + timestamps | |
self.last_timestamp = timestamps[-1][1] | |
is_first = False | |
else: | |
is_first = True | |
cur_focus_window = self.get_focus_activity() | |
if self.focus_window != cur_focus_window: | |
self.focus_window = cur_focus_window | |
continue | |
self.data_queue.put((refresh_period, timestamps, time.time())) | |
time_consume = time.time() - before | |
delta_inter = self.frequency - time_consume | |
if delta_inter > 0: | |
time.sleep(delta_inter) |
# 计算线程计算 fps 和 jank
# 旧版本方法
不知道为什么 fps 设置最高就是 60
通过一段时间内的帧总数除以时间差获取到 fps
td = data['timestamp'] - self.surface_before['timestamp'] | |
seconds = td.seconds + td.microseconds / 1e6 | |
frame_count = (data['page_flip_count'] - | |
self.surface_before['page_flip_count']) | |
fps = int(round(frame_count / seconds)) | |
if fps > 60: | |
fps = 60 | |
self.surface_before = data | |
# logger.debug('FPS:%2s'%fps) | |
collect_fps = fps |
# 新版本方法
- 首先拿到帧数
- 对异常数据的处理
- 2、3、4 帧数为啥这样处理?
def _calculate_results_new(self, refresh_period, timestamps): | |
frame_count = len(timestamps) | |
if frame_count == 0: | |
fps = 0 | |
jank = 0 | |
elif frame_count == 1: | |
fps = 1 | |
jank = 0 | |
elif frame_count == 2 or frame_count == 3 or frame_count == 4: | |
seconds = timestamps[-1][1] - timestamps[0][1] | |
if seconds > 0: | |
fps = int(round((frame_count - 1) / seconds)) | |
jank = self._calculate_janky(timestamps) | |
else: | |
fps = 1 | |
jank = 0 | |
else: | |
seconds = timestamps[-1][1] - timestamps[0][1] | |
if seconds > 0: | |
fps = int(round((frame_count - 1) / seconds)) | |
jank = self._calculate_jankey_new(timestamps) | |
else: | |
fps = 1 | |
jank = 0 | |
return fps, jank |
# 计算 jank
# 旧版本
def _calculate_janky(self, timestamps): | |
tempstamp = 0 | |
jank = 0 | |
for timestamp in timestamps: | |
if tempstamp == 0: | |
tempstamp = timestamp[1] | |
continue | |
costtime = timestamp[1] - tempstamp | |
if costtime > self.jank_threshold: | |
jank = jank + 1 | |
tempstamp = timestamp[1] | |
return jank |
# 新版本
通过预设的 jank_threshold 阈值判断是否有 jank(默认 166ms 的渲染时间)
通过过去 4 帧的时间,也即过去三帧的平均渲染时间 *2,如果当前帧渲染时间超过这个值,并且当前帧渲染时间大于上一帧渲染时间,判断为一次 jank
def _calculate_jankey_new(self, timestamps): | |
twofilmstamp = 83.3 / 1000.0 | |
tempstamp = 0 | |
jank = 0 | |
for index, timestamp in enumerate(timestamps): | |
if (index == 0) or (index == 1) or (index == 2) or (index == 3): | |
if tempstamp == 0: | |
tempstamp = timestamp[1] | |
continue | |
costtime = timestamp[1] - tempstamp | |
if costtime > self.jank_threshold: | |
jank = jank + 1 | |
tempstamp = timestamp[1] | |
elif index > 3: | |
currentstamp = timestamps[index][1] | |
lastonestamp = timestamps[index - 1][1] | |
lasttwostamp = timestamps[index - 2][1] | |
lastthreestamp = timestamps[index - 3][1] | |
lastfourstamp = timestamps[index - 4][1] | |
tempframetime = ((lastthreestamp - lastfourstamp) + (lasttwostamp - lastthreestamp) + ( | |
lastonestamp - lasttwostamp)) / 3 * 2 | |
currentframetime = currentstamp - lastonestamp | |
if (currentframetime > tempframetime) and (currentframetime > twofilmstamp): | |
jank = jank + 1 | |
return jank |
# IOS
同样使用 instrument 协议
def iter_fps(d: BaseDevice) -> Iterator[Any]: | |
with d.connect_instruments() as ts: | |
for data in ts.iter_opengl_data(): | |
fps = data['CoreAnimationFramesPerSecond'] # fps from GPU | |
# print("FPS:", fps) | |
yield DataType.FPS, {"fps": fps, "time": time.time(), "value": fps} |