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/apm.py
ADDED
|
@@ -0,0 +1,1306 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from logzero import logger
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
# 使用 pymobiledevice3 进行设备控制
|
|
11
|
+
try:
|
|
12
|
+
from pymobiledevice3.lockdown import create_using_usbmux, LockdownClient
|
|
13
|
+
from pymobiledevice3.usbmux import list_devices as pmd3_list_devices
|
|
14
|
+
from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService
|
|
15
|
+
PMD3_AVAILABLE = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
PMD3_AVAILABLE = False
|
|
18
|
+
create_using_usbmux = None
|
|
19
|
+
LockdownClient = None
|
|
20
|
+
pmd3_list_devices = None
|
|
21
|
+
RemoteServiceDiscoveryService = None
|
|
22
|
+
logger.warning("pymobiledevice3 not available, iOS features will be limited")
|
|
23
|
+
import multiprocessing
|
|
24
|
+
from solox.public.ios_perf_adapter import PyiOSDeviceAdapter
|
|
25
|
+
from solox.public.adb import adb
|
|
26
|
+
from solox.public.common import Devices, File, Method, Platform, Scrcpy
|
|
27
|
+
from solox.public.android_fps import FPSMonitor, TimeUtils
|
|
28
|
+
|
|
29
|
+
d = Devices()
|
|
30
|
+
f = File()
|
|
31
|
+
m = Method()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_ios_devices():
|
|
35
|
+
"""获取连接的iOS设备列表"""
|
|
36
|
+
if not PMD3_AVAILABLE:
|
|
37
|
+
logger.warning("pymobiledevice3 not available")
|
|
38
|
+
return []
|
|
39
|
+
try:
|
|
40
|
+
devices = pmd3_list_devices()
|
|
41
|
+
return devices
|
|
42
|
+
except Exception as e:
|
|
43
|
+
logger.error(f"Failed to list iOS devices: {e}")
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_ios_device_udids():
|
|
48
|
+
"""获取连接的iOS设备UDID列表"""
|
|
49
|
+
devices = get_ios_devices()
|
|
50
|
+
return [d.serial for d in devices]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_ios_lockdown_client(device_id):
|
|
54
|
+
"""获取iOS设备的lockdown client用于获取设备信息"""
|
|
55
|
+
if not PMD3_AVAILABLE:
|
|
56
|
+
logger.warning("pymobiledevice3 not available, some iOS features may not work")
|
|
57
|
+
return None
|
|
58
|
+
try:
|
|
59
|
+
# 使用 pymobiledevice3 创建 lockdown client
|
|
60
|
+
return create_using_usbmux(serial=device_id)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.error(f"Failed to create lockdown client for device {device_id}: {e}")
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
class Target:
|
|
66
|
+
CPU = 'cpu'
|
|
67
|
+
Memory = 'memory'
|
|
68
|
+
MemoryDetail = 'memory_detail'
|
|
69
|
+
Battery = 'battery'
|
|
70
|
+
Network = 'network'
|
|
71
|
+
FPS = 'fps'
|
|
72
|
+
GPU = 'gpu'
|
|
73
|
+
DISK = 'disk'
|
|
74
|
+
|
|
75
|
+
class CPU(object):
|
|
76
|
+
|
|
77
|
+
def __init__(self, pkgName, deviceId, platform=Platform.Android, pid=None):
|
|
78
|
+
self.pkgName = pkgName
|
|
79
|
+
self.deviceId = deviceId
|
|
80
|
+
self.platform = platform
|
|
81
|
+
self.pid = pid
|
|
82
|
+
if self.pid is None and self.platform == Platform.Android:
|
|
83
|
+
self.pid = d.getPid(pkgName=self.pkgName, deviceId=self.deviceId)[0].split(':')[0]
|
|
84
|
+
|
|
85
|
+
def getprocessCpuStat(self):
|
|
86
|
+
"""get the cpu usage of a process at a certain time"""
|
|
87
|
+
cmd = 'cat /proc/{}/stat'.format(self.pid)
|
|
88
|
+
result = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
89
|
+
r = re.compile("\\s+")
|
|
90
|
+
toks = r.split(result)
|
|
91
|
+
if len(toks) < 17:
|
|
92
|
+
logger.warning(f'[CPU] Invalid /proc/{self.pid}/stat output: {result[:100]}')
|
|
93
|
+
return 0
|
|
94
|
+
processCpu = float(toks[13]) + float(toks[14]) + float(toks[15]) + float(toks[16])
|
|
95
|
+
return processCpu
|
|
96
|
+
|
|
97
|
+
def getTotalCpuStat(self):
|
|
98
|
+
"""get the total cpu usage at a certain time"""
|
|
99
|
+
cmd = 'cat /proc/stat |{} ^cpu'.format(d.filterType())
|
|
100
|
+
result = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
101
|
+
totalCpu = 0
|
|
102
|
+
lines = result.split('\n')
|
|
103
|
+
lines.pop(0)
|
|
104
|
+
for line in lines:
|
|
105
|
+
toks = line.split()
|
|
106
|
+
if toks[1] in ['', ' ']:
|
|
107
|
+
toks.pop(1)
|
|
108
|
+
for i in range(1, 8):
|
|
109
|
+
totalCpu += float(toks[i])
|
|
110
|
+
return float(totalCpu)
|
|
111
|
+
|
|
112
|
+
def getCpuCoreStat(self):
|
|
113
|
+
"""get the core cpu usage at a certain time"""
|
|
114
|
+
cmd = 'cat /proc/stat |{} ^cpu'.format(d.filterType())
|
|
115
|
+
result = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
116
|
+
coreCpu = 0
|
|
117
|
+
coreCpuList = []
|
|
118
|
+
lines = result.split('\n')
|
|
119
|
+
lines.pop(0)
|
|
120
|
+
for line in lines:
|
|
121
|
+
toks = line.split()
|
|
122
|
+
if toks[1] in ['', ' ']:
|
|
123
|
+
toks.pop(1)
|
|
124
|
+
for i in range(1, 8):
|
|
125
|
+
coreCpu += float(toks[i])
|
|
126
|
+
coreCpuList.append(coreCpu)
|
|
127
|
+
coreCpu = 0
|
|
128
|
+
return coreCpuList
|
|
129
|
+
|
|
130
|
+
def getCoreIdleCpuStat(self):
|
|
131
|
+
"""get the core idel cpu usage at a certain time"""
|
|
132
|
+
cmd = 'cat /proc/stat |{} ^cpu'.format(d.filterType())
|
|
133
|
+
result = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
134
|
+
idleCpuList = []
|
|
135
|
+
idleCpu = 0
|
|
136
|
+
lines = result.split('\n')
|
|
137
|
+
lines.pop(0)
|
|
138
|
+
for line in lines:
|
|
139
|
+
toks = line.split()
|
|
140
|
+
if toks[1] in ['', ' ']:
|
|
141
|
+
toks.pop(1)
|
|
142
|
+
idleCpu += float(toks[4])
|
|
143
|
+
idleCpuList.append(idleCpu)
|
|
144
|
+
idleCpu = 0
|
|
145
|
+
return idleCpuList
|
|
146
|
+
|
|
147
|
+
def getIdleCpuStat(self):
|
|
148
|
+
"""get the total cpu usage at a certain time"""
|
|
149
|
+
cmd = 'cat /proc/stat |{} ^cpu'.format(d.filterType())
|
|
150
|
+
result = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
151
|
+
idleCpu = 0
|
|
152
|
+
lines = result.split('\n')
|
|
153
|
+
lines.pop(0)
|
|
154
|
+
for line in lines:
|
|
155
|
+
toks = line.split()
|
|
156
|
+
if toks[1] in ['', ' ']:
|
|
157
|
+
toks.pop(1)
|
|
158
|
+
idleCpu += float(toks[4])
|
|
159
|
+
return idleCpu
|
|
160
|
+
|
|
161
|
+
def getCoreCpuRate(self, cores=0,noLog=False):
|
|
162
|
+
coreCpuRateList = []
|
|
163
|
+
try:
|
|
164
|
+
processCpuTime_1 = self.getprocessCpuStat()
|
|
165
|
+
coreCpuTotalTime_List1 = self.getCpuCoreStat()
|
|
166
|
+
time.sleep(1)
|
|
167
|
+
processCpuTime_2 = self.getprocessCpuStat()
|
|
168
|
+
coreCpuTotalTime_List2 = self.getCpuCoreStat()
|
|
169
|
+
for i in range(len(coreCpuTotalTime_List1)):
|
|
170
|
+
divisor = coreCpuTotalTime_List2[i] - coreCpuTotalTime_List1[i]
|
|
171
|
+
if divisor == 0:
|
|
172
|
+
coreCpuRateList.append(0.0)
|
|
173
|
+
continue
|
|
174
|
+
coreCpuRate = round(float((processCpuTime_2 - processCpuTime_1) / divisor * 100), 2)
|
|
175
|
+
if cores > 0:
|
|
176
|
+
coreCpuRate /= cores
|
|
177
|
+
coreCpuRate = round(float(coreCpuRate), 2)
|
|
178
|
+
coreCpuRateList.append(coreCpuRate)
|
|
179
|
+
if noLog is False:
|
|
180
|
+
apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
|
|
181
|
+
f.add_log(os.path.join(f.report_dir,'cpu{}.log'.format(i)), apm_time, coreCpuRate)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
if len(d.getPid(self.deviceId, self.pkgName)) == 0:
|
|
184
|
+
logger.error('[CPU Core] {} : No process found'.format(self.pkgName))
|
|
185
|
+
else:
|
|
186
|
+
logger.exception(e)
|
|
187
|
+
return coreCpuRateList
|
|
188
|
+
|
|
189
|
+
def getAndroidCpuRate(self, noLog=False):
|
|
190
|
+
"""get the Android cpu rate of a process"""
|
|
191
|
+
try:
|
|
192
|
+
processCpuTime_1 = self.getprocessCpuStat()
|
|
193
|
+
totalCpuTime_1 = self.getTotalCpuStat()
|
|
194
|
+
idleCputime_1 = self.getIdleCpuStat()
|
|
195
|
+
time.sleep(1)
|
|
196
|
+
processCpuTime_2 = self.getprocessCpuStat()
|
|
197
|
+
totalCpuTime_2 = self.getTotalCpuStat()
|
|
198
|
+
idleCputime_2 = self.getIdleCpuStat()
|
|
199
|
+
appCpuRate = round(float((processCpuTime_2 - processCpuTime_1) / (totalCpuTime_2 - totalCpuTime_1) * 100), 2)
|
|
200
|
+
sysCpuRate = round(float(((totalCpuTime_2 - idleCputime_2) - (totalCpuTime_1 - idleCputime_1)) / (totalCpuTime_2 - totalCpuTime_1) * 100), 2)
|
|
201
|
+
if noLog is False:
|
|
202
|
+
apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
|
|
203
|
+
f.add_log(os.path.join(f.report_dir,'cpu_app.log'), apm_time, appCpuRate)
|
|
204
|
+
f.add_log(os.path.join(f.report_dir,'cpu_sys.log'), apm_time, sysCpuRate)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
appCpuRate, sysCpuRate = 0, 0
|
|
207
|
+
if len(d.getPid(self.deviceId, self.pkgName)) == 0:
|
|
208
|
+
logger.error('[CPU] {} : No process found'.format(self.pkgName))
|
|
209
|
+
else:
|
|
210
|
+
logger.exception(e)
|
|
211
|
+
return appCpuRate, sysCpuRate
|
|
212
|
+
|
|
213
|
+
def getiOSCpuRate(self, noLog=False):
|
|
214
|
+
"""get the iOS cpu rate of a process, unit:%"""
|
|
215
|
+
apm = iosPerformance(self.pkgName, self.deviceId)
|
|
216
|
+
appCpuRate = round(float(apm.getPerformance(apm.cpu)[0]), 2)
|
|
217
|
+
sysCpuRate = round(float(apm.getPerformance(apm.cpu)[1]), 2)
|
|
218
|
+
if noLog is False:
|
|
219
|
+
apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
|
|
220
|
+
f.add_log(os.path.join(f.report_dir,'cpu_app.log'), apm_time, appCpuRate)
|
|
221
|
+
f.add_log(os.path.join(f.report_dir,'cpu_sys.log'), apm_time, sysCpuRate)
|
|
222
|
+
return appCpuRate, sysCpuRate
|
|
223
|
+
|
|
224
|
+
def getCpuRate(self, noLog=False):
|
|
225
|
+
"""Get the cpu rate of a process, unit:%"""
|
|
226
|
+
appCpuRate, systemCpuRate = self.getAndroidCpuRate(noLog) if self.platform == Platform.Android else self.getiOSCpuRate(noLog)
|
|
227
|
+
return appCpuRate, systemCpuRate
|
|
228
|
+
|
|
229
|
+
class Memory(object):
|
|
230
|
+
def __init__(self, pkgName, deviceId, platform=Platform.Android, pid=None):
|
|
231
|
+
self.pkgName = pkgName
|
|
232
|
+
self.deviceId = deviceId
|
|
233
|
+
self.platform = platform
|
|
234
|
+
self.pid = pid
|
|
235
|
+
if self.pid is None and self.platform == Platform.Android:
|
|
236
|
+
self.pid = d.getPid(pkgName=self.pkgName, deviceId=self.deviceId)[0].split(':')[0]
|
|
237
|
+
|
|
238
|
+
def getAndroidMemory(self):
|
|
239
|
+
"""Get the Android memory ,unit:MB"""
|
|
240
|
+
try:
|
|
241
|
+
cmd = 'dumpsys meminfo {}'.format(self.pid)
|
|
242
|
+
output = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
243
|
+
m_total = re.search(r'TOTAL\s*(\d+)', output)
|
|
244
|
+
if not m_total:
|
|
245
|
+
m_total = re.search(r'TOTAL PSS:\s*(\d+)', output)
|
|
246
|
+
m_swap = re.search(r'TOTAL SWAP PSS:\s*(\d+)', output)
|
|
247
|
+
if not m_swap:
|
|
248
|
+
m_swap = re.search(r'TOTAL SWAP \(KB\):\s*(\d+)', output)
|
|
249
|
+
totalPass = round(float(float(m_total.group(1))) / 1024, 2)
|
|
250
|
+
swapPass = round(float(float(m_swap.group(1))) / 1024, 2)
|
|
251
|
+
except Exception as e:
|
|
252
|
+
totalPass, swapPass= 0, 0
|
|
253
|
+
if len(d.getPid(self.deviceId, self.pkgName)) == 0:
|
|
254
|
+
logger.error('[Memory] {} : No process found'.format(self.pkgName))
|
|
255
|
+
else:
|
|
256
|
+
logger.exception(e)
|
|
257
|
+
return totalPass, swapPass
|
|
258
|
+
|
|
259
|
+
def getAndroidMemoryDetail(self, noLog=False):
|
|
260
|
+
"""Get the Android detail memory ,unit:MB"""
|
|
261
|
+
try:
|
|
262
|
+
cmd = 'dumpsys meminfo {}'.format(self.pid)
|
|
263
|
+
output = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
264
|
+
m_java = re.search(r'Java Heap:\s*(\d+)', output)
|
|
265
|
+
m_native = re.search(r'Native Heap:\s*(\d+)', output)
|
|
266
|
+
m_code = re.search(r'Code:\s*(\d+)', output)
|
|
267
|
+
m_stack = re.search(r'Stack:\s*(\d+)', output)
|
|
268
|
+
m_graphics = re.search(r'Graphics:\s*(\d+)', output)
|
|
269
|
+
m_private = re.search(r'Private Other:\s*(\d+)', output)
|
|
270
|
+
m_system = re.search(r'System:\s*(\d+)', output)
|
|
271
|
+
java_heap = round(float(float(m_java.group(1))) / 1024, 2)
|
|
272
|
+
native_heap = round(float(float(m_native.group(1))) / 1024, 2)
|
|
273
|
+
code_pss = round(float(float(m_code.group(1))) / 1024, 2)
|
|
274
|
+
stack_pss = round(float(float(m_stack.group(1))) / 1024, 2)
|
|
275
|
+
graphics_pss = round(float(float(m_graphics.group(1))) / 1024, 2)
|
|
276
|
+
private_pss = round(float(float(m_private.group(1))) / 1024, 2)
|
|
277
|
+
system_pss = round(float(float(m_system.group(1))) / 1024, 2)
|
|
278
|
+
memory_dict = dict(
|
|
279
|
+
java_heap=java_heap,
|
|
280
|
+
native_heap=native_heap,
|
|
281
|
+
code_pss=code_pss,
|
|
282
|
+
stack_pss=stack_pss,
|
|
283
|
+
graphics_pss=graphics_pss,
|
|
284
|
+
private_pss=private_pss,
|
|
285
|
+
system_pss=system_pss
|
|
286
|
+
)
|
|
287
|
+
if noLog is False:
|
|
288
|
+
apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
|
|
289
|
+
f.add_log(os.path.join(f.report_dir,'mem_java_heap.log'), apm_time, memory_dict.get('java_heap'))
|
|
290
|
+
f.add_log(os.path.join(f.report_dir,'mem_native_heap.log'), apm_time, memory_dict.get('native_heap'))
|
|
291
|
+
f.add_log(os.path.join(f.report_dir,'mem_code_pss.log'), apm_time, memory_dict.get('code_pss'))
|
|
292
|
+
f.add_log(os.path.join(f.report_dir,'mem_stack_pss.log'), apm_time, memory_dict.get('stack_pss'))
|
|
293
|
+
f.add_log(os.path.join(f.report_dir,'mem_graphics_pss.log'), apm_time, memory_dict.get('graphics_pss'))
|
|
294
|
+
f.add_log(os.path.join(f.report_dir,'mem_private_pss.log'), apm_time, memory_dict.get('private_pss'))
|
|
295
|
+
f.add_log(os.path.join(f.report_dir,'mem_system_pss.log'), apm_time, memory_dict.get('system_pss'))
|
|
296
|
+
except Exception as e:
|
|
297
|
+
memory_dict = dict(
|
|
298
|
+
java_heap=0,
|
|
299
|
+
native_heap=0,
|
|
300
|
+
code_pss=0,
|
|
301
|
+
stack_pss=0,
|
|
302
|
+
graphics_pss=0,
|
|
303
|
+
private_pss=0,
|
|
304
|
+
system_pss=0
|
|
305
|
+
)
|
|
306
|
+
if len(d.getPid(self.deviceId, self.pkgName)) == 0:
|
|
307
|
+
logger.error('[Memory Detail] {} : No process found'.format(self.pkgName))
|
|
308
|
+
else:
|
|
309
|
+
logger.exception(e)
|
|
310
|
+
return memory_dict
|
|
311
|
+
|
|
312
|
+
def getiOSMemory(self):
|
|
313
|
+
"""Get the iOS memory"""
|
|
314
|
+
apm = iosPerformance(self.pkgName, self.deviceId)
|
|
315
|
+
totalPass = round(float(apm.getPerformance(apm.memory)), 2)
|
|
316
|
+
swapPass = 0
|
|
317
|
+
return totalPass, swapPass
|
|
318
|
+
|
|
319
|
+
def getProcessMemory(self, noLog=False):
|
|
320
|
+
"""Get the app memory"""
|
|
321
|
+
totalPass, swapPass = self.getAndroidMemory() if self.platform == Platform.Android else self.getiOSMemory()
|
|
322
|
+
if noLog is False:
|
|
323
|
+
apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
|
|
324
|
+
f.add_log(os.path.join(f.report_dir,'mem_total.log'), apm_time, totalPass)
|
|
325
|
+
if self.platform == Platform.Android:
|
|
326
|
+
f.add_log(os.path.join(f.report_dir,'mem_swap.log'), apm_time, swapPass)
|
|
327
|
+
return totalPass, swapPass
|
|
328
|
+
|
|
329
|
+
class Battery(object):
|
|
330
|
+
def __init__(self, deviceId, platform=Platform.Android):
|
|
331
|
+
self.deviceId = deviceId
|
|
332
|
+
self.platform = platform
|
|
333
|
+
|
|
334
|
+
def getBattery(self, noLog=False):
|
|
335
|
+
if self.platform == Platform.Android:
|
|
336
|
+
level, temperature = self.getAndroidBattery(noLog)
|
|
337
|
+
return level, temperature
|
|
338
|
+
else:
|
|
339
|
+
temperature, current, voltage, power = self.getiOSBattery(noLog)
|
|
340
|
+
return temperature, current, voltage, power
|
|
341
|
+
|
|
342
|
+
def getAndroidBattery(self, noLog=False):
|
|
343
|
+
"""Get android battery info, unit:%"""
|
|
344
|
+
# Switch mobile phone battery to non-charging state
|
|
345
|
+
self.recoverBattery()
|
|
346
|
+
cmd = 'dumpsys battery set status 1'
|
|
347
|
+
adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
348
|
+
# Get phone battery info
|
|
349
|
+
cmd = 'dumpsys battery'
|
|
350
|
+
output = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
351
|
+
level = int(re.findall(u'level:\s?(\d+)', output)[0])
|
|
352
|
+
temperature = int(re.findall(u'temperature:\s?(\d+)', output)[0]) / 10
|
|
353
|
+
if noLog is False:
|
|
354
|
+
apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
|
|
355
|
+
f.add_log(os.path.join(f.report_dir,'battery_level.log'), apm_time, level)
|
|
356
|
+
f.add_log(os.path.join(f.report_dir,'battery_tem.log'), apm_time, temperature)
|
|
357
|
+
return level, temperature
|
|
358
|
+
|
|
359
|
+
def getiOSBattery(self, noLog=False):
|
|
360
|
+
"""Get ios battery info, unit:%"""
|
|
361
|
+
try:
|
|
362
|
+
# 使用 pymobiledevice3 获取电池信息
|
|
363
|
+
lockdown_client = get_ios_lockdown_client(self.deviceId)
|
|
364
|
+
if lockdown_client is None:
|
|
365
|
+
logger.error("Failed to get lockdown client for iOS battery info")
|
|
366
|
+
return 0, 0, 0, 0
|
|
367
|
+
|
|
368
|
+
# Use DiagnosticsService.get_battery() - works without tunnel
|
|
369
|
+
from pymobiledevice3.services.diagnostics import DiagnosticsService
|
|
370
|
+
diagnostics = DiagnosticsService(lockdown_client)
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
battery_info = diagnostics.get_battery()
|
|
374
|
+
except Exception as e:
|
|
375
|
+
logger.warning(f"DiagnosticsService.get_battery() failed: {e}")
|
|
376
|
+
return 0, 0, 0, 0
|
|
377
|
+
|
|
378
|
+
if not battery_info:
|
|
379
|
+
logger.warning("Battery information not available")
|
|
380
|
+
return 0, 0, 0, 0
|
|
381
|
+
|
|
382
|
+
# Extract battery metrics from the response
|
|
383
|
+
# Temperature - check multiple possible keys
|
|
384
|
+
temperature = (battery_info.get('Temperature', 0) or
|
|
385
|
+
battery_info.get('BatteryData', {}).get('AlgoTemperature', 0) / 10000)
|
|
386
|
+
tem = m._setValue(temperature / 100 if temperature > 100 else temperature)
|
|
387
|
+
|
|
388
|
+
# Current (mA) - use Amperage or InstantAmperage
|
|
389
|
+
current = abs(battery_info.get('Amperage', 0) or
|
|
390
|
+
battery_info.get('InstantAmperage', 0))
|
|
391
|
+
current = m._setValue(current)
|
|
392
|
+
|
|
393
|
+
# Voltage (mV) - use AppleRawBatteryVoltage
|
|
394
|
+
voltage = (battery_info.get('AppleRawBatteryVoltage', 0) or
|
|
395
|
+
battery_info.get('Voltage', 0))
|
|
396
|
+
voltage = m._setValue(voltage)
|
|
397
|
+
|
|
398
|
+
# Power (mW)
|
|
399
|
+
power = current * voltage / 1000 if current and voltage else 0
|
|
400
|
+
|
|
401
|
+
if noLog is False:
|
|
402
|
+
apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
|
|
403
|
+
f.add_log(os.path.join(f.report_dir,'battery_tem.log'), apm_time, tem)
|
|
404
|
+
f.add_log(os.path.join(f.report_dir,'battery_current.log'), apm_time, current)
|
|
405
|
+
f.add_log(os.path.join(f.report_dir,'battery_voltage.log'), apm_time, voltage)
|
|
406
|
+
f.add_log(os.path.join(f.report_dir,'battery_power.log'), apm_time, power)
|
|
407
|
+
|
|
408
|
+
return tem, current, voltage, power
|
|
409
|
+
|
|
410
|
+
except Exception as e:
|
|
411
|
+
logger.error(f"Failed to get iOS battery info: {e}")
|
|
412
|
+
# 返回默认值以避免程序崩溃
|
|
413
|
+
return 0, 0, 0, 0
|
|
414
|
+
|
|
415
|
+
def recoverBattery(self):
|
|
416
|
+
"""Reset phone charging status"""
|
|
417
|
+
cmd = 'dumpsys battery reset'
|
|
418
|
+
adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
419
|
+
|
|
420
|
+
class Network(object):
|
|
421
|
+
|
|
422
|
+
def __init__(self, pkgName, deviceId, platform=Platform.Android, pid=None):
|
|
423
|
+
self.pkgName = pkgName
|
|
424
|
+
self.deviceId = deviceId
|
|
425
|
+
self.platform = platform
|
|
426
|
+
self.pid = pid
|
|
427
|
+
if self.pid is None and self.platform == Platform.Android:
|
|
428
|
+
self.pid = d.getPid(pkgName=self.pkgName, deviceId=self.deviceId)[0].split(':')[0]
|
|
429
|
+
|
|
430
|
+
def getAndroidNet(self, wifi=True):
|
|
431
|
+
"""Get Android send/recv data, unit:KB wlan0/rmnet_ipa0"""
|
|
432
|
+
try:
|
|
433
|
+
if wifi is True:
|
|
434
|
+
net = 'wlan0'
|
|
435
|
+
adb.shell(cmd='svc wifi enable', deviceId=self.deviceId)
|
|
436
|
+
else:
|
|
437
|
+
net = 'rmnet_ipa0'
|
|
438
|
+
adb.shell(cmd='svc wifi disable', deviceId=self.deviceId)
|
|
439
|
+
adb.shell(cmd='svc data enable', deviceId=self.deviceId)
|
|
440
|
+
cmd = 'cat /proc/{}/net/dev |{} {}'.format(self.pid, d.filterType(), net)
|
|
441
|
+
output_pre = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
442
|
+
m_pre = 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)
|
|
443
|
+
sendNum_pre = round(float(float(m_pre.group(2)) / 1024), 2)
|
|
444
|
+
recNum_pre = round(float(float(m_pre.group(1)) / 1024), 2)
|
|
445
|
+
time.sleep(0.5)
|
|
446
|
+
output_final = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
447
|
+
m_final = re.search(r'{}:\s*(\d+)\s*\d+\s*\d+\s*\d+\s*\d+\s*\d+\s*\d+\s*\d+\s*(\d+)'.format(net), output_final)
|
|
448
|
+
sendNum_final = round(float(float(m_final.group(2)) / 1024), 2)
|
|
449
|
+
recNum_final = round(float(float(m_final.group(1)) / 1024), 2)
|
|
450
|
+
sendNum = round(float(sendNum_final - sendNum_pre), 2)
|
|
451
|
+
recNum = round(float(recNum_final - recNum_pre), 2)
|
|
452
|
+
except Exception as e:
|
|
453
|
+
sendNum, recNum = 0, 0
|
|
454
|
+
if len(d.getPid(self.deviceId, self.pkgName)) == 0:
|
|
455
|
+
logger.error('[Network] {} : No process found'.format(self.pkgName))
|
|
456
|
+
else:
|
|
457
|
+
logger.exception(e)
|
|
458
|
+
return sendNum, recNum
|
|
459
|
+
|
|
460
|
+
def setAndroidNet(self, wifi=True):
|
|
461
|
+
try:
|
|
462
|
+
if wifi is True:
|
|
463
|
+
net = 'wlan0'
|
|
464
|
+
adb.shell(cmd='svc wifi enable', deviceId=self.deviceId)
|
|
465
|
+
else:
|
|
466
|
+
net = 'rmnet_ipa0'
|
|
467
|
+
adb.shell(cmd='svc wifi disable', deviceId=self.deviceId)
|
|
468
|
+
adb.shell(cmd='svc data enable', deviceId=self.deviceId)
|
|
469
|
+
cmd = f'cat /proc/{self.pid}/net/dev |{d.filterType()} {net}'
|
|
470
|
+
output_pre = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
471
|
+
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)
|
|
472
|
+
sendNum = round(float(float(m.group(2)) / 1024), 2)
|
|
473
|
+
recNum = round(float(float(m.group(1)) / 1024), 2)
|
|
474
|
+
except Exception as e:
|
|
475
|
+
sendNum, recNum = 0, 0
|
|
476
|
+
if len(d.getPid(self.deviceId, self.pkgName)) == 0:
|
|
477
|
+
logger.error('[Network] {} : No process found'.format(self.pkgName))
|
|
478
|
+
else:
|
|
479
|
+
logger.exception(e)
|
|
480
|
+
return sendNum, recNum
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def getiOSNet(self):
|
|
484
|
+
"""Get iOS upflow and downflow data"""
|
|
485
|
+
apm = iosPerformance(self.pkgName, self.deviceId)
|
|
486
|
+
apm_data = apm.getPerformance(apm.network)
|
|
487
|
+
sendNum = round(float(apm_data[1]), 2)
|
|
488
|
+
recNum = round(float(apm_data[0]), 2)
|
|
489
|
+
return sendNum, recNum
|
|
490
|
+
|
|
491
|
+
def getNetWorkData(self, wifi=True, noLog=False):
|
|
492
|
+
"""Get the upflow and downflow data, unit:KB"""
|
|
493
|
+
sendNum, recNum = self.getAndroidNet(wifi) if self.platform == Platform.Android else self.getiOSNet()
|
|
494
|
+
if noLog is False:
|
|
495
|
+
apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
|
|
496
|
+
f.add_log(os.path.join(f.report_dir,'upflow.log'), apm_time, sendNum)
|
|
497
|
+
f.add_log(os.path.join(f.report_dir,'downflow.log'), apm_time, recNum)
|
|
498
|
+
return sendNum, recNum
|
|
499
|
+
|
|
500
|
+
class FPS(object):
|
|
501
|
+
AndroidFPS = None
|
|
502
|
+
|
|
503
|
+
@classmethod
|
|
504
|
+
def getObject(cls, *args, **kwargs):
|
|
505
|
+
if kwargs['platform'] == Platform.Android:
|
|
506
|
+
if cls.AndroidFPS is None:
|
|
507
|
+
cls.AndroidFPS = FPS(*args, **kwargs)
|
|
508
|
+
return cls.AndroidFPS
|
|
509
|
+
return FPS(*args, **kwargs)
|
|
510
|
+
|
|
511
|
+
@classmethod
|
|
512
|
+
def clear(cls):
|
|
513
|
+
if cls.AndroidFPS is not None and cls.AndroidFPS.monitors is not None:
|
|
514
|
+
logger.info('[FPS] Stopping FPS monitor')
|
|
515
|
+
cls.AndroidFPS.monitors.stop()
|
|
516
|
+
cls.AndroidFPS.monitor_started = False
|
|
517
|
+
cls.AndroidFPS = None
|
|
518
|
+
|
|
519
|
+
def __init__(self, pkgName, deviceId, platform=Platform.Android, surfaceview=True):
|
|
520
|
+
self.pkgName = pkgName
|
|
521
|
+
self.deviceId = deviceId
|
|
522
|
+
self.platform = platform
|
|
523
|
+
self.surfaceview = surfaceview
|
|
524
|
+
self.apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
|
|
525
|
+
self.monitors = None
|
|
526
|
+
self.monitor_started = False
|
|
527
|
+
|
|
528
|
+
def getAndroidFps(self, noLog=False):
|
|
529
|
+
"""get Android Fps, unit:HZ"""
|
|
530
|
+
try:
|
|
531
|
+
# 如果监控器未启动,则初始化并启动
|
|
532
|
+
if not self.monitor_started or self.monitors is None:
|
|
533
|
+
logger.info(f'[FPS] Initializing FPS monitor for {self.pkgName}')
|
|
534
|
+
self.monitors = FPSMonitor(device_id=self.deviceId, package_name=self.pkgName, frequency=1,
|
|
535
|
+
surfaceview=self.surfaceview, start_time=TimeUtils.getCurrentTimeUnderline())
|
|
536
|
+
self.monitors.start()
|
|
537
|
+
self.monitor_started = True
|
|
538
|
+
|
|
539
|
+
# 初次启动需要等待一段时间让收集器收集足够的数据
|
|
540
|
+
import time
|
|
541
|
+
time.sleep(3) # 等待3秒确保有足够的数据点
|
|
542
|
+
logger.info(f'[FPS] FPS monitor initialized for {self.pkgName}')
|
|
543
|
+
|
|
544
|
+
# 从持久化的监控器获取当前FPS数据
|
|
545
|
+
# 使用全局变量,这些变量由监控器实时更新
|
|
546
|
+
from solox.public.android_fps import collect_fps, collect_jank
|
|
547
|
+
fps = collect_fps
|
|
548
|
+
jank = collect_jank
|
|
549
|
+
|
|
550
|
+
if noLog is False:
|
|
551
|
+
apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
|
|
552
|
+
f.add_log(os.path.join(f.report_dir,'fps.log'), apm_time, fps)
|
|
553
|
+
f.add_log(os.path.join(f.report_dir,'jank.log'), apm_time, jank)
|
|
554
|
+
|
|
555
|
+
logger.debug(f'[FPS] {self.pkgName}: fps={fps}, jank={jank}')
|
|
556
|
+
except Exception as e:
|
|
557
|
+
fps, jank = 0, 0
|
|
558
|
+
if len(d.getPid(self.deviceId, self.pkgName)) == 0:
|
|
559
|
+
logger.error('[FPS] {} : No process found'.format(self.pkgName))
|
|
560
|
+
else:
|
|
561
|
+
logger.error('[FPS] {} : Failed to get FPS data - {}'.format(self.pkgName, str(e)))
|
|
562
|
+
# 如果监控器出错,尝试重置以便下次重新初始化
|
|
563
|
+
if self.monitors is not None:
|
|
564
|
+
try:
|
|
565
|
+
self.monitors.stop()
|
|
566
|
+
except:
|
|
567
|
+
pass
|
|
568
|
+
self.monitors = None
|
|
569
|
+
self.monitor_started = False
|
|
570
|
+
logger.warning('[FPS] FPS monitor reset due to error, will reinitialize on next call')
|
|
571
|
+
logger.exception(e)
|
|
572
|
+
return fps, jank
|
|
573
|
+
|
|
574
|
+
def getiOSFps(self, noLog=False):
|
|
575
|
+
"""get iOS Fps"""
|
|
576
|
+
apm = iosPerformance(self.pkgName, self.deviceId)
|
|
577
|
+
fps = int(apm.getPerformance(apm.fps))
|
|
578
|
+
if noLog is False:
|
|
579
|
+
apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
|
|
580
|
+
f.add_log(os.path.join(f.report_dir,'fps.log'), apm_time, fps)
|
|
581
|
+
return fps, 0
|
|
582
|
+
|
|
583
|
+
def getFPS(self, noLog=False):
|
|
584
|
+
"""get fps、jank"""
|
|
585
|
+
fps, jank = self.getAndroidFps(noLog) if self.platform == Platform.Android else self.getiOSFps(noLog)
|
|
586
|
+
return fps, jank
|
|
587
|
+
|
|
588
|
+
def stopMonitor(self):
|
|
589
|
+
"""停止FPS监控器"""
|
|
590
|
+
if self.monitors is not None and self.monitor_started:
|
|
591
|
+
logger.info(f'[FPS] Stopping FPS monitor for {self.pkgName}')
|
|
592
|
+
self.monitors.stop()
|
|
593
|
+
self.monitor_started = False
|
|
594
|
+
self.monitors = None
|
|
595
|
+
|
|
596
|
+
class GPU(object):
|
|
597
|
+
def __init__(self, pkgName, deviceId, platform=Platform.Android):
|
|
598
|
+
self.pkgName = pkgName
|
|
599
|
+
self.deviceId = deviceId
|
|
600
|
+
self.platform = platform
|
|
601
|
+
|
|
602
|
+
def getAndroidGpuRate(self):
|
|
603
|
+
try:
|
|
604
|
+
cmd = 'cat /sys/class/kgsl/kgsl-3d0/gpubusy'
|
|
605
|
+
result = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
606
|
+
|
|
607
|
+
# 检查是否有错误或权限问题
|
|
608
|
+
if not result or result.strip() == '' or 'No such file' in result or 'Permission denied' in result or 'Operation not permitted' in result:
|
|
609
|
+
logger.warning(f'[GPU] kgsl方法获取失败,结果: {result}')
|
|
610
|
+
return self._getGpuRateFallback()
|
|
611
|
+
|
|
612
|
+
# 验证数据格式
|
|
613
|
+
parts = result.strip().split(' ')
|
|
614
|
+
if len(parts) < 2:
|
|
615
|
+
logger.warning(f'[GPU] kgsl数据格式错误: {result}')
|
|
616
|
+
return self._getGpuRateFallback()
|
|
617
|
+
|
|
618
|
+
# 验证数据是否为有效数字
|
|
619
|
+
try:
|
|
620
|
+
part1 = parts[0].strip()
|
|
621
|
+
part2 = parts[1].strip()
|
|
622
|
+
|
|
623
|
+
if part1 == '' or part2 == '':
|
|
624
|
+
logger.warning(f'[GPU] kgsl数据包含空值: part1="{part1}", part2="{part2}"')
|
|
625
|
+
return self._getGpuRateFallback()
|
|
626
|
+
|
|
627
|
+
val1 = int(part1)
|
|
628
|
+
val2 = int(part2)
|
|
629
|
+
|
|
630
|
+
if val2 == 0:
|
|
631
|
+
logger.warning('[GPU] kgsl除数为0')
|
|
632
|
+
return self._getGpuRateFallback()
|
|
633
|
+
|
|
634
|
+
gpu = round(float(val1 / val2) * 100, 2)
|
|
635
|
+
logger.debug(f'[GPU] kgsl获取成功: {gpu}% (busy={val1}, total={val2})')
|
|
636
|
+
return gpu
|
|
637
|
+
|
|
638
|
+
except ValueError as e:
|
|
639
|
+
logger.warning(f'[GPU] kgsl数据解析失败: {e}, 原始数据: {result}')
|
|
640
|
+
return self._getGpuRateFallback()
|
|
641
|
+
|
|
642
|
+
except Exception as e:
|
|
643
|
+
logger.error(f'[GPU] kgsl方法异常: {e}')
|
|
644
|
+
return self._getGpuRateFallback()
|
|
645
|
+
|
|
646
|
+
def _getGpuRateFallback(self):
|
|
647
|
+
"""GPU获取失败时的备用方案"""
|
|
648
|
+
try:
|
|
649
|
+
# 尝试其他GPU获取方法
|
|
650
|
+
cmd = 'cat /proc/gpuinfo'
|
|
651
|
+
result = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
652
|
+
|
|
653
|
+
if result and 'No such file' not in result and 'Permission denied' not in result:
|
|
654
|
+
logger.debug(f'[GPU] 备用方法gpuinfo结果: {result[:100]}...')
|
|
655
|
+
# 这里可以解析其他格式的GPU数据
|
|
656
|
+
# 暂时返回0作为安全值
|
|
657
|
+
|
|
658
|
+
except Exception as e:
|
|
659
|
+
logger.debug(f'[GPU] 备用方法失败: {e}')
|
|
660
|
+
|
|
661
|
+
# 返回默认值0,表示GPU数据不可用
|
|
662
|
+
logger.warning('[GPU] 所有GPU获取方法都失败,返回默认值0')
|
|
663
|
+
return 0.0
|
|
664
|
+
|
|
665
|
+
def getiOSGpuRate(self):
|
|
666
|
+
try:
|
|
667
|
+
apm = iosPerformance(self.pkgName, self.deviceId)
|
|
668
|
+
gpu = apm.getPerformance(apm.gpu)
|
|
669
|
+
return gpu
|
|
670
|
+
except Exception as e:
|
|
671
|
+
logger.error(f'[GPU] iOS GPU获取失败: {e}')
|
|
672
|
+
return 0.0
|
|
673
|
+
|
|
674
|
+
def getGPU(self, noLog=False):
|
|
675
|
+
try:
|
|
676
|
+
gpu = self.getAndroidGpuRate() if self.platform == Platform.Android else self.getiOSGpuRate()
|
|
677
|
+
|
|
678
|
+
# 确保返回值是有效的数字
|
|
679
|
+
if not isinstance(gpu, (int, float)) or gpu < 0 or gpu > 100:
|
|
680
|
+
logger.warning(f'[GPU] 获取到无效数值: {gpu},使用默认值0')
|
|
681
|
+
gpu = 0.0
|
|
682
|
+
|
|
683
|
+
if noLog is False:
|
|
684
|
+
apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
|
|
685
|
+
f.add_log(os.path.join(f.report_dir,'gpu.log'), apm_time, gpu)
|
|
686
|
+
return gpu
|
|
687
|
+
except Exception as e:
|
|
688
|
+
logger.error(f'[GPU] 获取GPU数据时发生异常: {e}')
|
|
689
|
+
if noLog is False:
|
|
690
|
+
apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
|
|
691
|
+
f.add_log(os.path.join(f.report_dir,'gpu.log'), apm_time, 0.0)
|
|
692
|
+
return 0.0
|
|
693
|
+
|
|
694
|
+
class Disk(object):
|
|
695
|
+
def __init__(self, deviceId, platform=Platform.Android):
|
|
696
|
+
self.deviceId = deviceId
|
|
697
|
+
self.platform = platform
|
|
698
|
+
|
|
699
|
+
def setInitialDisk(self):
|
|
700
|
+
disk_info = adb.shell(cmd='df', deviceId=self.deviceId)
|
|
701
|
+
with open(os.path.join(f.report_dir,'initail_disk.log'), 'a+', encoding="utf-8") as file:
|
|
702
|
+
file.write(disk_info)
|
|
703
|
+
|
|
704
|
+
def setCurrentDisk(self):
|
|
705
|
+
disk_info = adb.shell(cmd='df', deviceId=self.deviceId)
|
|
706
|
+
with open(os.path.join(f.report_dir,'current_disk.log'), 'a+', encoding="utf-8") as file:
|
|
707
|
+
file.write(disk_info)
|
|
708
|
+
|
|
709
|
+
def getAndroidDisk(self):
|
|
710
|
+
disk_info = adb.shell(cmd='df', deviceId=self.deviceId)
|
|
711
|
+
disk_lines = disk_info.splitlines()
|
|
712
|
+
disk_lines.pop(0)
|
|
713
|
+
size_list = list()
|
|
714
|
+
used_list = list()
|
|
715
|
+
free_list = list()
|
|
716
|
+
for line in disk_lines:
|
|
717
|
+
disk_value_list = line.split()
|
|
718
|
+
size_list.append(int(disk_value_list[1]))
|
|
719
|
+
used_list.append(int(disk_value_list[2]))
|
|
720
|
+
free_list.append(int(disk_value_list[3]))
|
|
721
|
+
sum_size = sum(size_list)
|
|
722
|
+
sum_used = sum(used_list)
|
|
723
|
+
sum_free = sum(free_list)
|
|
724
|
+
disk_dict = {'disk_size':sum_size, 'used':sum_used, 'free': sum_free}
|
|
725
|
+
return disk_dict
|
|
726
|
+
|
|
727
|
+
def getiOSDisk(self):
|
|
728
|
+
try:
|
|
729
|
+
# 使用pymobiledevice3获取存储信息
|
|
730
|
+
lockdown_client = get_ios_lockdown_client(self.deviceId)
|
|
731
|
+
if lockdown_client is None:
|
|
732
|
+
logger.debug("Failed to get lockdown client for iOS disk info")
|
|
733
|
+
return {'used': 0, 'free': 0, 'total': 0}
|
|
734
|
+
|
|
735
|
+
# 尝试从lockdown获取磁盘信息
|
|
736
|
+
device_info = lockdown_client.all_values
|
|
737
|
+
|
|
738
|
+
total_capacity = device_info.get('TotalDiskCapacity', 0)
|
|
739
|
+
total_data_capacity = device_info.get('TotalDataCapacity', 0)
|
|
740
|
+
total_data_available = device_info.get('TotalDataAvailable', 0)
|
|
741
|
+
|
|
742
|
+
# 如果lockdown没有磁盘信息,尝试通过sysmon获取IO统计
|
|
743
|
+
if total_capacity == 0:
|
|
744
|
+
import subprocess
|
|
745
|
+
disk_read = 0
|
|
746
|
+
disk_write = 0
|
|
747
|
+
try:
|
|
748
|
+
result = subprocess.run(
|
|
749
|
+
['python3', '-m', 'pymobiledevice3', 'developer', 'dvt', 'sysmon', 'system',
|
|
750
|
+
'--tunnel', '', '--udid', self.deviceId],
|
|
751
|
+
capture_output=True,
|
|
752
|
+
text=True,
|
|
753
|
+
timeout=10
|
|
754
|
+
)
|
|
755
|
+
if result.returncode == 0:
|
|
756
|
+
for line in result.stdout.split('\n'):
|
|
757
|
+
if ':' in line:
|
|
758
|
+
key, value = line.split(':', 1)
|
|
759
|
+
key = key.strip()
|
|
760
|
+
value = value.strip()
|
|
761
|
+
if key == 'diskBytesRead':
|
|
762
|
+
disk_read = int(value)
|
|
763
|
+
elif key == 'diskBytesWritten':
|
|
764
|
+
disk_write = int(value)
|
|
765
|
+
|
|
766
|
+
if disk_read > 0 or disk_write > 0:
|
|
767
|
+
# 返回IO统计(无法获取实际容量)
|
|
768
|
+
return {
|
|
769
|
+
'total': 0,
|
|
770
|
+
'used': round((disk_read + disk_write) / (1024 * 1024 * 1024), 2),
|
|
771
|
+
'free': 0,
|
|
772
|
+
'disk_read_gb': round(disk_read / (1024 * 1024 * 1024), 2),
|
|
773
|
+
'disk_write_gb': round(disk_write / (1024 * 1024 * 1024), 2)
|
|
774
|
+
}
|
|
775
|
+
except Exception:
|
|
776
|
+
pass
|
|
777
|
+
|
|
778
|
+
return {'used': 0, 'free': 0, 'total': 0}
|
|
779
|
+
|
|
780
|
+
# 计算已用空间
|
|
781
|
+
used_capacity = total_data_capacity - total_data_available
|
|
782
|
+
|
|
783
|
+
disk_dict = {
|
|
784
|
+
'total': round(total_capacity / (1024 * 1024 * 1024), 2),
|
|
785
|
+
'used': round(used_capacity / (1024 * 1024 * 1024), 2),
|
|
786
|
+
'free': round(total_data_available / (1024 * 1024 * 1024), 2)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return disk_dict
|
|
790
|
+
|
|
791
|
+
except Exception as e:
|
|
792
|
+
logger.debug(f"Failed to get iOS disk info: {e}")
|
|
793
|
+
return {'used': 0, 'free': 0, 'total': 0}
|
|
794
|
+
|
|
795
|
+
def getDisk(self, noLog=False):
|
|
796
|
+
disk = self.getAndroidDisk() if self.platform == Platform.Android else self.getiOSDisk()
|
|
797
|
+
if noLog is False:
|
|
798
|
+
apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
|
|
799
|
+
f.add_log(os.path.join(f.report_dir,'disk_used.log'), apm_time, disk.get('used'))
|
|
800
|
+
f.add_log(os.path.join(f.report_dir,'disk_free.log'), apm_time, disk.get('free'))
|
|
801
|
+
return disk
|
|
802
|
+
|
|
803
|
+
class ThermalSensor(object):
|
|
804
|
+
def __init__(self, deviceId, platform=Platform.Android):
|
|
805
|
+
self.deviceId = deviceId
|
|
806
|
+
self.platform = platform
|
|
807
|
+
|
|
808
|
+
def setInitalThermalTemp(self):
|
|
809
|
+
temp_list = list()
|
|
810
|
+
typeLength = len(self.getThermalType())
|
|
811
|
+
if typeLength > 3:
|
|
812
|
+
for i in range(len(self.getThermalType())):
|
|
813
|
+
cmd = 'cat /sys/class/thermal/thermal_zone{}/temp'.format(i)
|
|
814
|
+
temp = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
815
|
+
temp_dict = {
|
|
816
|
+
'type':self.getThermalType()[i],
|
|
817
|
+
'temp':temp
|
|
818
|
+
}
|
|
819
|
+
temp_list.append(temp_dict)
|
|
820
|
+
content = json.dumps(temp_list)
|
|
821
|
+
f.create_file(filename='init_thermal_temp.json', content=content)
|
|
822
|
+
|
|
823
|
+
def setCurrentThermalTemp(self):
|
|
824
|
+
temp_list = list()
|
|
825
|
+
typeLength = len(self.getThermalType())
|
|
826
|
+
if typeLength > 3:
|
|
827
|
+
for i in range(len(self.getThermalType())):
|
|
828
|
+
cmd = 'cat /sys/class/thermal/thermal_zone{}/temp'.format(i)
|
|
829
|
+
temp = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
830
|
+
temp_dict = {
|
|
831
|
+
'type':self.getThermalType()[i],
|
|
832
|
+
'temp':temp
|
|
833
|
+
}
|
|
834
|
+
temp_list.append(temp_dict)
|
|
835
|
+
content = json.dumps(temp_list)
|
|
836
|
+
f.create_file(filename='current_thermal_temp.json', content=content)
|
|
837
|
+
|
|
838
|
+
def getThermalType(self):
|
|
839
|
+
cmd = 'cat /sys/class/thermal/thermal_zone*/type'
|
|
840
|
+
result = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
841
|
+
typeList = result.splitlines()
|
|
842
|
+
return typeList
|
|
843
|
+
|
|
844
|
+
def getThermalTemp(self):
|
|
845
|
+
temp_list = list()
|
|
846
|
+
typeLength = len(self.getThermalType())
|
|
847
|
+
if typeLength > 3:
|
|
848
|
+
for i in range(len(self.getThermalType())):
|
|
849
|
+
cmd = 'cat /sys/class/thermal/thermal_zone{}/temp'.format(i)
|
|
850
|
+
temp = adb.shell(cmd=cmd, deviceId=self.deviceId)
|
|
851
|
+
temp_dict = {
|
|
852
|
+
'type':self.getThermalType()[i],
|
|
853
|
+
'temp':temp
|
|
854
|
+
}
|
|
855
|
+
temp_list.append(temp_dict)
|
|
856
|
+
return temp_list
|
|
857
|
+
else:
|
|
858
|
+
logger.exception('No permission')
|
|
859
|
+
|
|
860
|
+
class Energy(object):
|
|
861
|
+
def __init__(self, deviceId, packageName):
|
|
862
|
+
self.deviceId = deviceId
|
|
863
|
+
self.packageName = packageName
|
|
864
|
+
|
|
865
|
+
def _complete_udid(self, udid: Optional[str] = None) -> str:
|
|
866
|
+
"""获取完整的设备UDID"""
|
|
867
|
+
try:
|
|
868
|
+
device_udids = get_ios_device_udids()
|
|
869
|
+
|
|
870
|
+
# Find udid exactly match
|
|
871
|
+
if udid in device_udids:
|
|
872
|
+
return udid
|
|
873
|
+
|
|
874
|
+
if udid:
|
|
875
|
+
logger.error("Device for %s not detected" % udid)
|
|
876
|
+
return ""
|
|
877
|
+
|
|
878
|
+
if len(device_udids) == 1:
|
|
879
|
+
return device_udids[0]
|
|
880
|
+
|
|
881
|
+
# 简化逻辑,不再区分连接类型
|
|
882
|
+
if len(device_udids) >= 2:
|
|
883
|
+
logger.warning("More than 2 USB devices detected, using first one")
|
|
884
|
+
return device_udids[0]
|
|
885
|
+
if len(device_udids) == 0:
|
|
886
|
+
logger.error("No local device detected")
|
|
887
|
+
return ""
|
|
888
|
+
|
|
889
|
+
return device_udids[0]
|
|
890
|
+
except Exception as e:
|
|
891
|
+
logger.error(f"Failed to get device UDID: {e}")
|
|
892
|
+
return ""
|
|
893
|
+
|
|
894
|
+
def _get_lockdown_client(self, udid: Optional[str] = None):
|
|
895
|
+
"""获取设备的lockdown client"""
|
|
896
|
+
_udid = self._complete_udid(udid)
|
|
897
|
+
if not _udid:
|
|
898
|
+
return None
|
|
899
|
+
|
|
900
|
+
if _udid != udid:
|
|
901
|
+
logger.debug("AutoComplete udid %s", _udid)
|
|
902
|
+
|
|
903
|
+
return get_ios_lockdown_client(_udid)
|
|
904
|
+
|
|
905
|
+
def getEnergy(self):
|
|
906
|
+
"""获取iOS应用能耗信息 - 通过电池信息估算"""
|
|
907
|
+
# 默认返回值(兼容API期望的格式)
|
|
908
|
+
default_result = {
|
|
909
|
+
"energy.overhead": 0,
|
|
910
|
+
"energy.version": 0,
|
|
911
|
+
"energy.gpu.cost": 0,
|
|
912
|
+
"energy.cpu.cost": 0,
|
|
913
|
+
"energy.appstate.cost": 0,
|
|
914
|
+
"energy.thermalstate.cost": 0,
|
|
915
|
+
"energy.networking.cost": 0,
|
|
916
|
+
"energy.cost": 0,
|
|
917
|
+
"energy.display.cost": 0,
|
|
918
|
+
"energy.location.cost": 0,
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
try:
|
|
922
|
+
lockdown_client = self._get_lockdown_client(self.deviceId)
|
|
923
|
+
if lockdown_client is None:
|
|
924
|
+
logger.debug("Failed to get lockdown client for energy monitoring")
|
|
925
|
+
return default_result
|
|
926
|
+
|
|
927
|
+
# 使用DiagnosticsService获取电池功率作为能耗估算
|
|
928
|
+
from pymobiledevice3.services.diagnostics import DiagnosticsService
|
|
929
|
+
diagnostics = DiagnosticsService(lockdown_client)
|
|
930
|
+
|
|
931
|
+
try:
|
|
932
|
+
battery_info = diagnostics.get_battery()
|
|
933
|
+
if battery_info:
|
|
934
|
+
# 计算功率 (mW)
|
|
935
|
+
voltage = battery_info.get('AppleRawBatteryVoltage', 0) # mV
|
|
936
|
+
current = abs(battery_info.get('Amperage', 0)) # mA
|
|
937
|
+
power = (voltage * current) / 1000 # mW
|
|
938
|
+
|
|
939
|
+
# 返回兼容API的格式,将总功率作为energy.cost
|
|
940
|
+
return {
|
|
941
|
+
"energy.overhead": 0,
|
|
942
|
+
"energy.version": 1,
|
|
943
|
+
"energy.gpu.cost": 0,
|
|
944
|
+
"energy.cpu.cost": 0,
|
|
945
|
+
"energy.appstate.cost": 0,
|
|
946
|
+
"energy.thermalstate.cost": 0,
|
|
947
|
+
"energy.networking.cost": 0,
|
|
948
|
+
"energy.cost": round(power, 2), # 总功率 mW
|
|
949
|
+
"energy.display.cost": 0,
|
|
950
|
+
"energy.location.cost": 0,
|
|
951
|
+
"voltage": voltage,
|
|
952
|
+
"current": current,
|
|
953
|
+
}
|
|
954
|
+
except Exception as e:
|
|
955
|
+
logger.debug(f"Energy calculation failed: {e}")
|
|
956
|
+
|
|
957
|
+
return default_result
|
|
958
|
+
|
|
959
|
+
except Exception as e:
|
|
960
|
+
logger.debug(f"Failed to get iOS energy info: {e}")
|
|
961
|
+
return default_result
|
|
962
|
+
|
|
963
|
+
class DataType:
|
|
964
|
+
"""Performance data types for iOS monitoring."""
|
|
965
|
+
CPU = 'cpu'
|
|
966
|
+
MEMORY = 'memory'
|
|
967
|
+
NETWORK = 'network'
|
|
968
|
+
FPS = 'fps'
|
|
969
|
+
GPU = 'gpu'
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
class iosPerformance(object):
|
|
973
|
+
"""
|
|
974
|
+
iOS Performance Monitor using py-ios-device.
|
|
975
|
+
|
|
976
|
+
This class provides a unified interface for collecting iOS performance metrics
|
|
977
|
+
including CPU, Memory, FPS, GPU, and Network data.
|
|
978
|
+
"""
|
|
979
|
+
|
|
980
|
+
def __init__(self, pkgName, deviceId):
|
|
981
|
+
self.pkgName = pkgName
|
|
982
|
+
self.deviceId = deviceId
|
|
983
|
+
self.apm_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
|
|
984
|
+
self.cpu = DataType.CPU
|
|
985
|
+
self.memory = DataType.MEMORY
|
|
986
|
+
self.network = DataType.NETWORK
|
|
987
|
+
self.fps = DataType.FPS
|
|
988
|
+
self.gpu = DataType.GPU
|
|
989
|
+
self._adapter = None
|
|
990
|
+
|
|
991
|
+
def _get_adapter(self) -> PyiOSDeviceAdapter:
|
|
992
|
+
"""Get or create the PyiOSDeviceAdapter instance."""
|
|
993
|
+
if self._adapter is None:
|
|
994
|
+
self._adapter = PyiOSDeviceAdapter(self.deviceId, self.pkgName)
|
|
995
|
+
return self._adapter
|
|
996
|
+
|
|
997
|
+
def getPerformance(self, perfType: str):
|
|
998
|
+
"""
|
|
999
|
+
Get performance data for the specified type.
|
|
1000
|
+
|
|
1001
|
+
Args:
|
|
1002
|
+
perfType: One of DataType.CPU, DataType.MEMORY, DataType.NETWORK,
|
|
1003
|
+
DataType.FPS, or DataType.GPU
|
|
1004
|
+
|
|
1005
|
+
Returns:
|
|
1006
|
+
Performance data in the format expected by the caller:
|
|
1007
|
+
- CPU: (app_cpu%, sys_cpu%)
|
|
1008
|
+
- Memory: float (MB)
|
|
1009
|
+
- FPS: int
|
|
1010
|
+
- GPU: float (%)
|
|
1011
|
+
- Network: (download_kb, upload_kb)
|
|
1012
|
+
"""
|
|
1013
|
+
try:
|
|
1014
|
+
adapter = self._get_adapter()
|
|
1015
|
+
|
|
1016
|
+
if perfType == DataType.CPU:
|
|
1017
|
+
return adapter.get_cpu()
|
|
1018
|
+
elif perfType == DataType.MEMORY:
|
|
1019
|
+
return adapter.get_memory()
|
|
1020
|
+
elif perfType == DataType.FPS:
|
|
1021
|
+
return adapter.get_fps()
|
|
1022
|
+
elif perfType == DataType.GPU:
|
|
1023
|
+
return adapter.get_gpu()
|
|
1024
|
+
elif perfType == DataType.NETWORK:
|
|
1025
|
+
return adapter.get_network()
|
|
1026
|
+
else:
|
|
1027
|
+
logger.warning(f"[iOS Perf] Unknown performance type: {perfType}")
|
|
1028
|
+
return 0
|
|
1029
|
+
|
|
1030
|
+
except Exception as e:
|
|
1031
|
+
logger.error(f"[iOS Perf] Failed to get {perfType} data: {e}")
|
|
1032
|
+
# Return default values based on performance type
|
|
1033
|
+
if perfType == DataType.NETWORK:
|
|
1034
|
+
return 0.0, 0.0
|
|
1035
|
+
elif perfType == DataType.CPU:
|
|
1036
|
+
return 0.0, 0.0
|
|
1037
|
+
else:
|
|
1038
|
+
return 0
|
|
1039
|
+
|
|
1040
|
+
def close(self):
|
|
1041
|
+
"""Clean up resources."""
|
|
1042
|
+
if self._adapter:
|
|
1043
|
+
self._adapter.close()
|
|
1044
|
+
self._adapter = None
|
|
1045
|
+
|
|
1046
|
+
class initPerformanceService(object):
|
|
1047
|
+
CONFIG_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
1048
|
+
CONIFG_PATH = os.path.join(CONFIG_DIR, 'config.json')
|
|
1049
|
+
|
|
1050
|
+
@classmethod
|
|
1051
|
+
def get_status(cls):
|
|
1052
|
+
config_json = open(file=cls.CONIFG_PATH, mode='r').read()
|
|
1053
|
+
run_switch = json.loads(config_json).get('run_switch')
|
|
1054
|
+
return run_switch
|
|
1055
|
+
|
|
1056
|
+
@classmethod
|
|
1057
|
+
def start(cls):
|
|
1058
|
+
config_json = dict()
|
|
1059
|
+
config_json['run_switch'] = 'on'
|
|
1060
|
+
with open(cls.CONIFG_PATH, "w") as file:
|
|
1061
|
+
json.dump(config_json, file)
|
|
1062
|
+
|
|
1063
|
+
@classmethod
|
|
1064
|
+
def stop(cls):
|
|
1065
|
+
config_json = dict()
|
|
1066
|
+
config_json['run_switch'] = 'off'
|
|
1067
|
+
with open(cls.CONIFG_PATH, "w") as file:
|
|
1068
|
+
json.dump(config_json, file)
|
|
1069
|
+
logger.info('stop solox success')
|
|
1070
|
+
return True
|
|
1071
|
+
|
|
1072
|
+
class AppPerformanceMonitor(initPerformanceService):
|
|
1073
|
+
"""for python api"""
|
|
1074
|
+
|
|
1075
|
+
def __init__(self, pkgName=None, platform=Platform.Android, deviceId=None,
|
|
1076
|
+
surfaceview=True, noLog=True, pid=None, record=False, collect_all=False,
|
|
1077
|
+
duration=0):
|
|
1078
|
+
self.pkgName = pkgName
|
|
1079
|
+
self.deviceId = deviceId
|
|
1080
|
+
self.platform = platform
|
|
1081
|
+
self.surfaceview = surfaceview
|
|
1082
|
+
self.noLog = noLog
|
|
1083
|
+
self.pid = pid
|
|
1084
|
+
self.record = record
|
|
1085
|
+
self.collect_all = collect_all
|
|
1086
|
+
self.duration = duration
|
|
1087
|
+
self.end_time = time.time() + self.duration
|
|
1088
|
+
d.devicesCheck(platform=self.platform, deviceid=self.deviceId, pkgname=self.pkgName)
|
|
1089
|
+
self.start()
|
|
1090
|
+
|
|
1091
|
+
def collectCpu(self):
|
|
1092
|
+
_cpu = CPU(self.pkgName, self.deviceId, self.platform, pid=self.pid)
|
|
1093
|
+
result = {}
|
|
1094
|
+
while self.get_status() == 'on':
|
|
1095
|
+
appCpuRate, systemCpuRate = _cpu.getCpuRate(noLog=self.noLog)
|
|
1096
|
+
result = {'appCpuRate': appCpuRate, 'systemCpuRate': systemCpuRate}
|
|
1097
|
+
logger.info(f'cpu: {result}')
|
|
1098
|
+
if self.collect_all is False:
|
|
1099
|
+
break
|
|
1100
|
+
if self.duration > 0 and time.time() > self.end_time:
|
|
1101
|
+
break
|
|
1102
|
+
return result
|
|
1103
|
+
|
|
1104
|
+
def collectCoreCpu(self):
|
|
1105
|
+
_cpucore = CPU(self.pkgName, self.deviceId, self.platform, pid=self.pid)
|
|
1106
|
+
cores = d.getCpuCores(self.deviceId)
|
|
1107
|
+
value = _cpucore.getCoreCpuRate(cores=cores, noLog=self.noLog)
|
|
1108
|
+
result = {'cpu{}'.format(value.index(element)):element for element in value}
|
|
1109
|
+
logger.info(f'cpu core: {result}')
|
|
1110
|
+
return result
|
|
1111
|
+
|
|
1112
|
+
def collectMemory(self):
|
|
1113
|
+
_memory = Memory(self.pkgName, self.deviceId, self.platform, pid=self.pid)
|
|
1114
|
+
result = {}
|
|
1115
|
+
while self.get_status() == 'on':
|
|
1116
|
+
total, swap = _memory.getProcessMemory(noLog=self.noLog)
|
|
1117
|
+
result = {'total': total, 'swap': swap}
|
|
1118
|
+
logger.info(f'memory: {result}')
|
|
1119
|
+
if self.collect_all is False:
|
|
1120
|
+
break
|
|
1121
|
+
if self.duration > 0 and time.time() > self.end_time:
|
|
1122
|
+
break
|
|
1123
|
+
return result
|
|
1124
|
+
|
|
1125
|
+
def collectMemoryDetail(self):
|
|
1126
|
+
_memory = Memory(self.pkgName, self.deviceId, self.platform, pid=self.pid)
|
|
1127
|
+
result = {}
|
|
1128
|
+
while self.get_status() == 'on':
|
|
1129
|
+
if self.platform == Platform.iOS:
|
|
1130
|
+
break
|
|
1131
|
+
result = _memory.getAndroidMemoryDetail(noLog=self.noLog)
|
|
1132
|
+
logger.info(f'memory detail: {result}')
|
|
1133
|
+
if self.collect_all is False:
|
|
1134
|
+
break
|
|
1135
|
+
if self.duration > 0 and time.time() > self.end_time:
|
|
1136
|
+
break
|
|
1137
|
+
return result
|
|
1138
|
+
|
|
1139
|
+
def collectBattery(self):
|
|
1140
|
+
_battery = Battery(self.deviceId, self.platform)
|
|
1141
|
+
result = {}
|
|
1142
|
+
while self.get_status() == 'on':
|
|
1143
|
+
final = _battery.getBattery(noLog=self.noLog)
|
|
1144
|
+
if self.platform == Platform.Android:
|
|
1145
|
+
result = {'level': final[0], 'temperature': final[1]}
|
|
1146
|
+
else:
|
|
1147
|
+
result = {'temperature': final[0], 'current': final[1], 'voltage': final[2], 'power': final[3]}
|
|
1148
|
+
logger.info(f'battery: {result}')
|
|
1149
|
+
if self.collect_all is False:
|
|
1150
|
+
break
|
|
1151
|
+
if self.duration > 0 and time.time() > self.end_time:
|
|
1152
|
+
break
|
|
1153
|
+
return result
|
|
1154
|
+
|
|
1155
|
+
def collectNetwork(self, wifi=True):
|
|
1156
|
+
_network = Network(self.pkgName, self.deviceId, self.platform, pid=self.pid)
|
|
1157
|
+
if self.noLog is False and self.platform == Platform.Android:
|
|
1158
|
+
data = _network.setAndroidNet(wifi=wifi)
|
|
1159
|
+
f.record_net('pre', data[0], data[1])
|
|
1160
|
+
result = {}
|
|
1161
|
+
while self.get_status() == 'on':
|
|
1162
|
+
upFlow, downFlow = _network.getNetWorkData(wifi=wifi,noLog=self.noLog)
|
|
1163
|
+
result = {'send': upFlow, 'recv': downFlow}
|
|
1164
|
+
logger.info(f'network: {result}')
|
|
1165
|
+
if self.collect_all is False:
|
|
1166
|
+
break
|
|
1167
|
+
if self.duration > 0 and time.time() > self.end_time:
|
|
1168
|
+
break
|
|
1169
|
+
return result
|
|
1170
|
+
|
|
1171
|
+
def collectFps(self):
|
|
1172
|
+
_fps = FPS(self.pkgName, self.deviceId, self.platform, self.surfaceview)
|
|
1173
|
+
result = {}
|
|
1174
|
+
while self.get_status() == 'on':
|
|
1175
|
+
fps, jank = _fps.getFPS(noLog=self.noLog)
|
|
1176
|
+
result = {'fps': fps, 'jank': jank}
|
|
1177
|
+
logger.info(f'fps: {result}')
|
|
1178
|
+
if self.collect_all is False:
|
|
1179
|
+
break
|
|
1180
|
+
if self.duration > 0 and time.time() > self.end_time:
|
|
1181
|
+
break
|
|
1182
|
+
return result
|
|
1183
|
+
|
|
1184
|
+
def collectGpu(self):
|
|
1185
|
+
_gpu = GPU(self.pkgName, self.deviceId, self.platform)
|
|
1186
|
+
result = {}
|
|
1187
|
+
while self.get_status() == 'on':
|
|
1188
|
+
gpu = _gpu.getGPU(noLog=self.noLog)
|
|
1189
|
+
result = {'gpu': gpu}
|
|
1190
|
+
logger.info(f'gpu: {result}')
|
|
1191
|
+
if self.collect_all is False:
|
|
1192
|
+
break
|
|
1193
|
+
if self.duration > 0 and time.time() > self.end_time:
|
|
1194
|
+
break
|
|
1195
|
+
return result
|
|
1196
|
+
|
|
1197
|
+
def collectThermal(self):
|
|
1198
|
+
_thermal = ThermalSensor(self.deviceId, self.platform)
|
|
1199
|
+
result = _thermal.getThermalTemp()
|
|
1200
|
+
logger.info(f'thermal: {result}')
|
|
1201
|
+
return result
|
|
1202
|
+
|
|
1203
|
+
def collectDisk(self):
|
|
1204
|
+
_disk = Disk(self.deviceId, self.platform)
|
|
1205
|
+
result = _disk.getDisk()
|
|
1206
|
+
logger.info(f'disk: {result}')
|
|
1207
|
+
return result
|
|
1208
|
+
|
|
1209
|
+
def setPerfs(self, report_path=None):
|
|
1210
|
+
match(self.platform):
|
|
1211
|
+
case Platform.Android:
|
|
1212
|
+
adb.shell(cmd='dumpsys battery reset', deviceId=self.deviceId)
|
|
1213
|
+
_flow = Network(self.pkgName, self.deviceId, self.platform, pid=self.pid)
|
|
1214
|
+
data = _flow.setAndroidNet()
|
|
1215
|
+
f.record_net('end', data[0], data[1])
|
|
1216
|
+
scene = f.make_report(app=self.pkgName, devices=self.deviceId,
|
|
1217
|
+
video=0, platform=self.platform, model='normal')
|
|
1218
|
+
summary = f._setAndroidPerfs(scene)
|
|
1219
|
+
summary_dict = {}
|
|
1220
|
+
summary_dict['app'] = summary['app']
|
|
1221
|
+
summary_dict['platform'] = summary['platform']
|
|
1222
|
+
summary_dict['devices'] = summary['devices']
|
|
1223
|
+
summary_dict['ctime'] = summary['ctime']
|
|
1224
|
+
summary_dict['cpu_app'] = summary['cpuAppRate']
|
|
1225
|
+
summary_dict['cpu_sys'] = summary['cpuSystemRate']
|
|
1226
|
+
summary_dict['mem_total'] = summary['totalPassAvg']
|
|
1227
|
+
summary_dict['mem_swap'] = summary['swapPassAvg']
|
|
1228
|
+
summary_dict['fps'] = summary['fps']
|
|
1229
|
+
summary_dict['jank'] = summary['jank']
|
|
1230
|
+
summary_dict['level'] = summary['batteryLevel']
|
|
1231
|
+
summary_dict['tem'] = summary['batteryTeml']
|
|
1232
|
+
summary_dict['net_send'] = summary['flow_send']
|
|
1233
|
+
summary_dict['net_recv'] = summary['flow_recv']
|
|
1234
|
+
summary_dict['gpu'] = summary['gpu']
|
|
1235
|
+
summary_dict['cpu_charts'] = f.getCpuLog(Platform.Android, scene)
|
|
1236
|
+
summary_dict['mem_charts'] = f.getMemLog(Platform.Android, scene)
|
|
1237
|
+
summary_dict['mem_detail_charts'] = f.getMemDetailLog(Platform.Android, scene)
|
|
1238
|
+
summary_dict['net_charts'] = f.getFlowLog(Platform.Android, scene)
|
|
1239
|
+
summary_dict['battery_charts'] = f.getBatteryLog(Platform.Android, scene)
|
|
1240
|
+
summary_dict['fps_charts'] = f.getFpsLog(Platform.Android, scene)['fps']
|
|
1241
|
+
summary_dict['jank_charts'] = f.getFpsLog(Platform.Android, scene)['jank']
|
|
1242
|
+
summary_dict['gpu_charts'] = f.getGpuLog(Platform.Android, scene)
|
|
1243
|
+
f.make_android_html(scene=scene, summary=summary_dict, report_path=report_path)
|
|
1244
|
+
case Platform.iOS:
|
|
1245
|
+
scene = f.make_report(app=self.pkgName, devices=self.deviceId,
|
|
1246
|
+
video=0, platform=self.platform, model='normal')
|
|
1247
|
+
summary = f._setiOSPerfs(scene)
|
|
1248
|
+
summary_dict = {}
|
|
1249
|
+
summary_dict['app'] = summary['app']
|
|
1250
|
+
summary_dict['platform'] = summary['platform']
|
|
1251
|
+
summary_dict['devices'] = summary['devices']
|
|
1252
|
+
summary_dict['ctime'] = summary['ctime']
|
|
1253
|
+
summary_dict['cpu_app'] = summary['cpuAppRate']
|
|
1254
|
+
summary_dict['cpu_sys'] = summary['cpuSystemRate']
|
|
1255
|
+
summary_dict['mem_total'] = summary['totalPassAvg']
|
|
1256
|
+
summary_dict['fps'] = summary['fps']
|
|
1257
|
+
summary_dict['current'] = summary['batteryCurrent']
|
|
1258
|
+
summary_dict['voltage'] = summary['batteryVoltage']
|
|
1259
|
+
summary_dict['power'] = summary['batteryPower']
|
|
1260
|
+
summary_dict['tem'] = summary['batteryTeml']
|
|
1261
|
+
summary_dict['gpu'] = summary['gpu']
|
|
1262
|
+
summary_dict['net_send'] = summary['flow_send']
|
|
1263
|
+
summary_dict['net_recv'] = summary['flow_recv']
|
|
1264
|
+
summary_dict['cpu_charts'] = f.getCpuLog(Platform.iOS, scene)
|
|
1265
|
+
summary_dict['mem_charts'] = f.getMemLog(Platform.iOS, scene)
|
|
1266
|
+
summary_dict['net_charts'] = f.getFlowLog(Platform.iOS, scene)
|
|
1267
|
+
summary_dict['battery_charts'] = f.getBatteryLog(Platform.iOS, scene)
|
|
1268
|
+
summary_dict['fps_charts'] = f.getFpsLog(Platform.iOS, scene)
|
|
1269
|
+
summary_dict['gpu_charts'] = f.getGpuLog(Platform.iOS, scene)
|
|
1270
|
+
f.make_ios_html(scene=scene, summary=summary_dict, report_path=report_path)
|
|
1271
|
+
case _:
|
|
1272
|
+
raise Exception('platfrom is invalid')
|
|
1273
|
+
|
|
1274
|
+
def collectAll(self, report_path=None):
|
|
1275
|
+
try:
|
|
1276
|
+
f.clear_file()
|
|
1277
|
+
process_num = 8 if self.record else 7
|
|
1278
|
+
pool = multiprocessing.Pool(processes=process_num)
|
|
1279
|
+
pool.apply_async(self.collectCpu)
|
|
1280
|
+
pool.apply_async(self.collectMemory)
|
|
1281
|
+
pool.apply_async(self.collectMemoryDetail)
|
|
1282
|
+
pool.apply_async(self.collectBattery)
|
|
1283
|
+
pool.apply_async(self.collectFps)
|
|
1284
|
+
pool.apply_async(self.collectNetwork)
|
|
1285
|
+
pool.apply_async(self.collectGpu)
|
|
1286
|
+
if self.record:
|
|
1287
|
+
pool.apply_async(Scrcpy.start_record, (self.deviceId))
|
|
1288
|
+
pool.close()
|
|
1289
|
+
pool.join()
|
|
1290
|
+
self.setPerfs(report_path=report_path)
|
|
1291
|
+
except KeyboardInterrupt:
|
|
1292
|
+
if self.record:
|
|
1293
|
+
logger.info('收到中断信号,停止录屏...')
|
|
1294
|
+
Scrcpy.stop_record()
|
|
1295
|
+
logger.info('等待录屏文件释放...')
|
|
1296
|
+
time.sleep(2) # 等待文件释放
|
|
1297
|
+
self.setPerfs(report_path=report_path)
|
|
1298
|
+
except Exception as e:
|
|
1299
|
+
if self.record:
|
|
1300
|
+
logger.info('发生异常,停止录屏...')
|
|
1301
|
+
Scrcpy.stop_record()
|
|
1302
|
+
logger.info('等待录屏文件释放...')
|
|
1303
|
+
time.sleep(2) # 等待文件释放
|
|
1304
|
+
logger.exception(e)
|
|
1305
|
+
finally:
|
|
1306
|
+
logger.info('End of testing')
|