#!/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 ) )