test_onroad.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. #!/usr/bin/env python3
  2. import json
  3. import os
  4. import subprocess
  5. import time
  6. import numpy as np
  7. import unittest
  8. from collections import Counter, defaultdict
  9. from pathlib import Path
  10. from cereal import car
  11. import cereal.messaging as messaging
  12. from cereal.services import service_list
  13. from common.basedir import BASEDIR
  14. from common.timeout import Timeout
  15. from common.params import Params
  16. from selfdrive.controls.lib.events import EVENTS, ET
  17. from selfdrive.loggerd.config import ROOT
  18. from selfdrive.test.helpers import set_params_enabled, release_only
  19. from tools.lib.logreader import LogReader
  20. # Baseline CPU usage by process
  21. PROCS = {
  22. "selfdrive.controls.controlsd": 39.0,
  23. "./loggerd": 10.0,
  24. "./encoderd": 17.0,
  25. "./camerad": 14.5,
  26. "./locationd": 9.1,
  27. "selfdrive.controls.plannerd": 11.7,
  28. "./_ui": 19.2,
  29. "selfdrive.locationd.paramsd": 9.0,
  30. "./_sensord": 12.0,
  31. "selfdrive.controls.radard": 4.5,
  32. "./_modeld": 4.48,
  33. "./boardd": 3.63,
  34. "./_dmonitoringmodeld": 5.0,
  35. "selfdrive.thermald.thermald": 3.87,
  36. "selfdrive.locationd.calibrationd": 2.0,
  37. "selfdrive.locationd.torqued": 5.0,
  38. "./_soundd": 1.0,
  39. "selfdrive.monitoring.dmonitoringd": 4.0,
  40. "./proclogd": 1.54,
  41. "system.logmessaged": 0.2,
  42. "./clocksd": 0.02,
  43. "./ubloxd": 0.02,
  44. "selfdrive.tombstoned": 0,
  45. "./logcatd": 0,
  46. }
  47. TIMINGS = {
  48. # rtols: max/min, rsd
  49. "can": [2.5, 0.35],
  50. "pandaStates": [2.5, 0.35],
  51. "peripheralState": [2.5, 0.35],
  52. "sendcan": [2.5, 0.35],
  53. "carState": [2.5, 0.35],
  54. "carControl": [2.5, 0.35],
  55. "controlsState": [2.5, 0.35],
  56. "lateralPlan": [2.5, 0.5],
  57. "longitudinalPlan": [2.5, 0.5],
  58. "roadCameraState": [2.5, 0.35],
  59. "driverCameraState": [2.5, 0.35],
  60. "modelV2": [2.5, 0.35],
  61. "driverStateV2": [2.5, 0.40],
  62. "liveLocationKalman": [2.5, 0.35],
  63. "wideRoadCameraState": [1.5, 0.35],
  64. }
  65. def cputime_total(ct):
  66. return ct.cpuUser + ct.cpuSystem + ct.cpuChildrenUser + ct.cpuChildrenSystem
  67. def check_cpu_usage(proclogs):
  68. result = "\n"
  69. result += "------------------------------------------------\n"
  70. result += "------------------ CPU Usage -------------------\n"
  71. result += "------------------------------------------------\n"
  72. plogs_by_proc = defaultdict(list)
  73. for pl in proclogs:
  74. for x in pl.procLog.procs:
  75. if len(x.cmdline) > 0:
  76. n = list(x.cmdline)[0]
  77. plogs_by_proc[n].append(x)
  78. print(plogs_by_proc.keys())
  79. r = True
  80. dt = (proclogs[-1].logMonoTime - proclogs[0].logMonoTime) / 1e9
  81. for proc_name, expected_cpu in PROCS.items():
  82. err = ""
  83. cpu_usage = 0.
  84. x = plogs_by_proc[proc_name]
  85. if len(x) > 2:
  86. cpu_time = cputime_total(x[-1]) - cputime_total(x[0])
  87. cpu_usage = cpu_time / dt * 100.
  88. if cpu_usage > max(expected_cpu * 1.15, expected_cpu + 5.0):
  89. # cpu usage is high while playing sounds
  90. if not (proc_name == "./_soundd" and cpu_usage < 65.):
  91. err = "using more CPU than normal"
  92. elif cpu_usage < min(expected_cpu * 0.65, max(expected_cpu - 1.0, 0.0)):
  93. err = "using less CPU than normal"
  94. else:
  95. err = "NO METRICS FOUND"
  96. result += f"{proc_name.ljust(35)} {cpu_usage:5.2f}% ({expected_cpu:5.2f}%) {err}\n"
  97. if len(err) > 0:
  98. r = False
  99. result += "------------------------------------------------\n"
  100. print(result)
  101. return r
  102. class TestOnroad(unittest.TestCase):
  103. @classmethod
  104. def setUpClass(cls):
  105. if "DEBUG" in os.environ:
  106. segs = filter(lambda x: os.path.exists(os.path.join(x, "rlog")), Path(ROOT).iterdir())
  107. segs = sorted(segs, key=lambda x: x.stat().st_mtime)
  108. print(segs[-2])
  109. cls.lr = list(LogReader(os.path.join(segs[-2], "rlog")))
  110. return
  111. # setup env
  112. os.environ['REPLAY'] = "1"
  113. os.environ['SKIP_FW_QUERY'] = "1"
  114. os.environ['FINGERPRINT'] = "TOYOTA COROLLA TSS2 2019"
  115. os.environ['LOGPRINT'] = 'debug'
  116. params = Params()
  117. params.clear_all()
  118. set_params_enabled()
  119. # Make sure athena isn't running
  120. os.system("pkill -9 -f athena")
  121. # start manager and run openpilot for a minute
  122. proc = None
  123. try:
  124. manager_path = os.path.join(BASEDIR, "selfdrive/manager/manager.py")
  125. proc = subprocess.Popen(["python", manager_path])
  126. sm = messaging.SubMaster(['carState'])
  127. with Timeout(150, "controls didn't start"):
  128. while sm.rcv_frame['carState'] < 0:
  129. sm.update(1000)
  130. # make sure we get at least two full segments
  131. route = None
  132. cls.segments = []
  133. with Timeout(300, "timed out waiting for logs"):
  134. while route is None:
  135. route = params.get("CurrentRoute", encoding="utf-8")
  136. time.sleep(0.1)
  137. while len(cls.segments) < 3:
  138. segs = set()
  139. if Path(ROOT).exists():
  140. segs = set(Path(ROOT).glob(f"{route}--*"))
  141. cls.segments = sorted(segs, key=lambda s: int(str(s).rsplit('--')[-1]))
  142. time.sleep(2)
  143. # chop off last, incomplete segment
  144. cls.segments = cls.segments[:-1]
  145. finally:
  146. if proc is not None:
  147. proc.terminate()
  148. if proc.wait(60) is None:
  149. proc.kill()
  150. cls.lrs = [list(LogReader(os.path.join(str(s), "rlog"))) for s in cls.segments]
  151. # use the second segment by default as it's the first full segment
  152. cls.lr = list(LogReader(os.path.join(str(cls.segments[1]), "rlog")))
  153. def test_cloudlog_size(self):
  154. msgs = [m for m in self.lr if m.which() == 'logMessage']
  155. total_size = sum(len(m.as_builder().to_bytes()) for m in msgs)
  156. self.assertLess(total_size, 3.5e5)
  157. cnt = Counter(json.loads(m.logMessage)['filename'] for m in msgs)
  158. big_logs = [f for f, n in cnt.most_common(3) if n / sum(cnt.values()) > 30.]
  159. self.assertEqual(len(big_logs), 0, f"Log spam: {big_logs}")
  160. def test_ui_timings(self):
  161. result = "\n"
  162. result += "------------------------------------------------\n"
  163. result += "-------------- UI Draw Timing ------------------\n"
  164. result += "------------------------------------------------\n"
  165. ts = [m.uiDebug.drawTimeMillis for m in self.lr if m.which() == 'uiDebug']
  166. result += f"min {min(ts):.2f}ms\n"
  167. result += f"max {max(ts):.2f}ms\n"
  168. result += f"std {np.std(ts):.2f}ms\n"
  169. result += f"mean {np.mean(ts):.2f}ms\n"
  170. result += "------------------------------------------------\n"
  171. print(result)
  172. self.assertGreater(len(ts), 20*50, "insufficient samples")
  173. #self.assertLess(max(ts), 30.)
  174. self.assertLess(np.mean(ts), 10.)
  175. #self.assertLess(np.std(ts), 5.)
  176. def test_cpu_usage(self):
  177. proclogs = [m for m in self.lr if m.which() == 'procLog']
  178. self.assertGreater(len(proclogs), service_list['procLog'].frequency * 45, "insufficient samples")
  179. cpu_ok = check_cpu_usage(proclogs)
  180. self.assertTrue(cpu_ok)
  181. def test_camera_processing_time(self):
  182. result = "\n"
  183. result += "------------------------------------------------\n"
  184. result += "-------------- Debayer Timing ------------------\n"
  185. result += "------------------------------------------------\n"
  186. ts = [getattr(getattr(m, m.which()), "processingTime") for m in self.lr if 'CameraState' in m.which()]
  187. self.assertLess(min(ts), 0.025, f"high execution time: {min(ts)}")
  188. result += f"execution time: min {min(ts):.5f}s\n"
  189. result += f"execution time: max {max(ts):.5f}s\n"
  190. result += f"execution time: mean {np.mean(ts):.5f}s\n"
  191. result += "------------------------------------------------\n"
  192. print(result)
  193. def test_mpc_execution_timings(self):
  194. result = "\n"
  195. result += "------------------------------------------------\n"
  196. result += "----------------- MPC Timing ------------------\n"
  197. result += "------------------------------------------------\n"
  198. cfgs = [("lateralPlan", 0.05, 0.05), ("longitudinalPlan", 0.05, 0.05)]
  199. for (s, instant_max, avg_max) in cfgs:
  200. ts = [getattr(getattr(m, s), "solverExecutionTime") for m in self.lr if m.which() == s]
  201. self.assertLess(max(ts), instant_max, f"high '{s}' execution time: {max(ts)}")
  202. self.assertLess(np.mean(ts), avg_max, f"high avg '{s}' execution time: {np.mean(ts)}")
  203. result += f"'{s}' execution time: min {min(ts):.5f}s\n"
  204. result += f"'{s}' execution time: max {max(ts):.5f}s\n"
  205. result += f"'{s}' execution time: mean {np.mean(ts):.5f}s\n"
  206. result += "------------------------------------------------\n"
  207. print(result)
  208. def test_model_execution_timings(self):
  209. result = "\n"
  210. result += "------------------------------------------------\n"
  211. result += "----------------- Model Timing -----------------\n"
  212. result += "------------------------------------------------\n"
  213. # TODO: this went up when plannerd cpu usage increased, why?
  214. cfgs = [
  215. ("modelV2", 0.050, 0.036),
  216. ("driverStateV2", 0.050, 0.026),
  217. ]
  218. for (s, instant_max, avg_max) in cfgs:
  219. ts = [getattr(getattr(m, s), "modelExecutionTime") for m in self.lr if m.which() == s]
  220. self.assertLess(max(ts), instant_max, f"high '{s}' execution time: {max(ts)}")
  221. self.assertLess(np.mean(ts), avg_max, f"high avg '{s}' execution time: {np.mean(ts)}")
  222. result += f"'{s}' execution time: min {min(ts):.5f}s\n"
  223. result += f"'{s}' execution time: max {max(ts):.5f}s\n"
  224. result += f"'{s}' execution time: mean {np.mean(ts):.5f}s\n"
  225. result += "------------------------------------------------\n"
  226. print(result)
  227. def test_timings(self):
  228. passed = True
  229. result = "\n"
  230. result += "------------------------------------------------\n"
  231. result += "----------------- Service Timings --------------\n"
  232. result += "------------------------------------------------\n"
  233. for s, (maxmin, rsd) in TIMINGS.items():
  234. msgs = [m.logMonoTime for m in self.lr if m.which() == s]
  235. if not len(msgs):
  236. raise Exception(f"missing {s}")
  237. ts = np.diff(msgs) / 1e9
  238. dt = 1 / service_list[s].frequency
  239. try:
  240. np.testing.assert_allclose(np.mean(ts), dt, rtol=0.03, err_msg=f"{s} - failed mean timing check")
  241. np.testing.assert_allclose([np.max(ts), np.min(ts)], dt, rtol=maxmin, err_msg=f"{s} - failed max/min timing check")
  242. except Exception as e:
  243. result += str(e) + "\n"
  244. passed = False
  245. if np.std(ts) / dt > rsd:
  246. result += f"{s} - failed RSD timing check\n"
  247. passed = False
  248. result += f"{s.ljust(40)}: {np.array([np.mean(ts), np.max(ts), np.min(ts)])*1e3}\n"
  249. result += f"{''.ljust(40)} {np.max(np.absolute([np.max(ts)/dt, np.min(ts)/dt]))} {np.std(ts)/dt}\n"
  250. result += "="*67
  251. print(result)
  252. self.assertTrue(passed)
  253. @release_only
  254. def test_startup(self):
  255. startup_alert = None
  256. for msg in self.lrs[0]:
  257. # can't use carEvents because the first msg can be dropped while loggerd is starting up
  258. if msg.which() == "controlsState":
  259. startup_alert = msg.controlsState.alertText1
  260. break
  261. expected = EVENTS[car.CarEvent.EventName.startup][ET.PERMANENT].alert_text_1
  262. self.assertEqual(startup_alert, expected, "wrong startup alert")
  263. if __name__ == "__main__":
  264. unittest.main()