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
magnax/public/common.py
ADDED
|
@@ -0,0 +1,1598 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import time
|
|
7
|
+
import requests
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
import socket
|
|
11
|
+
from urllib.request import urlopen
|
|
12
|
+
import ssl
|
|
13
|
+
import openpyxl
|
|
14
|
+
import psutil
|
|
15
|
+
import signal
|
|
16
|
+
import cv2
|
|
17
|
+
from functools import wraps
|
|
18
|
+
from jinja2 import Environment, FileSystemLoader
|
|
19
|
+
|
|
20
|
+
# 使用 pymobiledevice3 进行 iOS 设备控制
|
|
21
|
+
try:
|
|
22
|
+
from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux
|
|
23
|
+
from pymobiledevice3.usbmux import list_devices as pmd3_list_devices
|
|
24
|
+
PMD3_AVAILABLE = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
LockdownClient = None
|
|
27
|
+
create_using_usbmux = None
|
|
28
|
+
pmd3_list_devices = None
|
|
29
|
+
PMD3_AVAILABLE = False
|
|
30
|
+
from magnax.public.adb import adb
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def downsample_lttb(data: list, target_points: int) -> list:
|
|
34
|
+
"""
|
|
35
|
+
LTTB (Largest Triangle Three Buckets) 降采样算法
|
|
36
|
+
专为时序数据设计,保留视觉特征(峰值、谷值、趋势)
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
data: [{"x": timestamp, "y": value}, ...] 格式的时序数据
|
|
40
|
+
target_points: 目标数据点数量(建议 500-2000)
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
降采样后的数据列表,保留首尾点和关键特征点
|
|
44
|
+
"""
|
|
45
|
+
n = len(data)
|
|
46
|
+
if n <= target_points or target_points < 3:
|
|
47
|
+
return data
|
|
48
|
+
|
|
49
|
+
# 始终保留第一个和最后一个点
|
|
50
|
+
sampled = [data[0]]
|
|
51
|
+
|
|
52
|
+
# 计算每个桶的大小
|
|
53
|
+
bucket_size = (n - 2) / (target_points - 2)
|
|
54
|
+
|
|
55
|
+
a = 0 # 上一个选中点的索引
|
|
56
|
+
|
|
57
|
+
for i in range(target_points - 2):
|
|
58
|
+
# 计算当前桶的范围
|
|
59
|
+
bucket_start = int((i + 1) * bucket_size) + 1
|
|
60
|
+
bucket_end = int((i + 2) * bucket_size) + 1
|
|
61
|
+
bucket_end = min(bucket_end, n - 1)
|
|
62
|
+
|
|
63
|
+
# 计算下一个桶的平均值(用于计算三角形面积)
|
|
64
|
+
next_start = int((i + 2) * bucket_size) + 1
|
|
65
|
+
next_end = int((i + 3) * bucket_size) + 1
|
|
66
|
+
next_end = min(next_end, n)
|
|
67
|
+
|
|
68
|
+
# 计算下一个桶的平均 x 和 y
|
|
69
|
+
if next_end > next_start:
|
|
70
|
+
avg_x = sum(j for j in range(next_start, next_end)) / (next_end - next_start)
|
|
71
|
+
avg_y = sum(data[j]['y'] for j in range(next_start, next_end)) / (next_end - next_start)
|
|
72
|
+
else:
|
|
73
|
+
avg_x = next_start
|
|
74
|
+
avg_y = data[min(next_start, n - 1)]['y']
|
|
75
|
+
|
|
76
|
+
# 在当前桶中找到与上一个点和平均点组成的三角形面积最大的点
|
|
77
|
+
max_area = -1
|
|
78
|
+
max_idx = bucket_start
|
|
79
|
+
|
|
80
|
+
for j in range(bucket_start, bucket_end):
|
|
81
|
+
# 计算三角形面积 (使用简化公式)
|
|
82
|
+
# 面积 = 0.5 * |x1(y2-y3) + x2(y3-y1) + x3(y1-y2)|
|
|
83
|
+
# 这里使用索引作为 x 坐标的近似
|
|
84
|
+
area = abs(
|
|
85
|
+
(a - avg_x) * (data[j]['y'] - data[a]['y']) -
|
|
86
|
+
(a - j) * (avg_y - data[a]['y'])
|
|
87
|
+
)
|
|
88
|
+
if area > max_area:
|
|
89
|
+
max_area = area
|
|
90
|
+
max_idx = j
|
|
91
|
+
|
|
92
|
+
sampled.append(data[max_idx])
|
|
93
|
+
a = max_idx
|
|
94
|
+
|
|
95
|
+
# 添加最后一个点
|
|
96
|
+
sampled.append(data[-1])
|
|
97
|
+
|
|
98
|
+
return sampled
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_ios_lockdown_client_in_common(device_id):
|
|
102
|
+
"""获取iOS设备的lockdown client (common.py专用版本)"""
|
|
103
|
+
try:
|
|
104
|
+
if create_using_usbmux is None:
|
|
105
|
+
logger.warning("pymobiledevice3 not available, some iOS features may not work")
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
return create_using_usbmux(serial=device_id)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Failed to create lockdown client for device {device_id}: {e}")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
def get_ios_device_udid_list():
|
|
114
|
+
"""获取连接的iOS设备UDID列表"""
|
|
115
|
+
if not PMD3_AVAILABLE:
|
|
116
|
+
logger.warning("pymobiledevice3 not available")
|
|
117
|
+
return []
|
|
118
|
+
try:
|
|
119
|
+
devices = pmd3_list_devices()
|
|
120
|
+
return [device.serial for device in devices]
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.error(f"Failed to get iOS device list: {e}")
|
|
123
|
+
return []
|
|
124
|
+
|
|
125
|
+
class Platform:
|
|
126
|
+
Android = 'Android'
|
|
127
|
+
iOS = 'iOS'
|
|
128
|
+
Mac = 'MacOS'
|
|
129
|
+
Windows = 'Windows'
|
|
130
|
+
|
|
131
|
+
class Devices:
|
|
132
|
+
|
|
133
|
+
def __init__(self, platform=Platform.Android):
|
|
134
|
+
self.platform = platform
|
|
135
|
+
self.adb = adb.adb_path
|
|
136
|
+
|
|
137
|
+
def execCmd(self, cmd):
|
|
138
|
+
"""Execute the command to get the terminal print result"""
|
|
139
|
+
r = os.popen(cmd)
|
|
140
|
+
try:
|
|
141
|
+
text = r.buffer.read().decode(encoding='gbk').replace('\x1b[0m','').strip()
|
|
142
|
+
except UnicodeDecodeError:
|
|
143
|
+
text = r.buffer.read().decode(encoding='utf-8').replace('\x1b[0m','').strip()
|
|
144
|
+
finally:
|
|
145
|
+
r.close()
|
|
146
|
+
return text
|
|
147
|
+
|
|
148
|
+
def filterType(self):
|
|
149
|
+
"""Select the pipe filtering method according to the system"""
|
|
150
|
+
filtertype = ('grep', 'findstr')[platform.system() == Platform.Windows]
|
|
151
|
+
return filtertype
|
|
152
|
+
|
|
153
|
+
def getDeviceIds(self):
|
|
154
|
+
"""Get all connected device ids"""
|
|
155
|
+
Ids = list(os.popen(f"{self.adb} devices").readlines())
|
|
156
|
+
deviceIds = []
|
|
157
|
+
for i in range(1, len(Ids) - 1):
|
|
158
|
+
id, state = Ids[i].strip().split()
|
|
159
|
+
if state == 'device':
|
|
160
|
+
deviceIds.append(id)
|
|
161
|
+
return deviceIds
|
|
162
|
+
|
|
163
|
+
def getDevicesName(self, deviceId):
|
|
164
|
+
"""Get the device name of the Android corresponding device ID"""
|
|
165
|
+
try:
|
|
166
|
+
devices_name = os.popen(f'{self.adb} -s {deviceId} shell getprop ro.product.model').readlines()[0].strip()
|
|
167
|
+
except Exception:
|
|
168
|
+
devices_name = os.popen(f'{self.adb} -s {deviceId} shell getprop ro.product.model').buffer.readlines()[0].decode("utf-8").strip()
|
|
169
|
+
return devices_name
|
|
170
|
+
|
|
171
|
+
def getDevices(self):
|
|
172
|
+
"""Get all Android devices"""
|
|
173
|
+
DeviceIds = self.getDeviceIds()
|
|
174
|
+
Devices = [f'{id}({self.getDevicesName(id)})' for id in DeviceIds]
|
|
175
|
+
logger.info('Connected devices: {}'.format(Devices))
|
|
176
|
+
return Devices
|
|
177
|
+
|
|
178
|
+
def getIdbyDevice(self, deviceinfo, platform):
|
|
179
|
+
"""Obtain the corresponding device id according to the Android device information"""
|
|
180
|
+
if platform == Platform.Android:
|
|
181
|
+
deviceId = re.sub(u"\\(.*?\\)|\\{.*?}|\\[.*?]", "", deviceinfo)
|
|
182
|
+
if deviceId not in self.getDeviceIds():
|
|
183
|
+
raise Exception('no device found')
|
|
184
|
+
else:
|
|
185
|
+
deviceId = deviceinfo
|
|
186
|
+
return deviceId
|
|
187
|
+
|
|
188
|
+
def getSdkVersion(self, deviceId):
|
|
189
|
+
version = adb.shell(cmd='getprop ro.build.version.sdk', deviceId=deviceId)
|
|
190
|
+
return version
|
|
191
|
+
|
|
192
|
+
def getCpuCores(self, deviceId):
|
|
193
|
+
"""get Android cpu cores"""
|
|
194
|
+
cmd = 'cat /sys/devices/system/cpu/online'
|
|
195
|
+
result = adb.shell(cmd=cmd, deviceId=deviceId)
|
|
196
|
+
try:
|
|
197
|
+
nums = int(result.split('-')[1]) + 1
|
|
198
|
+
except:
|
|
199
|
+
nums = 1
|
|
200
|
+
return nums
|
|
201
|
+
|
|
202
|
+
def getPid(self, deviceId, pkgName):
|
|
203
|
+
"""Get the pid corresponding to the Android package name"""
|
|
204
|
+
try:
|
|
205
|
+
sdkversion = self.getSdkVersion(deviceId)
|
|
206
|
+
if sdkversion and int(sdkversion) < 26:
|
|
207
|
+
result = os.popen(f"{self.adb} -s {deviceId} shell ps | {self.filterType()} {pkgName}").readlines()
|
|
208
|
+
processList = ['{}:{}'.format(process.split()[1],process.split()[8]) for process in result]
|
|
209
|
+
else:
|
|
210
|
+
result = os.popen(f"{self.adb} -s {deviceId} shell ps -ef | {self.filterType()} {pkgName}").readlines()
|
|
211
|
+
processList = ['{}:{}'.format(process.split()[1],process.split()[7]) for process in result]
|
|
212
|
+
for i in range(len(processList)):
|
|
213
|
+
if processList[i].count(':') == 1:
|
|
214
|
+
index = processList.index(processList[i])
|
|
215
|
+
processList.insert(0, processList.pop(index))
|
|
216
|
+
break
|
|
217
|
+
if len(processList) == 0:
|
|
218
|
+
logger.warning('{}: no pid found'.format(pkgName))
|
|
219
|
+
except Exception as e:
|
|
220
|
+
processList = []
|
|
221
|
+
logger.exception(e)
|
|
222
|
+
return processList
|
|
223
|
+
|
|
224
|
+
def checkPkgname(self, pkgname):
|
|
225
|
+
flag = True
|
|
226
|
+
replace_list = ['com.google']
|
|
227
|
+
for i in replace_list:
|
|
228
|
+
if i in pkgname:
|
|
229
|
+
flag = False
|
|
230
|
+
return flag
|
|
231
|
+
|
|
232
|
+
def getPkgname(self, deviceId):
|
|
233
|
+
"""Get all package names of Android devices"""
|
|
234
|
+
pkginfo = os.popen(f"{self.adb} -s {deviceId} shell pm list packages --user 0")
|
|
235
|
+
pkglist = [p.lstrip('package').lstrip(":").strip() for p in pkginfo]
|
|
236
|
+
if pkglist.__len__() > 0:
|
|
237
|
+
return pkglist
|
|
238
|
+
else:
|
|
239
|
+
pkginfo = os.popen(f"{self.adb} -s {deviceId} shell pm list packages")
|
|
240
|
+
pkglist = [p.lstrip('package').lstrip(":").strip() for p in pkginfo]
|
|
241
|
+
return pkglist
|
|
242
|
+
|
|
243
|
+
def getDeviceInfoByiOS(self):
|
|
244
|
+
"""Get a list of all successfully connected iOS devices"""
|
|
245
|
+
deviceInfo = get_ios_device_udid_list()
|
|
246
|
+
logger.info('Connected devices: {}'.format(deviceInfo))
|
|
247
|
+
return deviceInfo
|
|
248
|
+
|
|
249
|
+
def getPkgnameByiOS(self, udid):
|
|
250
|
+
"""Get all package names of the corresponding iOS device"""
|
|
251
|
+
try:
|
|
252
|
+
lockdown_client = get_ios_lockdown_client_in_common(udid)
|
|
253
|
+
if lockdown_client is None:
|
|
254
|
+
logger.error("Failed to get lockdown client for iOS package list")
|
|
255
|
+
return []
|
|
256
|
+
|
|
257
|
+
from pymobiledevice3.services.installation_proxy import InstallationProxyService
|
|
258
|
+
installation = InstallationProxyService(lockdown=lockdown_client)
|
|
259
|
+
|
|
260
|
+
# 获取用户安装的应用列表(新版 API 返回字典,key 为 bundle identifier)
|
|
261
|
+
apps = installation.get_apps(application_type='User')
|
|
262
|
+
pkgNames = list(apps.keys())
|
|
263
|
+
return pkgNames
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.error(f"Failed to get iOS package names: {e}")
|
|
266
|
+
return []
|
|
267
|
+
|
|
268
|
+
def get_pc_ip(self):
|
|
269
|
+
try:
|
|
270
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
271
|
+
s.connect(('8.8.8.8', 80))
|
|
272
|
+
ip = s.getsockname()[0]
|
|
273
|
+
except Exception:
|
|
274
|
+
logger.error('get local ip failed')
|
|
275
|
+
ip = '127.0.0.1'
|
|
276
|
+
finally:
|
|
277
|
+
s.close()
|
|
278
|
+
return ip
|
|
279
|
+
|
|
280
|
+
def get_device_ip(self, deviceId):
|
|
281
|
+
content = os.popen(f"{self.adb} -s {deviceId} shell ip addr show wlan0").read()
|
|
282
|
+
logger.info(content)
|
|
283
|
+
math_obj = re.search(r'inet\s(\d+\.\d+\.\d+\.\d+).*?wlan0', content)
|
|
284
|
+
if math_obj and math_obj.group(1):
|
|
285
|
+
return math_obj.group(1)
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
def devicesCheck(self, platform, deviceid=None, pkgname=None):
|
|
289
|
+
"""Check the device environment"""
|
|
290
|
+
match(platform):
|
|
291
|
+
case Platform.Android:
|
|
292
|
+
if len(self.getDeviceIds()) == 0:
|
|
293
|
+
raise Exception('no devices found')
|
|
294
|
+
if len(self.getPid(deviceId=deviceid, pkgName=pkgname)) == 0:
|
|
295
|
+
raise Exception('no process found')
|
|
296
|
+
case Platform.iOS:
|
|
297
|
+
if len(self.getDeviceInfoByiOS()) == 0:
|
|
298
|
+
raise Exception('no devices found')
|
|
299
|
+
case _:
|
|
300
|
+
raise Exception('platform must be Android or iOS')
|
|
301
|
+
|
|
302
|
+
def getDdeviceDetail(self, deviceId, platform):
|
|
303
|
+
result = dict()
|
|
304
|
+
match(platform):
|
|
305
|
+
case Platform.Android:
|
|
306
|
+
result['brand'] = adb.shell(cmd='getprop ro.product.brand', deviceId=deviceId)
|
|
307
|
+
result['name'] = adb.shell(cmd='getprop ro.product.model', deviceId=deviceId)
|
|
308
|
+
result['version'] = adb.shell(cmd='getprop ro.build.version.release', deviceId=deviceId)
|
|
309
|
+
result['serialno'] = adb.shell(cmd='getprop ro.serialno', deviceId=deviceId)
|
|
310
|
+
cmd = f'ip addr show wlan0 | {self.filterType()} link/ether'
|
|
311
|
+
wifiadr_content = adb.shell(cmd=cmd, deviceId=deviceId)
|
|
312
|
+
result['wifiadr'] = Method._index(wifiadr_content.split(), 1, '')
|
|
313
|
+
result['cpu_cores'] = self.getCpuCores(deviceId)
|
|
314
|
+
result['physical_size'] = adb.shell(cmd='wm size', deviceId=deviceId).replace('Physical size:','').strip()
|
|
315
|
+
case Platform.iOS:
|
|
316
|
+
try:
|
|
317
|
+
lockdown_client = get_ios_lockdown_client_in_common(deviceId)
|
|
318
|
+
if lockdown_client is None:
|
|
319
|
+
logger.error("Failed to get lockdown client for iOS device details")
|
|
320
|
+
return {'brand': '', 'name': '', 'version': '', 'serialno': deviceId, 'wifiadr': '', 'cpu_cores': 0, 'physical_size': ''}
|
|
321
|
+
|
|
322
|
+
# 从lockdown client获取设备信息
|
|
323
|
+
device_info = lockdown_client.all_values
|
|
324
|
+
|
|
325
|
+
result['brand'] = device_info.get("DeviceClass", "")
|
|
326
|
+
result['name'] = device_info.get("DeviceName", "")
|
|
327
|
+
result['version'] = device_info.get("ProductVersion", "")
|
|
328
|
+
result['serialno'] = deviceId
|
|
329
|
+
result['wifiadr'] = device_info.get("WiFiAddress", "")
|
|
330
|
+
result['cpu_cores'] = 0
|
|
331
|
+
result['physical_size'] = self.getPhysicalSzieOfiOS(deviceId)
|
|
332
|
+
except Exception as e:
|
|
333
|
+
logger.error(f"Failed to get iOS device details: {e}")
|
|
334
|
+
result = {'brand': '', 'name': '', 'version': '', 'serialno': deviceId, 'wifiadr': '', 'cpu_cores': 0, 'physical_size': ''}
|
|
335
|
+
case _:
|
|
336
|
+
raise Exception('{} is undefined'.format(platform))
|
|
337
|
+
return result
|
|
338
|
+
|
|
339
|
+
def getPhysicalSzieOfiOS(self, deviceId):
|
|
340
|
+
try:
|
|
341
|
+
lockdown_client = get_ios_lockdown_client_in_common(deviceId)
|
|
342
|
+
if lockdown_client is None:
|
|
343
|
+
logger.error("Failed to get lockdown client for iOS screen info")
|
|
344
|
+
return ''
|
|
345
|
+
|
|
346
|
+
# 获取屏幕信息
|
|
347
|
+
device_info = lockdown_client.all_values
|
|
348
|
+
screen_width = device_info.get('ScreenWidth', 0)
|
|
349
|
+
screen_height = device_info.get('ScreenHeight', 0)
|
|
350
|
+
|
|
351
|
+
if screen_width and screen_height:
|
|
352
|
+
PhysicalSzie = '{}x{}'.format(screen_width, screen_height)
|
|
353
|
+
else:
|
|
354
|
+
PhysicalSzie = ''
|
|
355
|
+
except Exception as e:
|
|
356
|
+
PhysicalSzie = ''
|
|
357
|
+
logger.exception(e)
|
|
358
|
+
return PhysicalSzie
|
|
359
|
+
|
|
360
|
+
def getCurrentActivity(self, deviceId):
|
|
361
|
+
result = adb.shell(cmd='dumpsys window | {} mCurrentFocus'.format(self.filterType()), deviceId=deviceId)
|
|
362
|
+
if result.__contains__('mCurrentFocus'):
|
|
363
|
+
activity = str(result).split(' ')[-1].replace('}','')
|
|
364
|
+
return activity
|
|
365
|
+
else:
|
|
366
|
+
raise Exception('No activity found')
|
|
367
|
+
|
|
368
|
+
def getStartupTimeByAndroid(self, activity, deviceId):
|
|
369
|
+
result = adb.shell(cmd='am start -W {}'.format(activity), deviceId=deviceId)
|
|
370
|
+
return result
|
|
371
|
+
|
|
372
|
+
def getStartupTimeByiOS(self, pkgname):
|
|
373
|
+
try:
|
|
374
|
+
import ios_device
|
|
375
|
+
except ImportError:
|
|
376
|
+
logger.error('py-ios-devices not found, please run [pip install py-ios-devices]')
|
|
377
|
+
result = self.execCmd('pyidevice instruments app_lifecycle -b {}'.format(pkgname))
|
|
378
|
+
return result
|
|
379
|
+
|
|
380
|
+
class File:
|
|
381
|
+
|
|
382
|
+
def __init__(self, fileroot='.'):
|
|
383
|
+
self.fileroot = fileroot
|
|
384
|
+
self.report_dir = self.get_repordir()
|
|
385
|
+
|
|
386
|
+
def _safe_remove_file(self, filepath, max_retries=5, retry_delay=1):
|
|
387
|
+
"""安全删除文件,处理文件被占用的情况"""
|
|
388
|
+
for attempt in range(max_retries):
|
|
389
|
+
try:
|
|
390
|
+
if os.path.exists(filepath):
|
|
391
|
+
os.remove(filepath)
|
|
392
|
+
logger.info(f'文件删除成功: {filepath}')
|
|
393
|
+
return True
|
|
394
|
+
|
|
395
|
+
except PermissionError as e:
|
|
396
|
+
if attempt < max_retries - 1:
|
|
397
|
+
logger.warning(f'文件被占用,等待后重试删除 ({attempt + 1}/{max_retries}): {filepath}')
|
|
398
|
+
time.sleep(retry_delay)
|
|
399
|
+
retry_delay *= 2 # 指数退避
|
|
400
|
+
else:
|
|
401
|
+
logger.error(f'删除文件失败,已达到最大重试次数: {filepath}')
|
|
402
|
+
return False
|
|
403
|
+
except Exception as e:
|
|
404
|
+
logger.warning(f'删除文件时发生错误: {filepath}, 错误: {e}')
|
|
405
|
+
return False
|
|
406
|
+
return False
|
|
407
|
+
|
|
408
|
+
def clear_file(self):
|
|
409
|
+
logger.info('Clean up useless files ...')
|
|
410
|
+
if os.path.exists(self.report_dir):
|
|
411
|
+
files_to_remove = []
|
|
412
|
+
for f in os.listdir(self.report_dir):
|
|
413
|
+
filename = os.path.join(self.report_dir, f)
|
|
414
|
+
if f.split(".")[-1] in ['log', 'json', 'mkv']:
|
|
415
|
+
files_to_remove.append(filename)
|
|
416
|
+
|
|
417
|
+
# 先停止所有录屏进程,确保文件不被占用
|
|
418
|
+
Scrcpy.stop_record()
|
|
419
|
+
|
|
420
|
+
# 等待一些时间让进程完全结束
|
|
421
|
+
time.sleep(2)
|
|
422
|
+
|
|
423
|
+
# 安全删除文件
|
|
424
|
+
for filename in files_to_remove:
|
|
425
|
+
success = self._safe_remove_file(filename)
|
|
426
|
+
if not success:
|
|
427
|
+
logger.warning(f'无法删除文件,将在下次清理时重试: {filename}')
|
|
428
|
+
|
|
429
|
+
logger.info('Clean up useless files success')
|
|
430
|
+
|
|
431
|
+
def export_excel(self, platform, scene):
|
|
432
|
+
logger.info('Exporting excel ...')
|
|
433
|
+
android_log_file_list = ['cpu_app','cpu_sys','mem_total','mem_swap',
|
|
434
|
+
'battery_level', 'battery_tem','upflow','downflow','fps','gpu']
|
|
435
|
+
ios_log_file_list = ['cpu_app','cpu_sys', 'mem_total', 'battery_tem', 'battery_current',
|
|
436
|
+
'battery_voltage', 'battery_power','upflow','downflow','fps','gpu']
|
|
437
|
+
log_file_list = android_log_file_list if platform == 'Android' else ios_log_file_list
|
|
438
|
+
wb = openpyxl.Workbook()
|
|
439
|
+
# Remove the default sheet created by openpyxl
|
|
440
|
+
wb.remove(wb.active)
|
|
441
|
+
for name in log_file_list:
|
|
442
|
+
ws = wb.create_sheet(title=name)
|
|
443
|
+
ws.cell(row=1, column=1, value='Time')
|
|
444
|
+
ws.cell(row=1, column=2, value='Value')
|
|
445
|
+
row = 2 # start row (1-based, header is row 1)
|
|
446
|
+
if os.path.exists(f'{self.report_dir}/{scene}/{name}.log'):
|
|
447
|
+
with open(f'{self.report_dir}/{scene}/{name}.log', 'r', encoding='utf-8') as f:
|
|
448
|
+
for lines in f:
|
|
449
|
+
target = lines.split('=')
|
|
450
|
+
for i in range(len(target)):
|
|
451
|
+
ws.cell(row=row, column=i + 1, value=target[i])
|
|
452
|
+
row += 1
|
|
453
|
+
xlsx_path = os.path.join(self.report_dir, scene, f'{scene}.xlsx')
|
|
454
|
+
wb.save(xlsx_path)
|
|
455
|
+
logger.info('Exporting excel success : {}'.format(xlsx_path))
|
|
456
|
+
return xlsx_path
|
|
457
|
+
|
|
458
|
+
def make_android_html(self, scene, summary : dict, report_path=None):
|
|
459
|
+
logger.info('Generating HTML ...')
|
|
460
|
+
STATICPATH = os.path.dirname(os.path.realpath(__file__))
|
|
461
|
+
file_loader = FileSystemLoader(os.path.join(STATICPATH, 'report_template'))
|
|
462
|
+
env = Environment(loader=file_loader)
|
|
463
|
+
template = env.get_template('android.html')
|
|
464
|
+
if report_path:
|
|
465
|
+
html_path = report_path
|
|
466
|
+
else:
|
|
467
|
+
html_path = os.path.join(self.report_dir, scene, 'report.html')
|
|
468
|
+
with open(html_path,'w+') as fout:
|
|
469
|
+
html_content = template.render(devices=summary['devices'],app=summary['app'],
|
|
470
|
+
platform=summary['platform'],ctime=summary['ctime'],
|
|
471
|
+
cpu_app=summary['cpu_app'],cpu_sys=summary['cpu_sys'],
|
|
472
|
+
mem_total=summary['mem_total'],mem_swap=summary['mem_swap'],
|
|
473
|
+
fps=summary['fps'],jank=summary['jank'],level=summary['level'],
|
|
474
|
+
tem=summary['tem'],net_send=summary['net_send'],
|
|
475
|
+
net_recv=summary['net_recv'],cpu_charts=summary['cpu_charts'],
|
|
476
|
+
mem_charts=summary['mem_charts'],net_charts=summary['net_charts'],
|
|
477
|
+
battery_charts=summary['battery_charts'],fps_charts=summary['fps_charts'],
|
|
478
|
+
jank_charts=summary['jank_charts'],mem_detail_charts=summary['mem_detail_charts'],
|
|
479
|
+
gpu=summary['gpu'], gpu_charts=summary['gpu_charts'])
|
|
480
|
+
|
|
481
|
+
fout.write(html_content)
|
|
482
|
+
logger.info('Generating HTML success : {}'.format(html_path))
|
|
483
|
+
return html_path
|
|
484
|
+
|
|
485
|
+
def make_ios_html(self, scene, summary : dict, report_path=None):
|
|
486
|
+
logger.info('Generating HTML ...')
|
|
487
|
+
STATICPATH = os.path.dirname(os.path.realpath(__file__))
|
|
488
|
+
file_loader = FileSystemLoader(os.path.join(STATICPATH, 'report_template'))
|
|
489
|
+
env = Environment(loader=file_loader)
|
|
490
|
+
template = env.get_template('ios.html')
|
|
491
|
+
if report_path:
|
|
492
|
+
html_path = report_path
|
|
493
|
+
else:
|
|
494
|
+
html_path = os.path.join(self.report_dir, scene, 'report.html')
|
|
495
|
+
with open(html_path,'w+') as fout:
|
|
496
|
+
html_content = template.render(devices=summary['devices'],app=summary['app'],
|
|
497
|
+
platform=summary['platform'],ctime=summary['ctime'],
|
|
498
|
+
cpu_app=summary['cpu_app'],cpu_sys=summary['cpu_sys'],gpu=summary['gpu'],
|
|
499
|
+
mem_total=summary['mem_total'],fps=summary['fps'],
|
|
500
|
+
tem=summary['tem'],current=summary['current'],
|
|
501
|
+
voltage=summary['voltage'],power=summary['power'],
|
|
502
|
+
net_send=summary['net_send'],net_recv=summary['net_recv'],
|
|
503
|
+
cpu_charts=summary['cpu_charts'],mem_charts=summary['mem_charts'],
|
|
504
|
+
net_charts=summary['net_charts'],battery_charts=summary['battery_charts'],
|
|
505
|
+
fps_charts=summary['fps_charts'],gpu_charts=summary['gpu_charts'])
|
|
506
|
+
fout.write(html_content)
|
|
507
|
+
logger.info('Generating HTML success : {}'.format(html_path))
|
|
508
|
+
return html_path
|
|
509
|
+
|
|
510
|
+
def filter_secen(self, scene):
|
|
511
|
+
dirs = os.listdir(self.report_dir)
|
|
512
|
+
dir_list = list(reversed(sorted(dirs, key=lambda x: os.path.getmtime(os.path.join(self.report_dir, x)))))
|
|
513
|
+
dir_list.remove(scene)
|
|
514
|
+
return dir_list
|
|
515
|
+
|
|
516
|
+
def get_repordir(self):
|
|
517
|
+
report_dir = os.path.join(os.getcwd(), 'report')
|
|
518
|
+
if not os.path.exists(report_dir):
|
|
519
|
+
os.mkdir(report_dir)
|
|
520
|
+
return report_dir
|
|
521
|
+
|
|
522
|
+
def create_file(self, filename, content=''):
|
|
523
|
+
if not os.path.exists(self.report_dir):
|
|
524
|
+
os.mkdir(self.report_dir)
|
|
525
|
+
with open(os.path.join(self.report_dir, filename), 'a+', encoding="utf-8") as file:
|
|
526
|
+
file.write(content)
|
|
527
|
+
|
|
528
|
+
def add_log(self, path, log_time, value):
|
|
529
|
+
if value >= 0:
|
|
530
|
+
with open(path, 'a+', encoding="utf-8") as file:
|
|
531
|
+
file.write(f'{log_time}={str(value)}' + '\n')
|
|
532
|
+
|
|
533
|
+
def record_net(self, type, send , recv):
|
|
534
|
+
net_dict = dict()
|
|
535
|
+
match(type):
|
|
536
|
+
case 'pre':
|
|
537
|
+
net_dict['send'] = send
|
|
538
|
+
net_dict['recv'] = recv
|
|
539
|
+
content = json.dumps(net_dict)
|
|
540
|
+
self.create_file(filename='pre_net.json', content=content)
|
|
541
|
+
case 'end':
|
|
542
|
+
net_dict['send'] = send
|
|
543
|
+
net_dict['recv'] = recv
|
|
544
|
+
content = json.dumps(net_dict)
|
|
545
|
+
self.create_file(filename='end_net.json', content=content)
|
|
546
|
+
case _:
|
|
547
|
+
logger.error('record network data failed')
|
|
548
|
+
|
|
549
|
+
def _safe_move_file(self, src, dst, max_retries=5, retry_delay=1):
|
|
550
|
+
"""安全移动文件,处理文件被占用的情况"""
|
|
551
|
+
for attempt in range(max_retries):
|
|
552
|
+
try:
|
|
553
|
+
# 检查目标路径,如果是目录则生成完整的目标文件路径
|
|
554
|
+
if os.path.isdir(dst):
|
|
555
|
+
dst_file = os.path.join(dst, os.path.basename(src))
|
|
556
|
+
else:
|
|
557
|
+
dst_file = dst
|
|
558
|
+
|
|
559
|
+
# 如果目标文件已存在,先尝试删除(可能是之前不完整的移动)
|
|
560
|
+
if os.path.exists(dst_file):
|
|
561
|
+
logger.warning(f'目标文件已存在,尝试删除: {dst_file}')
|
|
562
|
+
try:
|
|
563
|
+
os.remove(dst_file)
|
|
564
|
+
logger.info(f'成功删除已存在的目标文件: {dst_file}')
|
|
565
|
+
except Exception as delete_error:
|
|
566
|
+
logger.warning(f'无法删除已存在文件: {delete_error}')
|
|
567
|
+
# 如果无法删除,生成新的文件名
|
|
568
|
+
base, ext = os.path.splitext(dst_file)
|
|
569
|
+
counter = 1
|
|
570
|
+
while os.path.exists(dst_file):
|
|
571
|
+
dst_file = f"{base}_{counter}{ext}"
|
|
572
|
+
counter += 1
|
|
573
|
+
logger.info(f'生成新的目标文件名: {dst_file}')
|
|
574
|
+
|
|
575
|
+
shutil.move(src, dst_file)
|
|
576
|
+
logger.info(f'文件移动成功: {src} -> {dst_file}')
|
|
577
|
+
return True
|
|
578
|
+
|
|
579
|
+
except PermissionError as e:
|
|
580
|
+
if attempt < max_retries - 1:
|
|
581
|
+
logger.warning(f'文件被占用,等待后重试 ({attempt + 1}/{max_retries}): {src}')
|
|
582
|
+
time.sleep(retry_delay)
|
|
583
|
+
retry_delay *= 2 # 指数退避
|
|
584
|
+
else:
|
|
585
|
+
logger.error(f'移动文件失败,已达到最大重试次数: {src} -> {dst}')
|
|
586
|
+
raise e
|
|
587
|
+
except Exception as e:
|
|
588
|
+
logger.error(f'移动文件时发生错误: {src} -> {dst}, 错误: {e}')
|
|
589
|
+
# 对于非权限错误,不重试,直接抛出
|
|
590
|
+
raise e
|
|
591
|
+
return False
|
|
592
|
+
|
|
593
|
+
def make_report(self, app, devices, video, platform=Platform.Android, model='normal', cores=0):
|
|
594
|
+
logger.info('Generating test results ...')
|
|
595
|
+
current_time = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())
|
|
596
|
+
result_dict = {
|
|
597
|
+
"app": app,
|
|
598
|
+
"icon": "",
|
|
599
|
+
"platform": platform,
|
|
600
|
+
"model": model,
|
|
601
|
+
"devices": devices,
|
|
602
|
+
"ctime": current_time,
|
|
603
|
+
"video": video,
|
|
604
|
+
"cores":cores
|
|
605
|
+
}
|
|
606
|
+
content = json.dumps(result_dict)
|
|
607
|
+
self.create_file(filename='result.json', content=content)
|
|
608
|
+
report_new_dir = os.path.join(self.report_dir, f'apm_{current_time}')
|
|
609
|
+
if not os.path.exists(report_new_dir):
|
|
610
|
+
os.mkdir(report_new_dir)
|
|
611
|
+
|
|
612
|
+
# 安全移动文件,特别处理可能被占用的录屏文件
|
|
613
|
+
moved_files = []
|
|
614
|
+
for f in os.listdir(self.report_dir):
|
|
615
|
+
filename = os.path.join(self.report_dir, f)
|
|
616
|
+
if f.split(".")[-1] in ['log', 'json', 'mkv']:
|
|
617
|
+
# 检查文件是否真实存在且不是目录
|
|
618
|
+
if not os.path.isfile(filename):
|
|
619
|
+
logger.warning(f'跳过非文件项: {filename}')
|
|
620
|
+
continue
|
|
621
|
+
|
|
622
|
+
try:
|
|
623
|
+
if f.endswith('.mkv'):
|
|
624
|
+
# 录屏文件可能被占用,使用安全移动方法
|
|
625
|
+
logger.info(f'移动录屏文件: {filename}')
|
|
626
|
+
self._safe_move_file(filename, report_new_dir)
|
|
627
|
+
else:
|
|
628
|
+
# 普通文件直接移动
|
|
629
|
+
dst_file = os.path.join(report_new_dir, f)
|
|
630
|
+
if os.path.exists(dst_file):
|
|
631
|
+
logger.warning(f'目标文件已存在,删除后重新移动: {dst_file}')
|
|
632
|
+
os.remove(dst_file)
|
|
633
|
+
shutil.move(filename, report_new_dir)
|
|
634
|
+
moved_files.append(f)
|
|
635
|
+
except Exception as e:
|
|
636
|
+
logger.error(f'移动文件失败: {filename}, 错误: {e}')
|
|
637
|
+
# 继续处理其他文件,不中断整个流程
|
|
638
|
+
continue
|
|
639
|
+
|
|
640
|
+
if moved_files:
|
|
641
|
+
logger.info(f'成功移动文件: {moved_files}')
|
|
642
|
+
else:
|
|
643
|
+
logger.info('没有文件需要移动')
|
|
644
|
+
|
|
645
|
+
logger.info('Generating test results success: {}'.format(report_new_dir))
|
|
646
|
+
return f'apm_{current_time}'
|
|
647
|
+
|
|
648
|
+
def instance_type(self, data):
|
|
649
|
+
if isinstance(data, float):
|
|
650
|
+
return 'float'
|
|
651
|
+
elif isinstance(data, int):
|
|
652
|
+
return 'int'
|
|
653
|
+
else:
|
|
654
|
+
return 'int'
|
|
655
|
+
|
|
656
|
+
def open_file(self, path, mode):
|
|
657
|
+
with open(path, mode) as f:
|
|
658
|
+
for line in f:
|
|
659
|
+
yield line
|
|
660
|
+
|
|
661
|
+
def readJson(self, scene):
|
|
662
|
+
path = os.path.join(self.report_dir,scene,'result.json')
|
|
663
|
+
result_json = open(file=path, mode='r').read()
|
|
664
|
+
result_dict = json.loads(result_json)
|
|
665
|
+
return result_dict
|
|
666
|
+
|
|
667
|
+
def readLog(self, scene, filename, max_points=0):
|
|
668
|
+
"""
|
|
669
|
+
Read apmlog file data with optional downsampling
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
scene: 场景名称
|
|
673
|
+
filename: 日志文件名
|
|
674
|
+
max_points: 最大数据点数,0 表示不采样,默认 0
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
(log_data_list, target_data_list, total_points) 三元组
|
|
678
|
+
- log_data_list: [{"x": timestamp, "y": value}, ...]
|
|
679
|
+
- target_data_list: [value, ...]
|
|
680
|
+
- total_points: 原始数据点总数
|
|
681
|
+
"""
|
|
682
|
+
log_data_list = list()
|
|
683
|
+
target_data_list = list()
|
|
684
|
+
if os.path.exists(os.path.join(self.report_dir, scene, filename)):
|
|
685
|
+
lines = self.open_file(os.path.join(self.report_dir, scene, filename), "r")
|
|
686
|
+
for line in lines:
|
|
687
|
+
if isinstance(line.split('=')[1].strip(), int):
|
|
688
|
+
log_data_list.append({
|
|
689
|
+
"x": line.split('=')[0].strip(),
|
|
690
|
+
"y": int(line.split('=')[1].strip())
|
|
691
|
+
})
|
|
692
|
+
target_data_list.append(int(line.split('=')[1].strip()))
|
|
693
|
+
else:
|
|
694
|
+
log_data_list.append({
|
|
695
|
+
"x": line.split('=')[0].strip(),
|
|
696
|
+
"y": float(line.split('=')[1].strip())
|
|
697
|
+
})
|
|
698
|
+
target_data_list.append(float(line.split('=')[1].strip()))
|
|
699
|
+
|
|
700
|
+
# 记录原始数据点数量
|
|
701
|
+
total_points = len(log_data_list)
|
|
702
|
+
|
|
703
|
+
# 应用 LTTB 降采样
|
|
704
|
+
if max_points > 0 and len(log_data_list) > max_points:
|
|
705
|
+
log_data_list = downsample_lttb(log_data_list, max_points)
|
|
706
|
+
# 重建 target_data_list
|
|
707
|
+
target_data_list = [item['y'] for item in log_data_list]
|
|
708
|
+
|
|
709
|
+
return log_data_list, target_data_list, total_points
|
|
710
|
+
|
|
711
|
+
def getCpuLog(self, platform, scene, max_points=0):
|
|
712
|
+
targetDic = dict()
|
|
713
|
+
cpu_app_data, _, cpu_app_total = self.readLog(scene=scene, filename='cpu_app.log', max_points=max_points)
|
|
714
|
+
cpu_sys_data, _, cpu_sys_total = self.readLog(scene=scene, filename='cpu_sys.log', max_points=max_points)
|
|
715
|
+
targetDic['cpuAppData'] = cpu_app_data
|
|
716
|
+
targetDic['cpuSysData'] = cpu_sys_data
|
|
717
|
+
result = {
|
|
718
|
+
'status': 1,
|
|
719
|
+
'cpuAppData': targetDic['cpuAppData'],
|
|
720
|
+
'cpuSysData': targetDic['cpuSysData'],
|
|
721
|
+
'meta': {
|
|
722
|
+
'sampled': max_points > 0 and max(cpu_app_total, cpu_sys_total) > max_points,
|
|
723
|
+
'max_points': max_points,
|
|
724
|
+
'total_points': max(cpu_app_total, cpu_sys_total)
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return result
|
|
728
|
+
|
|
729
|
+
def getCpuLogCompare(self, platform, scene1, scene2, max_points=0):
|
|
730
|
+
targetDic = dict()
|
|
731
|
+
scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='cpu_app.log', max_points=max_points)
|
|
732
|
+
scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='cpu_app.log', max_points=max_points)
|
|
733
|
+
targetDic['scene1'] = scene1_data
|
|
734
|
+
targetDic['scene2'] = scene2_data
|
|
735
|
+
result = {
|
|
736
|
+
'status': 1,
|
|
737
|
+
'scene1': targetDic['scene1'],
|
|
738
|
+
'scene2': targetDic['scene2'],
|
|
739
|
+
'meta': {
|
|
740
|
+
'sampled': max_points > 0 and max(scene1_total, scene2_total) > max_points,
|
|
741
|
+
'max_points': max_points,
|
|
742
|
+
'total_points': max(scene1_total, scene2_total)
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return result
|
|
746
|
+
|
|
747
|
+
def getGpuLog(self, platform, scene, max_points=0):
|
|
748
|
+
targetDic = dict()
|
|
749
|
+
gpu_data, _, total_points = self.readLog(scene=scene, filename='gpu.log', max_points=max_points)
|
|
750
|
+
targetDic['gpu'] = gpu_data
|
|
751
|
+
result = {
|
|
752
|
+
'status': 1,
|
|
753
|
+
'gpu': targetDic['gpu'],
|
|
754
|
+
'meta': {
|
|
755
|
+
'sampled': max_points > 0 and total_points > max_points,
|
|
756
|
+
'max_points': max_points,
|
|
757
|
+
'total_points': total_points
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return result
|
|
761
|
+
|
|
762
|
+
def getGpuLogCompare(self, platform, scene1, scene2, max_points=0):
|
|
763
|
+
targetDic = dict()
|
|
764
|
+
scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='gpu.log', max_points=max_points)
|
|
765
|
+
scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='gpu.log', max_points=max_points)
|
|
766
|
+
targetDic['scene1'] = scene1_data
|
|
767
|
+
targetDic['scene2'] = scene2_data
|
|
768
|
+
result = {
|
|
769
|
+
'status': 1,
|
|
770
|
+
'scene1': targetDic['scene1'],
|
|
771
|
+
'scene2': targetDic['scene2'],
|
|
772
|
+
'meta': {
|
|
773
|
+
'sampled': max_points > 0 and max(scene1_total, scene2_total) > max_points,
|
|
774
|
+
'max_points': max_points,
|
|
775
|
+
'total_points': max(scene1_total, scene2_total)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return result
|
|
779
|
+
|
|
780
|
+
def getMemLog(self, platform, scene, max_points=0):
|
|
781
|
+
targetDic = dict()
|
|
782
|
+
mem_total_data, _, mem_total_total = self.readLog(scene=scene, filename='mem_total.log', max_points=max_points)
|
|
783
|
+
targetDic['memTotalData'] = mem_total_data
|
|
784
|
+
total_points = mem_total_total
|
|
785
|
+
if platform == Platform.Android:
|
|
786
|
+
mem_swap_data, _, mem_swap_total = self.readLog(scene=scene, filename='mem_swap.log', max_points=max_points)
|
|
787
|
+
targetDic['memSwapData'] = mem_swap_data
|
|
788
|
+
total_points = max(mem_total_total, mem_swap_total)
|
|
789
|
+
result = {
|
|
790
|
+
'status': 1,
|
|
791
|
+
'memTotalData': targetDic['memTotalData'],
|
|
792
|
+
'memSwapData': targetDic['memSwapData'],
|
|
793
|
+
'meta': {
|
|
794
|
+
'sampled': max_points > 0 and total_points > max_points,
|
|
795
|
+
'max_points': max_points,
|
|
796
|
+
'total_points': total_points
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
else:
|
|
800
|
+
result = {
|
|
801
|
+
'status': 1,
|
|
802
|
+
'memTotalData': targetDic['memTotalData'],
|
|
803
|
+
'meta': {
|
|
804
|
+
'sampled': max_points > 0 and total_points > max_points,
|
|
805
|
+
'max_points': max_points,
|
|
806
|
+
'total_points': total_points
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return result
|
|
810
|
+
|
|
811
|
+
def getMemDetailLog(self, platform, scene, max_points=0):
|
|
812
|
+
targetDic = dict()
|
|
813
|
+
total_points_list = []
|
|
814
|
+
for key, filename in [
|
|
815
|
+
('java_heap', 'mem_java_heap.log'),
|
|
816
|
+
('native_heap', 'mem_native_heap.log'),
|
|
817
|
+
('code_pss', 'mem_code_pss.log'),
|
|
818
|
+
('stack_pss', 'mem_stack_pss.log'),
|
|
819
|
+
('graphics_pss', 'mem_graphics_pss.log'),
|
|
820
|
+
('private_pss', 'mem_private_pss.log'),
|
|
821
|
+
('system_pss', 'mem_system_pss.log')
|
|
822
|
+
]:
|
|
823
|
+
data, _, total = self.readLog(scene=scene, filename=filename, max_points=max_points)
|
|
824
|
+
targetDic[key] = data
|
|
825
|
+
total_points_list.append(total)
|
|
826
|
+
max_total = max(total_points_list) if total_points_list else 0
|
|
827
|
+
result = {
|
|
828
|
+
'status': 1,
|
|
829
|
+
'memory_detail': targetDic,
|
|
830
|
+
'meta': {
|
|
831
|
+
'sampled': max_points > 0 and max_total > max_points,
|
|
832
|
+
'max_points': max_points,
|
|
833
|
+
'total_points': max_total
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return result
|
|
837
|
+
|
|
838
|
+
def getCpuCoreLog(self, platform, scene, max_points=0):
|
|
839
|
+
targetDic = dict()
|
|
840
|
+
cores = self.readJson(scene=scene).get('cores', 0)
|
|
841
|
+
total_points_list = []
|
|
842
|
+
if int(cores) > 0:
|
|
843
|
+
for i in range(int(cores)):
|
|
844
|
+
data, _, total = self.readLog(scene=scene, filename='cpu{}.log'.format(i), max_points=max_points)
|
|
845
|
+
targetDic['cpu{}'.format(i)] = data
|
|
846
|
+
total_points_list.append(total)
|
|
847
|
+
max_total = max(total_points_list) if total_points_list else 0
|
|
848
|
+
result = {
|
|
849
|
+
'status': 1,
|
|
850
|
+
'cores': cores,
|
|
851
|
+
'cpu_core': targetDic,
|
|
852
|
+
'meta': {
|
|
853
|
+
'sampled': max_points > 0 and max_total > max_points,
|
|
854
|
+
'max_points': max_points,
|
|
855
|
+
'total_points': max_total
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return result
|
|
859
|
+
|
|
860
|
+
def getMemLogCompare(self, platform, scene1, scene2, max_points=0):
|
|
861
|
+
targetDic = dict()
|
|
862
|
+
scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='mem_total.log', max_points=max_points)
|
|
863
|
+
scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='mem_total.log', max_points=max_points)
|
|
864
|
+
targetDic['scene1'] = scene1_data
|
|
865
|
+
targetDic['scene2'] = scene2_data
|
|
866
|
+
result = {
|
|
867
|
+
'status': 1,
|
|
868
|
+
'scene1': targetDic['scene1'],
|
|
869
|
+
'scene2': targetDic['scene2'],
|
|
870
|
+
'meta': {
|
|
871
|
+
'sampled': max_points > 0 and max(scene1_total, scene2_total) > max_points,
|
|
872
|
+
'max_points': max_points,
|
|
873
|
+
'total_points': max(scene1_total, scene2_total)
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return result
|
|
877
|
+
|
|
878
|
+
def getBatteryLog(self, platform, scene, max_points=0):
|
|
879
|
+
targetDic = dict()
|
|
880
|
+
total_points_list = []
|
|
881
|
+
if platform == Platform.Android:
|
|
882
|
+
level_data, _, level_total = self.readLog(scene=scene, filename='battery_level.log', max_points=max_points)
|
|
883
|
+
tem_data, _, tem_total = self.readLog(scene=scene, filename='battery_tem.log', max_points=max_points)
|
|
884
|
+
targetDic['batteryLevel'] = level_data
|
|
885
|
+
targetDic['batteryTem'] = tem_data
|
|
886
|
+
total_points_list = [level_total, tem_total]
|
|
887
|
+
result = {
|
|
888
|
+
'status': 1,
|
|
889
|
+
'batteryLevel': targetDic['batteryLevel'],
|
|
890
|
+
'batteryTem': targetDic['batteryTem'],
|
|
891
|
+
'meta': {
|
|
892
|
+
'sampled': max_points > 0 and max(total_points_list) > max_points,
|
|
893
|
+
'max_points': max_points,
|
|
894
|
+
'total_points': max(total_points_list)
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
else:
|
|
898
|
+
tem_data, _, tem_total = self.readLog(scene=scene, filename='battery_tem.log', max_points=max_points)
|
|
899
|
+
current_data, _, current_total = self.readLog(scene=scene, filename='battery_current.log', max_points=max_points)
|
|
900
|
+
voltage_data, _, voltage_total = self.readLog(scene=scene, filename='battery_voltage.log', max_points=max_points)
|
|
901
|
+
power_data, _, power_total = self.readLog(scene=scene, filename='battery_power.log', max_points=max_points)
|
|
902
|
+
targetDic['batteryTem'] = tem_data
|
|
903
|
+
targetDic['batteryCurrent'] = current_data
|
|
904
|
+
targetDic['batteryVoltage'] = voltage_data
|
|
905
|
+
targetDic['batteryPower'] = power_data
|
|
906
|
+
total_points_list = [tem_total, current_total, voltage_total, power_total]
|
|
907
|
+
result = {
|
|
908
|
+
'status': 1,
|
|
909
|
+
'batteryTem': targetDic['batteryTem'],
|
|
910
|
+
'batteryCurrent': targetDic['batteryCurrent'],
|
|
911
|
+
'batteryVoltage': targetDic['batteryVoltage'],
|
|
912
|
+
'batteryPower': targetDic['batteryPower'],
|
|
913
|
+
'meta': {
|
|
914
|
+
'sampled': max_points > 0 and max(total_points_list) > max_points,
|
|
915
|
+
'max_points': max_points,
|
|
916
|
+
'total_points': max(total_points_list)
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return result
|
|
920
|
+
|
|
921
|
+
def getBatteryLogCompare(self, platform, scene1, scene2, max_points=0):
|
|
922
|
+
targetDic = dict()
|
|
923
|
+
if platform == Platform.Android:
|
|
924
|
+
scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='battery_level.log', max_points=max_points)
|
|
925
|
+
scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='battery_level.log', max_points=max_points)
|
|
926
|
+
else:
|
|
927
|
+
scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='batteryPower.log', max_points=max_points)
|
|
928
|
+
scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='batteryPower.log', max_points=max_points)
|
|
929
|
+
targetDic['scene1'] = scene1_data
|
|
930
|
+
targetDic['scene2'] = scene2_data
|
|
931
|
+
result = {
|
|
932
|
+
'status': 1,
|
|
933
|
+
'scene1': targetDic['scene1'],
|
|
934
|
+
'scene2': targetDic['scene2'],
|
|
935
|
+
'meta': {
|
|
936
|
+
'sampled': max_points > 0 and max(scene1_total, scene2_total) > max_points,
|
|
937
|
+
'max_points': max_points,
|
|
938
|
+
'total_points': max(scene1_total, scene2_total)
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return result
|
|
942
|
+
|
|
943
|
+
def getFlowLog(self, platform, scene, max_points=0):
|
|
944
|
+
targetDic = dict()
|
|
945
|
+
up_data, _, up_total = self.readLog(scene=scene, filename='upflow.log', max_points=max_points)
|
|
946
|
+
down_data, _, down_total = self.readLog(scene=scene, filename='downflow.log', max_points=max_points)
|
|
947
|
+
targetDic['upFlow'] = up_data
|
|
948
|
+
targetDic['downFlow'] = down_data
|
|
949
|
+
max_total = max(up_total, down_total)
|
|
950
|
+
result = {
|
|
951
|
+
'status': 1,
|
|
952
|
+
'upFlow': targetDic['upFlow'],
|
|
953
|
+
'downFlow': targetDic['downFlow'],
|
|
954
|
+
'meta': {
|
|
955
|
+
'sampled': max_points > 0 and max_total > max_points,
|
|
956
|
+
'max_points': max_points,
|
|
957
|
+
'total_points': max_total
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return result
|
|
961
|
+
|
|
962
|
+
def getFlowSendLogCompare(self, platform, scene1, scene2, max_points=0):
|
|
963
|
+
targetDic = dict()
|
|
964
|
+
scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='upflow.log', max_points=max_points)
|
|
965
|
+
scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='upflow.log', max_points=max_points)
|
|
966
|
+
targetDic['scene1'] = scene1_data
|
|
967
|
+
targetDic['scene2'] = scene2_data
|
|
968
|
+
result = {
|
|
969
|
+
'status': 1,
|
|
970
|
+
'scene1': targetDic['scene1'],
|
|
971
|
+
'scene2': targetDic['scene2'],
|
|
972
|
+
'meta': {
|
|
973
|
+
'sampled': max_points > 0 and max(scene1_total, scene2_total) > max_points,
|
|
974
|
+
'max_points': max_points,
|
|
975
|
+
'total_points': max(scene1_total, scene2_total)
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return result
|
|
979
|
+
|
|
980
|
+
def getFlowRecvLogCompare(self, platform, scene1, scene2, max_points=0):
|
|
981
|
+
targetDic = dict()
|
|
982
|
+
scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='downflow.log', max_points=max_points)
|
|
983
|
+
scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='downflow.log', max_points=max_points)
|
|
984
|
+
targetDic['scene1'] = scene1_data
|
|
985
|
+
targetDic['scene2'] = scene2_data
|
|
986
|
+
result = {
|
|
987
|
+
'status': 1,
|
|
988
|
+
'scene1': targetDic['scene1'],
|
|
989
|
+
'scene2': targetDic['scene2'],
|
|
990
|
+
'meta': {
|
|
991
|
+
'sampled': max_points > 0 and max(scene1_total, scene2_total) > max_points,
|
|
992
|
+
'max_points': max_points,
|
|
993
|
+
'total_points': max(scene1_total, scene2_total)
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return result
|
|
997
|
+
|
|
998
|
+
def getFpsLog(self, platform, scene, max_points=0):
|
|
999
|
+
targetDic = dict()
|
|
1000
|
+
fps_data, _, fps_total = self.readLog(scene=scene, filename='fps.log', max_points=max_points)
|
|
1001
|
+
targetDic['fps'] = fps_data
|
|
1002
|
+
total_points = fps_total
|
|
1003
|
+
if platform == Platform.Android:
|
|
1004
|
+
jank_data, _, jank_total = self.readLog(scene=scene, filename='jank.log', max_points=max_points)
|
|
1005
|
+
targetDic['jank'] = jank_data
|
|
1006
|
+
total_points = max(fps_total, jank_total)
|
|
1007
|
+
result = {
|
|
1008
|
+
'status': 1,
|
|
1009
|
+
'fps': targetDic['fps'],
|
|
1010
|
+
'jank': targetDic['jank'],
|
|
1011
|
+
'meta': {
|
|
1012
|
+
'sampled': max_points > 0 and total_points > max_points,
|
|
1013
|
+
'max_points': max_points,
|
|
1014
|
+
'total_points': total_points
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
else:
|
|
1018
|
+
result = {
|
|
1019
|
+
'status': 1,
|
|
1020
|
+
'fps': targetDic['fps'],
|
|
1021
|
+
'meta': {
|
|
1022
|
+
'sampled': max_points > 0 and total_points > max_points,
|
|
1023
|
+
'max_points': max_points,
|
|
1024
|
+
'total_points': total_points
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return result
|
|
1028
|
+
|
|
1029
|
+
def getDiskLog(self, platform, scene, max_points=0):
|
|
1030
|
+
targetDic = dict()
|
|
1031
|
+
used_data, _, used_total = self.readLog(scene=scene, filename='disk_used.log', max_points=max_points)
|
|
1032
|
+
free_data, _, free_total = self.readLog(scene=scene, filename='disk_free.log', max_points=max_points)
|
|
1033
|
+
targetDic['used'] = used_data
|
|
1034
|
+
targetDic['free'] = free_data
|
|
1035
|
+
max_total = max(used_total, free_total)
|
|
1036
|
+
result = {
|
|
1037
|
+
'status': 1,
|
|
1038
|
+
'used': targetDic['used'],
|
|
1039
|
+
'free': targetDic['free'],
|
|
1040
|
+
'meta': {
|
|
1041
|
+
'sampled': max_points > 0 and max_total > max_points,
|
|
1042
|
+
'max_points': max_points,
|
|
1043
|
+
'total_points': max_total
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return result
|
|
1047
|
+
|
|
1048
|
+
def analysisDisk(self, scene):
|
|
1049
|
+
initail_disk_list = list()
|
|
1050
|
+
current_disk_list = list()
|
|
1051
|
+
sum_init_disk = dict()
|
|
1052
|
+
sum_current_disk = dict()
|
|
1053
|
+
if os.path.exists(os.path.join(self.report_dir,scene,'initail_disk.log')):
|
|
1054
|
+
size_list = list()
|
|
1055
|
+
used_list = list()
|
|
1056
|
+
free_list = list()
|
|
1057
|
+
lines = self.open_file(os.path.join(self.report_dir,scene,'initail_disk.log'), "r")
|
|
1058
|
+
for line in lines:
|
|
1059
|
+
if 'Filesystem' not in line and line.strip() != '':
|
|
1060
|
+
disk_value_list = line.split()
|
|
1061
|
+
disk_dict = dict(
|
|
1062
|
+
filesystem = disk_value_list[0],
|
|
1063
|
+
blocks = disk_value_list[1],
|
|
1064
|
+
used = disk_value_list[2],
|
|
1065
|
+
available = disk_value_list[3],
|
|
1066
|
+
use_percent = disk_value_list[4],
|
|
1067
|
+
mounted = disk_value_list[5]
|
|
1068
|
+
)
|
|
1069
|
+
initail_disk_list.append(disk_dict)
|
|
1070
|
+
size_list.append(int(disk_value_list[1]))
|
|
1071
|
+
used_list.append(int(disk_value_list[2]))
|
|
1072
|
+
free_list.append(int(disk_value_list[3]))
|
|
1073
|
+
sum_init_disk['sum_size'] = int(sum(size_list) / 1024 / 1024)
|
|
1074
|
+
sum_init_disk['sum_used'] = int(sum(used_list) / 1024)
|
|
1075
|
+
sum_init_disk['sum_free'] = int(sum(free_list) / 1024)
|
|
1076
|
+
|
|
1077
|
+
if os.path.exists(os.path.join(self.report_dir,scene,'current_disk.log')):
|
|
1078
|
+
size_list = list()
|
|
1079
|
+
used_list = list()
|
|
1080
|
+
free_list = list()
|
|
1081
|
+
lines = self.open_file(os.path.join(self.report_dir,scene,'current_disk.log'), "r")
|
|
1082
|
+
for line in lines:
|
|
1083
|
+
if 'Filesystem' not in line and line.strip() != '':
|
|
1084
|
+
disk_value_list = line.split()
|
|
1085
|
+
disk_dict = dict(
|
|
1086
|
+
filesystem = disk_value_list[0],
|
|
1087
|
+
blocks = disk_value_list[1],
|
|
1088
|
+
used = disk_value_list[2],
|
|
1089
|
+
available = disk_value_list[3],
|
|
1090
|
+
use_percent = disk_value_list[4],
|
|
1091
|
+
mounted = disk_value_list[5]
|
|
1092
|
+
)
|
|
1093
|
+
current_disk_list.append(disk_dict)
|
|
1094
|
+
size_list.append(int(disk_value_list[1]))
|
|
1095
|
+
used_list.append(int(disk_value_list[2]))
|
|
1096
|
+
free_list.append(int(disk_value_list[3]))
|
|
1097
|
+
sum_current_disk['sum_size'] = int(sum(size_list) / 1024 / 1024)
|
|
1098
|
+
sum_current_disk['sum_used'] = int(sum(used_list) / 1024)
|
|
1099
|
+
sum_current_disk['sum_free'] = int(sum(free_list) / 1024)
|
|
1100
|
+
|
|
1101
|
+
return initail_disk_list, current_disk_list, sum_init_disk, sum_current_disk
|
|
1102
|
+
|
|
1103
|
+
def getFpsLogCompare(self, platform, scene1, scene2, max_points=0):
|
|
1104
|
+
targetDic = dict()
|
|
1105
|
+
scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='fps.log', max_points=max_points)
|
|
1106
|
+
scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='fps.log', max_points=max_points)
|
|
1107
|
+
targetDic['scene1'] = scene1_data
|
|
1108
|
+
targetDic['scene2'] = scene2_data
|
|
1109
|
+
result = {
|
|
1110
|
+
'status': 1,
|
|
1111
|
+
'scene1': targetDic['scene1'],
|
|
1112
|
+
'scene2': targetDic['scene2'],
|
|
1113
|
+
'meta': {
|
|
1114
|
+
'sampled': max_points > 0 and max(scene1_total, scene2_total) > max_points,
|
|
1115
|
+
'max_points': max_points,
|
|
1116
|
+
'total_points': max(scene1_total, scene2_total)
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return result
|
|
1120
|
+
|
|
1121
|
+
def approximateSize(self, size, a_kilobyte_is_1024_bytes=True):
|
|
1122
|
+
'''
|
|
1123
|
+
convert a file size to human-readable form.
|
|
1124
|
+
Keyword arguments:
|
|
1125
|
+
size -- file size in bytes
|
|
1126
|
+
a_kilobyte_is_1024_bytes -- if True (default),use multiples of 1024
|
|
1127
|
+
if False, use multiples of 1000
|
|
1128
|
+
Returns: string
|
|
1129
|
+
'''
|
|
1130
|
+
|
|
1131
|
+
suffixes = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
|
|
1132
|
+
1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}
|
|
1133
|
+
|
|
1134
|
+
if size < 0:
|
|
1135
|
+
raise ValueError('number must be non-negative')
|
|
1136
|
+
|
|
1137
|
+
multiple = 1024 if a_kilobyte_is_1024_bytes else 1000
|
|
1138
|
+
|
|
1139
|
+
for suffix in suffixes[multiple]:
|
|
1140
|
+
size /= multiple
|
|
1141
|
+
if size < multiple:
|
|
1142
|
+
return '{0:.2f} {1}'.format(size, suffix)
|
|
1143
|
+
|
|
1144
|
+
def _setAndroidPerfs(self, scene):
|
|
1145
|
+
"""Aggregate APM data for Android"""
|
|
1146
|
+
|
|
1147
|
+
app = self.readJson(scene=scene).get('app')
|
|
1148
|
+
devices = self.readJson(scene=scene).get('devices')
|
|
1149
|
+
platform = self.readJson(scene=scene).get('platform')
|
|
1150
|
+
ctime = self.readJson(scene=scene).get('ctime')
|
|
1151
|
+
|
|
1152
|
+
_, cpuAppData, _ = self.readLog(scene=scene, filename=f'cpu_app.log')
|
|
1153
|
+
_, cpuSystemData, _ = self.readLog(scene=scene, filename=f'cpu_sys.log')
|
|
1154
|
+
if cpuAppData.__len__() > 0 and cpuSystemData.__len__() > 0:
|
|
1155
|
+
cpuAppRate = f'{round(sum(cpuAppData) / len(cpuAppData), 2)}%'
|
|
1156
|
+
cpuSystemRate = f'{round(sum(cpuSystemData) / len(cpuSystemData), 2)}%'
|
|
1157
|
+
else:
|
|
1158
|
+
cpuAppRate, cpuSystemRate = 0, 0
|
|
1159
|
+
|
|
1160
|
+
_, batteryLevelData, _ = self.readLog(scene=scene, filename=f'battery_level.log')
|
|
1161
|
+
_, batteryTemlData, _ = self.readLog(scene=scene, filename=f'battery_tem.log')
|
|
1162
|
+
if batteryLevelData.__len__() > 0 and batteryTemlData.__len__() > 0:
|
|
1163
|
+
batteryLevel = f'{batteryLevelData[-1]}%'
|
|
1164
|
+
batteryTeml = f'{batteryTemlData[-1]}°C'
|
|
1165
|
+
else:
|
|
1166
|
+
batteryLevel, batteryTeml = 0, 0
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
_, totalPassData, _ = self.readLog(scene=scene, filename=f'mem_total.log')
|
|
1170
|
+
|
|
1171
|
+
if totalPassData.__len__() > 0:
|
|
1172
|
+
_, swapPassData, _ = self.readLog(scene=scene, filename=f'mem_swap.log')
|
|
1173
|
+
totalPassAvg = f'{round(sum(totalPassData) / len(totalPassData), 2)}MB'
|
|
1174
|
+
swapPassAvg = f'{round(sum(swapPassData) / len(swapPassData), 2)}MB'
|
|
1175
|
+
else:
|
|
1176
|
+
totalPassAvg, swapPassAvg = 0, 0
|
|
1177
|
+
|
|
1178
|
+
_, fpsData, _ = self.readLog(scene=scene, filename=f'fps.log')
|
|
1179
|
+
_, jankData, _ = self.readLog(scene=scene, filename=f'jank.log')
|
|
1180
|
+
if fpsData.__len__() > 0:
|
|
1181
|
+
fpsAvg = f'{int(sum(fpsData) / len(fpsData))}HZ/s'
|
|
1182
|
+
jankAvg = f'{int(sum(jankData))}'
|
|
1183
|
+
else:
|
|
1184
|
+
fpsAvg, jankAvg = 0, 0
|
|
1185
|
+
|
|
1186
|
+
pre_net_path = os.path.join(self.report_dir, scene, 'pre_net.json')
|
|
1187
|
+
end_net_path = os.path.join(self.report_dir, scene, 'end_net.json')
|
|
1188
|
+
if os.path.exists(pre_net_path) and os.path.exists(end_net_path):
|
|
1189
|
+
with open(pre_net_path) as f_pre, open(end_net_path) as f_end:
|
|
1190
|
+
json_pre = json.loads(f_pre.read())
|
|
1191
|
+
json_end = json.loads(f_end.read())
|
|
1192
|
+
send = json_end['send'] - json_pre['send']
|
|
1193
|
+
recv = json_end['recv'] - json_pre['recv']
|
|
1194
|
+
else:
|
|
1195
|
+
send, recv = 0, 0
|
|
1196
|
+
flowSend = f'{round(float(send / 1024), 2)}MB'
|
|
1197
|
+
flowRecv = f'{round(float(recv / 1024), 2)}MB'
|
|
1198
|
+
|
|
1199
|
+
_, gpuData, _ = self.readLog(scene=scene, filename='gpu.log')
|
|
1200
|
+
if gpuData.__len__() > 0:
|
|
1201
|
+
gpu = round(sum(gpuData) / len(gpuData), 2)
|
|
1202
|
+
else:
|
|
1203
|
+
gpu = 0
|
|
1204
|
+
|
|
1205
|
+
mem_detail_flag = os.path.exists(os.path.join(self.report_dir,scene,'mem_java_heap.log'))
|
|
1206
|
+
disk_flag = os.path.exists(os.path.join(self.report_dir,scene,'disk_free.log'))
|
|
1207
|
+
thermal_flag = os.path.exists(os.path.join(self.report_dir,scene,'init_thermal_temp.json'))
|
|
1208
|
+
cpu_core_flag = os.path.exists(os.path.join(self.report_dir,scene,'cpu0.log'))
|
|
1209
|
+
apm_dict = dict()
|
|
1210
|
+
apm_dict['app'] = app
|
|
1211
|
+
apm_dict['devices'] = devices
|
|
1212
|
+
apm_dict['platform'] = platform
|
|
1213
|
+
apm_dict['ctime'] = ctime
|
|
1214
|
+
apm_dict['cpuAppRate'] = cpuAppRate
|
|
1215
|
+
apm_dict['cpuSystemRate'] = cpuSystemRate
|
|
1216
|
+
apm_dict['totalPassAvg'] = totalPassAvg
|
|
1217
|
+
apm_dict['swapPassAvg'] = swapPassAvg
|
|
1218
|
+
apm_dict['fps'] = fpsAvg
|
|
1219
|
+
apm_dict['jank'] = jankAvg
|
|
1220
|
+
apm_dict['flow_send'] = flowSend
|
|
1221
|
+
apm_dict['flow_recv'] = flowRecv
|
|
1222
|
+
apm_dict['batteryLevel'] = batteryLevel
|
|
1223
|
+
apm_dict['batteryTeml'] = batteryTeml
|
|
1224
|
+
apm_dict['mem_detail_flag'] = mem_detail_flag
|
|
1225
|
+
apm_dict['disk_flag'] = disk_flag
|
|
1226
|
+
apm_dict['gpu'] = gpu
|
|
1227
|
+
apm_dict['thermal_flag'] = thermal_flag
|
|
1228
|
+
apm_dict['cpu_core_flag'] = cpu_core_flag
|
|
1229
|
+
|
|
1230
|
+
if thermal_flag:
|
|
1231
|
+
init_thermal_temp = json.loads(open(os.path.join(self.report_dir,scene,'init_thermal_temp.json')).read())
|
|
1232
|
+
current_thermal_temp = json.loads(open(os.path.join(self.report_dir,scene,'current_thermal_temp.json')).read())
|
|
1233
|
+
apm_dict['init_thermal_temp'] = init_thermal_temp
|
|
1234
|
+
apm_dict['current_thermal_temp'] = current_thermal_temp
|
|
1235
|
+
|
|
1236
|
+
return apm_dict
|
|
1237
|
+
|
|
1238
|
+
def _setiOSPerfs(self, scene):
|
|
1239
|
+
"""Aggregate APM data for iOS"""
|
|
1240
|
+
|
|
1241
|
+
app = self.readJson(scene=scene).get('app')
|
|
1242
|
+
devices = self.readJson(scene=scene).get('devices')
|
|
1243
|
+
platform = self.readJson(scene=scene).get('platform')
|
|
1244
|
+
ctime = self.readJson(scene=scene).get('ctime')
|
|
1245
|
+
|
|
1246
|
+
_, cpuAppData, _ = self.readLog(scene=scene, filename=f'cpu_app.log')
|
|
1247
|
+
_, cpuSystemData, _ = self.readLog(scene=scene, filename=f'cpu_sys.log')
|
|
1248
|
+
if cpuAppData.__len__() > 0 and cpuSystemData.__len__() > 0:
|
|
1249
|
+
cpuAppRate = f'{round(sum(cpuAppData) / len(cpuAppData), 2)}%'
|
|
1250
|
+
cpuSystemRate = f'{round(sum(cpuSystemData) / len(cpuSystemData), 2)}%'
|
|
1251
|
+
else:
|
|
1252
|
+
cpuAppRate, cpuSystemRate = 0, 0
|
|
1253
|
+
|
|
1254
|
+
_, totalPassData, _ = self.readLog(scene=scene, filename='mem_total.log')
|
|
1255
|
+
if totalPassData.__len__() > 0:
|
|
1256
|
+
totalPassAvg = f'{round(sum(totalPassData) / len(totalPassData), 2)}MB'
|
|
1257
|
+
else:
|
|
1258
|
+
totalPassAvg = 0
|
|
1259
|
+
|
|
1260
|
+
_, fpsData, _ = self.readLog(scene=scene, filename='fps.log')
|
|
1261
|
+
if fpsData.__len__() > 0:
|
|
1262
|
+
fpsAvg = f'{int(sum(fpsData) / len(fpsData))}HZ/s'
|
|
1263
|
+
else:
|
|
1264
|
+
fpsAvg = 0
|
|
1265
|
+
|
|
1266
|
+
_, flowSendData, _ = self.readLog(scene=scene, filename='upflow.log')
|
|
1267
|
+
_, flowRecvData, _ = self.readLog(scene=scene, filename='downflow.log')
|
|
1268
|
+
if flowSendData.__len__() > 0:
|
|
1269
|
+
flowSend = f'{round(float(sum(flowSendData) / 1024), 2)}MB'
|
|
1270
|
+
flowRecv = f'{round(float(sum(flowRecvData) / 1024), 2)}MB'
|
|
1271
|
+
else:
|
|
1272
|
+
flowSend, flowRecv = 0, 0
|
|
1273
|
+
|
|
1274
|
+
_, batteryTemlData, _ = self.readLog(scene=scene, filename='battery_tem.log')
|
|
1275
|
+
_, batteryCurrentData, _ = self.readLog(scene=scene, filename='battery_current.log')
|
|
1276
|
+
_, batteryVoltageData, _ = self.readLog(scene=scene, filename='battery_voltage.log')
|
|
1277
|
+
_, batteryPowerData, _ = self.readLog(scene=scene, filename='battery_power.log')
|
|
1278
|
+
if batteryTemlData.__len__() > 0:
|
|
1279
|
+
batteryTeml = int(batteryTemlData[-1])
|
|
1280
|
+
batteryCurrent = int(sum(batteryCurrentData) / len(batteryCurrentData))
|
|
1281
|
+
batteryVoltage = int(sum(batteryVoltageData) / len(batteryVoltageData))
|
|
1282
|
+
batteryPower = int(sum(batteryPowerData) / len(batteryPowerData))
|
|
1283
|
+
else:
|
|
1284
|
+
batteryTeml, batteryCurrent, batteryVoltage, batteryPower = 0, 0, 0, 0
|
|
1285
|
+
|
|
1286
|
+
_, gpuData, _ = self.readLog(scene=scene, filename='gpu.log')
|
|
1287
|
+
if gpuData.__len__() > 0:
|
|
1288
|
+
gpu = round(sum(gpuData) / len(gpuData), 2)
|
|
1289
|
+
else:
|
|
1290
|
+
gpu = 0
|
|
1291
|
+
disk_flag = os.path.exists(os.path.join(self.report_dir, scene, 'disk_free.log'))
|
|
1292
|
+
apm_dict = dict()
|
|
1293
|
+
apm_dict['app'] = app
|
|
1294
|
+
apm_dict['devices'] = devices
|
|
1295
|
+
apm_dict['platform'] = platform
|
|
1296
|
+
apm_dict['ctime'] = ctime
|
|
1297
|
+
apm_dict['cpuAppRate'] = cpuAppRate
|
|
1298
|
+
apm_dict['cpuSystemRate'] = cpuSystemRate
|
|
1299
|
+
apm_dict['totalPassAvg'] = totalPassAvg
|
|
1300
|
+
apm_dict['nativePassAvg'] = 0
|
|
1301
|
+
apm_dict['dalvikPassAvg'] = 0
|
|
1302
|
+
apm_dict['fps'] = fpsAvg
|
|
1303
|
+
apm_dict['jank'] = 0
|
|
1304
|
+
apm_dict['flow_send'] = flowSend
|
|
1305
|
+
apm_dict['flow_recv'] = flowRecv
|
|
1306
|
+
apm_dict['batteryTeml'] = batteryTeml
|
|
1307
|
+
apm_dict['batteryCurrent'] = batteryCurrent
|
|
1308
|
+
apm_dict['batteryVoltage'] = batteryVoltage
|
|
1309
|
+
apm_dict['batteryPower'] = batteryPower
|
|
1310
|
+
apm_dict['gpu'] = gpu
|
|
1311
|
+
apm_dict['disk_flag'] = disk_flag
|
|
1312
|
+
return apm_dict
|
|
1313
|
+
|
|
1314
|
+
def _setpkPerfs(self, scene):
|
|
1315
|
+
"""Aggregate APM data for pk model"""
|
|
1316
|
+
_, cpuAppData1, _ = self.readLog(scene=scene, filename='cpu_app1.log')
|
|
1317
|
+
cpuAppRate1 = f'{round(sum(cpuAppData1) / len(cpuAppData1), 2)}%'
|
|
1318
|
+
_, cpuAppData2, _ = self.readLog(scene=scene, filename='cpu_app2.log')
|
|
1319
|
+
cpuAppRate2 = f'{round(sum(cpuAppData2) / len(cpuAppData2), 2)}%'
|
|
1320
|
+
|
|
1321
|
+
_, totalPassData1, _ = self.readLog(scene=scene, filename='mem1.log')
|
|
1322
|
+
totalPassAvg1 = f'{round(sum(totalPassData1) / len(totalPassData1), 2)}MB'
|
|
1323
|
+
_, totalPassData2, _ = self.readLog(scene=scene, filename='mem2.log')
|
|
1324
|
+
totalPassAvg2 = f'{round(sum(totalPassData2) / len(totalPassData2), 2)}MB'
|
|
1325
|
+
|
|
1326
|
+
_, fpsData1, _ = self.readLog(scene=scene, filename='fps1.log')
|
|
1327
|
+
fpsAvg1 = f'{int(sum(fpsData1) / len(fpsData1))}HZ/s'
|
|
1328
|
+
_, fpsData2, _ = self.readLog(scene=scene, filename='fps2.log')
|
|
1329
|
+
fpsAvg2 = f'{int(sum(fpsData2) / len(fpsData2))}HZ/s'
|
|
1330
|
+
|
|
1331
|
+
_, networkData1, _ = self.readLog(scene=scene, filename='network1.log')
|
|
1332
|
+
network1 = f'{round(float(sum(networkData1) / 1024), 2)}MB'
|
|
1333
|
+
_, networkData2, _ = self.readLog(scene=scene, filename='network2.log')
|
|
1334
|
+
network2 = f'{round(float(sum(networkData2) / 1024), 2)}MB'
|
|
1335
|
+
|
|
1336
|
+
apm_dict = dict()
|
|
1337
|
+
apm_dict['cpuAppRate1'] = cpuAppRate1
|
|
1338
|
+
apm_dict['cpuAppRate2'] = cpuAppRate2
|
|
1339
|
+
apm_dict['totalPassAvg1'] = totalPassAvg1
|
|
1340
|
+
apm_dict['totalPassAvg2'] = totalPassAvg2
|
|
1341
|
+
apm_dict['network1'] = network1
|
|
1342
|
+
apm_dict['network2'] = network2
|
|
1343
|
+
apm_dict['fpsAvg1'] = fpsAvg1
|
|
1344
|
+
apm_dict['fpsAvg2'] = fpsAvg2
|
|
1345
|
+
return apm_dict
|
|
1346
|
+
|
|
1347
|
+
class Method:
|
|
1348
|
+
|
|
1349
|
+
@classmethod
|
|
1350
|
+
def _request(cls, request, object):
|
|
1351
|
+
match(request.method):
|
|
1352
|
+
case 'POST':
|
|
1353
|
+
return request.form[object]
|
|
1354
|
+
case 'GET':
|
|
1355
|
+
return request.args[object]
|
|
1356
|
+
case _:
|
|
1357
|
+
raise Exception('request method error')
|
|
1358
|
+
|
|
1359
|
+
@classmethod
|
|
1360
|
+
def _setValue(cls, value, default = 0):
|
|
1361
|
+
try:
|
|
1362
|
+
result = value
|
|
1363
|
+
except ZeroDivisionError :
|
|
1364
|
+
result = default
|
|
1365
|
+
except IndexError:
|
|
1366
|
+
result = default
|
|
1367
|
+
except Exception:
|
|
1368
|
+
result = default
|
|
1369
|
+
return result
|
|
1370
|
+
|
|
1371
|
+
@classmethod
|
|
1372
|
+
def _settings(cls, request):
|
|
1373
|
+
content = {}
|
|
1374
|
+
content['cpuWarning'] = (0, request.cookies.get('cpuWarning'))[request.cookies.get('cpuWarning') not in [None, 'NaN']]
|
|
1375
|
+
content['memWarning'] = (0, request.cookies.get('memWarning'))[request.cookies.get('memWarning') not in [None, 'NaN']]
|
|
1376
|
+
content['fpsWarning'] = (0, request.cookies.get('fpsWarning'))[request.cookies.get('fpsWarning') not in [None, 'NaN']]
|
|
1377
|
+
content['netdataRecvWarning'] = (0, request.cookies.get('netdataRecvWarning'))[request.cookies.get('netdataRecvWarning') not in [None, 'NaN']]
|
|
1378
|
+
content['netdataSendWarning'] = (0, request.cookies.get('netdataSendWarning'))[request.cookies.get('netdataSendWarning') not in [None, 'NaN']]
|
|
1379
|
+
content['betteryWarning'] = (0, request.cookies.get('betteryWarning'))[request.cookies.get('betteryWarning') not in [None, 'NaN']]
|
|
1380
|
+
content['gpuWarning'] = (0, request.cookies.get('gpuWarning'))[request.cookies.get('gpuWarning') not in [None, 'NaN']]
|
|
1381
|
+
content['duration'] = (0, request.cookies.get('duration'))[request.cookies.get('duration') not in [None, 'NaN']]
|
|
1382
|
+
content['magnax_host'] = ('', request.cookies.get('magnax_host'))[request.cookies.get('magnax_host') not in [None, 'NaN']]
|
|
1383
|
+
content['host_switch'] = request.cookies.get('host_switch')
|
|
1384
|
+
return content
|
|
1385
|
+
|
|
1386
|
+
@classmethod
|
|
1387
|
+
def _index(cls, target: list, index: int, default: any):
|
|
1388
|
+
try:
|
|
1389
|
+
return target[index]
|
|
1390
|
+
except IndexError:
|
|
1391
|
+
return default
|
|
1392
|
+
|
|
1393
|
+
class Install:
|
|
1394
|
+
|
|
1395
|
+
def uploadFile(self, file_path, file_obj):
|
|
1396
|
+
"""save upload file"""
|
|
1397
|
+
try:
|
|
1398
|
+
file_obj.save(file_path)
|
|
1399
|
+
return True
|
|
1400
|
+
except Exception as e:
|
|
1401
|
+
logger.exception(e)
|
|
1402
|
+
return False
|
|
1403
|
+
|
|
1404
|
+
def downloadLink(self,filelink=None, path=None, name=None):
|
|
1405
|
+
try:
|
|
1406
|
+
logger.info('Install link : {}'.format(filelink))
|
|
1407
|
+
ssl._create_default_https_context = ssl._create_unverified_context
|
|
1408
|
+
file_size = int(urlopen(filelink).info().get('Content-Length', -1))
|
|
1409
|
+
header = {"Range": "bytes=%s-%s" % (0, file_size)}
|
|
1410
|
+
pbar = tqdm(
|
|
1411
|
+
total=file_size, initial=0,
|
|
1412
|
+
unit='B', unit_scale=True, desc=filelink.split('/')[-1])
|
|
1413
|
+
req = requests.get(filelink, headers=header, stream=True)
|
|
1414
|
+
with(open(os.path.join(path, name), 'ab')) as f:
|
|
1415
|
+
for chunk in req.iter_content(chunk_size=1024):
|
|
1416
|
+
if chunk:
|
|
1417
|
+
f.write(chunk)
|
|
1418
|
+
pbar.update(1024)
|
|
1419
|
+
pbar.close()
|
|
1420
|
+
return True
|
|
1421
|
+
except Exception as e:
|
|
1422
|
+
logger.exception(e)
|
|
1423
|
+
return False
|
|
1424
|
+
|
|
1425
|
+
def installAPK(self, path):
|
|
1426
|
+
result = adb.shell_noDevice(cmd='install -r {}'.format(path))
|
|
1427
|
+
if result == 0:
|
|
1428
|
+
os.remove(path)
|
|
1429
|
+
return True, result
|
|
1430
|
+
else:
|
|
1431
|
+
return False, result
|
|
1432
|
+
|
|
1433
|
+
def installIPA(self, path, device_id=None):
|
|
1434
|
+
"""使用 pymobiledevice3 安装 IPA"""
|
|
1435
|
+
try:
|
|
1436
|
+
if not PMD3_AVAILABLE:
|
|
1437
|
+
logger.error("pymobiledevice3 not available, cannot install IPA")
|
|
1438
|
+
return False, -1
|
|
1439
|
+
|
|
1440
|
+
from pymobiledevice3.services.installation_proxy import InstallationProxyService
|
|
1441
|
+
|
|
1442
|
+
# 获取设备列表,如果没有指定设备则使用第一个
|
|
1443
|
+
if device_id is None:
|
|
1444
|
+
devices = pmd3_list_devices()
|
|
1445
|
+
if not devices:
|
|
1446
|
+
logger.error("No iOS device connected")
|
|
1447
|
+
return False, -1
|
|
1448
|
+
device_id = devices[0].serial
|
|
1449
|
+
|
|
1450
|
+
lockdown_client = create_using_usbmux(serial=device_id)
|
|
1451
|
+
if lockdown_client is None:
|
|
1452
|
+
logger.error("Failed to connect to device")
|
|
1453
|
+
return False, -1
|
|
1454
|
+
|
|
1455
|
+
# 使用 InstallationProxyService 安装 IPA
|
|
1456
|
+
installation = InstallationProxyService(lockdown=lockdown_client)
|
|
1457
|
+
installation.install_from_local(path)
|
|
1458
|
+
|
|
1459
|
+
logger.info(f"Successfully installed IPA: {path}")
|
|
1460
|
+
os.remove(path)
|
|
1461
|
+
return True, 0
|
|
1462
|
+
|
|
1463
|
+
except Exception as e:
|
|
1464
|
+
logger.error(f"Failed to install IPA: {e}")
|
|
1465
|
+
return False, -1
|
|
1466
|
+
|
|
1467
|
+
class Scrcpy:
|
|
1468
|
+
|
|
1469
|
+
STATICPATH = os.path.dirname(os.path.realpath(__file__))
|
|
1470
|
+
DEFAULT_SCRCPY_PATH = {
|
|
1471
|
+
"64": os.path.join(STATICPATH, "scrcpy", "scrcpy-win64-v2.4", "scrcpy.exe"),
|
|
1472
|
+
"32": os.path.join(STATICPATH, "scrcpy", "scrcpy-win32-v2.4", "scrcpy.exe"),
|
|
1473
|
+
"default":"scrcpy"
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
@classmethod
|
|
1477
|
+
def scrcpy_path(cls):
|
|
1478
|
+
bit = platform.architecture()[0]
|
|
1479
|
+
path = cls.DEFAULT_SCRCPY_PATH["default"]
|
|
1480
|
+
if platform.system().lower().__contains__('windows'):
|
|
1481
|
+
if bit.__contains__('64'):
|
|
1482
|
+
path = cls.DEFAULT_SCRCPY_PATH["64"]
|
|
1483
|
+
elif bit.__contains__('32'):
|
|
1484
|
+
path = cls.DEFAULT_SCRCPY_PATH["32"]
|
|
1485
|
+
return path
|
|
1486
|
+
|
|
1487
|
+
@classmethod
|
|
1488
|
+
def start_record(cls, device):
|
|
1489
|
+
f = File()
|
|
1490
|
+
logger.info('start record screen')
|
|
1491
|
+
win_cmd = "start /b {scrcpy_path} -s {deviceId} --no-playback --no-power-on --record={video}".format(
|
|
1492
|
+
scrcpy_path = cls.scrcpy_path(),
|
|
1493
|
+
deviceId = device,
|
|
1494
|
+
video = os.path.join(f.report_dir, 'record.mkv')
|
|
1495
|
+
)
|
|
1496
|
+
mac_cmd = "nohup {scrcpy_path} -s {deviceId} --no-playback --no-power-on --record={video} &".format(
|
|
1497
|
+
scrcpy_path = cls.scrcpy_path(),
|
|
1498
|
+
deviceId = device,
|
|
1499
|
+
video = os.path.join(f.report_dir, 'record.mkv')
|
|
1500
|
+
)
|
|
1501
|
+
if platform.system().lower().__contains__('windows'):
|
|
1502
|
+
result = os.system(win_cmd)
|
|
1503
|
+
else:
|
|
1504
|
+
result = os.system(mac_cmd)
|
|
1505
|
+
if result == 0:
|
|
1506
|
+
logger.info("record screen success : {}".format(os.path.join(f.report_dir, 'record.mkv')))
|
|
1507
|
+
else:
|
|
1508
|
+
logger.error("magnax's scrcpy is incompatible with your PC")
|
|
1509
|
+
logger.info("Please install the software yourself : brew install scrcpy")
|
|
1510
|
+
return result
|
|
1511
|
+
|
|
1512
|
+
@classmethod
|
|
1513
|
+
def stop_record(cls):
|
|
1514
|
+
logger.info('stop scrcpy process')
|
|
1515
|
+
pids = psutil.pids()
|
|
1516
|
+
scrcpy_processes = []
|
|
1517
|
+
|
|
1518
|
+
# 首先找到所有scrcpy进程
|
|
1519
|
+
try:
|
|
1520
|
+
for pid in pids:
|
|
1521
|
+
try:
|
|
1522
|
+
p = psutil.Process(pid)
|
|
1523
|
+
if p.name().__contains__('scrcpy'):
|
|
1524
|
+
scrcpy_processes.append(p)
|
|
1525
|
+
logger.info(f'发现scrcpy进程: {pid}')
|
|
1526
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
1527
|
+
continue
|
|
1528
|
+
except Exception as e:
|
|
1529
|
+
logger.exception(e)
|
|
1530
|
+
|
|
1531
|
+
# 尝试温和地终止进程,给scrcpy足够时间完成MKV容器写入
|
|
1532
|
+
if scrcpy_processes:
|
|
1533
|
+
logger.info('尝试温和地停止scrcpy进程...')
|
|
1534
|
+
for process in scrcpy_processes:
|
|
1535
|
+
try:
|
|
1536
|
+
process.terminate() # 发送SIGTERM信号
|
|
1537
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
1538
|
+
continue
|
|
1539
|
+
|
|
1540
|
+
# 等待进程优雅退出,scrcpy需要时间来正确关闭MKV容器(写入索引和元数据)
|
|
1541
|
+
gone, alive = psutil.wait_procs(scrcpy_processes, timeout=15)
|
|
1542
|
+
|
|
1543
|
+
if gone:
|
|
1544
|
+
logger.info(f'scrcpy进程已优雅退出: {[p.pid for p in gone]}')
|
|
1545
|
+
|
|
1546
|
+
if alive:
|
|
1547
|
+
logger.warning(f'scrcpy进程超时未退出,强制终止: {[p.pid for p in alive]}')
|
|
1548
|
+
for process in alive:
|
|
1549
|
+
try:
|
|
1550
|
+
process.kill()
|
|
1551
|
+
logger.info(f'强制终止进程: {process.pid}')
|
|
1552
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
1553
|
+
continue
|
|
1554
|
+
time.sleep(1)
|
|
1555
|
+
|
|
1556
|
+
logger.info('scrcpy进程停止完成')
|
|
1557
|
+
else:
|
|
1558
|
+
logger.info('没有发现运行中的scrcpy进程')
|
|
1559
|
+
|
|
1560
|
+
@classmethod
|
|
1561
|
+
def cast_screen(cls, device):
|
|
1562
|
+
logger.info('start cast screen')
|
|
1563
|
+
win_cmd = "start /i {scrcpy_path} -s {deviceId} --no-power-on".format(
|
|
1564
|
+
scrcpy_path = cls.scrcpy_path(),
|
|
1565
|
+
deviceId = device
|
|
1566
|
+
)
|
|
1567
|
+
mac_cmd = "nohup {scrcpy_path} -s {deviceId} --no-power-on &".format(
|
|
1568
|
+
scrcpy_path = cls.scrcpy_path(),
|
|
1569
|
+
deviceId = device
|
|
1570
|
+
)
|
|
1571
|
+
if platform.system().lower().__contains__('windows'):
|
|
1572
|
+
result = os.system(win_cmd)
|
|
1573
|
+
else:
|
|
1574
|
+
result = os.system(mac_cmd)
|
|
1575
|
+
if result == 0:
|
|
1576
|
+
logger.info("cast screen success")
|
|
1577
|
+
else:
|
|
1578
|
+
logger.error("magnax's scrcpy is incompatible with your PC")
|
|
1579
|
+
logger.info("Please install the software yourself : brew install scrcpy")
|
|
1580
|
+
return result
|
|
1581
|
+
|
|
1582
|
+
@classmethod
|
|
1583
|
+
def play_video(cls, video):
|
|
1584
|
+
logger.info('start play video : {}'.format(video))
|
|
1585
|
+
cap = cv2.VideoCapture(video)
|
|
1586
|
+
while(cap.isOpened()):
|
|
1587
|
+
ret, frame = cap.read()
|
|
1588
|
+
if ret:
|
|
1589
|
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
1590
|
+
cv2.namedWindow("frame", 0)
|
|
1591
|
+
cv2.resizeWindow("frame", 430, 900)
|
|
1592
|
+
cv2.imshow('frame', gray)
|
|
1593
|
+
if cv2.waitKey(25) & 0xFF == ord('q') or not cv2.getWindowProperty("frame", cv2.WND_PROP_VISIBLE):
|
|
1594
|
+
break
|
|
1595
|
+
else:
|
|
1596
|
+
break
|
|
1597
|
+
cap.release()
|
|
1598
|
+
cv2.destroyAllWindows()
|