This commit is contained in:
Your Name
2024-04-27 13:03:43 -05:00
parent fe5f722eda
commit 71b83ffe7f
22 changed files with 986 additions and 28 deletions

View File

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
import argparse
import collections
import multiprocessing
import os
import requests
from tqdm import tqdm
import openpilot.system.hardware.tici.casync as casync
def get_chunk_download_size(chunk):
sha = chunk.sha.hex()
path = os.path.join(remote_url, sha[:4], sha + ".cacnk")
if os.path.isfile(path):
return os.path.getsize(path)
else:
r = requests.head(path, timeout=10)
r.raise_for_status()
return int(r.headers['content-length'])
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Compute overlap between two casync manifests')
parser.add_argument('frm')
parser.add_argument('to')
args = parser.parse_args()
frm = casync.parse_caibx(args.frm)
to = casync.parse_caibx(args.to)
remote_url = args.to.replace('.caibx', '')
most_common = collections.Counter(t.sha for t in to).most_common(1)[0][0]
frm_dict = casync.build_chunk_dict(frm)
# Get content-length for each chunk
with multiprocessing.Pool() as pool:
szs = list(tqdm(pool.imap(get_chunk_download_size, to), total=len(to)))
chunk_sizes = {t.sha: sz for (t, sz) in zip(to, szs, strict=True)}
sources: dict[str, list[int]] = {
'seed': [],
'remote_uncompressed': [],
'remote_compressed': [],
}
for chunk in to:
# Assume most common chunk is the zero chunk
if chunk.sha == most_common:
continue
if chunk.sha in frm_dict:
sources['seed'].append(chunk.length)
else:
sources['remote_uncompressed'].append(chunk.length)
sources['remote_compressed'].append(chunk_sizes[chunk.sha])
print()
print("Update statistics (excluding zeros)")
print()
print("Download only with no seed:")
print(f" Remote (uncompressed)\t\t{sum(sources['seed'] + sources['remote_uncompressed']) / 1000 / 1000:.2f} MB\tn = {len(to)}")
print(f" Remote (compressed download)\t{sum(chunk_sizes.values()) / 1000 / 1000:.2f} MB\tn = {len(to)}")
print()
print("Upgrade with seed partition:")
print(f" Seed (uncompressed)\t\t{sum(sources['seed']) / 1000 / 1000:.2f} MB\t\t\t\tn = {len(sources['seed'])}")
sz, n = sum(sources['remote_uncompressed']), len(sources['remote_uncompressed'])
print(f" Remote (uncompressed)\t\t{sz / 1000 / 1000:.2f} MB\t(avg {sz / 1000 / 1000 / n:4f} MB)\tn = {n}")
sz, n = sum(sources['remote_compressed']), len(sources['remote_compressed'])
print(f" Remote (compressed download)\t{sz / 1000 / 1000:.2f} MB\t(avg {sz / 1000 / 1000 / n:4f} MB)\tn = {n}")

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python3
import json
import os
import unittest
import requests
TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
MANIFEST = os.path.join(TEST_DIR, "../agnos.json")
class TestAgnosUpdater(unittest.TestCase):
def test_manifest(self):
with open(MANIFEST) as f:
m = json.load(f)
for img in m:
r = requests.head(img['url'], timeout=10)
r.raise_for_status()
self.assertEqual(r.headers['Content-Type'], "application/x-xz")
if not img['sparse']:
assert img['hash'] == img['hash_raw']
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
import time
import random
import unittest
import subprocess
from panda import Panda
from openpilot.system.hardware import TICI, HARDWARE
from openpilot.system.hardware.tici.hardware import Tici
from openpilot.system.hardware.tici.amplifier import Amplifier
class TestAmplifier(unittest.TestCase):
@classmethod
def setUpClass(cls):
if not TICI:
raise unittest.SkipTest
def setUp(self):
# clear dmesg
subprocess.check_call("sudo dmesg -C", shell=True)
HARDWARE.reset_internal_panda()
Panda.wait_for_panda(None, 30)
self.panda = Panda()
def tearDown(self):
HARDWARE.reset_internal_panda()
def _check_for_i2c_errors(self, expected):
dmesg = subprocess.check_output("dmesg", shell=True, encoding='utf8')
i2c_lines = [l for l in dmesg.strip().splitlines() if 'i2c_geni a88000.i2c' in l]
i2c_str = '\n'.join(i2c_lines)
if not expected:
return len(i2c_lines) == 0
else:
return "i2c error :-107" in i2c_str or "Bus arbitration lost" in i2c_str
def test_init(self):
amp = Amplifier(debug=True)
r = amp.initialize_configuration(Tici().get_device_type())
assert r
assert self._check_for_i2c_errors(False)
def test_shutdown(self):
amp = Amplifier(debug=True)
for _ in range(10):
r = amp.set_global_shutdown(True)
r = amp.set_global_shutdown(False)
# amp config should be successful, with no i2c errors
assert r
assert self._check_for_i2c_errors(False)
def test_init_while_siren_play(self):
for _ in range(10):
self.panda.set_siren(False)
time.sleep(0.1)
self.panda.set_siren(True)
time.sleep(random.randint(0, 5))
amp = Amplifier(debug=True)
r = amp.initialize_configuration(Tici().get_device_type())
assert r
if self._check_for_i2c_errors(True):
break
else:
self.fail("didn't hit any i2c errors")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,153 @@
#!/usr/bin/env python3
import os
import unittest
import tempfile
import subprocess
import openpilot.system.hardware.tici.casync as casync
# dd if=/dev/zero of=/tmp/img.raw bs=1M count=2
# sudo losetup -f /tmp/img.raw
# losetup -a | grep img.raw
LOOPBACK = os.environ.get('LOOPBACK', None)
class TestCasync(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.tmpdir = tempfile.TemporaryDirectory()
# Build example contents
chunk_a = [i % 256 for i in range(1024)] * 512
chunk_b = [(256 - i) % 256 for i in range(1024)] * 512
zeroes = [0] * (1024 * 128)
contents = chunk_a + chunk_b + zeroes + chunk_a
cls.contents = bytes(contents)
# Write to file
cls.orig_fn = os.path.join(cls.tmpdir.name, 'orig.bin')
with open(cls.orig_fn, 'wb') as f:
f.write(cls.contents)
# Create casync files
cls.manifest_fn = os.path.join(cls.tmpdir.name, 'orig.caibx')
cls.store_fn = os.path.join(cls.tmpdir.name, 'store')
subprocess.check_output(["casync", "make", "--compression=xz", "--store", cls.store_fn, cls.manifest_fn, cls.orig_fn])
target = casync.parse_caibx(cls.manifest_fn)
hashes = [c.sha.hex() for c in target]
# Ensure we have chunk reuse
assert len(hashes) > len(set(hashes))
def setUp(self):
# Clear target_lo
if LOOPBACK is not None:
self.target_lo = LOOPBACK
with open(self.target_lo, 'wb') as f:
f.write(b"0" * len(self.contents))
self.target_fn = os.path.join(self.tmpdir.name, next(tempfile._get_candidate_names()))
self.seed_fn = os.path.join(self.tmpdir.name, next(tempfile._get_candidate_names()))
def tearDown(self):
for fn in [self.target_fn, self.seed_fn]:
try:
os.unlink(fn)
except FileNotFoundError:
pass
def test_simple_extract(self):
target = casync.parse_caibx(self.manifest_fn)
sources = [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
stats = casync.extract(target, sources, self.target_fn)
with open(self.target_fn, 'rb') as target_f:
self.assertEqual(target_f.read(), self.contents)
self.assertEqual(stats['remote'], len(self.contents))
def test_seed(self):
target = casync.parse_caibx(self.manifest_fn)
# Populate seed with half of the target contents
with open(self.seed_fn, 'wb') as seed_f:
seed_f.write(self.contents[:len(self.contents) // 2])
sources = [('seed', casync.FileChunkReader(self.seed_fn), casync.build_chunk_dict(target))]
sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
stats = casync.extract(target, sources, self.target_fn)
with open(self.target_fn, 'rb') as target_f:
self.assertEqual(target_f.read(), self.contents)
self.assertGreater(stats['seed'], 0)
self.assertLess(stats['remote'], len(self.contents))
def test_already_done(self):
"""Test that an already flashed target doesn't download any chunks"""
target = casync.parse_caibx(self.manifest_fn)
with open(self.target_fn, 'wb') as f:
f.write(self.contents)
sources = [('target', casync.FileChunkReader(self.target_fn), casync.build_chunk_dict(target))]
sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
stats = casync.extract(target, sources, self.target_fn)
with open(self.target_fn, 'rb') as f:
self.assertEqual(f.read(), self.contents)
self.assertEqual(stats['target'], len(self.contents))
def test_chunk_reuse(self):
"""Test that chunks that are reused are only downloaded once"""
target = casync.parse_caibx(self.manifest_fn)
# Ensure target exists
with open(self.target_fn, 'wb'):
pass
sources = [('target', casync.FileChunkReader(self.target_fn), casync.build_chunk_dict(target))]
sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
stats = casync.extract(target, sources, self.target_fn)
with open(self.target_fn, 'rb') as f:
self.assertEqual(f.read(), self.contents)
self.assertLess(stats['remote'], len(self.contents))
@unittest.skipUnless(LOOPBACK, "requires loopback device")
def test_lo_simple_extract(self):
target = casync.parse_caibx(self.manifest_fn)
sources = [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
stats = casync.extract(target, sources, self.target_lo)
with open(self.target_lo, 'rb') as target_f:
self.assertEqual(target_f.read(len(self.contents)), self.contents)
self.assertEqual(stats['remote'], len(self.contents))
@unittest.skipUnless(LOOPBACK, "requires loopback device")
def test_lo_chunk_reuse(self):
"""Test that chunks that are reused are only downloaded once"""
target = casync.parse_caibx(self.manifest_fn)
sources = [('target', casync.FileChunkReader(self.target_lo), casync.build_chunk_dict(target))]
sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
stats = casync.extract(target, sources, self.target_lo)
with open(self.target_lo, 'rb') as f:
self.assertEqual(f.read(len(self.contents)), self.contents)
self.assertLess(stats['remote'], len(self.contents))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env python3
import pytest
import time
import unittest
import numpy as np
from openpilot.system.hardware.tici.hardware import Tici
HARDWARE = Tici()
@pytest.mark.tici
class TestHardware(unittest.TestCase):
def test_power_save_time(self):
ts = []
for _ in range(5):
for on in (True, False):
st = time.monotonic()
HARDWARE.set_power_save(on)
ts.append(time.monotonic() - st)
assert 0.1 < np.mean(ts) < 0.25
assert max(ts) < 0.3
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python3
from collections import defaultdict, deque
import pytest
import unittest
import time
import numpy as np
from dataclasses import dataclass
from tabulate import tabulate
import cereal.messaging as messaging
from cereal.services import SERVICE_LIST
from openpilot.common.mock import mock_messages
from openpilot.selfdrive.car.car_helpers import write_car_param
from openpilot.system.hardware.tici.power_monitor import get_power
from openpilot.selfdrive.manager.process_config import managed_processes
from openpilot.selfdrive.manager.manager import manager_cleanup
SAMPLE_TIME = 8 # seconds to sample power
MAX_WARMUP_TIME = 30 # seconds to wait for SAMPLE_TIME consecutive valid samples
@dataclass
class Proc:
procs: list[str]
power: float
msgs: list[str]
rtol: float = 0.05
atol: float = 0.12
@property
def name(self):
return '+'.join(self.procs)
PROCS = [
Proc(['camerad'], 2.1, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']),
Proc(['modeld'], 1.12, atol=0.2, msgs=['modelV2']),
Proc(['dmonitoringmodeld'], 0.4, msgs=['driverStateV2']),
Proc(['encoderd'], 0.23, msgs=[]),
Proc(['mapsd', 'navmodeld'], 0.05, msgs=['mapRenderState', 'navModel']),
]
@pytest.mark.tici
class TestPowerDraw(unittest.TestCase):
def setUp(self):
write_car_param()
# wait a bit for power save to disable
time.sleep(5)
def tearDown(self):
manager_cleanup()
def get_expected_messages(self, proc):
return int(sum(SAMPLE_TIME * SERVICE_LIST[msg].frequency for msg in proc.msgs))
def valid_msg_count(self, proc, msg_counts):
msgs_received = sum(msg_counts[msg] for msg in proc.msgs)
msgs_expected = self.get_expected_messages(proc)
return np.core.numeric.isclose(msgs_expected, msgs_received, rtol=.02, atol=2)
def valid_power_draw(self, proc, used):
return np.core.numeric.isclose(used, proc.power, rtol=proc.rtol, atol=proc.atol)
def tabulate_msg_counts(self, msgs_and_power):
msg_counts = defaultdict(int)
for _, counts in msgs_and_power:
for msg, count in counts.items():
msg_counts[msg] += count
return msg_counts
def get_power_with_warmup_for_target(self, proc, prev):
socks = {msg: messaging.sub_sock(msg) for msg in proc.msgs}
for sock in socks.values():
messaging.drain_sock_raw(sock)
msgs_and_power = deque([], maxlen=SAMPLE_TIME)
start_time = time.monotonic()
while (time.monotonic() - start_time) < MAX_WARMUP_TIME:
power = get_power(1)
iteration_msg_counts = {}
for msg,sock in socks.items():
iteration_msg_counts[msg] = len(messaging.drain_sock_raw(sock))
msgs_and_power.append((power, iteration_msg_counts))
if len(msgs_and_power) < SAMPLE_TIME:
continue
msg_counts = self.tabulate_msg_counts(msgs_and_power)
now = np.mean([m[0] for m in msgs_and_power])
if self.valid_msg_count(proc, msg_counts) and self.valid_power_draw(proc, now - prev):
break
return now, msg_counts, time.monotonic() - start_time - SAMPLE_TIME
@mock_messages(['liveLocationKalman'])
def test_camera_procs(self):
baseline = get_power()
prev = baseline
used = {}
warmup_time = {}
msg_counts = {}
for proc in PROCS:
for p in proc.procs:
managed_processes[p].start()
now, local_msg_counts, warmup_time[proc.name] = self.get_power_with_warmup_for_target(proc, prev)
msg_counts.update(local_msg_counts)
used[proc.name] = now - prev
prev = now
manager_cleanup()
tab = [['process', 'expected (W)', 'measured (W)', '# msgs expected', '# msgs received', "warmup time (s)"]]
for proc in PROCS:
cur = used[proc.name]
expected = proc.power
msgs_received = sum(msg_counts[msg] for msg in proc.msgs)
tab.append([proc.name, round(expected, 2), round(cur, 2), self.get_expected_messages(proc), msgs_received, round(warmup_time[proc.name], 2)])
with self.subTest(proc=proc.name):
self.assertTrue(self.valid_msg_count(proc, msg_counts), f"expected {self.get_expected_messages(proc)} msgs, got {msgs_received} msgs")
self.assertTrue(self.valid_power_draw(proc, cur), f"expected {expected:.2f}W, got {cur:.2f}W")
print(tabulate(tab))
print(f"Baseline {baseline:.2f}W\n")
if __name__ == "__main__":
unittest.main()