|  |  |  | #!/usr/bin/env python3
 | 
					
						
							|  |  |  | import argparse
 | 
					
						
							|  |  |  | import json
 | 
					
						
							|  |  |  | import matplotlib.patches as mpatches
 | 
					
						
							|  |  |  | import matplotlib.pyplot as plt
 | 
					
						
							|  |  |  | import mpld3
 | 
					
						
							|  |  |  | import sys
 | 
					
						
							|  |  |  | from bisect import bisect_left, bisect_right
 | 
					
						
							|  |  |  | from collections import defaultdict
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from tools.lib.logreader import logreader_from_route_or_segment
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | DEMO_ROUTE = "9f583b1d93915c31|2022-05-18--10-49-51--0"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | SERVICES = ['camerad', 'modeld', 'plannerd', 'controlsd', 'boardd']
 | 
					
						
							|  |  |  | # Retrieve controlsd frameId from lateralPlan, mismatch with longitudinalPlan will be ignored
 | 
					
						
							|  |  |  | MONOTIME_KEYS = ['modelMonoTime', 'lateralPlanMonoTime']
 | 
					
						
							|  |  |  | MSGQ_TO_SERVICE = {
 | 
					
						
							|  |  |  |   'roadCameraState': 'camerad',
 | 
					
						
							|  |  |  |   'wideRoadCameraState': 'camerad',
 | 
					
						
							|  |  |  |   'modelV2': 'modeld',
 | 
					
						
							|  |  |  |   'lateralPlan': 'plannerd',
 | 
					
						
							|  |  |  |   'longitudinalPlan': 'plannerd',
 | 
					
						
							|  |  |  |   'sendcan': 'controlsd',
 | 
					
						
							|  |  |  |   'controlsState': 'controlsd'
 | 
					
						
							|  |  |  | }
 | 
					
						
							|  |  |  | SERVICE_TO_DURATIONS = {
 | 
					
						
							|  |  |  |   'camerad': ['processingTime'],
 | 
					
						
							|  |  |  |   'modeld': ['modelExecutionTime', 'gpuExecutionTime'],
 | 
					
						
							|  |  |  |   'plannerd': ['solverExecutionTime'],
 | 
					
						
							|  |  |  | }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def read_logs(lr):
 | 
					
						
							|  |  |  |   data = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
 | 
					
						
							|  |  |  |   mono_to_frame = {}
 | 
					
						
							|  |  |  |   frame_mismatches = []
 | 
					
						
							|  |  |  |   frame_id_fails = 0
 | 
					
						
							|  |  |  |   latest_sendcan_monotime = 0
 | 
					
						
							|  |  |  |   for msg in lr:
 | 
					
						
							|  |  |  |     if msg.which() == 'sendcan':
 | 
					
						
							|  |  |  |       latest_sendcan_monotime = msg.logMonoTime
 | 
					
						
							|  |  |  |       continue
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if msg.which() in MSGQ_TO_SERVICE:
 | 
					
						
							|  |  |  |       service = MSGQ_TO_SERVICE[msg.which()]
 | 
					
						
							|  |  |  |       msg_obj = getattr(msg, msg.which())
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       frame_id = -1
 | 
					
						
							|  |  |  |       if hasattr(msg_obj, "frameId"):
 | 
					
						
							|  |  |  |         frame_id = msg_obj.frameId
 | 
					
						
							|  |  |  |       else:
 | 
					
						
							|  |  |  |         continue_outer = False
 | 
					
						
							|  |  |  |         for key in MONOTIME_KEYS:
 | 
					
						
							|  |  |  |           if hasattr(msg_obj, key):
 | 
					
						
							|  |  |  |             if getattr(msg_obj, key) == 0:
 | 
					
						
							|  |  |  |               # Filter out controlsd messages which arrive before the camera loop
 | 
					
						
							|  |  |  |               continue_outer = True
 | 
					
						
							|  |  |  |             elif getattr(msg_obj, key) in mono_to_frame:
 | 
					
						
							|  |  |  |               frame_id = mono_to_frame[getattr(msg_obj, key)]
 | 
					
						
							|  |  |  |         if continue_outer:
 | 
					
						
							|  |  |  |           continue
 | 
					
						
							|  |  |  |       if frame_id == -1:
 | 
					
						
							|  |  |  |         frame_id_fails += 1
 | 
					
						
							|  |  |  |         continue
 | 
					
						
							|  |  |  |       mono_to_frame[msg.logMonoTime] = frame_id
 | 
					
						
							|  |  |  |       data['timestamp'][frame_id][service].append((msg.which()+" published", msg.logMonoTime))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       next_service = SERVICES[SERVICES.index(service)+1]
 | 
					
						
							|  |  |  |       if not data['start'][frame_id][next_service]:
 | 
					
						
							|  |  |  |         data['start'][frame_id][next_service] = msg.logMonoTime
 | 
					
						
							|  |  |  |       data['end'][frame_id][service] = msg.logMonoTime
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if service in SERVICE_TO_DURATIONS:
 | 
					
						
							|  |  |  |         for duration in SERVICE_TO_DURATIONS[service]:
 | 
					
						
							|  |  |  |           data['duration'][frame_id][service].append((msg.which()+"."+duration, getattr(msg_obj, duration)))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if service == SERVICES[0]:
 | 
					
						
							|  |  |  |         data['timestamp'][frame_id][service].append((msg.which()+" start of frame", msg_obj.timestampSof))
 | 
					
						
							|  |  |  |         if not data['start'][frame_id][service]:
 | 
					
						
							|  |  |  |           data['start'][frame_id][service] = msg_obj.timestampSof
 | 
					
						
							|  |  |  |       elif msg.which() == 'controlsState':
 | 
					
						
							|  |  |  |         # Sendcan is published before controlsState, but the frameId is retrieved in CS
 | 
					
						
							|  |  |  |         data['timestamp'][frame_id][service].append(("sendcan published", latest_sendcan_monotime))
 | 
					
						
							|  |  |  |       elif msg.which() == 'modelV2':
 | 
					
						
							|  |  |  |         if msg_obj.frameIdExtra != frame_id:
 | 
					
						
							|  |  |  |           frame_mismatches.append(frame_id)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if frame_id_fails > 20:
 | 
					
						
							|  |  |  |     print("Warning, many frameId fetch fails", frame_id_fails)
 | 
					
						
							|  |  |  |   if len(frame_mismatches) > 20:
 | 
					
						
							|  |  |  |     print("Warning, many frame mismatches", len(frame_mismatches))
 | 
					
						
							|  |  |  |   return (data, frame_mismatches)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # This is not needed in 3.10 as a "key" parameter is added to bisect
 | 
					
						
							|  |  |  | class KeyifyList(object):
 | 
					
						
							|  |  |  |   def __init__(self, inner, key):
 | 
					
						
							|  |  |  |     self.inner = inner
 | 
					
						
							|  |  |  |     self.key = key
 | 
					
						
							|  |  |  |   def __len__(self):
 | 
					
						
							|  |  |  |     return len(self.inner)
 | 
					
						
							|  |  |  |   def __getitem__(self, k):
 | 
					
						
							|  |  |  |     return self.key(self.inner[k])
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def find_frame_id(time, service, start_times, end_times):
 | 
					
						
							|  |  |  |   left = bisect_left(KeyifyList(list(start_times.items()),
 | 
					
						
							|  |  |  |                      lambda x: x[1][service] if x[1][service] else -1), time) - 1
 | 
					
						
							|  |  |  |   right = bisect_right(KeyifyList(list(end_times.items()),
 | 
					
						
							|  |  |  |                       lambda x: x[1][service] if x[1][service] else float("inf")), time)
 | 
					
						
							|  |  |  |   return left, right
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def find_t0(start_times, frame_id=-1):
 | 
					
						
							|  |  |  |   frame_id = frame_id if frame_id > -1 else min(start_times.keys())
 | 
					
						
							|  |  |  |   m = max(start_times.keys())
 | 
					
						
							|  |  |  |   while frame_id <= m:
 | 
					
						
							|  |  |  |     for service in SERVICES:
 | 
					
						
							|  |  |  |       if start_times[frame_id][service]:
 | 
					
						
							|  |  |  |         return start_times[frame_id][service]
 | 
					
						
							|  |  |  |     frame_id += 1
 | 
					
						
							|  |  |  |   raise Exception('No start time has been set')
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def insert_cloudlogs(lr, timestamps, start_times, end_times):
 | 
					
						
							|  |  |  |   # at least one cloudlog must be made in controlsd
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   t0 = find_t0(start_times)
 | 
					
						
							|  |  |  |   failed_inserts = 0
 | 
					
						
							|  |  |  |   latest_controls_frameid = 0
 | 
					
						
							|  |  |  |   for msg in lr:
 | 
					
						
							|  |  |  |     if msg.which() == "logMessage":
 | 
					
						
							|  |  |  |       jmsg = json.loads(msg.logMessage)
 | 
					
						
							|  |  |  |       if "timestamp" in jmsg['msg']:
 | 
					
						
							|  |  |  |         time = int(jmsg['msg']['timestamp']['time'])
 | 
					
						
							|  |  |  |         service = jmsg['ctx']['daemon']
 | 
					
						
							|  |  |  |         event = jmsg['msg']['timestamp']['event']
 | 
					
						
							|  |  |  |         if time < t0:
 | 
					
						
							|  |  |  |           # Filter out controlsd messages which arrive before the camera loop
 | 
					
						
							|  |  |  |           continue
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if "frame_id" in jmsg['msg']['timestamp']:
 | 
					
						
							|  |  |  |           timestamps[int(jmsg['msg']['timestamp']['frame_id'])][service].append((event, time))
 | 
					
						
							|  |  |  |           continue
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if service == "boardd":
 | 
					
						
							|  |  |  |           timestamps[latest_controls_frameid][service].append((event, time))
 | 
					
						
							|  |  |  |           end_times[latest_controls_frameid][service] = time
 | 
					
						
							|  |  |  |         else:
 | 
					
						
							|  |  |  |           frame_id = find_frame_id(time, service, start_times, end_times)
 | 
					
						
							|  |  |  |           if frame_id:
 | 
					
						
							|  |  |  |             if frame_id[0] != frame_id[1]:
 | 
					
						
							|  |  |  |               event += " (warning: ambiguity)"
 | 
					
						
							|  |  |  |             frame_id = frame_id[0]
 | 
					
						
							|  |  |  |             if service == 'controlsd':
 | 
					
						
							|  |  |  |               latest_controls_frameid = frame_id
 | 
					
						
							|  |  |  |             timestamps[frame_id][service].append((event, time))
 | 
					
						
							|  |  |  |           else:
 | 
					
						
							|  |  |  |             failed_inserts += 1
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if latest_controls_frameid == 0:
 | 
					
						
							|  |  |  |     print("Warning: failed to bind boardd logs to a frame ID. Add a timestamp cloudlog in controlsd.")
 | 
					
						
							|  |  |  |   elif failed_inserts > len(timestamps):
 | 
					
						
							|  |  |  |     print(f"Warning: failed to bind {failed_inserts} cloudlog timestamps to a frame ID")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def print_timestamps(timestamps, durations, start_times, relative):
 | 
					
						
							|  |  |  |   t0 = find_t0(start_times)
 | 
					
						
							|  |  |  |   for frame_id in timestamps.keys():
 | 
					
						
							|  |  |  |     print('='*80)
 | 
					
						
							|  |  |  |     print("Frame ID:", frame_id)
 | 
					
						
							|  |  |  |     if relative:
 | 
					
						
							|  |  |  |       t0 = find_t0(start_times, frame_id)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for service in SERVICES:
 | 
					
						
							|  |  |  |       print("  "+service)
 | 
					
						
							|  |  |  |       events = timestamps[frame_id][service]
 | 
					
						
							|  |  |  |       for event, time in sorted(events, key = lambda x: x[1]):
 | 
					
						
							|  |  |  |         print("    "+'%-53s%-53s' %(event, str((time-t0)/1e6)))
 | 
					
						
							|  |  |  |       for event, time in durations[frame_id][service]:
 | 
					
						
							|  |  |  |         print("    "+'%-53s%-53s' %(event, str(time*1000)))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def graph_timestamps(timestamps, start_times, end_times, relative, offset_services=False, title=""):
 | 
					
						
							|  |  |  |   # mpld3 doesn't convert properly to D3 font sizes
 | 
					
						
							|  |  |  |   plt.rcParams.update({'font.size': 18})
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   t0 = find_t0(start_times)
 | 
					
						
							|  |  |  |   fig, ax = plt.subplots()
 | 
					
						
							|  |  |  |   ax.set_xlim(0, 130 if relative else 750)
 | 
					
						
							|  |  |  |   ax.set_ylim(0, 17)
 | 
					
						
							|  |  |  |   ax.set_xlabel('Time (milliseconds)')
 | 
					
						
							|  |  |  |   colors = ['blue', 'green', 'red', 'yellow', 'purple']
 | 
					
						
							|  |  |  |   offsets = [[0, -5*j] for j in range(len(SERVICES))] if offset_services else None
 | 
					
						
							|  |  |  |   height = 0.3 if offset_services else 0.9
 | 
					
						
							|  |  |  |   assert len(colors) == len(SERVICES), 'Each service needs a color'
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   points = {"x": [], "y": [], "labels": []}
 | 
					
						
							|  |  |  |   for i, (frame_id, services) in enumerate(timestamps.items()):
 | 
					
						
							|  |  |  |     if relative:
 | 
					
						
							|  |  |  |       t0 = find_t0(start_times, frame_id)
 | 
					
						
							|  |  |  |     service_bars = []
 | 
					
						
							|  |  |  |     for service, events in services.items():
 | 
					
						
							|  |  |  |       if start_times[frame_id][service] and end_times[frame_id][service]:
 | 
					
						
							|  |  |  |         start = start_times[frame_id][service]
 | 
					
						
							|  |  |  |         end = end_times[frame_id][service]
 | 
					
						
							|  |  |  |         service_bars.append(((start-t0)/1e6,(end-start)/1e6))
 | 
					
						
							|  |  |  |         for event in events:
 | 
					
						
							|  |  |  |           points['x'].append((event[1]-t0)/1e6)
 | 
					
						
							|  |  |  |           points['y'].append(i)
 | 
					
						
							|  |  |  |           points['labels'].append(event[0])
 | 
					
						
							|  |  |  |     ax.broken_barh(service_bars, (i-height/2, height), facecolors=(colors), alpha=0.5, offsets=offsets)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   scatter = ax.scatter(points['x'], points['y'], marker='d', edgecolor='black')
 | 
					
						
							|  |  |  |   tooltip = mpld3.plugins.PointLabelTooltip(scatter, labels=points['labels'])
 | 
					
						
							|  |  |  |   mpld3.plugins.connect(fig, tooltip)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   plt.title(title)
 | 
					
						
							|  |  |  |   # Set size relative window size is not trivial: https://github.com/mpld3/mpld3/issues/65
 | 
					
						
							|  |  |  |   fig.set_size_inches(18, 9)
 | 
					
						
							|  |  |  |   plt.legend(handles=[mpatches.Patch(color=colors[i], label=SERVICES[i]) for i in range(len(SERVICES))])
 | 
					
						
							|  |  |  |   return fig
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_timestamps(lr):
 | 
					
						
							|  |  |  |   lr = list(lr)
 | 
					
						
							|  |  |  |   data, frame_mismatches = read_logs(lr)
 | 
					
						
							|  |  |  |   insert_cloudlogs(lr, data['timestamp'], data['start'], data['end'])
 | 
					
						
							|  |  |  |   return data, frame_mismatches
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == "__main__":
 | 
					
						
							|  |  |  |   parser = argparse.ArgumentParser(description="A tool for analyzing openpilot's end-to-end latency",
 | 
					
						
							|  |  |  |                                    formatter_class = argparse.ArgumentDefaultsHelpFormatter)
 | 
					
						
							|  |  |  |   parser.add_argument("--relative", action="store_true", help="Make timestamps relative to the start of each frame")
 | 
					
						
							|  |  |  |   parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one")
 | 
					
						
							|  |  |  |   parser.add_argument("--plot", action="store_true", help="If a plot should be generated")
 | 
					
						
							|  |  |  |   parser.add_argument("--offset", action="store_true", help="Vertically offset service to better visualize overlap")
 | 
					
						
							|  |  |  |   parser.add_argument("route_or_segment_name", nargs='?', help="The route to print")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if len(sys.argv) == 1:
 | 
					
						
							|  |  |  |     parser.print_help()
 | 
					
						
							|  |  |  |     sys.exit()
 | 
					
						
							|  |  |  |   args = parser.parse_args()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   r = DEMO_ROUTE if args.demo else args.route_or_segment_name.strip()
 | 
					
						
							|  |  |  |   lr = logreader_from_route_or_segment(r, sort_by_time=True)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   data, _ = get_timestamps(lr)
 | 
					
						
							|  |  |  |   print_timestamps(data['timestamp'], data['duration'], data['start'], args.relative)
 | 
					
						
							|  |  |  |   if args.plot:
 | 
					
						
							|  |  |  |     mpld3.show(graph_timestamps(data['timestamp'], data['start'], data['end'], args.relative, offset_services=args.offset, title=r))
 |