merge selfdrive/debug/ into tools/scripts/ (#38195)

* merge selfdrive/debug/ into tools/scripts/

* rm unused

* single timings script

* lil more

* profiling is scripts quality
This commit is contained in:
Adeeb Shihadeh
2026-06-20 13:52:16 -07:00
committed by GitHub
parent f257544f1b
commit 62b97fabf7
67 changed files with 106 additions and 589 deletions
-3
View File
@@ -12,9 +12,6 @@ collect_ignore = [
"selfdrive/test/process_replay/test_processes.py",
"selfdrive/test/process_replay/test_regen.py",
]
collect_ignore_glob = [
"selfdrive/debug/*.py",
]
def pytest_sessionstart(session):
-10
View File
@@ -1,9 +1,5 @@
import os
from openpilot.common.basedir import BASEDIR
from opendbc.car.docs import generate_cars_md, get_all_car_docs
from openpilot.selfdrive.debug.dump_car_docs import dump_car_docs
from openpilot.selfdrive.debug.print_docs_diff import print_car_docs_diff
from openpilot.selfdrive.car.docs import CARS_MD_TEMPLATE
@@ -14,9 +10,3 @@ class TestCarDocs:
def test_generator(self):
generate_cars_md(self.all_cars, CARS_MD_TEMPLATE)
def test_docs_diff(self):
dump_path = os.path.join(BASEDIR, "selfdrive", "car", "tests", "cars_dump")
dump_car_docs(dump_path)
print_car_docs_diff(dump_path)
os.remove(dump_path)
-59
View File
@@ -1,59 +0,0 @@
# debug scripts
## [can_printer.py](can_printer.py)
```
usage: can_printer.py [-h] [--bus BUS] [--max_msg MAX_MSG] [--addr ADDR]
simple CAN data viewer
optional arguments:
-h, --help show this help message and exit
--bus BUS CAN bus to print out (default: 0)
--max_msg MAX_MSG max addr (default: None)
--addr ADDR
```
## [dump.py](dump.py)
```
usage: dump.py [-h] [--pipe] [--raw] [--json] [--dump-json] [--no-print] [--addr ADDR] [--values VALUES] [socket [socket ...]]
Dump communication sockets. See cereal/services.py for a complete list of available sockets.
positional arguments:
socket socket names to dump. defaults to all services defined in cereal
optional arguments:
-h, --help show this help message and exit
--pipe
--raw
--json
--dump-json
--no-print
--addr ADDR
--values VALUES values to monitor (instead of entire event)
```
## [vw_mqb_config.py](vw_mqb_config.py)
```
usage: vw_mqb_config.py [-h] [--debug] {enable,show,disable}
Shows Volkswagen EPS software and coding info, and enables or disables Heading Control
Assist (Lane Assist). Useful for enabling HCA on cars without factory Lane Assist that want
to use openpilot integrated at the CAN gateway (J533).
positional arguments:
{enable,show,disable}
show or modify current EPS HCA config
optional arguments:
-h, --help show this help message and exit
--debug enable ISO-TP/UDS stack debugging output
This tool is meant to run directly on a vehicle-installed comma three, with
the openpilot/tmux processes stopped. It should also work on a separate PC with a USB-
attached comma panda. Vehicle ignition must be on. Recommend engine not be running when
making changes. Must turn ignition off and on again for any changes to take effect.
```
-82
View File
@@ -1,82 +0,0 @@
#!/usr/bin/env python3
import argparse
from tqdm import tqdm
from cereal.services import SERVICE_LIST, QueueSize
from openpilot.tools.lib.logreader import LogReader
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Analyze message sizes from a log route")
parser.add_argument("route", nargs="?", default="98395b7c5b27882e/000000a8--f87e7cd255",
help="Log route to analyze (default: 98395b7c5b27882e/000000a8--f87e7cd255)")
args = parser.parse_args()
lr = LogReader(args.route)
szs = {}
for msg in tqdm(lr):
sz = len(msg.as_builder().to_bytes())
msg_type = msg.which()
if msg_type not in szs:
szs[msg_type] = {'min': sz, 'max': sz, 'sum': sz, 'count': 1}
else:
szs[msg_type]['min'] = min(szs[msg_type]['min'], sz)
szs[msg_type]['max'] = max(szs[msg_type]['max'], sz)
szs[msg_type]['sum'] += sz
szs[msg_type]['count'] += 1
print()
print(f"{'Service':<36} {'Min (KB)':>12} {'Max (KB)':>12} {'Avg (KB)':>12} {'KB/min':>12} {'KB/sec':>12} {'Minutes in 10MB':>18} {'Seconds in Queue':>18}")
print("-" * 132)
def sort_key(x):
k, v = x
avg = v['sum'] / v['count']
freq = SERVICE_LIST.get(k, None)
freq_val = freq.frequency if freq else 0.0
kb_per_min = (avg * freq_val * 60) / 1024 if freq_val > 0 else 0.0
return kb_per_min
total_kb_per_min = 0.0
RINGBUFFER_SIZE_KB = 10 * 1024 # 10MB old default
for k, v in sorted(szs.items(), key=sort_key, reverse=True):
avg = v['sum'] / v['count']
service = SERVICE_LIST.get(k, None)
freq_val = service.frequency if service else 0.0
queue_size_kb = (service.queue_size / 1024) if service else 250 # default to SMALL
kb_per_min = (avg * freq_val * 60) / 1024 if freq_val > 0 else 0.0
kb_per_sec = kb_per_min / 60
minutes_in_buffer = RINGBUFFER_SIZE_KB / kb_per_min if kb_per_min > 0 else float('inf')
seconds_in_queue = (queue_size_kb / kb_per_sec) if kb_per_sec > 0 else float('inf')
total_kb_per_min += kb_per_min
min_str = f"{minutes_in_buffer:.2f}" if minutes_in_buffer != float('inf') else "inf"
sec_queue_str = f"{seconds_in_queue:.2f}" if seconds_in_queue != float('inf') else "inf"
print(f"{k:<36} {v['min']/1024:>12.2f} {v['max']/1024:>12.2f} {avg/1024:>12.2f} {kb_per_min:>12.2f} {kb_per_sec:>12.2f} {min_str:>18} {sec_queue_str:>18}")
# Summary section
print()
print(f"Total usage: {total_kb_per_min / 1024:.2f} MB/min")
# Calculate memory usage: old (10MB for all) vs new (from services.py)
OLD_SIZE = 10 * 1024 * 1024 # 10MB was the old default
old_total = len(SERVICE_LIST) * OLD_SIZE
new_total = sum(s.queue_size for s in SERVICE_LIST.values())
# Count by queue size
size_counts = {QueueSize.BIG: 0, QueueSize.MEDIUM: 0, QueueSize.SMALL: 0}
for s in SERVICE_LIST.values():
size_counts[s.queue_size] += 1
savings_pct = (1 - new_total / old_total) * 100
print()
print(f"{'Queue Size Comparison':<40}")
print("-" * 60)
print(f"{'Old (10MB default):':<30} {old_total / 1024 / 1024:>10.2f} MB")
print(f"{'New (from services.py):':<30} {new_total / 1024 / 1024:>10.2f} MB")
print(f"{'Savings:':<30} {savings_pct:>10.1f}%")
print()
print(f"{'Breakdown:':<30}")
print(f" BIG (10MB): {size_counts[QueueSize.BIG]:>3} services")
print(f" MEDIUM (2MB): {size_counts[QueueSize.MEDIUM]:>3} services")
print(f" SMALL (250KB): {size_counts[QueueSize.SMALL]:>3} services")
@@ -1,36 +0,0 @@
#!/usr/bin/env python3
import numpy as np
import time
from tqdm import tqdm
from cereal import car
from opendbc.car.tests.routes import CarTestRoute
from openpilot.selfdrive.car.tests.test_models import TestCarModelBase
from openpilot.tools.plotjuggler.juggle import DEMO_ROUTE
N_RUNS = 10
class CarModelTestCase(TestCarModelBase):
test_route = CarTestRoute(DEMO_ROUTE, None)
if __name__ == '__main__':
# Get CAN messages and parsers
tm = CarModelTestCase()
tm.setUpClass()
tm.setUp()
CC = car.CarControl.new_message()
ets = []
for _ in tqdm(range(N_RUNS)):
start_t = time.process_time_ns()
for msg in tm.can_msgs:
for cp in tm.CI.can_parsers.values():
if cp is not None:
cp.update_strings(msg)
ets.append((time.process_time_ns() - start_t) * 1e-6)
print(f'{len(tm.can_msgs)} CAN packets, {N_RUNS} runs')
print(f'{np.mean(ets):.2f} mean ms, {max(ets):.2f} max ms, {min(ets):.2f} min ms, {np.std(ets):.2f} std ms')
print(f'{np.mean(ets) / len(tm.can_msgs):.4f} mean ms / CAN packet')
-50
View File
@@ -1,50 +0,0 @@
#!/usr/bin/env python3
import argparse
import numpy as np
import time
from collections import defaultdict, deque
from collections.abc import MutableSequence
import cereal.messaging as messaging
if __name__ == "__main__":
context = messaging.Context()
poller = messaging.Poller()
parser = argparse.ArgumentParser()
parser.add_argument("socket", type=str, nargs='*', help="socket name")
args = parser.parse_args()
socket_names = args.socket
sockets = {}
rcv_times: defaultdict[str, MutableSequence[float]] = defaultdict(lambda: deque(maxlen=100))
valids: defaultdict[str, deque[bool]] = defaultdict(lambda: deque(maxlen=100))
t = time.monotonic()
for name in socket_names:
sock = messaging.sub_sock(name, poller=poller)
sockets[sock] = name
prev_print = t
while True:
for socket in poller.poll(100):
msg = messaging.recv_one(socket)
if msg is None:
continue
name = msg.which()
t = time.monotonic()
rcv_times[name].append(msg.logMonoTime / 1e9)
valids[name].append(msg.valid)
if t - prev_print > 1:
print()
for name in socket_names:
dts = np.diff(rcv_times[name])
mean = np.mean(dts)
print(f"{name}: Freq {1.0 / mean:.2f} Hz, Min {np.min(dts) / mean * 100:.2f}%, Max {np.max(dts) / mean * 100:.2f}%, valid ", all(valids[name]))
prev_print = t
-27
View File
@@ -1,27 +0,0 @@
#!/usr/bin/env python3
import cereal.messaging as messaging
from cereal.services import SERVICE_LIST
TO_CHECK = ['carState']
if __name__ == "__main__":
sm = messaging.SubMaster(TO_CHECK)
prev_t: dict[str, float] = {}
while True:
sm.update()
for s in TO_CHECK:
if sm.updated[s]:
t = sm.logMonoTime[s] / 1e9
if s in prev_t:
expected = 1.0 / (SERVICE_LIST[s].frequency)
dt = t - prev_t[s]
if dt > 10 * expected:
print(t, s, dt)
prev_t[s] = t
-36
View File
@@ -1,36 +0,0 @@
#!/usr/bin/env python3
import sys
import time
import numpy as np
import datetime
from collections.abc import MutableSequence
from collections import defaultdict
import cereal.messaging as messaging
if __name__ == "__main__":
ts: defaultdict[str, MutableSequence[float]] = defaultdict(list)
socks = {s: messaging.sub_sock(s, conflate=False) for s in sys.argv[1:]}
try:
st = time.monotonic()
while True:
print()
for s, sock in socks.items():
msgs = messaging.drain_sock(sock)
for m in msgs:
ts[s].append(m.logMonoTime / 1e6)
if len(ts[s]) > 2:
d = np.diff(ts[s])[-100:]
print(f"{s:25} {np.mean(d):7.2f} {np.std(d):7.2f} {np.max(d):7.2f} {np.min(d):7.2f}")
time.sleep(1)
except KeyboardInterrupt:
print("\n")
print("="*5, "timing summary", "="*5)
for s, sock in socks.items():
msgs = messaging.drain_sock(sock)
if len(ts[s]) > 2:
d = np.diff(ts[s])
print(f"{s:25} {np.mean(d):7.2f} {np.std(d):7.2f} {np.max(d):7.2f} {np.min(d):7.2f}")
print("="*5, datetime.timedelta(seconds=time.monotonic()-st), "="*5)
-18
View File
@@ -1,18 +0,0 @@
#!/usr/bin/env python3
import argparse
import pickle
from opendbc.car.docs import get_all_car_docs
def dump_car_docs(path):
with open(path, 'wb') as f:
pickle.dump(get_all_car_docs(), f)
print(f'Dumping car info to {path}')
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--path", required=True)
args = parser.parse_args()
dump_car_docs(args.path)
-120
View File
@@ -1,120 +0,0 @@
#!/usr/bin/env python3
import argparse
from collections import defaultdict
import difflib
import pickle
from opendbc.car.docs import get_all_car_docs
from opendbc.car.docs_definitions import Column
FOOTNOTE_TAG = "<sup>{}</sup>"
STAR_ICON = '<a href="##"><img valign="top" ' + \
'src="https://media.githubusercontent.com/media/commaai/openpilot/master/docs/assets/icon-star-{}.svg" width="22" /></a>'
VIDEO_ICON = '<a href="{}" target="_blank">' + \
'<img height="18px" src="https://media.githubusercontent.com/media/commaai/openpilot/master/docs/assets/icon-youtube.svg" /></a>'
COLUMNS = "|" + "|".join([column.value for column in Column]) + "|"
COLUMN_HEADER = "|---|---|---|{}|".format("|".join([":---:"] * (len(Column) - 3)))
ARROW_SYMBOL = "➡️"
def load_base_car_docs(path):
with open(path, "rb") as f:
return pickle.load(f)
def match_cars(base_cars, new_cars):
changes = []
additions = []
for new in new_cars:
# Addition if no close matches or close match already used
# Change if close match and not already used
matches = difflib.get_close_matches(new.name, [b.name for b in base_cars], cutoff=0.)
if not len(matches) or matches[0] in [c[1].name for c in changes]:
additions.append(new)
else:
changes.append((new, next(car for car in base_cars if car.name == matches[0])))
# Removal if base car not in changes
removals = [b for b in base_cars if b.name not in [c[1].name for c in changes]]
return changes, additions, removals
def build_column_diff(base_car, new_car):
row_builder = []
for column in Column:
base_column = base_car.get_column(column, STAR_ICON, VIDEO_ICON, FOOTNOTE_TAG)
new_column = new_car.get_column(column, STAR_ICON, VIDEO_ICON, FOOTNOTE_TAG)
if base_column != new_column:
row_builder.append(f"{base_column} {ARROW_SYMBOL} {new_column}")
else:
row_builder.append(new_column)
return format_row(row_builder)
def format_row(builder):
return "|" + "|".join(builder) + "|"
def print_car_docs_diff(path):
base_car_docs = defaultdict(list)
new_car_docs = defaultdict(list)
for car in load_base_car_docs(path):
base_car_docs[car.car_fingerprint].append(car)
for car in get_all_car_docs():
new_car_docs[car.car_fingerprint].append(car)
# Add new platforms to base cars so we can detect additions and removals in one pass
base_car_docs.update({car: [] for car in new_car_docs if car not in base_car_docs})
changes = defaultdict(list)
for base_car_model, base_cars in base_car_docs.items():
# Match car info changes, and get additions and removals
new_cars = new_car_docs[base_car_model]
car_changes, car_additions, car_removals = match_cars(base_cars, new_cars)
# Removals
for car_docs in car_removals:
changes["removals"].append(format_row([car_docs.get_column(column, STAR_ICON, VIDEO_ICON, FOOTNOTE_TAG) for column in Column]))
# Additions
for car_docs in car_additions:
changes["additions"].append(format_row([car_docs.get_column(column, STAR_ICON, VIDEO_ICON, FOOTNOTE_TAG) for column in Column]))
for new_car, base_car in car_changes:
# Column changes
row_diff = build_column_diff(base_car, new_car)
if ARROW_SYMBOL in row_diff:
changes["column"].append(row_diff)
# Detail sentence changes
if base_car.detail_sentence != new_car.detail_sentence:
changes["detail"].append(f"- Sentence for {base_car.name} changed!\n" +
" ```diff\n" +
f" - {base_car.detail_sentence}\n" +
f" + {new_car.detail_sentence}\n" +
" ```")
# Print diff
if any(len(c) for c in changes.values()):
markdown_builder = ["### ⚠️ This PR makes changes to [CARS.md](../blob/master/docs/CARS.md) ⚠️"]
for title, category in (("## 🔀 Column Changes", "column"), ("## ❌ Removed", "removals"),
("## Added", "additions"), ("## 📖 Detail Sentence Changes", "detail")):
if len(changes[category]):
markdown_builder.append(title)
if category not in ("detail",):
markdown_builder.append(COLUMNS)
markdown_builder.append(COLUMN_HEADER)
markdown_builder.extend(changes[category])
print("\n".join(markdown_builder))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--path", required=True)
args = parser.parse_args()
print_car_docs_diff(args.path)
-54
View File
@@ -1,54 +0,0 @@
#!/usr/bin/env python3
import argparse
import numpy as np
import matplotlib.pyplot as plt
from openpilot.tools.lib.logreader import LogReader
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--width', default=2160, type=int)
parser.add_argument('--height', default=1080, type=int)
parser.add_argument('--route', default='rlog', type=str)
args = parser.parse_args()
w = args.width
h = args.height
route = args.route
fingers = [[-1, -1]] * 5
touch_points = []
current_slot = 0
lr = list(LogReader(route))
for msg in lr:
if msg.which() == 'touch':
for event in msg.touch:
if event.type == 3 and event.code == 47:
current_slot = event.value
elif event.type == 3 and event.code == 57 and event.value == -1:
fingers[current_slot] = [-1, -1]
elif event.type == 3 and event.code == 53:
fingers[current_slot][1] = event.value
if fingers[current_slot][0] != -1:
touch_points.append(fingers[current_slot].copy())
elif event.type == 3 and event.code == 54:
fingers[current_slot][0] = w - event.value
if fingers[current_slot][1] != -1:
touch_points.append(fingers[current_slot].copy())
if not touch_points:
print(f'No touch events found for {route}')
quit()
unique_points, counts = np.unique(touch_points, axis=0, return_counts=True)
plt.figure(figsize=(10, 3))
plt.scatter(unique_points[:, 0], unique_points[:, 1], c=counts, s=counts * 20, edgecolors='red')
plt.colorbar()
plt.title(f'Touches for {route}')
plt.xlim(0, w)
plt.ylim(0, h)
plt.grid(True)
plt.show()
+1 -1
View File
@@ -284,7 +284,7 @@ class TestOnroad:
print("--------------- Memory Usage -------------------")
print("------------------------------------------------")
from openpilot.selfdrive.debug.mem_usage import print_report
from openpilot.tools.scripts.mem_usage import print_report
print_report(self.msgs['procLog'], self.msgs['deviceState'])
offset = int(SERVICE_LIST['deviceState'].frequency * LOG_OFFSET)
+1 -1
View File
@@ -6,7 +6,7 @@ Welcome to the first part of the comma CTF!
* everything you'll need to find the flags is in the openpilot repo
* grep is also your friend
* first, [setup](https://github.com/commaai/openpilot/tree/master/tools#setup-your-pc) your PC
* read the docs & checkout out the tools in tools/ and selfdrive/debug/
* read the docs & checkout out the tools in tools/
* tip: once you get the replay and UI up, start by familiarizing yourself with seeking in replay
getting started
@@ -5,7 +5,7 @@ import time
from collections import defaultdict
import cereal.messaging as messaging
from openpilot.selfdrive.debug.can_table import can_table
from openpilot.tools.scripts.can_table import can_table
from openpilot.tools.lib.logreader import LogIterable, LogReader
RED = '\033[91m'
@@ -8,7 +8,7 @@ System tools like top/htop can only show current cpu usage values, so I write th
Calculate minumium/maximum/accumulated_average cpu usage as long term inspections.
Monitor multiple processes simuteneously.
Sample usage:
root@localhost:/data/openpilot$ python selfdrive/debug/cpu_usage_stat.py pandad,ubloxd
root@localhost:/data/openpilot$ python tools/scripts/cpu_usage_stat.py pandad,ubloxd
('Add monitored proc:', './pandad')
('Add monitored proc:', 'python locationd/ubloxd.py')
pandad: 1.96%, min: 1.96%, max: 1.96%, acc: 1.96%
-43
View File
@@ -1,43 +0,0 @@
#!/usr/bin/env python3
import sys
if len(sys.argv) < 4:
print(f"{sys.argv[0]} <route> <segment> <frame number> [front|wide|driver]")
print('example: ./fetch_image_from_route.py "02c45f73a2e5c6e9|2020-06-01--18-03-08" 3 500 driver')
exit(0)
cameras = {
"front": "cameras",
"wide": "ecameras",
"driver": "dcameras"
}
import requests
from PIL import Image
from openpilot.tools.lib.auth_config import get_token
from openpilot.tools.lib.framereader import FrameReader
jwt = get_token()
route = sys.argv[1]
segment = int(sys.argv[2])
frame = int(sys.argv[3])
camera = cameras[sys.argv[4]] if len(sys.argv) > 4 and sys.argv[4] in cameras else "cameras"
url = f'https://api.commadotai.com/v1/route/{route}/files'
r = requests.get(url, headers={"Authorization": f"JWT {jwt}"}, timeout=10)
assert r.status_code == 200
print("got api response")
segments = r.json()[camera]
if segment >= len(segments):
raise Exception(f"segment {segment} not found, got {len(segments)} segments")
fr = FrameReader(segments[segment])
if frame >= fr.frame_count:
raise Exception(f"frame {frame} not found, got {fr.frame_count} frames")
im = Image.fromarray(fr.get(frame))
fn = f"uxxx_{route.replace('|', '_')}_{segment}_{frame}.png"
im.save(fn)
print(f"saved {fn}")
-47
View File
@@ -1,47 +0,0 @@
#!/usr/bin/env python3
import argparse
import os
import sys
from openpilot.common.basedir import BASEDIR
from openpilot.tools.lib.logreader import LogReader
os.environ['BASEDIR'] = BASEDIR
def get_arg_parser():
parser = argparse.ArgumentParser(
description="Unlogging and save to file",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("route", type=(lambda x: x.replace("#", "|")), nargs="?",
help="The route whose messages will be published.")
parser.add_argument("--out_path", nargs='?', default='/data/ubloxRaw.stream',
help="Output pickle file path")
return parser
def main():
args = get_arg_parser().parse_args(sys.argv[1:])
lr = LogReader(args.route)
with open(args.out_path, 'wb') as f:
try:
done = False
i = 0
while not done:
msg = next(lr)
if not msg:
break
smsg = msg.as_builder()
typ = smsg.which()
if typ == 'ubloxRaw':
f.write(smsg.to_bytes())
i += 1
except StopIteration:
print('All done')
print(f'Writed {i} msgs')
if __name__ == "__main__":
main()
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
import argparse
import datetime
import time
from collections import deque
from dataclasses import dataclass, field
import numpy as np
import cereal.messaging as messaging
from cereal.services import SERVICE_LIST
@dataclass
class ServiceTiming:
times: list[float] = field(default_factory=list)
window: deque[float] = field(default_factory=lambda: deque(maxlen=100))
valids: deque[bool] = field(default_factory=lambda: deque(maxlen=100))
lag_events: list[tuple[float, float]] = field(default_factory=list)
def add(self, mono_time: float, valid: bool, expected_interval: float | None, lag_threshold: float) -> None:
if self.times:
dt = mono_time - self.times[-1]
self.window.append(dt)
if expected_interval is not None and dt > lag_threshold * expected_interval:
self.lag_events.append((mono_time, dt))
self.times.append(mono_time)
self.valids.append(valid)
def intervals(self, latest_only: bool) -> np.ndarray:
if latest_only:
return np.array(self.window)
return np.diff(self.times)
def format_row(name: str, timing: ServiceTiming, latest_only: bool) -> str:
dts = timing.intervals(latest_only)
if len(dts) == 0:
return f"{name:25} waiting for messages"
mean = np.mean(dts)
hz = 1.0 / mean if mean > 0 else 0.0
valid = all(timing.valids) if timing.valids else False
return f"{name:25} {hz:8.2f}Hz {mean * 1e3:8.2f}ms {np.std(dts) * 1e3:8.2f}ms {np.max(dts) * 1e3:8.2f}ms {np.min(dts) * 1e3:8.2f}ms valid={valid}"
def print_lag_events(name: str, timing: ServiceTiming, printed_lags: dict[str, int]) -> None:
start = printed_lags.get(name, 0)
for mono_time, dt in timing.lag_events[start:]:
print(f"{mono_time:.3f} {name} lag {dt:.3f}s", flush=True)
printed_lags[name] = len(timing.lag_events)
def monitor_services(socket_names: list[str], print_interval: float, lag_threshold: float, lag_only: bool) -> None:
sockets = {name: messaging.sub_sock(name, conflate=False) for name in socket_names}
timings = {name: ServiceTiming() for name in socket_names}
printed_lags: dict[str, int] = {}
start_time = time.monotonic()
last_print = start_time
try:
while True:
for name, sock in sockets.items():
for msg in messaging.drain_sock(sock):
expected_interval = 1.0 / SERVICE_LIST[name].frequency if name in SERVICE_LIST else None
timings[name].add(msg.logMonoTime / 1e9, msg.valid, expected_interval, lag_threshold)
now = time.monotonic()
if now - last_print < print_interval:
time.sleep(0.01)
continue
if not lag_only:
print(flush=True)
print(f"{'service':25} {'freq':>10} {'mean':>10} {'std':>10} {'max':>10} {'min':>10} valid", flush=True)
for name in socket_names:
print(format_row(name, timings[name], latest_only=True), flush=True)
for name in socket_names:
print_lag_events(name, timings[name], printed_lags)
last_print = now
except KeyboardInterrupt:
print("\n", flush=True)
print("=" * 5, "timing summary", "=" * 5, flush=True)
print(f"{'service':25} {'freq':>10} {'mean':>10} {'std':>10} {'max':>10} {'min':>10} valid", flush=True)
for name in socket_names:
print(format_row(name, timings[name], latest_only=False), flush=True)
print("=" * 5, datetime.timedelta(seconds=time.monotonic() - start_time), "=" * 5, flush=True)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Check live service timing, frequency, validity, and lag")
parser.add_argument("socket", nargs="*", default=["carState"], help="service/socket name")
parser.add_argument("--lag-threshold", type=float, default=10.0, help="report intervals above this multiple of the expected service interval")
parser.add_argument("--lag-only", action="store_true", help="only print lag events")
parser.add_argument("--print-interval", type=float, default=1.0, help="seconds between table updates")
args = parser.parse_args()
monitor_services(args.socket, args.print_interval, args.lag_threshold, args.lag_only)