#!/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 /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']['rtt'] 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: # Convert to float data['read_lat'] = \ [r['benchmark']['data']['read_lat'] for r in records] data['read_lat'] = \ [float(i) for i in data['read_lat']] # Convert to int data['read_bw'] = \ [r['benchmark']['data']['read_bw'] for r in records] data['read_bw'] = \ [int(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'] for r in records] data['write_lat'] = \ [float(i) for i in data['write_lat']] data['write_bw'] = \ [r['benchmark']['data']['write_bw'] for r in records] data['write_bw'] = \ [int(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()