From b91601d0e317f190873edfae504a3a70af1582b6 Mon Sep 17 00:00:00 2001 From: chenhq Date: Mon, 20 Nov 2023 17:44:39 +0800 Subject: [PATCH] fix #236 --- solox/public/apm.py | 32 ++- solox/public/fps_api.py | 498 ++++++++++++++++++++++++++++++++++++++ solox/templates/base.html | 4 +- 3 files changed, 529 insertions(+), 5 deletions(-) create mode 100644 solox/public/fps_api.py diff --git a/solox/public/apm.py b/solox/public/apm.py index 80c8a8e..587b699 100644 --- a/solox/public/apm.py +++ b/solox/public/apm.py @@ -10,6 +10,8 @@ from solox.public.adb import adb from solox.public.common import Devices, File, Method, Platform, Scrcpy from solox.public.fps import FPSMonitor, TimeUtils +from solox.public.fps_api import FPSMonitor_API, TimeUtils_API + d = Devices() f = File() @@ -340,7 +342,26 @@ def getAndroidFps(self, noLog=False): else: logger.exception(e) return fps, jank - + + def getAndroidFpsApi(self, noLog=False): + """get Android Fps, unit:HZ""" + try: + monitors = FPSMonitor_API(device_id=self.deviceId, package_name=self.pkgName, frequency=1, + surfaceview=self.surfaceview, start_time=TimeUtils_API.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) + except Exception as e: + fps, jank = 0 + if len(d.getPid(self.deviceId, self.pkgName)) == 0: + logger.error('[FPS] {} : No process found'.format(self.pkgName)) + else: + logger.exception(e) + return fps, jank + def getiOSFps(self, noLog=False): """get iOS Fps""" apm = iosAPM(self.pkgName, self.deviceId) @@ -354,6 +375,11 @@ def getFPS(self, noLog=False): """get fps、jank""" fps, jank = self.getAndroidFps(noLog) if self.platform == Platform.Android else self.getiOSFps(noLog) return fps, jank + + def getFPS_API(self, noLog=False): + """get fps、jank""" + fps, jank = self.getAndroidFpsApi(noLog) if self.platform == Platform.Android else self.getiOSFps(noLog) + return fps, jank class GPU(object): def __init__(self, pkgName, deviceId): @@ -474,7 +500,7 @@ def collectFps(self): _fps = FPS(self.pkgName, self.deviceId, self.platform, self.surfaceview) result = {} while True: - fps, jank = _fps.getFPS(noLog=self.noLog) + fps, jank = _fps.getFPS_API(noLog=self.noLog) result = {'fps': fps, 'jank': jank} logger.info(f'fps: {result}') if time.time() > self.end_time: @@ -486,7 +512,7 @@ def collectGpu(self): result = {} while True: if self.platform == Platform.Android: - raise Exception('not support android') + break gpu = _gpu.getGPU(noLog=self.noLog) result = {'gpu': gpu} logger.info(f'gpu: {result}') diff --git a/solox/public/fps_api.py b/solox/public/fps_api.py new file mode 100644 index 0000000..797c863 --- /dev/null +++ b/solox/public/fps_api.py @@ -0,0 +1,498 @@ +# -*- coding: utf-8 -*- +import datetime +import queue +import re +import threading +import time +import traceback +from logzero import logger +from solox.public.adb import adb +from solox.public.common import Devices + +d = Devices() + +collect_fps = 0 +collect_jank = 0 + + +class SurfaceStatsCollector(object): + def __init__(self, device, frequency, package_name, fps_queue, jank_threshold, surfaceview, use_legacy=False): + self.device = device + self.frequency = frequency + self.package_name = package_name + self.jank_threshold = jank_threshold / 1000.0 + self.use_legacy_method = use_legacy + self.surface_before = 0 + self.last_timestamp = 0 + self.data_queue = queue.Queue() + self.stop_event = threading.Event() + self.focus_window = None + self.surfaceview = surfaceview + self.fps_queue = fps_queue + + 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() + + def stop(self): + if self.collector_thread: + self.stop_event.set() + self.collector_thread.join() + self.collector_thread = None + if self.fps_queue: + self.fps_queue.task_done() + + 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 + + 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 "" + + def _calculate_results(self, refresh_period, timestamps): + frame_count = len(timestamps) + if frame_count == 0: + fps = 0 + jank = 0 + elif frame_count == 1: + 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_janky(timestamps) + else: + fps = 1 + jank = 0 + return fps, jank + + 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 + + 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 + + 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 + + def _calculator_thread(self, start_time): + global collect_fps + global collect_jank + while True: + try: + data = self.data_queue.get() + if isinstance(data, str) and data == 'Stop': + break + before = time.time() + if self.use_legacy_method: + 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 + else: + refresh_period = data[0] + timestamps = data[1] + collect_time = data[2] + # fps,jank = self._calculate_results(refresh_period, timestamps) + fps, jank = self._calculate_results_new(refresh_period, timestamps) + # logger.debug('FPS:%2s Jank:%s'%(fps,jank)) + collect_fps = fps + collect_jank = jank + time_consume = time.time() - before + delta_inter = self.frequency - time_consume + if delta_inter > 0: + time.sleep(delta_inter) + except: + logger.error("an exception hanpend in fps _calculator_thread ,reason unkown!") + s = traceback.format_exc() + logger.debug(s) + if self.fps_queue: + self.fps_queue.task_done() + + def _collector_thread(self): + is_first = True + while not self.stop_event.is_set(): + try: + before = time.time() + if self.use_legacy_method: + surface_state = self._get_surface_stats_legacy() + if surface_state: + self.data_queue.put(surface_state) + else: + 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) + except: + logger.error("an exception hanpend in fps _collector_thread , reason unkown!") + s = traceback.format_exc() + logger.debug(s) + if self.fps_queue: + self.fps_queue.task_done() + self.data_queue.put(u'Stop') + + def _clear_surfaceflinger_latency_data(self): + """Clears the SurfaceFlinger latency data. + + Returns: + True if SurfaceFlinger latency is supported by the device, otherwise + False. + """ + # The command returns nothing if it is supported, otherwise returns many + # lines of result just like 'dumpsys SurfaceFlinger'. + if self.focus_window is None: + results = adb.shell(cmd='dumpsys SurfaceFlinger --latency-clear', deviceId=self.device) + else: + results = adb.shell(cmd='dumpsys SurfaceFlinger --latency-clear %s' % self.focus_window, + deviceId=self.device) + return not len(results) + + def get_sdk_version(self): + sdk_version = int(adb.shell(cmd='getprop ro.build.version.sdk', deviceId=self.device)) + return sdk_version + + 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 + # 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) + + 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 + + +class Monitor(object): + def __init__(self, **kwargs): + self.config = kwargs + self.matched_data = {} + + def start(self): + logger.warn("请在%s类中实现start方法" % type(self)) + + def clear(self): + self.matched_data = {} + + def stop(self): + logger.warning("请在%s类中实现stop方法" % type(self)) + + def save(self): + logger.warning("请在%s类中实现save方法" % type(self)) + + +class TimeUtils_API(object): + UnderLineFormatter = "%Y_%m_%d_%H_%M_%S" + NormalFormatter = "%Y-%m-%d %H-%M-%S" + ColonFormatter = "%Y-%m-%d %H:%M:%S" + + @staticmethod + def getCurrentTimeUnderline(): + return time.strftime(TimeUtils_API.UnderLineFormatter, time.localtime(time.time())) + + +class FPSMonitor_API(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) + + def start(self): + self.fpscollector.start(self.start_time) + + def stop(self): + global collect_fps + global collect_jank + self.fpscollector.stop() + return collect_fps, collect_jank + + def save(self): + pass + + def parse(self, file_path): + pass + + def get_fps_collector(self): + return self.fpscollector diff --git a/solox/templates/base.html b/solox/templates/base.html index 0e98ac5..78c345b 100644 --- a/solox/templates/base.html +++ b/solox/templates/base.html @@ -129,8 +129,8 @@

SoloX.{% if lan == 'cn' %} 保留所有权利 {% else %} All rights reserved {% endif %}
  • - - {% if lan == 'cn' %} 版本 {% else %} Releases {% endif %} . V2.7.9 + + solox preview