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