| 1 | import bt2 |
| 2 | import itertools |
| 3 | import matplotlib.pyplot as plt |
| 4 | import sys |
| 5 | import statistics |
| 6 | import csv |
| 7 | from collections import defaultdict |
| 8 | |
| 9 | |
| 10 | class DataLogger(object): |
| 11 | def __init__(self, name="Untitled"): |
| 12 | self._name = name |
| 13 | |
| 14 | def get_name(self): |
| 15 | return self._name |
| 16 | |
| 17 | def get_x_data(self): |
| 18 | raise NotImplementedError |
| 19 | |
| 20 | def get_y_data(self): |
| 21 | raise NotImplementedError |
| 22 | |
| 23 | def received_event(self, ts, event): |
| 24 | raise NotImplementedError |
| 25 | |
| 26 | |
| 27 | class DurationDataLogger(DataLogger): |
| 28 | """ |
| 29 | This class allow to create a duration histogram for the given pair of |
| 30 | event and unique tuple key generator. |
| 31 | |
| 32 | """ |
| 33 | def __init__(self, start_event, end_event, *args, **kwargs): |
| 34 | super(DurationDataLogger, self).__init__(*args, **kwargs) |
| 35 | |
| 36 | (self._event_start, self._start_fields) = start_event |
| 37 | (self._event_end, self._end_fields) = end_event |
| 38 | |
| 39 | self._durations = [] |
| 40 | self._pair = dict() |
| 41 | |
| 42 | def get_x_data(self): |
| 43 | return self._durations |
| 44 | |
| 45 | def received_event(self, ts, event): |
| 46 | if event.name == self._event_start: |
| 47 | key = () |
| 48 | for field in self._start_fields: |
| 49 | value = event.payload_field[str(field)] |
| 50 | key = key + (value,) |
| 51 | self._pair[key] = ts |
| 52 | return |
| 53 | |
| 54 | if event.name == self._event_end: |
| 55 | key = () |
| 56 | for field in self._end_fields: |
| 57 | value = event.payload_field[str(field)] |
| 58 | key = key + (value,) |
| 59 | |
| 60 | if key not in self._pair: |
| 61 | print("unmatched end event") |
| 62 | return |
| 63 | |
| 64 | start_ts = self._pair[key] |
| 65 | duration = (ts - start_ts) / 1000000.0 |
| 66 | self._durations.append(duration) |
| 67 | |
| 68 | class DurationCSVDataLogger(DataLogger): |
| 69 | """ |
| 70 | This class allow to create a duration histogram for the given csv. |
| 71 | """ |
| 72 | def __init__(self, filepath, *args, **kwargs): |
| 73 | super(DurationCSVDataLogger, self).__init__(*args, **kwargs) |
| 74 | |
| 75 | self._filepath = filepath |
| 76 | |
| 77 | |
| 78 | self._durations = [] |
| 79 | with open(filepath, newline='') as file: |
| 80 | reader = csv.reader(file, quoting=csv.QUOTE_NONE) |
| 81 | next(reader) |
| 82 | for row in reader: |
| 83 | self._durations.append(float(row[0])) |
| 84 | |
| 85 | def get_x_data(self): |
| 86 | return self._durations |
| 87 | |
| 88 | def received_event(self, ts, event): |
| 89 | return |
| 90 | |
| 91 | |
| 92 | class Plot(object): |
| 93 | def __init__( |
| 94 | self, loggers, title="Untitled", x_label="Untitled", y_label="Untitled" |
| 95 | ): |
| 96 | self._loggers = loggers |
| 97 | self._title = title |
| 98 | self._x_label = x_label |
| 99 | self._y_label = y_label |
| 100 | |
| 101 | def received_event(self, ts, event): |
| 102 | for logger in self._loggers: |
| 103 | logger.received_event(ts, event) |
| 104 | |
| 105 | def plot(self): |
| 106 | raise NotImplementedError |
| 107 | |
| 108 | def generate_csv(self): |
| 109 | raise NotImplementedError |
| 110 | |
| 111 | @staticmethod |
| 112 | def _format_filename(title, ext): |
| 113 | title = title.lower() |
| 114 | title = "".join("-" if not c.isalnum() else c for c in title) |
| 115 | title = "".join( |
| 116 | ["".join(j) if i != "-" else i for (i, j) in itertools.groupby(title)] |
| 117 | ) |
| 118 | return f"{title}.{ext}" |
| 119 | |
| 120 | class HistogramPlot(Plot): |
| 121 | def __init__(self, *args, **kwargs): |
| 122 | super(HistogramPlot, self).__init__(*args, **kwargs) |
| 123 | |
| 124 | @staticmethod |
| 125 | def get_statistics_header(): |
| 126 | return ["minimum", "maximum", "mean", "pstdev", "count"] |
| 127 | |
| 128 | @staticmethod |
| 129 | def get_statistics(samples): |
| 130 | stats = [] |
| 131 | stats.append('%f' % min(samples)) |
| 132 | stats.append('%f' % max(samples)) |
| 133 | stats.append('%f' % statistics.mean(samples)) |
| 134 | stats.append('%f' % statistics.pstdev(samples)) |
| 135 | stats.append('%d' % len(samples)) |
| 136 | return stats |
| 137 | |
| 138 | def plot(self): |
| 139 | sys.argv = [''] |
| 140 | complete_set = []; |
| 141 | logger_statistic = defaultdict(dict) |
| 142 | |
| 143 | figure = plt.figure() |
| 144 | plt.title(self._title) |
| 145 | plt.xlabel(self._x_label, figure=figure) |
| 146 | plt.ylabel(self._y_label, figure=figure) |
| 147 | plt.yscale('log', nonposy='clip') |
| 148 | |
| 149 | table_rows_label = [] |
| 150 | table_celltext = [] |
| 151 | for logger in self._loggers: |
| 152 | x = logger.get_x_data() |
| 153 | table_rows_label.append(logger.get_name()) |
| 154 | table_celltext.append(HistogramPlot.get_statistics(x)) |
| 155 | |
| 156 | complete_set +=x; |
| 157 | plt.hist(x, bins='auto', alpha=0.5, figure=figure, label=logger.get_name()) |
| 158 | |
| 159 | table_rows_label.append("all") |
| 160 | table_celltext.append(HistogramPlot.get_statistics(complete_set)) |
| 161 | the_table = plt.table(cellText=table_celltext, |
| 162 | rowLabels=table_rows_label, |
| 163 | colLabels=HistogramPlot.get_statistics_header(), |
| 164 | loc='bottom', |
| 165 | bbox=[0.0,-0.45,1,.28], |
| 166 | ) |
| 167 | |
| 168 | the_table.auto_set_font_size(False) |
| 169 | the_table.set_fontsize(8) |
| 170 | |
| 171 | plt.subplots_adjust(bottom=0.20) |
| 172 | plt.legend(loc='center left', bbox_to_anchor=(1, 0.5)) |
| 173 | plt.savefig(Plot._format_filename(self._title, "pdf"), bbox_inches="tight") |
| 174 | |
| 175 | def generate_csv(self): |
| 176 | for logger in self._loggers: |
| 177 | x_data = logger.get_x_data() |
| 178 | with open(Plot._format_filename(self._title, "%s.csv" % logger.get_name()), 'w', newline='') as export: |
| 179 | wr = csv.writer(export, quoting=csv.QUOTE_NONE) |
| 180 | wr.writerow([self._x_label]) |
| 181 | for x in x_data: |
| 182 | wr.writerow([x]) |
| 183 | |
| 184 | |
| 185 | @bt2.plugin_component_class |
| 186 | class PlotSink(bt2._UserSinkComponent): |
| 187 | def __init__(self, config, params, obj): |
| 188 | self._plots = [] |
| 189 | |
| 190 | if "histograms" in params: |
| 191 | for plot in params["histograms"]: |
| 192 | self._plots.append(PlotSink.create_histogram(plot)) |
| 193 | |
| 194 | self._add_input_port("in") |
| 195 | |
| 196 | def _user_consume(self): |
| 197 | msg = next(self._iter) |
| 198 | if type(msg) in [ |
| 199 | bt2._PacketBeginningMessageConst, |
| 200 | bt2._PacketEndMessageConst, |
| 201 | bt2._StreamBeginningMessageConst, |
| 202 | bt2._StreamEndMessageConst, |
| 203 | ]: |
| 204 | return |
| 205 | |
| 206 | ts = msg.default_clock_snapshot.value |
| 207 | for plot in self._plots: |
| 208 | plot.received_event(ts, msg.event) |
| 209 | |
| 210 | def _user_finalize(self): |
| 211 | {plot.plot() for plot in self._plots} |
| 212 | {plot.generate_csv () for plot in self._plots} |
| 213 | return |
| 214 | |
| 215 | def _user_graph_is_configured(self): |
| 216 | self._iter = self._create_message_iterator(self._input_ports["in"]) |
| 217 | |
| 218 | @staticmethod |
| 219 | def create_histogram(params): |
| 220 | loggers = [] |
| 221 | for logger in params[3]: |
| 222 | if logger[0] == "duration": |
| 223 | logger = PlotSink.create_duration_logger(logger) |
| 224 | elif logger[0] == "duration-csv": |
| 225 | logger = PlotSink.create_duration_logger_csv(logger) |
| 226 | else: |
| 227 | raise ValueError |
| 228 | |
| 229 | loggers.append(logger) |
| 230 | |
| 231 | title = str(params[0]) |
| 232 | x_label = str(params[1]) |
| 233 | y_label = str(params[2]) |
| 234 | |
| 235 | return HistogramPlot(loggers, title=title, x_label=x_label, |
| 236 | y_label=y_label) |
| 237 | |
| 238 | @staticmethod |
| 239 | def create_duration_logger(params): |
| 240 | return DurationDataLogger( |
| 241 | (str(params[2]), params[3]), |
| 242 | (str(params[4]), params[5]), |
| 243 | name=str(params[1]), |
| 244 | ) |
| 245 | |
| 246 | def create_duration_logger_csv(params): |
| 247 | return DurationCSVDataLogger( |
| 248 | str(params[2]), |
| 249 | name=str(params[1]), |
| 250 | ) |
| 251 | |
| 252 | |
| 253 | bt2.register_plugin( |
| 254 | module_name=__name__, |
| 255 | name="plot", |
| 256 | description="Plot Sink", |
| 257 | author="EfficiOS inc.", |
| 258 | license="GPL", |
| 259 | version=(1, 0, 0), |
| 260 | ) |