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.
Files changed (87) hide show
  1. magnax/__init__.py +3 -0
  2. magnax/__main__.py +25 -0
  3. magnax/debug.py +65 -0
  4. magnax/public/__init__.py +1 -0
  5. magnax/public/adb/linux/adb +0 -0
  6. magnax/public/adb/linux_arm/adb +0 -0
  7. magnax/public/adb/mac/adb +0 -0
  8. magnax/public/adb/windows/AdbWinApi.dll +0 -0
  9. magnax/public/adb/windows/AdbWinUsbApi.dll +0 -0
  10. magnax/public/adb/windows/adb.exe +0 -0
  11. magnax/public/adb.py +96 -0
  12. magnax/public/android_fps.py +750 -0
  13. magnax/public/apm.py +1306 -0
  14. magnax/public/apm_pk.py +184 -0
  15. magnax/public/common.py +1598 -0
  16. magnax/public/config.json +1 -0
  17. magnax/public/ios_perf_adapter.py +790 -0
  18. magnax/public/report_template/android.html +526 -0
  19. magnax/public/report_template/ios.html +482 -0
  20. magnax/public/scrcpy/scrcpy-win32-v2.4/AdbWinApi.dll +0 -0
  21. magnax/public/scrcpy/scrcpy-win32-v2.4/AdbWinUsbApi.dll +0 -0
  22. magnax/public/scrcpy/scrcpy-win32-v2.4/SDL2.dll +0 -0
  23. magnax/public/scrcpy/scrcpy-win32-v2.4/adb.exe +0 -0
  24. magnax/public/scrcpy/scrcpy-win32-v2.4/avcodec-60.dll +0 -0
  25. magnax/public/scrcpy/scrcpy-win32-v2.4/avformat-60.dll +0 -0
  26. magnax/public/scrcpy/scrcpy-win32-v2.4/avutil-58.dll +0 -0
  27. magnax/public/scrcpy/scrcpy-win32-v2.4/icon.png +0 -0
  28. magnax/public/scrcpy/scrcpy-win32-v2.4/libusb-1.0.dll +0 -0
  29. magnax/public/scrcpy/scrcpy-win32-v2.4/open_a_terminal_here.bat +1 -0
  30. magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy-console.bat +2 -0
  31. magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy-noconsole.vbs +7 -0
  32. magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy-server +0 -0
  33. magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy.exe +0 -0
  34. magnax/public/scrcpy/scrcpy-win32-v2.4/swresample-4.dll +0 -0
  35. magnax/public/scrcpy/scrcpy-win64-v2.4/AdbWinApi.dll +0 -0
  36. magnax/public/scrcpy/scrcpy-win64-v2.4/AdbWinUsbApi.dll +0 -0
  37. magnax/public/scrcpy/scrcpy-win64-v2.4/SDL2.dll +0 -0
  38. magnax/public/scrcpy/scrcpy-win64-v2.4/avformat-60.dll +0 -0
  39. magnax/public/scrcpy/scrcpy-win64-v2.4/avutil-58.dll +0 -0
  40. magnax/public/scrcpy/scrcpy-win64-v2.4/open_a_terminal_here.bat +1 -0
  41. magnax/public/scrcpy/scrcpy-win64-v2.4/scrcpy-noconsole.vbs +7 -0
  42. magnax/public/scrcpy/scrcpy-win64-v2.4/scrcpy-server +0 -0
  43. magnax/public/scrcpy/scrcpy-win64-v2.4/scrcpy.exe +0 -0
  44. magnax/public/scrcpy/scrcpy-win64-v2.4/swresample-4.dll +0 -0
  45. magnax/static/css/highlight.min.css +9 -0
  46. magnax/static/css/magnax-dark-theme.css +1237 -0
  47. magnax/static/css/select2-bootstrap-5-theme.min.css +3 -0
  48. magnax/static/css/select2-bootstrap-5-theme.rtl.min.css +3 -0
  49. magnax/static/css/select2.min.css +1 -0
  50. magnax/static/css/sweetalert2.min.css +1 -0
  51. magnax/static/css/tabler.demo.min.css +9 -0
  52. magnax/static/css/tabler.min.css +14 -0
  53. magnax/static/image/500.png +0 -0
  54. magnax/static/image/avatar.png +0 -0
  55. magnax/static/image/empty.png +0 -0
  56. magnax/static/image/readme/home.png +0 -0
  57. magnax/static/image/readme/pk.png +0 -0
  58. magnax/static/js/apexcharts.js +14 -0
  59. magnax/static/js/gray.js +16 -0
  60. magnax/static/js/highlight.min.js +1173 -0
  61. magnax/static/js/highstock.js +803 -0
  62. magnax/static/js/html2canvas.min.js +20 -0
  63. magnax/static/js/jquery.min.js +2 -0
  64. magnax/static/js/magnax-chart-theme.js +492 -0
  65. magnax/static/js/select2.min.js +2 -0
  66. magnax/static/js/sweetalert2.min.js +1 -0
  67. magnax/static/js/tabler.demo.min.js +9 -0
  68. magnax/static/js/tabler.min.js +9 -0
  69. magnax/static/logo/logo.png +0 -0
  70. magnax/templates/404.html +30 -0
  71. magnax/templates/analysis.html +1375 -0
  72. magnax/templates/analysis_compare.html +600 -0
  73. magnax/templates/analysis_pk.html +680 -0
  74. magnax/templates/base.html +365 -0
  75. magnax/templates/index.html +2471 -0
  76. magnax/templates/pk.html +743 -0
  77. magnax/templates/report.html +416 -0
  78. magnax/view/__init__.py +1 -0
  79. magnax/view/apis.py +952 -0
  80. magnax/view/pages.py +146 -0
  81. magnax/web.py +345 -0
  82. magnax-1.0.0.dist-info/METADATA +242 -0
  83. magnax-1.0.0.dist-info/RECORD +87 -0
  84. magnax-1.0.0.dist-info/WHEEL +5 -0
  85. magnax-1.0.0.dist-info/entry_points.txt +2 -0
  86. magnax-1.0.0.dist-info/licenses/LICENSE +21 -0
  87. 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')