summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKristian Hunt <kristian.hunt@gmail.com>2015-07-15 10:43:09 +0200
committerJörgen Karlsson <jorgen.w.karlsson@ericsson.com>2015-08-14 09:14:23 +0000
commit1747afd41c228bd5bff17c5233ded5061791363e (patch)
tree4cd821e24575f33a1dc626f25e5219ebbc3fed9c
parenteca1e81cac34d7569fa5dcb15e5df10a6583559b (diff)
Add support for generating graphs from output
Command line tool yardstick-plot is to be used to visualize results gathered from yardstick framework's output file. Currently supports plotting graphs from ping, pktgen, iperf3 and fio tests. Yardstick-plot takes two arguments - input file and output folder and both of them have defaults to fall to if left unspecified. Supports having multiple different scenario types in an input file, while assuming that all results from the same scenario type belong to one graph. Thus, results plotted from a single scenario type with different parameters are currently non-informative. yardstick-plot is declared as an extra for yardstick in setup.py as it is not required for all use cases of the yardstick framework. It can be installed for example using command: $ pip install -e .[plot] from the folder where setup.py is located. Example invocation: yardstick-plot -i /tmp/yardstick.out -o /tmp/plots/ JIRA: YARDSTICK-65 Change-Id: Ic436ca360ba2496aa829ca817b1d9d5f3c944c6c Signed-off-by: Kristian Hunt <kristian.hunt@gmail.com>
-rw-r--r--setup.py6
-rw-r--r--yardstick/plot/__init__.py0
-rw-r--r--yardstick/plot/plotter.py311
3 files changed, 316 insertions, 1 deletions
diff --git a/setup.py b/setup.py
index a346f5765..f73094ac1 100644
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,7 @@ setup(
'benchmark/scenarios/networking/*.bash',
'benchmark/scenarios/storage/*.bash',
'resources/files/*'
- ]
+ ]
},
url="https://www.opnfv.org",
install_requires=["backport_ipaddress", # remove with python3
@@ -30,9 +30,13 @@ setup(
"paramiko",
"six"
],
+ extras_require={
+ 'plot': ["matplotlib>=1.4.2"]
+ },
entry_points={
'console_scripts': [
'yardstick=yardstick.main:main',
+ 'yardstick-plot=yardstick.plot.plotter:main [plot]'
],
},
scripts=['tools/yardstick-img-modify']
diff --git a/yardstick/plot/__init__.py b/yardstick/plot/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/yardstick/plot/__init__.py
diff --git a/yardstick/plot/plotter.py b/yardstick/plot/plotter.py
new file mode 100644
index 000000000..f3fb75d3e
--- /dev/null
+++ b/yardstick/plot/plotter.py
@@ -0,0 +1,311 @@
+#!/usr/bin/env python
+
+##############################################################################
+# Copyright (c) 2015 Ericsson AB and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+''' yardstick-plot - a command line tool for visualizing results from the
+ output file of yardstick framework.
+
+ Example invocation:
+ $ yardstick-plot -i /tmp/yardstick.out -o /tmp/plots/
+'''
+
+import argparse
+import json
+import os
+import sys
+import time
+import matplotlib.pyplot as plt
+import matplotlib.lines as mlines
+
+
+class Parser(object):
+ ''' Command-line argument and input file parser for yardstick-plot tool'''
+
+ def __init__(self):
+ self.data = {
+ 'ping': [],
+ 'pktgen': [],
+ 'iperf3': [],
+ 'fio': []
+ }
+ self.default_input_loc = "/tmp/yardstick.out"
+
+ def _get_parser(self):
+ '''get a command-line parser'''
+ parser = argparse.ArgumentParser(
+ prog='yardstick-plot',
+ description="A tool for visualizing results from yardstick. "
+ "Currently supports plotting graphs for output files "
+ "from tests: " + str(self.data.keys())
+ )
+ parser.add_argument(
+ '-i', '--input',
+ help="The input file name. If left unspecified then "
+ "it defaults to %s" % self.default_input_loc
+ )
+ parser.add_argument(
+ '-o', '--output-folder',
+ help="The output folder location. If left unspecified then "
+ "it defaults to <script_directory>/plots/"
+ )
+ return parser
+
+ def _add_record(self, record):
+ '''add record to the relevant scenario'''
+ runner_object = record['sargs']['runner']['object']
+ for test_type in self.data.keys():
+ if test_type in runner_object:
+ self.data[test_type].append(record)
+
+ def parse_args(self):
+ '''parse command-line arguments'''
+ parser = self._get_parser()
+ self.args = parser.parse_args()
+ return self.args
+
+ def parse_input_file(self):
+ '''parse the input test results file'''
+ if self.args.input:
+ input_file = self.args.input
+ else:
+ print("No input file specified, reading from %s"
+ % self.default_input_loc)
+ input_file = self.default_input_loc
+
+ try:
+ with open(input_file) as f:
+ for line in f:
+ record = json.loads(line)
+ self._add_record(record)
+ except IOError as e:
+ print(os.strerror(e.errno))
+ sys.exit(1)
+
+
+class Plotter(object):
+ '''Graph plotter for scenario-specific results from yardstick framework'''
+
+ def __init__(self, data, output_folder):
+ self.data = data
+ self.output_folder = output_folder
+ self.fig_counter = 1
+ self.colors = ['g', 'b', 'c', 'm', 'y']
+
+ def plot(self):
+ '''plot the graph(s)'''
+ for test_type in self.data.keys():
+ if self.data[test_type]:
+ plt.figure(self.fig_counter)
+ self.fig_counter += 1
+
+ plt.title(test_type, loc="left")
+ method_name = "_plot_" + test_type
+ getattr(self, method_name)(self.data[test_type])
+ self._save_plot(test_type)
+
+ def _save_plot(self, test_type):
+ '''save the graph to output folder'''
+ timestr = time.strftime("%Y%m%d-%H%M%S")
+ file_name = test_type + "_" + timestr + ".png"
+ if not self.output_folder:
+ curr_path = os.path.dirname(os.path.abspath(__file__))
+ self.output_folder = os.path.join(curr_path, "plots")
+ if not os.path.isdir(self.output_folder):
+ os.makedirs(self.output_folder)
+ new_file = os.path.join(self.output_folder, file_name)
+ plt.savefig(new_file)
+ print("Saved graph to " + new_file)
+
+ def _plot_ping(self, records):
+ '''ping test result interpretation and visualization on the graph'''
+ rtts = [r['benchmark']['data'] for r in records]
+ seqs = [r['benchmark']['sequence'] for r in records]
+
+ for i in range(0, len(rtts)):
+ # If SLA failed
+ if not rtts[i]:
+ rtts[i] = 0.0
+ plt.axvline(seqs[i], color='r')
+
+ # If there is a single data-point then display a bar-chart
+ if len(rtts) == 1:
+ plt.bar(1, rtts[0], 0.35, color=self.colors[0])
+ else:
+ plt.plot(seqs, rtts, self.colors[0]+'-')
+
+ self._construct_legend(['rtt'])
+ plt.xlabel("sequence number")
+ plt.xticks(seqs, seqs)
+ plt.ylabel("round trip time in milliseconds (rtt)")
+
+ def _plot_pktgen(self, records):
+ '''pktgen test result interpretation and visualization on the graph'''
+ flows = [r['benchmark']['data']['flows'] for r in records]
+ sent = [r['benchmark']['data']['packets_sent'] for r in records]
+ received = [int(r['benchmark']['data']['packets_received'])
+ for r in records]
+
+ for i in range(0, len(sent)):
+ # If SLA failed
+ if not sent[i] or not received[i]:
+ sent[i] = 0.0
+ received[i] = 0.0
+ plt.axvline(flows[i], color='r')
+
+ ppm = [1000000.0*(i - j)/i for i, j in zip(sent, received)]
+
+ # If there is a single data-point then display a bar-chart
+ if len(ppm) == 1:
+ plt.bar(1, ppm[0], 0.35, color=self.colors[0])
+ else:
+ plt.plot(flows, ppm, self.colors[0]+'-')
+
+ self._construct_legend(['ppm'])
+ plt.xlabel("number of flows")
+ plt.ylabel("lost packets per million packets (ppm)")
+
+ def _plot_iperf3(self, records):
+ '''iperf3 test result interpretation and visualization on the graph'''
+ intervals = []
+ for r in records:
+ # If did not fail the SLA
+ if r['benchmark']['data']:
+ intervals.append(r['benchmark']['data']['intervals'])
+ else:
+ intervals.append(None)
+
+ kbps = [0]
+ seconds = [0]
+ for i, val in enumerate(intervals):
+ if val:
+ for j, _ in enumerate(intervals):
+ kbps.append(val[j]['sum']['bits_per_second']/1000)
+ seconds.append(seconds[-1] + val[j]['sum']['seconds'])
+ else:
+ kbps.append(0.0)
+ # Don't know how long the failed test took, add 1 second
+ # TODO more accurate solution or replace x-axis from seconds
+ # to measurement nr
+ seconds.append(seconds[-1] + 1)
+ plt.axvline(seconds[-1], color='r')
+
+ self._construct_legend(['bandwidth'])
+ plt.plot(seconds[1:], kbps[1:], self.colors[0]+'-')
+ plt.xlabel("time in seconds")
+ plt.ylabel("bandwidth in Kb/s")
+
+ def _plot_fio(self, records):
+ '''fio test result interpretation and visualization on the graph'''
+ rw_types = [r['sargs']['options']['rw'] for r in records]
+ seqs = [x for x in range(1, len(records) + 1)]
+ data = {}
+
+ for i in range(0, len(records)):
+ is_r_type = rw_types[i] == "read" or rw_types[i] == "randread"
+ is_w_type = rw_types[i] == "write" or rw_types[i] == "randwrite"
+ is_rw_type = rw_types[i] == "rw" or rw_types[i] == "randrw"
+
+ if is_r_type or is_rw_type:
+ # Remove trailing 'usec' and convert to float
+ data['read_lat'] = \
+ [r['benchmark']['data']['read_lat'][:-4] for r in records]
+ data['read_lat'] = \
+ [float(i) for i in data['read_lat']]
+ # Remove trailing 'KB/s' and convert to float
+ data['read_bw'] = \
+ [r['benchmark']['data']['read_bw'][:-4] for r in records]
+ data['read_bw'] = \
+ [float(i) for i in data['read_bw']]
+ # Convert to int
+ data['read_iops'] = \
+ [r['benchmark']['data']['read_iops'] for r in records]
+ data['read_iops'] = \
+ [int(i) for i in data['read_iops']]
+
+ if is_w_type or is_rw_type:
+ data['write_lat'] = \
+ [r['benchmark']['data']['write_lat'][:-4] for r in records]
+ data['write_lat'] = \
+ [float(i) for i in data['write_lat']]
+
+ data['write_bw'] = \
+ [r['benchmark']['data']['write_bw'][:-4] for r in records]
+ data['write_bw'] = \
+ [float(i) for i in data['write_bw']]
+
+ data['write_iops'] = \
+ [r['benchmark']['data']['write_iops'] for r in records]
+ data['write_iops'] = \
+ [int(i) for i in data['write_iops']]
+
+ # Divide the area into 3 subplots, sharing a common x-axis
+ fig, axl = plt.subplots(3, sharex=True)
+ axl[0].set_title("fio", loc="left")
+
+ self._plot_fio_helper(data, seqs, 'read_bw', self.colors[0], axl[0])
+ self._plot_fio_helper(data, seqs, 'write_bw', self.colors[1], axl[0])
+ axl[0].set_ylabel("Bandwidth in KB/s")
+
+ self._plot_fio_helper(data, seqs, 'read_iops', self.colors[0], axl[1])
+ self._plot_fio_helper(data, seqs, 'write_iops', self.colors[1], axl[1])
+ axl[1].set_ylabel("IOPS")
+
+ self._plot_fio_helper(data, seqs, 'read_lat', self.colors[0], axl[2])
+ self._plot_fio_helper(data, seqs, 'write_lat', self.colors[1], axl[2])
+ axl[2].set_ylabel("Latency in " + u"\u00B5s")
+
+ self._construct_legend(['read', 'write'], obj=axl[0])
+ plt.xlabel("Sequence number")
+ plt.xticks(seqs, seqs)
+
+ def _plot_fio_helper(self, data, seqs, key, bar_color, axl):
+ '''check if measurements exist for a key and then plot the
+ data to a given subplot'''
+ if key in data:
+ if len(data[key]) == 1:
+ axl.bar(0.1, data[key], 0.35, color=bar_color)
+ else:
+ line_style = bar_color + '-'
+ axl.plot(seqs, data[key], line_style)
+
+ def _construct_legend(self, legend_texts, obj=plt):
+ '''construct legend for the plot or subplot'''
+ ci = 0
+ lines = []
+
+ for text in legend_texts:
+ line = mlines.Line2D([], [], color=self.colors[ci], label=text)
+ lines.append(line)
+ ci += 1
+
+ lines.append(mlines.Line2D([], [], color='r', label="SLA failed"))
+
+ getattr(obj, "legend")(
+ bbox_to_anchor=(0.25, 1.02, 0.75, .102),
+ loc=3,
+ borderaxespad=0.0,
+ ncol=len(lines),
+ mode="expand",
+ handles=lines
+ )
+
+
+def main():
+ parser = Parser()
+ args = parser.parse_args()
+ print("Parsing input file")
+ parser.parse_input_file()
+ print("Initializing plotter")
+ plotter = Plotter(parser.data, args.output_folder)
+ print("Plotting graph(s)")
+ plotter.plot()
+
+if __name__ == '__main__':
+ main()