Merge "Update plotter.py to new yardstick.out format"
[yardstick.git] / yardstick / plot / plotter.py
1 #!/usr/bin/env python
2
3 ##############################################################################
4 # Copyright (c) 2015 Ericsson AB and others.
5 #
6 # All rights reserved. This program and the accompanying materials
7 # are made available under the terms of the Apache License, Version 2.0
8 # which accompanies this distribution, and is available at
9 # http://www.apache.org/licenses/LICENSE-2.0
10 ##############################################################################
11
12 ''' yardstick-plot - a command line tool for visualizing results from the
13     output file of yardstick framework.
14
15     Example invocation:
16     $ yardstick-plot -i /tmp/yardstick.out -o /tmp/plots/
17 '''
18
19 import argparse
20 import json
21 import os
22 import sys
23 import time
24 import matplotlib.pyplot as plt
25 import matplotlib.lines as mlines
26
27
28 class Parser(object):
29     ''' Command-line argument and input file parser for yardstick-plot tool'''
30
31     def __init__(self):
32         self.data = {
33             'ping': [],
34             'pktgen': [],
35             'iperf3': [],
36             'fio': []
37         }
38         self.default_input_loc = "/tmp/yardstick.out"
39         self.scenarios = {}
40
41     def _get_parser(self):
42         '''get a command-line parser'''
43         parser = argparse.ArgumentParser(
44             prog='yardstick-plot',
45             description="A tool for visualizing results from yardstick. "
46                         "Currently supports plotting graphs for output files "
47                         "from tests: " + str(self.data.keys())
48         )
49         parser.add_argument(
50             '-i', '--input',
51             help="The input file name. If left unspecified then "
52                  "it defaults to %s" % self.default_input_loc
53         )
54         parser.add_argument(
55             '-o', '--output-folder',
56             help="The output folder location. If left unspecified then "
57                  "it defaults to <script_directory>/plots/"
58         )
59         return parser
60
61     def _add_record(self, record):
62         '''add record to the relevant scenario'''
63         if "runner_id" in record and "benchmark" not in record:
64             obj_name = record["scenario_cfg"]["runner"]["object"]
65             self.scenarios[record["runner_id"]] = obj_name
66             return
67         runner_object = self.scenarios[record["runner_id"]]
68         for test_type in self.data.keys():
69             if test_type in runner_object:
70                 self.data[test_type].append(record)
71
72     def parse_args(self):
73         '''parse command-line arguments'''
74         parser = self._get_parser()
75         self.args = parser.parse_args()
76         return self.args
77
78     def parse_input_file(self):
79         '''parse the input test results file'''
80         if self.args.input:
81             input_file = self.args.input
82         else:
83             print("No input file specified, reading from %s"
84                   % self.default_input_loc)
85             input_file = self.default_input_loc
86
87         try:
88             with open(input_file) as f:
89                 for line in f:
90                     record = json.loads(line)
91                     self._add_record(record)
92         except IOError as e:
93             print(os.strerror(e.errno))
94             sys.exit(1)
95
96
97 class Plotter(object):
98     '''Graph plotter for scenario-specific results from yardstick framework'''
99
100     def __init__(self, data, output_folder):
101         self.data = data
102         self.output_folder = output_folder
103         self.fig_counter = 1
104         self.colors = ['g', 'b', 'c', 'm', 'y']
105
106     def plot(self):
107         '''plot the graph(s)'''
108         for test_type in self.data.keys():
109             if self.data[test_type]:
110                 plt.figure(self.fig_counter)
111                 self.fig_counter += 1
112
113                 plt.title(test_type, loc="left")
114                 method_name = "_plot_" + test_type
115                 getattr(self, method_name)(self.data[test_type])
116                 self._save_plot(test_type)
117
118     def _save_plot(self, test_type):
119         '''save the graph to output folder'''
120         timestr = time.strftime("%Y%m%d-%H%M%S")
121         file_name = test_type + "_" + timestr + ".png"
122         if not self.output_folder:
123             curr_path = os.path.dirname(os.path.abspath(__file__))
124             self.output_folder = os.path.join(curr_path, "plots")
125         if not os.path.isdir(self.output_folder):
126             os.makedirs(self.output_folder)
127         new_file = os.path.join(self.output_folder, file_name)
128         plt.savefig(new_file)
129         print("Saved graph to " + new_file)
130
131     def _plot_ping(self, records):
132         '''ping test result interpretation and visualization on the graph'''
133         rtts = [r['benchmark']['data']['rtt'] for r in records]
134         seqs = [r['benchmark']['sequence'] for r in records]
135
136         for i in range(0, len(rtts)):
137             # If SLA failed
138             if not rtts[i]:
139                 rtts[i] = 0.0
140                 plt.axvline(seqs[i], color='r')
141
142         # If there is a single data-point then display a bar-chart
143         if len(rtts) == 1:
144             plt.bar(1, rtts[0], 0.35, color=self.colors[0])
145         else:
146             plt.plot(seqs, rtts, self.colors[0]+'-')
147
148         self._construct_legend(['rtt'])
149         plt.xlabel("sequence number")
150         plt.xticks(seqs, seqs)
151         plt.ylabel("round trip time in milliseconds (rtt)")
152
153     def _plot_pktgen(self, records):
154         '''pktgen test result interpretation and visualization on the graph'''
155         flows = [r['benchmark']['data']['flows'] for r in records]
156         sent = [r['benchmark']['data']['packets_sent'] for r in records]
157         received = [int(r['benchmark']['data']['packets_received'])
158                     for r in records]
159
160         for i in range(0, len(sent)):
161             # If SLA failed
162             if not sent[i] or not received[i]:
163                 sent[i] = 0.0
164                 received[i] = 0.0
165                 plt.axvline(flows[i], color='r')
166
167         ppm = [1000000.0*(i - j)/i for i, j in zip(sent, received)]
168
169         # If there is a single data-point then display a bar-chart
170         if len(ppm) == 1:
171             plt.bar(1, ppm[0], 0.35, color=self.colors[0])
172         else:
173             plt.plot(flows, ppm, self.colors[0]+'-')
174
175         self._construct_legend(['ppm'])
176         plt.xlabel("number of flows")
177         plt.ylabel("lost packets per million packets (ppm)")
178
179     def _plot_iperf3(self, records):
180         '''iperf3 test result interpretation and visualization on the graph'''
181         intervals = []
182         for r in records:
183             #  If did not fail the SLA
184             if r['benchmark']['data']:
185                 intervals.append(r['benchmark']['data']['intervals'])
186             else:
187                 intervals.append(None)
188
189         kbps = [0]
190         seconds = [0]
191         for i, val in enumerate(intervals):
192             if val:
193                 for j, _ in enumerate(intervals):
194                     kbps.append(val[j]['sum']['bits_per_second']/1000)
195                     seconds.append(seconds[-1] + val[j]['sum']['seconds'])
196             else:
197                 kbps.append(0.0)
198                 # Don't know how long the failed test took, add 1 second
199                 # TODO more accurate solution or replace x-axis from seconds
200                 # to measurement nr
201                 seconds.append(seconds[-1] + 1)
202                 plt.axvline(seconds[-1], color='r')
203
204         self._construct_legend(['bandwidth'])
205         plt.plot(seconds[1:], kbps[1:], self.colors[0]+'-')
206         plt.xlabel("time in seconds")
207         plt.ylabel("bandwidth in Kb/s")
208
209     def _plot_fio(self, records):
210         '''fio test result interpretation and visualization on the graph'''
211         rw_types = [r['sargs']['options']['rw'] for r in records]
212         seqs = [x for x in range(1, len(records) + 1)]
213         data = {}
214
215         for i in range(0, len(records)):
216             is_r_type = rw_types[i] == "read" or rw_types[i] == "randread"
217             is_w_type = rw_types[i] == "write" or rw_types[i] == "randwrite"
218             is_rw_type = rw_types[i] == "rw" or rw_types[i] == "randrw"
219
220             if is_r_type or is_rw_type:
221                 # Convert to float
222                 data['read_lat'] = \
223                     [r['benchmark']['data']['read_lat'] for r in records]
224                 data['read_lat'] = \
225                     [float(i) for i in data['read_lat']]
226                 # Convert to int
227                 data['read_bw'] = \
228                     [r['benchmark']['data']['read_bw'] for r in records]
229                 data['read_bw'] =  \
230                     [int(i) for i in data['read_bw']]
231                 # Convert to int
232                 data['read_iops'] = \
233                     [r['benchmark']['data']['read_iops'] for r in records]
234                 data['read_iops'] = \
235                     [int(i) for i in data['read_iops']]
236
237             if is_w_type or is_rw_type:
238                 data['write_lat'] = \
239                     [r['benchmark']['data']['write_lat'] for r in records]
240                 data['write_lat'] = \
241                     [float(i) for i in data['write_lat']]
242
243                 data['write_bw'] = \
244                     [r['benchmark']['data']['write_bw'] for r in records]
245                 data['write_bw'] = \
246                     [int(i) for i in data['write_bw']]
247
248                 data['write_iops'] = \
249                     [r['benchmark']['data']['write_iops'] for r in records]
250                 data['write_iops'] = \
251                     [int(i) for i in data['write_iops']]
252
253         # Divide the area into 3 subplots, sharing a common x-axis
254         fig, axl = plt.subplots(3, sharex=True)
255         axl[0].set_title("fio", loc="left")
256
257         self._plot_fio_helper(data, seqs, 'read_bw', self.colors[0], axl[0])
258         self._plot_fio_helper(data, seqs, 'write_bw', self.colors[1], axl[0])
259         axl[0].set_ylabel("Bandwidth in KB/s")
260
261         self._plot_fio_helper(data, seqs, 'read_iops', self.colors[0], axl[1])
262         self._plot_fio_helper(data, seqs, 'write_iops', self.colors[1], axl[1])
263         axl[1].set_ylabel("IOPS")
264
265         self._plot_fio_helper(data, seqs, 'read_lat', self.colors[0], axl[2])
266         self._plot_fio_helper(data, seqs, 'write_lat', self.colors[1], axl[2])
267         axl[2].set_ylabel("Latency in " + u"\u00B5s")
268
269         self._construct_legend(['read', 'write'], obj=axl[0])
270         plt.xlabel("Sequence number")
271         plt.xticks(seqs, seqs)
272
273     def _plot_fio_helper(self, data, seqs, key, bar_color, axl):
274         '''check if measurements exist for a key and then plot the
275            data to a given subplot'''
276         if key in data:
277             if len(data[key]) == 1:
278                 axl.bar(0.1, data[key], 0.35, color=bar_color)
279             else:
280                 line_style = bar_color + '-'
281                 axl.plot(seqs, data[key], line_style)
282
283     def _construct_legend(self, legend_texts, obj=plt):
284         '''construct legend for the plot or subplot'''
285         ci = 0
286         lines = []
287
288         for text in legend_texts:
289             line = mlines.Line2D([], [], color=self.colors[ci], label=text)
290             lines.append(line)
291             ci += 1
292
293         lines.append(mlines.Line2D([], [], color='r', label="SLA failed"))
294
295         getattr(obj, "legend")(
296             bbox_to_anchor=(0.25, 1.02, 0.75, .102),
297             loc=3,
298             borderaxespad=0.0,
299             ncol=len(lines),
300             mode="expand",
301             handles=lines
302         )
303
304
305 def main():
306     parser = Parser()
307     args = parser.parse_args()
308     print("Parsing input file")
309     parser.parse_input_file()
310     print("Initializing plotter")
311     plotter = Plotter(parser.data, args.output_folder)
312     print("Plotting graph(s)")
313     plotter.plot()
314
315 if __name__ == '__main__':
316     main()