#!/usr/bin/env python # Copyright (c) 2018 Texas Instruments Incorporated - http://www.ti.com/ # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of Texas Instruments Incorporated nor the # names of its contributors may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF # THE POSSIBILITY OF SUCH DAMAGE. """ Frame Execution Graph This script parses timestamp output generated from the TIDL API and displays a graph of frame execution. The script requires `matplotlib` to be installed. """ import argparse import matplotlib.pyplot as mpyplot import matplotlib.patches as mpatch USAGE = """ Display frame execution using trace data generated by the TIDL API. """ # Supported TIDL API classes COMPONENTS = ('eop', 'eo1', 'eo2') # APIs with timestamps # ProcessFrameStartAsync, ProcessFrameWait, RunAsyncNext KEY_PFSA = 'pfsa' KEY_PFW = 'pfw' KEY_RAN = 'ran' KEY_START = 'start' KEY_END = 'end' BARH_COLORS = ('lightgray', 'green', 'blue', 'yellow', 'black', 'orange', 'red', 'cyan') BARH_LEGEND_STR = {'eop' : 'ExecutionObjectPipeline', 'eo1' : 'ExecutionObject 0', 'eo2' : 'ExecutionObject 1', 'pfw' : 'Process Frame Wait', 'pfsa': 'Process Frame Start Async', 'ran' : 'Run Async Next', 'total': 'Total frame time'} class Range: """ Defines a range in terms of start and stop timestamps. """ def __init__(self): self.start = 0 self.end = 0 def set_start(self, start): """Set the start time.""" self.start = start def set_end(self, end): """ Set the end time.""" self.end = end def subtract(self, val): """ Subtracl val from each start/end.""" self.start -= val self.end -= val def get_range(self): """Return (start, duration).""" return (self.start, self.end-self.start) class FrameInfo: """ All recorded events for a single frame. """ def __init__(self, index): """Set up dictionaries to store timestamp data""" self.index = index self.eo_type = {} self.eo_id = {} self.data = {} for component in COMPONENTS: self.data[component] = {} def update(self, component, api, phase, val): """Update the [c][api][phase] with timestamp""" if component not in COMPONENTS: print('Invalid component: {}'.format(component)) return if api not in self.data[component]: self.data[component][api] = Range() if phase == KEY_START: self.data[component][api].set_start(val) elif phase == KEY_END: self.data[component][api].set_end(val) else: raise Exception('Invalid key: {}'.format(phase)) def update_eo_info(self, component, eo_type, eo_id): """Set the Execution Object type and index. Used to generate the EVEx or DSPx labels""" self.eo_type[component] = eo_type self.eo_id[component] = eo_id def eo_info(self): """Return a string corresponding to the EO info. Device numbering in TRM starts at 1. E.g. EVE1, EVE2, DSP1, DSP2 etc. """ device_list = [] for component in ('eo1', 'eo2'): device = "" if not self.data[component]: continue # Corresponds to DeviceType enum in inc/executor.h if self.eo_type[component] == 0: device += 'DSP' else: device += 'EVE' device += str(self.eo_id[component]+1) device_list.append(device) return '+'.join(device_list) def get_range(self, component, api): """Return the range for the specified component:api combination""" if api in self.data[component]: return self.data[component][api].get_range() return None def get_max_range(self): """Return a tuple corresponding to the maximum range""" return (self.min(), self.max() - self.min()) def get_total(self, component): """Return the range for the specified component""" if not self.data[component]: print("{} not available".format(component)) return None start = self.data[component][KEY_PFSA].start end = self.data[component][KEY_PFW].end return (start, int(end-start)) def min(self): """ Return the lowest timestamp for a frame""" vals = [] for component in self.data: for api in self.data[component]: vals.append(self.data[component][api].start) return min(vals) def max(self): """ Return the highest timestamp for a frame""" vals = [] for component in self.data: for api in self.data[component]: vals.append(self.data[component][api].end) return max(vals) def subtract(self, val): """Subtract val from every timestamp in this frame. Use to adjust start of frame 0 to 0. """ for component in self.data: for api in self.data[component]: self.data[component][api].subtract(val) def get_plot_ranges(self, components): """ Return a list of (component, range) tuples for all api data available for the specified components. """ ranges = [] for component in self.data: if component not in components: continue for api in self.data[component]: range_tuple = self.get_range(component, api) if range_tuple is not None: label = component + ':' + api ranges.append((label, range_tuple)) # Reverse sort by duration (improves graph rendering) ranges.sort(key=lambda kv: kv[1][1], reverse=True) return ranges def get_plot_totals(self): """ Return (component, range) tuples for all available components. The range corresponds to the execution time for the entire component. """ ranges = [] for component in self.data: range_tuple = self.get_total(component) if range_tuple is not None: ranges.append((component, range_tuple)) # Sort by start time stamp ranges.sort(key=lambda kv: kv[1][0]) return ranges def get_barh_ranges(self, found, verbosity): """Return a list of range tuples for plotting. Also return corresponding labels""" label_range_tuples = [] if verbosity == 0: if not found['eo2']: if found['eop']: component = 'eop' else: component = 'eo1' label_range_tuples.append(('total', self.get_max_range())) label_range_tuples.extend(self.get_plot_ranges((component))) else: label_range_tuples = self.get_plot_totals() elif verbosity == 1: label_range_tuples.append(('total', self.get_max_range())) for component in COMPONENTS: label_range_tuples.extend(self.get_plot_ranges(component)) labels = [i[0] for i in label_range_tuples] ranges = [i[1] for i in label_range_tuples] return ranges, labels def __repr__(self): string = '