__init__.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. # coding: utf-8
  2. #
  3. from __future__ import absolute_import, print_function
  4. import threading
  5. import re
  6. import time
  7. import datetime
  8. import csv
  9. import sys
  10. import os
  11. import atexit
  12. from collections import namedtuple
  13. _MEM_PATTERN = re.compile(r'TOTAL[:\s]+(\d+)')
  14. # acct_tag_hex is a socket tag
  15. # cnt_set==0 are for background data
  16. # cnt_set==1 are for foreground data
  17. _NetStats = namedtuple(
  18. "NetStats",
  19. """idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets
  20. tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets
  21. tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets"""
  22. .split())
  23. class Perf(object):
  24. def __init__(self, d, package_name=None):
  25. self.d = d
  26. self.package_name = package_name
  27. self.csv_output = "perf.csv"
  28. self.debug = False
  29. self.interval = 1.0
  30. self._th = None
  31. self._event = threading.Event()
  32. self._condition = threading.Condition()
  33. self._data = {}
  34. def shell(self, *args, **kwargs):
  35. # print("Shell:", args)
  36. return self.d.shell(*args, **kwargs)
  37. def memory(self):
  38. """ PSS(KB) """
  39. output = self.shell(['dumpsys', 'meminfo', self.package_name]).output
  40. m = _MEM_PATTERN.search(output)
  41. if m:
  42. return int(m.group(1))
  43. return 0
  44. def _cpu_rawdata_collect(self, pid):
  45. """
  46. pjiff maybe 0 if /proc/<pid>stat not exists
  47. """
  48. first_line = self.shell(['cat', '/proc/stat']).output.splitlines()[0]
  49. assert first_line.startswith('cpu ')
  50. # ds: user, nice, system, idle, iowait, irq, softirq, stealstolen, guest, guest_nice
  51. ds = list(map(int, first_line.split()[1:]))
  52. total_cpu = sum(ds)
  53. idle = ds[3]
  54. proc_stat = self.shell(['cat',
  55. '/proc/%d/stat' % pid]).output.split(') ')
  56. pjiff = 0
  57. if len(proc_stat) > 1:
  58. proc_values = proc_stat[1].split()
  59. utime = int(proc_values[11])
  60. stime = int(proc_values[12])
  61. pjiff = utime + stime
  62. return (total_cpu, idle, pjiff)
  63. def cpu(self, pid):
  64. """ CPU
  65. Refs:
  66. - http://man7.org/linux/man-pages/man5/proc.5.html
  67. - [安卓性能测试之cpu占用率统计方法总结](https://www.jianshu.com/p/6bf564f7cdf0)
  68. """
  69. store_key = 'cpu-%d' % pid
  70. # first time jiffies, t: total, p: process
  71. if store_key in self._data:
  72. tjiff1, idle1, pjiff1 = self._data[store_key]
  73. else:
  74. tjiff1, idle1, pjiff1 = self._cpu_rawdata_collect(pid)
  75. time.sleep(.3)
  76. # second time jiffies
  77. self._data[
  78. store_key] = tjiff2, idle2, pjiff2 = self._cpu_rawdata_collect(pid)
  79. # calculate
  80. pcpu = 0.0
  81. if pjiff1 > 0 and pjiff2 > 0:
  82. pcpu = 100.0 * (pjiff2 - pjiff1) / (tjiff2 - tjiff1) # process cpu
  83. scpu = 100.0 * ((tjiff2 - idle2) -
  84. (tjiff1 - idle1)) / (tjiff2 - tjiff1) # system cpu
  85. assert scpu > -1 # maybe -0.5, sometimes happens
  86. scpu = max(0, scpu)
  87. return round(pcpu, 1), round(scpu, 1)
  88. def netstat(self, pid):
  89. """
  90. Returns:
  91. (rall, tall, rtcp, ttcp, rudp, tudp)
  92. """
  93. m = re.search(r'^Uid:\s+(\d+)',
  94. self.shell(['cat', '/proc/%d/status' % pid]).output,
  95. re.M)
  96. if not m:
  97. return (0, 0, 0, 0, 0, 0)
  98. uid = m.group(1)
  99. lines = self.shell(['cat',
  100. '/proc/net/xt_qtaguid/stats']).output.splitlines()
  101. traffic = [0] * 6
  102. def plus_array(arr, *args):
  103. for i, v in enumerate(args):
  104. arr[i] = arr[i] + int(v)
  105. for line in lines:
  106. vs = line.split()
  107. if len(vs) != 21:
  108. continue
  109. v = _NetStats(*vs)
  110. if v.uid_tag_int != uid:
  111. continue
  112. if v.iface != 'wlan0':
  113. continue
  114. # all, tcp, udp
  115. plus_array(traffic, v.rx_bytes, v.tx_bytes, v.rx_tcp_bytes,
  116. v.tx_tcp_bytes, v.rx_udp_bytes, v.tx_udp_bytes)
  117. store_key = 'netstat-%s' % uid
  118. result = []
  119. if store_key in self._data:
  120. last_traffic = self._data[store_key]
  121. for i in range(len(traffic)):
  122. result.append(traffic[i] - last_traffic[i])
  123. self._data[store_key] = traffic
  124. return result or [0] * 6
  125. def _current_view(self, app=None):
  126. d = self.d
  127. views = self.shell(['dumpsys', 'SurfaceFlinger',
  128. '--list']).output.splitlines()
  129. if not app:
  130. app = d.current_app()
  131. current = app['package'] + "/" + app['activity']
  132. surface_curr = 'SurfaceView - ' + current
  133. if surface_curr in views:
  134. return surface_curr
  135. return current
  136. def _dump_surfaceflinger(self, view):
  137. valid_lines = []
  138. MAX_N = 9223372036854775807
  139. for line in self.shell(
  140. ['dumpsys', 'SurfaceFlinger', '--latency',
  141. view]).output.splitlines():
  142. fields = line.split()
  143. if len(fields) != 3:
  144. continue
  145. a, b, c = map(int, fields)
  146. if a == 0:
  147. continue
  148. if MAX_N in (a, b, c):
  149. continue
  150. valid_lines.append((a, b, c))
  151. return valid_lines
  152. def _fps_init(self):
  153. view = self._current_view()
  154. self.shell(["dumpsys", "SurfaceFlinger", "--latency-clear", view])
  155. self._data['fps-start-time'] = time.time()
  156. self._data['fps-last-vsync'] = None
  157. self._data['fps-inited'] = True
  158. def fps(self, app=None):
  159. """
  160. Return float
  161. """
  162. if 'fps-inited' not in self._data:
  163. self._fps_init()
  164. time.sleep(.2)
  165. view = self._current_view(app)
  166. values = self._dump_surfaceflinger(view)
  167. last_vsync = self._data.get('fps-last-vsync')
  168. last_start = self._data.get('fps-start-time')
  169. try:
  170. idx = values.index(last_vsync)
  171. values = values[idx + 1:]
  172. except ValueError:
  173. pass
  174. duration = time.time() - last_start
  175. if len(values):
  176. self._data['fps-last-vsync'] = values[-1]
  177. self._data['fps-start-time'] = time.time()
  178. return round(len(values) / duration, 1)
  179. def collect(self):
  180. pid = self.d._pidof_app(self.package_name)
  181. if pid is None:
  182. return
  183. app = self.d.current_app()
  184. pss = self.memory()
  185. cpu, scpu = self.cpu(pid)
  186. rbytes, tbytes, rtcp, ttcp = self.netstat(pid)[:4]
  187. fps = self.fps(app)
  188. timestr = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
  189. return {
  190. 'time': timestr,
  191. 'package': app['package'],
  192. 'pss': round(pss / 1024.0, 2), # MB
  193. 'cpu': cpu,
  194. 'systemCpu': scpu,
  195. 'rxBytes': rbytes,
  196. 'txBytes': tbytes,
  197. 'rxTcpBytes': rtcp,
  198. 'txTcpBytes': ttcp,
  199. 'fps': fps,
  200. }
  201. def continue_collect(self, f):
  202. try:
  203. headers = [
  204. 'time', 'package', 'pss', 'cpu', 'systemCpu', 'rxBytes',
  205. 'txBytes', 'rxTcpBytes', 'txTcpBytes', 'fps'
  206. ]
  207. fcsv = csv.writer(f)
  208. fcsv.writerow(headers)
  209. update_time = time.time()
  210. while not self._event.isSet():
  211. perfdata = self.collect()
  212. if self.debug:
  213. print("DEBUG:", perfdata)
  214. if not perfdata:
  215. print("perf package is not alive:", self.package_name)
  216. time.sleep(1)
  217. continue
  218. fcsv.writerow([perfdata[k] for k in headers])
  219. wait_seconds = max(0,
  220. self.interval - (time.time() - update_time))
  221. time.sleep(wait_seconds)
  222. update_time = time.time()
  223. f.close()
  224. finally:
  225. self._condition.acquire()
  226. self._th = None
  227. self._condition.notify()
  228. self._condition.release()
  229. def start(self):
  230. csv_dir = os.path.dirname(self.csv_output)
  231. if not os.path.isdir(csv_dir):
  232. os.makedirs(csv_dir)
  233. if sys.version_info.major < 3:
  234. f = open(self.csv_output, "wb")
  235. else:
  236. f = open(self.csv_output, "w", newline='\n')
  237. def defer_close():
  238. if not f.closed:
  239. f.close()
  240. atexit.register(defer_close)
  241. if self._th:
  242. raise RuntimeError("perf is already running")
  243. if not self.package_name:
  244. raise EnvironmentError("package_name need to be set")
  245. self._data.clear()
  246. self._event = threading.Event()
  247. self._condition = threading.Condition()
  248. self._th = threading.Thread(target=self.continue_collect, args=(f, ))
  249. self._th.daemon = True
  250. self._th.start()
  251. def stop(self):
  252. self._event.set()
  253. self._condition.acquire()
  254. self._condition.wait(timeout=2)
  255. self._condition.release()
  256. if self.debug:
  257. print("DEBUG: perf collect stopped")
  258. def csv2images(self, src=None, target_dir='.'):
  259. """
  260. Args:
  261. src: csv file, default to perf record csv path
  262. target_dir: images store dir
  263. """
  264. import pandas as pd
  265. import matplotlib.pyplot as plt
  266. import matplotlib.ticker as ticker
  267. import datetime
  268. import os
  269. import humanize
  270. src = src or self.csv_output
  271. if not os.path.exists(target_dir):
  272. os.makedirs(target_dir)
  273. data = pd.read_csv(src)
  274. data['time'] = data['time'].apply(
  275. lambda x: datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S.%f"))
  276. timestr = time.strftime("%Y-%m-%d %H:%M")
  277. # network
  278. rx_str = humanize.naturalsize(data['rxBytes'].sum(), gnu=True)
  279. tx_str = humanize.naturalsize(data['txBytes'].sum(), gnu=True)
  280. plt.subplot(2, 1, 1)
  281. plt.plot(data['time'], data['rxBytes'] / 1024, label='all')
  282. plt.plot(data['time'], data['rxTcpBytes'] / 1024, 'r--', label='tcp')
  283. plt.legend()
  284. plt.title(
  285. '\n'.join(
  286. ["Network", timestr,
  287. 'Recv %s, Send %s' % (rx_str, tx_str)]),
  288. loc='left')
  289. plt.gca().xaxis.set_major_formatter(ticker.NullFormatter())
  290. plt.ylabel('Recv(KB)')
  291. plt.ylim(ymin=0)
  292. plt.subplot(2, 1, 2)
  293. plt.plot(data['time'], data['txBytes'] / 1024, label='all')
  294. plt.plot(data['time'], data['txTcpBytes'] / 1024, 'r--', label='tcp')
  295. plt.legend()
  296. plt.xlabel('Time')
  297. plt.ylabel('Send(KB)')
  298. plt.ylim(ymin=0)
  299. plt.savefig(os.path.join(target_dir, "net.png"))
  300. plt.clf()
  301. plt.subplot(3, 1, 1)
  302. plt.title(
  303. '\n'.join(['Summary', timestr, self.package_name]), loc='left')
  304. plt.plot(data['time'], data['pss'], '-')
  305. plt.ylabel('PSS(MB)')
  306. plt.gca().xaxis.set_major_formatter(ticker.NullFormatter())
  307. plt.subplot(3, 1, 2)
  308. plt.plot(data['time'], data['cpu'], '-')
  309. plt.ylim(0, max(100, data['cpu'].max()))
  310. plt.ylabel('CPU')
  311. plt.gca().xaxis.set_major_formatter(ticker.NullFormatter())
  312. plt.subplot(3, 1, 3)
  313. plt.plot(data['time'], data['fps'], '-')
  314. plt.ylabel('FPS')
  315. plt.ylim(0, 60)
  316. plt.xlabel('Time')
  317. plt.savefig(os.path.join(target_dir, "summary.png"))
  318. if __name__ == '__main__':
  319. import uiautomator2 as u2
  320. pkgname = "com.tencent.tmgp.sgame"
  321. # pkgname = "com.netease.cloudmusic"
  322. u2.plugin_register('perf', Perf, pkgname)
  323. d = u2.connect()
  324. print(d.current_app())
  325. # print(d.ext_perf.netstat(5350))
  326. # d.app_start(pkgname)
  327. d.ext_perf.start()
  328. d.ext_perf.debug = True
  329. try:
  330. time.sleep(500)
  331. except KeyboardInterrupt:
  332. d.ext_perf.stop()
  333. d.ext_perf.csv2images()
  334. print("threading stopped")