You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
			
				
					190 lines
				
				7.5 KiB
			
		
		
			
		
	
	
					190 lines
				
				7.5 KiB
			| 
								 
											4 years ago
										 
									 | 
							
								import argparse
							 | 
						||
| 
								 | 
							
								import json
							 | 
						||
| 
								 | 
							
								import mpld3
							 | 
						||
| 
								 | 
							
								import sys
							 | 
						||
| 
								 | 
							
								from collections import defaultdict
							 | 
						||
| 
								 | 
							
								import matplotlib.pyplot as plt
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								from tools.lib.logreader import logreader_from_route_or_segment
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								DEMO_ROUTE = "9f583b1d93915c31|2022-04-01--17-51-29"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								SERVICES = ['camerad', 'modeld', 'plannerd', 'controlsd', 'boardd']
							 | 
						||
| 
								 | 
							
								# Retrive 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 = []
							 | 
						||
| 
								 | 
							
								  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
							 | 
						||
| 
								 | 
							
								      mono_to_frame[msg.logMonoTime] = frame_id
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      data['timestamp'][frame_id][service].append((msg.which()+" published", 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))
							 | 
						||
| 
								 | 
							
								      elif msg.which() == 'controlsState':
							 | 
						||
| 
								 | 
							
								        # Sendcan is published before controlsState, but the frameId is retrived 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)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  # Failed frameId fetches are stored in -1
							 | 
						||
| 
								 | 
							
								  assert sum([len(data['timestamp'][-1][service]) for service in data['timestamp'][-1].keys()]) < 20, "Too many frameId fetch fails"
							 | 
						||
| 
								 | 
							
								  del data['timestamp'][-1]
							 | 
						||
| 
								 | 
							
								  assert len(frame_mismatches) < 20, "Too many frame mismatches"
							 | 
						||
| 
								 | 
							
								  return (data, frame_mismatches)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_min_time(frame_id, service, timestamps, use_max=False):
							 | 
						||
| 
								 | 
							
								  return min(timestamps[frame_id][service], key=lambda x: x[1])[1] if not use_max else max(timestamps[frame_id][service], key=lambda x: x[1])[1]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_interval(frame_id, service, timestamps):
							 | 
						||
| 
								 | 
							
								  try:
							 | 
						||
| 
								 | 
							
								    service_max = get_min_time(frame_id, service, timestamps, use_max=True) 
							 | 
						||
| 
								 | 
							
								    if service == SERVICES[0]:
							 | 
						||
| 
								 | 
							
								      service_min = get_min_time(frame_id, service, timestamps) 
							 | 
						||
| 
								 | 
							
								      return (service_min, service_max)
							 | 
						||
| 
								 | 
							
								    prev_service = SERVICES[SERVICES.index(service)-1]
							 | 
						||
| 
								 | 
							
								    prev_service_max = get_min_time(frame_id, prev_service, timestamps, use_max=True) 
							 | 
						||
| 
								 | 
							
								    return (prev_service_max, service_max)
							 | 
						||
| 
								 | 
							
								  except ValueError:
							 | 
						||
| 
								 | 
							
								    return (-1,-1)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def find_frame_id(time, service, timestamps):
							 | 
						||
| 
								 | 
							
								  for frame_id in reversed(timestamps):
							 | 
						||
| 
								 | 
							
								    start, end = get_interval(frame_id, service, timestamps)
							 | 
						||
| 
								 | 
							
								    if start <= time <= end:
							 | 
						||
| 
								 | 
							
								      return frame_id
							 | 
						||
| 
								 | 
							
								  return -1
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								## ASSUMES THAT AT LEAST ONE CLOUDLOG IS MADE IN CONTROLSD
							 | 
						||
| 
								 | 
							
								def insert_cloudlogs(lr, timestamps):
							 | 
						||
| 
								 | 
							
								  t0 = get_min_time(min(timestamps.keys()), SERVICES[0], timestamps) 
							 | 
						||
| 
								 | 
							
								  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
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        frame_id = latest_controls_frameid if service == "boardd" else find_frame_id(time, service, timestamps)
							 | 
						||
| 
								 | 
							
								        if frame_id > -1:
							 | 
						||
| 
								 | 
							
								          timestamps[frame_id][service].append((event, time))
							 | 
						||
| 
								 | 
							
								          if service == 'controlsd':
							 | 
						||
| 
								 | 
							
								            latest_controls_frameid = frame_id
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								          failed_inserts += 1
							 | 
						||
| 
								 | 
							
								  assert failed_inserts < len(timestamps), "Too many failed cloudlog inserts"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def print_timestamps(timestamps, durations ,relative):
							 | 
						||
| 
								 | 
							
								  t0 = get_min_time(min(timestamps.keys()), SERVICES[0], timestamps) 
							 | 
						||
| 
								 | 
							
								  for frame_id in timestamps.keys():
							 | 
						||
| 
								 | 
							
								    print('='*80)
							 | 
						||
| 
								 | 
							
								    print("Frame ID:", frame_id)
							 | 
						||
| 
								 | 
							
								    if relative:
							 | 
						||
| 
								 | 
							
								      t0 = get_min_time(frame_id, SERVICES[0], timestamps) 
							 | 
						||
| 
								 | 
							
								    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, relative):
							 | 
						||
| 
								 | 
							
								  t0 = get_min_time(min(timestamps.keys()), SERVICES[0], timestamps) 
							 | 
						||
| 
								 | 
							
								  fig, ax = plt.subplots()
							 | 
						||
| 
								 | 
							
								  ax.set_xlim(0, 150 if relative else 750)
							 | 
						||
| 
								 | 
							
								  ax.set_ylim(0, 15)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  points = {"x": [], "y": [], "labels": []}
							 | 
						||
| 
								 | 
							
								  for frame_id, services in timestamps.items():
							 | 
						||
| 
								 | 
							
								    if relative:
							 | 
						||
| 
								 | 
							
								      t0 = get_min_time(frame_id, SERVICES[0], timestamps) 
							 | 
						||
| 
								 | 
							
								    service_bars = []
							 | 
						||
| 
								 | 
							
								    for service, events in services.items():
							 | 
						||
| 
								 | 
							
								      start, end = get_interval(frame_id, service,timestamps)
							 | 
						||
| 
								 | 
							
								      service_bars.append(((start-t0)/1e6,(end-start)/1e6))
							 | 
						||
| 
								 | 
							
								      for event in events:
							 | 
						||
| 
								 | 
							
								        points["x"].append((event[1]-t0)/1e6)
							 | 
						||
| 
								 | 
							
								        points["y"].append(frame_id+0.45)
							 | 
						||
| 
								 | 
							
								        points["labels"].append(event[0])
							 | 
						||
| 
								 | 
							
								    ax.broken_barh(service_bars, (frame_id, 0.9), facecolors=(["blue", 'green', 'red', 'yellow', 'purple']))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  scatter = ax.scatter(points['x'], points['y'], marker="d", edgecolor='black')
							 | 
						||
| 
								 | 
							
								  tooltip = mpld3.plugins.PointLabelTooltip(scatter, labels=points["labels"])
							 | 
						||
| 
								 | 
							
								  ax.legend()
							 | 
						||
| 
								 | 
							
								  mpld3.plugins.connect(fig, tooltip)
							 | 
						||
| 
								 | 
							
								  #mpld3.save_html(fig, 'test.html')
							 | 
						||
| 
								 | 
							
								  mpld3.show()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								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("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, frame_mismatches = read_logs(lr)
							 | 
						||
| 
								 | 
							
								  lr.reset()
							 | 
						||
| 
								 | 
							
								  insert_cloudlogs(lr, data['timestamp'])
							 | 
						||
| 
								 | 
							
								  print_timestamps(data['timestamp'], data['duration'], args.relative)
							 | 
						||
| 
								 | 
							
								  if args.plot:
							 | 
						||
| 
								 | 
							
								    graph_timestamps(data['timestamp'], args.relative)
							 |