|
|
@ -10,7 +10,6 @@ import time |
|
|
|
import numpy as np |
|
|
|
import numpy as np |
|
|
|
import zstandard as zstd |
|
|
|
import zstandard as zstd |
|
|
|
from collections import Counter, defaultdict |
|
|
|
from collections import Counter, defaultdict |
|
|
|
from functools import cached_property |
|
|
|
|
|
|
|
from pathlib import Path |
|
|
|
from pathlib import Path |
|
|
|
|
|
|
|
|
|
|
|
from cereal import car, log |
|
|
|
from cereal import car, log |
|
|
@ -33,6 +32,9 @@ CPU usage budget |
|
|
|
should not exceed MAX_TOTAL_CPU |
|
|
|
should not exceed MAX_TOTAL_CPU |
|
|
|
""" |
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TEST_DURATION = 25 |
|
|
|
|
|
|
|
LOG_OFFSET = 8 |
|
|
|
|
|
|
|
|
|
|
|
MAX_TOTAL_CPU = 265. # total for all 8 cores |
|
|
|
MAX_TOTAL_CPU = 265. # total for all 8 cores |
|
|
|
PROCS = { |
|
|
|
PROCS = { |
|
|
|
# Baseline CPU usage by process |
|
|
|
# Baseline CPU usage by process |
|
|
@ -58,12 +60,12 @@ PROCS = { |
|
|
|
"./proclogd": 1.54, |
|
|
|
"./proclogd": 1.54, |
|
|
|
"system.logmessaged": 0.2, |
|
|
|
"system.logmessaged": 0.2, |
|
|
|
"system.tombstoned": 0, |
|
|
|
"system.tombstoned": 0, |
|
|
|
"./logcatd": 0, |
|
|
|
"./logcatd": 1.0, |
|
|
|
"system.micd": 5.0, |
|
|
|
"system.micd": 5.0, |
|
|
|
"system.timed": 0, |
|
|
|
"system.timed": 0, |
|
|
|
"selfdrive.pandad.pandad": 0, |
|
|
|
"selfdrive.pandad.pandad": 0, |
|
|
|
"system.statsd": 0.4, |
|
|
|
"system.statsd": 0.4, |
|
|
|
"system.loggerd.uploader": (0.5, 15.0), |
|
|
|
"system.loggerd.uploader": (0.0, 15.0), |
|
|
|
"system.loggerd.deleter": 0.1, |
|
|
|
"system.loggerd.deleter": 0.1, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -98,6 +100,13 @@ TIMINGS = { |
|
|
|
"wideRoadCameraState": [1.5, 0.35], |
|
|
|
"wideRoadCameraState": [1.5, 0.35], |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LOGS_SIZE_RATE = { |
|
|
|
|
|
|
|
"qlog": 0.0083, |
|
|
|
|
|
|
|
"rlog": 0.1528, |
|
|
|
|
|
|
|
"qcamera.ts": 0.03828, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
LOGS_SIZE_RATE.update(dict.fromkeys(['ecamera.hevc', 'fcamera.hevc'], 1.2740)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cputime_total(ct): |
|
|
|
def cputime_total(ct): |
|
|
|
return ct.cpuUser + ct.cpuSystem + ct.cpuChildrenUser + ct.cpuChildrenSystem |
|
|
|
return ct.cpuUser + ct.cpuSystem + ct.cpuChildrenUser + ct.cpuChildrenSystem |
|
|
@ -124,7 +133,7 @@ class TestOnroad: |
|
|
|
if os.path.exists(Paths.log_root()): |
|
|
|
if os.path.exists(Paths.log_root()): |
|
|
|
shutil.rmtree(Paths.log_root()) |
|
|
|
shutil.rmtree(Paths.log_root()) |
|
|
|
|
|
|
|
|
|
|
|
# start manager and run openpilot for a minute |
|
|
|
# start manager and run openpilot for TEST_DURATION |
|
|
|
proc = None |
|
|
|
proc = None |
|
|
|
try: |
|
|
|
try: |
|
|
|
manager_path = os.path.join(BASEDIR, "system/manager/manager.py") |
|
|
|
manager_path = os.path.join(BASEDIR, "system/manager/manager.py") |
|
|
@ -135,26 +144,24 @@ class TestOnroad: |
|
|
|
while sm.recv_frame['carState'] < 0: |
|
|
|
while sm.recv_frame['carState'] < 0: |
|
|
|
sm.update(1000) |
|
|
|
sm.update(1000) |
|
|
|
|
|
|
|
|
|
|
|
# make sure we get at least two full segments |
|
|
|
|
|
|
|
route = None |
|
|
|
route = None |
|
|
|
cls.segments = [] |
|
|
|
cls.segments = [] |
|
|
|
with Timeout(300, "timed out waiting for logs"): |
|
|
|
with Timeout(300, "timed out waiting for logs"): |
|
|
|
while route is None: |
|
|
|
while route is None: |
|
|
|
route = params.get("CurrentRoute", encoding="utf-8") |
|
|
|
route = params.get("CurrentRoute", encoding="utf-8") |
|
|
|
time.sleep(0.1) |
|
|
|
time.sleep(0.01) |
|
|
|
|
|
|
|
|
|
|
|
# test car params caching |
|
|
|
# test car params caching |
|
|
|
params.put("CarParamsCache", car.CarParams().to_bytes()) |
|
|
|
params.put("CarParamsCache", car.CarParams().to_bytes()) |
|
|
|
|
|
|
|
|
|
|
|
while len(cls.segments) < 3: |
|
|
|
while len(cls.segments) < 1: |
|
|
|
segs = set() |
|
|
|
segs = set() |
|
|
|
if Path(Paths.log_root()).exists(): |
|
|
|
if Path(Paths.log_root()).exists(): |
|
|
|
segs = set(Path(Paths.log_root()).glob(f"{route}--*")) |
|
|
|
segs = set(Path(Paths.log_root()).glob(f"{route}--*")) |
|
|
|
cls.segments = sorted(segs, key=lambda s: int(str(s).rsplit('--')[-1])) |
|
|
|
cls.segments = sorted(segs, key=lambda s: int(str(s).rsplit('--')[-1])) |
|
|
|
time.sleep(2) |
|
|
|
time.sleep(0.01) |
|
|
|
|
|
|
|
|
|
|
|
# chop off last, incomplete segment |
|
|
|
time.sleep(TEST_DURATION) |
|
|
|
cls.segments = cls.segments[:-1] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
finally: |
|
|
|
finally: |
|
|
|
cls.gpu_procs = {psutil.Process(int(f.name)).name() for f in pathlib.Path('/sys/devices/virtual/kgsl/kgsl/proc/').iterdir() if f.is_dir()} |
|
|
|
cls.gpu_procs = {psutil.Process(int(f.name)).name() for f in pathlib.Path('/sys/devices/virtual/kgsl/kgsl/proc/').iterdir() if f.is_dir()} |
|
|
@ -166,9 +173,8 @@ class TestOnroad: |
|
|
|
|
|
|
|
|
|
|
|
cls.lrs = [list(LogReader(os.path.join(str(s), "rlog"))) for s in cls.segments] |
|
|
|
cls.lrs = [list(LogReader(os.path.join(str(s), "rlog"))) for s in cls.segments] |
|
|
|
|
|
|
|
|
|
|
|
# use the second segment by default as it's the first full segment |
|
|
|
cls.lr = list(LogReader(os.path.join(str(cls.segments[0]), "rlog"))) |
|
|
|
cls.lr = list(LogReader(os.path.join(str(cls.segments[1]), "rlog"))) |
|
|
|
cls.log_path = cls.segments[0] |
|
|
|
cls.log_path = cls.segments[1] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cls.log_sizes = {} |
|
|
|
cls.log_sizes = {} |
|
|
|
for f in cls.log_path.iterdir(): |
|
|
|
for f in cls.log_path.iterdir(): |
|
|
@ -178,16 +184,13 @@ class TestOnroad: |
|
|
|
with open(f, 'rb') as ff: |
|
|
|
with open(f, 'rb') as ff: |
|
|
|
cls.log_sizes[f] = len(zstd.compress(ff.read(), LOG_COMPRESSION_LEVEL)) / 1e6 |
|
|
|
cls.log_sizes[f] = len(zstd.compress(ff.read(), LOG_COMPRESSION_LEVEL)) / 1e6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cls.msgs = defaultdict(list) |
|
|
|
|
|
|
|
for m in cls.lr: |
|
|
|
|
|
|
|
cls.msgs[m.which()].append(m) |
|
|
|
|
|
|
|
|
|
|
|
@cached_property |
|
|
|
|
|
|
|
def service_msgs(self): |
|
|
|
|
|
|
|
msgs = defaultdict(list) |
|
|
|
|
|
|
|
for m in self.lr: |
|
|
|
|
|
|
|
msgs[m.which()].append(m) |
|
|
|
|
|
|
|
return msgs |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_service_frequencies(self, subtests): |
|
|
|
def test_service_frequencies(self, subtests): |
|
|
|
for s, msgs in self.service_msgs.items(): |
|
|
|
for s, msgs in self.msgs.items(): |
|
|
|
if s in ('initData', 'sentinel'): |
|
|
|
if s in ('initData', 'sentinel'): |
|
|
|
continue |
|
|
|
continue |
|
|
|
|
|
|
|
|
|
|
@ -196,10 +199,10 @@ class TestOnroad: |
|
|
|
continue |
|
|
|
continue |
|
|
|
|
|
|
|
|
|
|
|
with subtests.test(service=s): |
|
|
|
with subtests.test(service=s): |
|
|
|
assert len(msgs) >= math.floor(SERVICE_LIST[s].frequency*55) |
|
|
|
assert len(msgs) >= math.floor(SERVICE_LIST[s].frequency*int(TEST_DURATION*0.8)) |
|
|
|
|
|
|
|
|
|
|
|
def test_cloudlog_size(self): |
|
|
|
def test_cloudlog_size(self): |
|
|
|
msgs = [m for m in self.lr if m.which() == 'logMessage'] |
|
|
|
msgs = self.msgs['logMessage'] |
|
|
|
|
|
|
|
|
|
|
|
total_size = sum(len(m.as_builder().to_bytes()) for m in msgs) |
|
|
|
total_size = sum(len(m.as_builder().to_bytes()) for m in msgs) |
|
|
|
assert total_size < 3.5e5 |
|
|
|
assert total_size < 3.5e5 |
|
|
@ -210,16 +213,10 @@ class TestOnroad: |
|
|
|
|
|
|
|
|
|
|
|
def test_log_sizes(self): |
|
|
|
def test_log_sizes(self): |
|
|
|
for f, sz in self.log_sizes.items(): |
|
|
|
for f, sz in self.log_sizes.items(): |
|
|
|
if f.name == "qcamera.ts": |
|
|
|
rate = LOGS_SIZE_RATE[f.name] |
|
|
|
assert 2.15 < sz < 2.6 |
|
|
|
minn = rate * TEST_DURATION * 0.8 |
|
|
|
elif f.name == "qlog": |
|
|
|
maxx = rate * TEST_DURATION * 1.2 |
|
|
|
assert 0.4 < sz < 0.55 |
|
|
|
assert minn < sz < maxx |
|
|
|
elif f.name == "rlog": |
|
|
|
|
|
|
|
assert 5 < sz < 50 |
|
|
|
|
|
|
|
elif f.name.endswith('.hevc'): |
|
|
|
|
|
|
|
assert 70 < sz < 80 |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
raise NotImplementedError |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ui_timings(self): |
|
|
|
def test_ui_timings(self): |
|
|
|
result = "\n" |
|
|
|
result = "\n" |
|
|
@ -227,7 +224,7 @@ class TestOnroad: |
|
|
|
result += "-------------- UI Draw Timing ------------------\n" |
|
|
|
result += "-------------- UI Draw Timing ------------------\n" |
|
|
|
result += "------------------------------------------------\n" |
|
|
|
result += "------------------------------------------------\n" |
|
|
|
|
|
|
|
|
|
|
|
ts = [m.uiDebug.drawTimeMillis for m in self.service_msgs['uiDebug']] |
|
|
|
ts = [m.uiDebug.drawTimeMillis for m in self.msgs['uiDebug']] |
|
|
|
result += f"min {min(ts):.2f}ms\n" |
|
|
|
result += f"min {min(ts):.2f}ms\n" |
|
|
|
result += f"max {max(ts):.2f}ms\n" |
|
|
|
result += f"max {max(ts):.2f}ms\n" |
|
|
|
result += f"std {np.std(ts):.2f}ms\n" |
|
|
|
result += f"std {np.std(ts):.2f}ms\n" |
|
|
@ -250,7 +247,7 @@ class TestOnroad: |
|
|
|
result += "------------------------------------------------\n" |
|
|
|
result += "------------------------------------------------\n" |
|
|
|
|
|
|
|
|
|
|
|
plogs_by_proc = defaultdict(list) |
|
|
|
plogs_by_proc = defaultdict(list) |
|
|
|
for pl in self.service_msgs['procLog']: |
|
|
|
for pl in self.msgs['procLog']: |
|
|
|
for x in pl.procLog.procs: |
|
|
|
for x in pl.procLog.procs: |
|
|
|
if len(x.cmdline) > 0: |
|
|
|
if len(x.cmdline) > 0: |
|
|
|
n = list(x.cmdline)[0] |
|
|
|
n = list(x.cmdline)[0] |
|
|
@ -258,7 +255,7 @@ class TestOnroad: |
|
|
|
print(plogs_by_proc.keys()) |
|
|
|
print(plogs_by_proc.keys()) |
|
|
|
|
|
|
|
|
|
|
|
cpu_ok = True |
|
|
|
cpu_ok = True |
|
|
|
dt = (self.service_msgs['procLog'][-1].logMonoTime - self.service_msgs['procLog'][0].logMonoTime) / 1e9 |
|
|
|
dt = (self.msgs['procLog'][-1].logMonoTime - self.msgs['procLog'][0].logMonoTime) / 1e9 |
|
|
|
for proc_name, expected_cpu in PROCS.items(): |
|
|
|
for proc_name, expected_cpu in PROCS.items(): |
|
|
|
|
|
|
|
|
|
|
|
err = "" |
|
|
|
err = "" |
|
|
@ -290,7 +287,7 @@ class TestOnroad: |
|
|
|
result += "------------------------------------------------\n" |
|
|
|
result += "------------------------------------------------\n" |
|
|
|
|
|
|
|
|
|
|
|
# Ensure there's no missing procs |
|
|
|
# Ensure there's no missing procs |
|
|
|
all_procs = {p.name for p in self.service_msgs['managerState'][0].managerState.processes if p.shouldBeRunning} |
|
|
|
all_procs = {p.name for p in self.msgs['managerState'][0].managerState.processes if p.shouldBeRunning} |
|
|
|
for p in all_procs: |
|
|
|
for p in all_procs: |
|
|
|
with subtests.test(proc=p): |
|
|
|
with subtests.test(proc=p): |
|
|
|
assert any(p in pp for pp in PROCS.keys()), f"Expected CPU usage missing for {p}" |
|
|
|
assert any(p in pp for pp in PROCS.keys()), f"Expected CPU usage missing for {p}" |
|
|
@ -308,7 +305,8 @@ class TestOnroad: |
|
|
|
assert cpu_ok |
|
|
|
assert cpu_ok |
|
|
|
|
|
|
|
|
|
|
|
def test_memory_usage(self): |
|
|
|
def test_memory_usage(self): |
|
|
|
mems = [m.deviceState.memoryUsagePercent for m in self.service_msgs['deviceState']] |
|
|
|
offset = int(SERVICE_LIST['deviceState'].frequency * LOG_OFFSET) |
|
|
|
|
|
|
|
mems = [m.deviceState.memoryUsagePercent for m in self.msgs['deviceState'][offset:]] |
|
|
|
print("Memory usage: ", mems) |
|
|
|
print("Memory usage: ", mems) |
|
|
|
|
|
|
|
|
|
|
|
# check for big leaks. note that memory usage is |
|
|
|
# check for big leaks. note that memory usage is |
|
|
@ -324,7 +322,9 @@ class TestOnroad: |
|
|
|
result += "-------------- ImgProc Timing ------------------\n" |
|
|
|
result += "-------------- ImgProc Timing ------------------\n" |
|
|
|
result += "------------------------------------------------\n" |
|
|
|
result += "------------------------------------------------\n" |
|
|
|
|
|
|
|
|
|
|
|
ts = [getattr(m, m.which()).processingTime for m in self.lr if 'CameraState' in m.which()] |
|
|
|
ts = [] |
|
|
|
|
|
|
|
for s in ['roadCameraState', 'driverCameraState', 'wideCameraState']: |
|
|
|
|
|
|
|
ts.extend(getattr(m, s).processingTime for m in self.msgs[s]) |
|
|
|
assert min(ts) < 0.025, f"high execution time: {min(ts)}" |
|
|
|
assert min(ts) < 0.025, f"high execution time: {min(ts)}" |
|
|
|
result += f"execution time: min {min(ts):.5f}s\n" |
|
|
|
result += f"execution time: min {min(ts):.5f}s\n" |
|
|
|
result += f"execution time: max {max(ts):.5f}s\n" |
|
|
|
result += f"execution time: max {max(ts):.5f}s\n" |
|
|
@ -357,7 +357,7 @@ class TestOnroad: |
|
|
|
|
|
|
|
|
|
|
|
cfgs = [("longitudinalPlan", 0.05, 0.05),] |
|
|
|
cfgs = [("longitudinalPlan", 0.05, 0.05),] |
|
|
|
for (s, instant_max, avg_max) in cfgs: |
|
|
|
for (s, instant_max, avg_max) in cfgs: |
|
|
|
ts = [getattr(m, s).solverExecutionTime for m in self.service_msgs[s]] |
|
|
|
ts = [getattr(m, s).solverExecutionTime for m in self.msgs[s]] |
|
|
|
assert max(ts) < instant_max, f"high '{s}' execution time: {max(ts)}" |
|
|
|
assert max(ts) < instant_max, f"high '{s}' execution time: {max(ts)}" |
|
|
|
assert np.mean(ts) < avg_max, f"high avg '{s}' execution time: {np.mean(ts)}" |
|
|
|
assert np.mean(ts) < avg_max, f"high avg '{s}' execution time: {np.mean(ts)}" |
|
|
|
result += f"'{s}' execution time: min {min(ts):.5f}s\n" |
|
|
|
result += f"'{s}' execution time: min {min(ts):.5f}s\n" |
|
|
@ -377,7 +377,7 @@ class TestOnroad: |
|
|
|
("driverStateV2", 0.050, 0.026), |
|
|
|
("driverStateV2", 0.050, 0.026), |
|
|
|
] |
|
|
|
] |
|
|
|
for (s, instant_max, avg_max) in cfgs: |
|
|
|
for (s, instant_max, avg_max) in cfgs: |
|
|
|
ts = [getattr(m, s).modelExecutionTime for m in self.service_msgs[s]] |
|
|
|
ts = [getattr(m, s).modelExecutionTime for m in self.msgs[s]] |
|
|
|
assert max(ts) < instant_max, f"high '{s}' execution time: {max(ts)}" |
|
|
|
assert max(ts) < instant_max, f"high '{s}' execution time: {max(ts)}" |
|
|
|
assert np.mean(ts) < avg_max, f"high avg '{s}' execution time: {np.mean(ts)}" |
|
|
|
assert np.mean(ts) < avg_max, f"high avg '{s}' execution time: {np.mean(ts)}" |
|
|
|
result += f"'{s}' execution time: min {min(ts):.5f}s\n" |
|
|
|
result += f"'{s}' execution time: min {min(ts):.5f}s\n" |
|
|
@ -393,7 +393,8 @@ class TestOnroad: |
|
|
|
result += "----------------- Service Timings --------------\n" |
|
|
|
result += "----------------- Service Timings --------------\n" |
|
|
|
result += "------------------------------------------------\n" |
|
|
|
result += "------------------------------------------------\n" |
|
|
|
for s, (maxmin, rsd) in TIMINGS.items(): |
|
|
|
for s, (maxmin, rsd) in TIMINGS.items(): |
|
|
|
msgs = [m.logMonoTime for m in self.service_msgs[s]] |
|
|
|
offset = int(SERVICE_LIST[s].frequency * LOG_OFFSET) |
|
|
|
|
|
|
|
msgs = [m.logMonoTime for m in self.msgs[s][offset:]] |
|
|
|
if not len(msgs): |
|
|
|
if not len(msgs): |
|
|
|
raise Exception(f"missing {s}") |
|
|
|
raise Exception(f"missing {s}") |
|
|
|
|
|
|
|
|
|
|
@ -430,11 +431,12 @@ class TestOnroad: |
|
|
|
|
|
|
|
|
|
|
|
def test_engagable(self): |
|
|
|
def test_engagable(self): |
|
|
|
no_entries = Counter() |
|
|
|
no_entries = Counter() |
|
|
|
for m in self.service_msgs['onroadEvents']: |
|
|
|
for m in self.msgs['onroadEvents']: |
|
|
|
for evt in m.onroadEvents: |
|
|
|
for evt in m.onroadEvents: |
|
|
|
if evt.noEntry: |
|
|
|
if evt.noEntry: |
|
|
|
no_entries[evt.name] += 1 |
|
|
|
no_entries[evt.name] += 1 |
|
|
|
|
|
|
|
|
|
|
|
eng = [m.selfdriveState.engageable for m in self.service_msgs['selfdriveState']] |
|
|
|
offset = int(SERVICE_LIST['selfdriveState'].frequency * LOG_OFFSET) |
|
|
|
|
|
|
|
eng = [m.selfdriveState.engageable for m in self.msgs['selfdriveState'][offset:]] |
|
|
|
assert all(eng), \ |
|
|
|
assert all(eng), \ |
|
|
|
f"Not engageable for whole segment:\n- selfdriveState.engageable: {Counter(eng)}\n- No entry events: {no_entries}" |
|
|
|
f"Not engageable for whole segment:\n- selfdriveState.engageable: {Counter(eng)}\n- No entry events: {no_entries}" |
|
|
|