From b8a02b9d2f80e4c8e651caec30afa9e40c8f1677 Mon Sep 17 00:00:00 2001 From: chenhq Date: Tue, 4 Apr 2023 18:27:32 +0800 Subject: [PATCH] fixed #138 #125 --- README.md | 23 ++++--- README.zh.md | 18 +++--- solox/__init__.py | 2 +- solox/debug.py | 20 +++--- solox/public/apm.py | 12 +++- solox/public/common.py | 114 ++++++++++++++++++++-------------- solox/public/fps.py | 54 ++-------------- solox/templates/analysis.html | 2 +- solox/templates/base.html | 61 ++++++++++++++++-- solox/templates/index.html | 91 +++++++++++++++++++++++---- solox/templates/report.html | 2 +- solox/view/apis.py | 29 +++++++++ solox/view/pages.py | 18 +++--- solox/web.py | 56 +++-------------- 14 files changed, 305 insertions(+), 197 deletions(-) diff --git a/README.md b/README.md index 586a9d5..bbe8d4d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- English | 中文 | DocForAndroid + English | 中文

@@ -13,6 +13,7 @@

solox preview +

@@ -26,10 +27,10 @@ We are committed to solving inefficient, cumbersome test execution, and our goal ## Installation -``` -1.Python:3.10+ (python3.6+ lower v2.5.3) -2.pip install -U solox -3.pip install -i https://mirrors.ustc.edu.cn/pypi/web/simple -U solox (Recommend) +```shell +1.Python:3.10+ (Python 3.6 3.7 3.8 3.9 Please download a version of solox lower than 2.5.4) +2.pip install -U solox +3.pip install -i https://mirrors.ustc.edu.cn/pypi/web/simple -U solox (China) notice: If Windows users need to test ios, install and start Itunes ``` @@ -52,11 +53,11 @@ from solox.public.apm import APM apm = APM(pkgName='com.bilibili.app.in',deviceId='ca6bd5a5',platform='Android', surfaceview=True) # apm = APM(pkgName='com.bilibili.app.in', platform='iOS') only supports one device -# surfaceview: false = gfxinfo (Developer - GPU rendering mode - adb shell dumpsys gfxinfo) +# surfaceview: False = gfxinfo (Developer - GPU rendering mode - adb shell dumpsys gfxinfo) cpu = apm.collectCpu() # % memory = apm.collectMemory() # MB -flow = apm.collectFlow() # KB +flow = apm.collectFlow(wifi=True) # KB fps = apm.collectFps() # HZ battery = apm.collectBattery() # level:% temperature:°C current:mA voltage:mV power:w gpu = apm.collectGpu() # % only supports ios @@ -73,9 +74,9 @@ Windows: start /min python3 -m solox & ``` ### Request apm data from api -``` -- Android: http://{ip}:{port}/apm/collect?platform=Android&deviceid=ca6bd5a5&pkgname=com.bilibili.app.in&target=cpu -- iOS: http://{ip}:{port}/apm/collect?platform=iOS&pkgname=com.bilibili.app.in&target=cpu +```shell +Android: http://{ip}:{port}/apm/collect?platform=Android&deviceid=ca6bd5a5&pkgname=com.bilibili.app.in&target=cpu +iOS: http://{ip}:{port}/apm/collect?platform=iOS&pkgname=com.bilibili.app.in&target=cpu target in ['cpu','memory','network','fps','battery'] ``` @@ -84,6 +85,8 @@ target in ['cpu','memory','network','fps','battery'] - 2-devices: test the same app on two different phones - 2-apps: test two different apps on two phones with the same configuration +notice: only supports android + ## Browser diff --git a/README.zh.md b/README.zh.md index b87eaa3..69895d6 100644 --- a/README.zh.md +++ b/README.zh.md @@ -1,5 +1,5 @@

- 中文 | English | DocForAndroid + 中文 | English

@@ -26,8 +26,8 @@ SoloX - Android/iOS性能数据实时采集工具。 ## 安装 -``` -1.Python:3.6+ (python3.6+ 小于v2.5.3) +```shell +1.Python:3.10+ (Python 3.6 3.7 3.8 3.9 请下载低于2.5.4的版本) 2.pip install -U solox 3.pip install -i https://mirrors.ustc.edu.cn/pypi/web/simple -U solox (在国内推荐使用镜像下载) @@ -52,10 +52,10 @@ from solox.public.apm import APM apm = APM(pkgName='com.bilibili.app.in',deviceId='ca6bd5a5',platform='Android', surfaceview=True) # apm = APM(pkgName='com.bilibili.app.in', platform='iOS') only supports one device -# surfaceview: false = gfxinfo (手机开发者-GPU呈现模式- adb shell dumpsys gfxinfo) +# surfaceview: False = gfxinfo (手机开发者-GPU呈现模式- adb shell dumpsys gfxinfo) cpu = apm.collectCpu() # % memory = apm.collectMemory() # MB -flow = apm.collectFlow() # KB +flow = apm.collectFlow(wifi=True) # KB fps = apm.collectFps() # HZ battery = apm.collectBattery() # level:% temperature:°C current:mA voltage:mV power:w gpu = apm.collectGpu() # % only supports ios @@ -72,9 +72,9 @@ Windows: start /min python3 -m solox & ``` ### 2.通过api请求性能数据 -``` -- Android: http://{ip}:{port}/apm/collect?platform=Android&deviceid=ca6bd5a5&pkgname=com.bilibili.app.in&target=cpu -- iOS: http://{ip}:{port}/apm/collect?platform=iOS&pkgname=com.bilibili.app.in&target=cpu +```shell +Android: http://{ip}:{port}/apm/collect?platform=Android&deviceid=ca6bd5a5&pkgname=com.bilibili.app.in&target=cpu +iOS: http://{ip}:{port}/apm/collect?platform=iOS&pkgname=com.bilibili.app.in&target=cpu target in ['cpu','memory','network','fps','battery'] ``` @@ -83,6 +83,8 @@ target in ['cpu','memory','network','fps','battery'] - 2-devices: 在两部不同的手机上测试同一个应用 - 2-apps: 在具有相同配置的两部手机上测试两个不同的应用程序 +注意: 目前只支持安卓 + ## 推荐浏览器 diff --git a/solox/__init__.py b/solox/__init__.py index 7770923..46c168a 100644 --- a/solox/__init__.py +++ b/solox/__init__.py @@ -2,4 +2,4 @@ from __future__ import absolute_import -__version__ = '2.5.5' +__version__ = '2.5.6' diff --git a/solox/debug.py b/solox/debug.py index ba06db8..a975ae5 100644 --- a/solox/debug.py +++ b/solox/debug.py @@ -16,6 +16,8 @@ from flask_socketio import SocketIO, disconnect from flask import Flask import fire as fire +import signal + app = Flask(__name__, template_folder='templates', static_folder='static') app.register_blueprint(api) @@ -141,7 +143,7 @@ def openUrl(host: str, port: int): """ flag = True while flag: - logger.info('start solox server...') + logger.info('start solox server') flag = getServerStatus(host, port) webbrowser.open(f'http://{host}:{port}/?platform=Android&lan=en', new=2) logger.info(f'Running on http://{host}:{port}/?platform=Android&lan=en (Press CTRL+C to quit)') @@ -155,6 +157,7 @@ def startServer(host: str, port: int): :return: """ try: + logger.info(f'Running on http://{host}:{port}/?platform=Android&lan=en (Press CTRL+C to quit)') socketio.run(app, host=host, debug=False, port=port) except Exception: sys.exit(0) @@ -169,15 +172,16 @@ def main(host=_hostIP(), port=50003): try: checkPyVer() listeningPort(port=port) - pool = multiprocessing.Pool(processes=2) - pool.apply_async(startServer, (host, port)) - pool.apply_async(openUrl, (host, port)) - pool.close() - pool.join() + startServer(host, port) + # pool = multiprocessing.Pool(processes=2) + # pool.apply_async(startServer, (host, port)) + # pool.apply_async(openUrl, (host, port)) + # pool.close() + # pool.join() except Exception: - pass + sys.exit(0) except KeyboardInterrupt: logger.info('stop solox success') if __name__ == '__main__': - fire.Fire(main) + fire.Fire(main) \ No newline at end of file diff --git a/solox/public/apm.py b/solox/public/apm.py index 865890d..c72ea6f 100644 --- a/solox/public/apm.py +++ b/solox/public/apm.py @@ -222,6 +222,16 @@ def getAndroidNet(self, wifi=True): recNum = round(float(recNum_final - recNum_pre), 2) return sendNum, recNum + def setAndroidNet(self, wifi=True): + net = 'wlan0' if wifi else 'rmnet0' + pid = d.getPid(pkgName=self.pkgName, deviceId=self.deviceId) + cmd = f'cat /proc/{pid}/net/dev |{d.filterType()} {net}' + output_pre = adb.shell(cmd=cmd, deviceId=self.deviceId) + m = re.search(r'{}:\s*(\d+)\s*\d+\s*\d+\s*\d+\s*\d+\s*\d+\s*\d+\s*\d+\s*(\d+)'.format(net), output_pre) + sendNum = round(float(float(m.group(2)) / 1024), 2) + recNum = round(float(float(m.group(1)) / 1024), 2) + return sendNum, recNum + def getiOSNet(self): """Get iOS upflow and downflow data""" @@ -333,7 +343,7 @@ def __init__(self, pkgName, deviceId='', platform=Platform.Android, surfaceview= self.deviceId = deviceId self.platform = platform self.surfaceview = surfaceview - d.devicesCheck(pf=self.platform, id=self.deviceId, pkg=self.pkgName) + d.devicesCheck(platform=self.platform, deviceid=self.deviceId, pkgname=self.pkgName) def collectCpu(self): _cpu = CPU(self.pkgName, self.deviceId, self.platform) diff --git a/solox/public/common.py b/solox/public/common.py index 197de42..ca9fb1e 100644 --- a/solox/public/common.py +++ b/solox/public/common.py @@ -6,7 +6,6 @@ import time import requests from logzero import logger -from flask import request from solox.public.adb import adb from tqdm import tqdm import traceback @@ -176,16 +175,17 @@ def export_excel(self, platform, scene): ws1.write(0,1,'Value') row = 1 #start row col = 0 #start col - f = open(f'{self.report_dir}/{scene}/{name}.log','r',encoding='utf-8') - for lines in f: - target = lines.split('=') - k += 1 - for i in range(len(target)): - ws1.write(row, col ,target[i]) - col += 1 - row += 1 - col = 0 - wb.save(f'{scene}.xls') # xxx.xls + if os.path.exists(f'{self.report_dir}/{scene}/{name}.log'): + f = open(f'{self.report_dir}/{scene}/{name}.log','r',encoding='utf-8') + for lines in f: + target = lines.split('=') + k += 1 + for i in range(len(target)): + ws1.write(row, col ,target[i]) + col += 1 + row += 1 + col = 0 + wb.save(f'{scene}.xls') def get_repordir(self): report_dir = os.path.join(os.getcwd(), 'report') @@ -194,16 +194,32 @@ def get_repordir(self): return report_dir def create_file(self, filename, content=''): - if not os.path.exists(f'{self.report_dir}'): - os.mkdir(f'{self.report_dir}') - with open(f'{self.report_dir}/{filename}', 'a+', encoding="utf-8") as file: + if not os.path.exists(self.report_dir): + os.mkdir(self.report_dir) + with open(os.path.join(self.report_dir, filename), 'a+', encoding="utf-8") as file: file.write(content) def add_log(self, path, log_time, value): if value >= 0: with open(path, 'a+', encoding="utf-8") as file: file.write(f'{log_time}={str(value)}' + '\n') - + + def record_net(self, type, send , recv): + net_dict = {} + match(type): + case 'pre': + net_dict['send'] = send + net_dict['recv'] = recv + content = json.dumps(net_dict) + self.create_file(filename='pre_net.json', content=content) + case 'end': + net_dict['send'] = send + net_dict['recv'] = recv + content = json.dumps(net_dict) + self.create_file(filename='end_net.json', content=content) + case _: + logger.error('record network data failed') + def make_report(self, app, devices, platform='Android', model='normal'): current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) result_dict = { @@ -216,7 +232,7 @@ def make_report(self, app, devices, platform='Android', model='normal'): } content = json.dumps(result_dict) self.create_file(filename='result.json', content=content) - report_new_dir = f'{self.report_dir}/{self.fileroot}' + report_new_dir = os.path.join(self.report_dir, self.fileroot) if not os.path.exists(report_new_dir): os.mkdir(report_new_dir) @@ -237,22 +253,24 @@ def readLog(self, scene, filename): """Read apmlog file data""" log_data_list = [] target_data_list = [] - f = open(f'{self.report_dir}/{scene}/{filename}', "r") - lines = f.readlines() - for line in lines: - if isinstance(line.split('=')[1].strip(), int): - log_data_list.append({ - "x": line.split('=')[0].strip(), - "y": int(line.split('=')[1].strip()) - }) - target_data_list.append(int(line.split('=')[1].strip())) - else: - log_data_list.append({ - "x": line.split('=')[0].strip(), - "y": float(line.split('=')[1].strip()) - }) - target_data_list.append(float(line.split('=')[1].strip())) + if os.path.exists(os.path.join(self.report_dir,scene,filename)): + f = open(os.path.join(self.report_dir,scene,filename), "r") + lines = f.readlines() + for line in lines: + if isinstance(line.split('=')[1].strip(), int): + log_data_list.append({ + "x": line.split('=')[0].strip(), + "y": int(line.split('=')[1].strip()) + }) + target_data_list.append(int(line.split('=')[1].strip())) + else: + log_data_list.append({ + "x": line.split('=')[0].strip(), + "y": float(line.split('=')[1].strip()) + }) + target_data_list.append(float(line.split('=')[1].strip())) return log_data_list, target_data_list + def getCpuLog(self, platform, scene): targetDic = {} @@ -363,14 +381,16 @@ def _setAndroidPerfs(self, scene): fpsAvg = f'{int(sum(fpsData) / len(fpsData))}HZ/s' jankData = self.readLog(scene=scene, filename=f'jank.log')[1] - jankAvg = f'{int(sum(jankData) / len(jankData))}' - - flowSendData = self.readLog(scene=scene, filename=f'upflow.log')[1] - flowSend = f'{round(float(sum(flowSendData) / 1024), 2)}MB' - - flowRecvData = self.readLog(scene=scene, filename=f'downflow.log')[1] - flowRecv = f'{round(float(sum(flowRecvData) / 1024), 2)}MB' + jankAvg = f'{int(sum(jankData))}' + f_pre = open(os.path.join(self.report_dir,scene,'pre_net.json')) + f_end = open(os.path.join(self.report_dir,scene,'end_net.json')) + json_pre = json.loads(f_pre.read()) + json_end = json.loads(f_end.read()) + send = json_end['send'] - json_pre['send'] + recv = json_end['recv'] - json_pre['recv'] + flowSend = f'{round(float(send / 1024), 2)}MB' + flowRecv = f'{round(float(recv / 1024), 2)}MB' apm_dict = {} apm_dict['cpuAppRate'] = cpuAppRate apm_dict['cpuSystemRate'] = cpuSystemRate @@ -470,8 +490,9 @@ def _setpkPerfs(self, scene): class Method: - - def _request(self, request, object): + + @classmethod + def _request(cls, request, object): match(request.method): case 'POST': return request.form[object] @@ -479,12 +500,15 @@ def _request(self, request, object): return request.args[object] case _: raise Exception('request method error') - - def _setValue(self, value): + + @classmethod + def _setValue(cls, value, default = 0): try: result = value - except: - result = 0 + except ZeroDivisionError : + result = default + except Exception: + result = default return result class Install: @@ -533,4 +557,4 @@ def installIPA(self, path): os.remove(path) return True, result else: - return False, result \ No newline at end of file + return False, result diff --git a/solox/public/fps.py b/solox/public/fps.py index b452397..22bcfe9 100644 --- a/solox/public/fps.py +++ b/solox/public/fps.py @@ -20,7 +20,7 @@ def __init__(self, device, frequency, package_name, fps_queue, jank_threshold, s self.device = device self.frequency = frequency self.package_name = package_name - self.jank_threshold = jank_threshold / 1000.0 # 内部的时间戳是秒为单位 + self.jank_threshold = jank_threshold / 1000.0 self.use_legacy_method = use_legacy self.surface_before = 0 self.last_timestamp = 0 @@ -28,20 +28,16 @@ def __init__(self, device, frequency, package_name, fps_queue, jank_threshold, s self.stop_event = threading.Event() self.focus_window = None self.surfaceview = surfaceview - # queue 上报线程用 self.fps_queue = fps_queue def start(self, start_time): - """打开SurfaceStatsCollector - """ if not self.use_legacy_method: try: self.focus_window = self.get_focus_activity() - # 如果self.focus_window里包含字符'$',必须将其转义 if self.focus_window.find('$') != -1: self.focus_window = self.focus_window.replace('$', '\$') except Exception: - logger.warning(u'无法动态获取当前Activity名称,使用page_flip统计全屏帧率!') + 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: @@ -73,7 +69,6 @@ def get_surfaceview_activity(self): if activity_line: activity_name = activity_line.split(' ')[2] 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') @@ -153,25 +148,15 @@ def _calculate_results_new(self, refresh_period, timestamps): return fps, jank def _calculate_jankey_new(self, timestamps): - - """同时满足两个条件计算为一次卡顿: - ①Display FrameTime>前三帧平均耗时2倍。 - ②Display FrameTime>两帧电影帧耗时 (1000ms/24*2≈83.33ms)。 - """ - twofilmstamp = 83.3 / 1000.0 tempstamp = 0 - # 统计丢帧卡顿 jank = 0 for index, timestamp in enumerate(timestamps): - # 前面四帧按超过166ms计算为卡顿 if (index == 0) or (index == 1) or (index == 2) or (index == 3): if tempstamp == 0: tempstamp = timestamp[1] continue - # 绘制帧耗时 costtime = timestamp[1] - tempstamp - # 耗时大于阈值10个时钟周期,用户能感受到卡顿感 if costtime > self.jank_threshold: jank = jank + 1 tempstamp = timestamp[1] @@ -190,15 +175,12 @@ def _calculate_jankey_new(self, timestamps): def _calculate_janky(self, timestamps): tempstamp = 0 - # 统计丢帧卡顿 jank = 0 for timestamp in timestamps: if tempstamp == 0: tempstamp = timestamp[1] continue - # 绘制帧耗时 costtime = timestamp[1] - tempstamp - # 耗时大于阈值10个时钟周期,用户能感受到卡顿感 if costtime > self.jank_threshold: jank = jank + 1 tempstamp = timestamp[1] @@ -207,8 +189,6 @@ def _calculate_janky(self, timestamps): def _calculator_thread(self, start_time): global collect_fps global collect_jank - """处理surfaceflinger数据 - """ while True: try: data = self.data_queue.get() @@ -247,13 +227,6 @@ def _calculator_thread(self, start_time): self.fps_queue.task_done() def _collector_thread(self): - """收集surfaceflinger数据 - 用了两种方式:use_legacy_method 为ture时,需要root权限: - service call SurfaceFlinger 1013 得到帧数 - 为false,dumpsys SurfaceFlinger --latency - Android 8.0 dumpsys SurfaceFlinger 没有内容 - 则用dumpsys gfxinfo package_name framestats - """ is_first = True while not self.stop_event.is_set(): try: @@ -266,11 +239,9 @@ def _collector_thread(self): timestamps = [] refresh_period, new_timestamps = self._get_surfaceflinger_frame_data() if refresh_period is None or new_timestamps is None: - # activity发生变化,旧的activity不存时,取的时间戳为空, self.focus_window = self.get_focus_activity() - logger.debug("refresh_period is None or timestamps is None") + 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): @@ -280,14 +251,11 @@ def _collector_thread(self): self.last_timestamp = timestamps[-1][1] is_first = False else: - # 两种情况:1)activity发生变化,但旧的activity仍然存时,取的时间戳不为空,但时间全部小于等于last_timestamp - # 2)activity没有发生变化,也没有任何刷新 is_first = True cur_focus_window = self.get_focus_activity() if self.focus_window != cur_focus_window: self.focus_window = cur_focus_window continue - # logger.debug(timestamps) self.data_queue.put((refresh_period, timestamps, time.time())) time_consume = time.time() - before delta_inter = self.frequency - time_consume @@ -399,13 +367,11 @@ def _get_surfaceflinger_frame_data(self): fields = [] fields = line.split(",") if fields and '0' == fields[0]: - # 获取INTENDED_VSYNC VSYNC FRAME_COMPLETED时间 利用VSYNC计算fps jank 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: @@ -450,7 +416,6 @@ def _get_surface_stats_legacy(self): """ cur_surface = None timestamp = datetime.datetime.now() - # 这个命令可能需要root ret = adb.shell(cmd="service call SurfaceFlinger 1013", deviceId=self.device) if not ret: return None @@ -463,8 +428,8 @@ def _get_surface_stats_legacy(self): class Monitor(object): def __init__(self, **kwargs): - self.config = kwargs # 配置项 - self.matched_data = {} # 采集到匹配的性能数据 + self.config = kwargs + self.matched_data = {} def start(self): logger.warn("请在%s类中实现start方法" % type(self)) @@ -484,7 +449,6 @@ class TimeUtils(object): NormalFormatter = "%Y-%m-%d %H-%M-%S" ColonFormatter = "%Y-%m-%d %H:%M:%S" - # 文件路径要用这个,mac有空格,很麻烦 @staticmethod def getCurrentTimeUnderline(): return time.strftime(TimeUtils.UnderLineFormatter, time.localtime(time.time())) @@ -493,14 +457,6 @@ def getCurrentTimeUnderline(): 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): - """ - 构造器 - :param str device_id: 设备id - :param float frequency: 帧率统计频率,默认1秒 - :param int jank_threshold: 计算jank值的阈值,单位毫秒,默认10个时钟周期,166ms - :param bool use_legacy: 当指定该参数为True时总是使用page_flip统计帧率,此时反映的是全屏内容的刷新帧率。 - 当不指定该参数时,对4.1以上的系统将统计当前获得焦点的Activity的刷新帧率 - """ super().__init__(**kwargs) self.start_time = start_time self.use_legacy = use_legacy diff --git a/solox/templates/analysis.html b/solox/templates/analysis.html index ec6093f..374293b 100644 --- a/solox/templates/analysis.html +++ b/solox/templates/analysis.html @@ -189,7 +189,7 @@ Jank

- Avg:{{ apm_data.jank }} + All:{{ apm_data.jank }}
diff --git a/solox/templates/base.html b/solox/templates/base.html index 5099aef..3e057e5 100644 --- a/solox/templates/base.html +++ b/solox/templates/base.html @@ -137,7 +137,7 @@

  • - {% if lan == 'cn' %} 版本 {% else %} Releases {% endif %} . V2.5.4 + {% if lan == 'cn' %} 版本 {% else %} Releases {% endif %} . V2.5.6
  • @@ -146,7 +146,7 @@

    -
    +

    @@ -159,6 +159,19 @@

    @@ -200,13 +213,47 @@

    - +

    +
    +
    +
    + + + + + + + + + + + + + + + +
    PCHOSTSWITCH
    + + + + + +
    +
    +
    +
    @@ -253,6 +300,9 @@