| #! /usr/bin/env python |
| # |
| # btt_plot.py: Generate matplotlib plots for BTT generate data files |
| # |
| # (C) Copyright 2009 Hewlett-Packard Development Company, L.P. |
| # |
| # This program is free software; you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation; either version 2 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| # GNU General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program; if not, write to the Free Software |
| # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| # |
| |
| """ |
| btt_plot.py: Generate matplotlib plots for BTT generated data files |
| |
| Files handled: |
| AQD - Average Queue Depth Running average of queue depths |
| |
| BNOS - Block numbers accessed Markers for each block |
| |
| Q2D - Queue to Issue latencies Running averages |
| D2C - Issue to Complete latencies Running averages |
| Q2C - Queue to Complete latencies Running averages |
| |
| Usage: |
| btt_plot_aqd.py equivalent to: btt_plot.py -t aqd <type>=aqd |
| btt_plot_bnos.py equivalent to: btt_plot.py -t bnos <type>=bnos |
| btt_plot_q2d.py equivalent to: btt_plot.py -t q2d <type>=q2d |
| btt_plot_d2c.py equivalent to: btt_plot.py -t d2c <type>=d2c |
| btt_plot_q2c.py equivalent to: btt_plot.py -t q2c <type>=q2c |
| |
| Arguments: |
| [ -A | --generate-all ] Default: False |
| [ -L | --no-legend ] Default: Legend table produced |
| [ -o <file> | --output=<file> ] Default: <type>.png |
| [ -T <string> | --title=<string> ] Default: Based upon <type> |
| [ -v | --verbose ] Default: False |
| <data-files...> |
| |
| The -A (--generate-all) argument is different: when this is specified, |
| an attempt is made to generate default plots for all 5 types (aqd, bnos, |
| q2d, d2c and q2c). It will find files with the appropriate suffix for |
| each type ('aqd.dat' for example). If such files are found, a plot for |
| that type will be made. The output file name will be the default for |
| each type. The -L (--no-legend) option will be obeyed for all plots, |
| but the -o (--output) and -T (--title) options will be ignored. |
| """ |
| |
| __author__ = 'Alan D. Brunelle <[email protected]>' |
| |
| #------------------------------------------------------------------------------ |
| |
| import matplotlib |
| matplotlib.use('Agg') |
| import getopt, glob, os, sys |
| import matplotlib.pyplot as plt |
| |
| plot_size = [10.9, 8.4] # inches... |
| |
| add_legend = True |
| generate_all = False |
| output_file = None |
| title_str = None |
| type = None |
| verbose = False |
| |
| types = [ 'aqd', 'q2d', 'd2c', 'q2c', 'live', 'bnos' ] |
| progs = [ 'btt_plot_%s.py' % t for t in types ] |
| |
| get_base = lambda file: file[file.find('_')+1:file.rfind('_')] |
| |
| #------------------------------------------------------------------------------ |
| def fatal(msg): |
| """Generate fatal error message and exit""" |
| |
| print >>sys.stderr, 'FATAL: %s' % msg |
| sys.exit(1) |
| |
| #------------------------------------------------------------------------------ |
| def gen_legends(ax, legends): |
| leg = ax.legend(legends, 'best', shadow=True) |
| frame = leg.get_frame() |
| frame.set_facecolor('0.80') |
| for t in leg.get_texts(): |
| t.set_fontsize('xx-small') |
| |
| #---------------------------------------------------------------------- |
| def get_data(files): |
| """Retrieve data from files provided. |
| |
| Returns a database containing: |
| 'min_x', 'max_x' - Minimum and maximum X values found |
| 'min_y', 'max_y' - Minimum and maximum Y values found |
| 'x', 'y' - X & Y value arrays |
| 'ax', 'ay' - Running average over X & Y -- |
| if > 10 values provided... |
| """ |
| #-------------------------------------------------------------- |
| def check(mn, mx, v): |
| """Returns new min, max, and float value for those passed in""" |
| |
| v = float(v) |
| if mn == None or v < mn: mn = v |
| if mx == None or v > mx: mx = v |
| return mn, mx, v |
| |
| #-------------------------------------------------------------- |
| def avg(xs, ys): |
| """Computes running average for Xs and Ys""" |
| |
| #------------------------------------------------------ |
| def _avg(vals): |
| """Computes average for array of values passed""" |
| |
| total = 0.0 |
| for val in vals: |
| total += val |
| return total / len(vals) |
| |
| #------------------------------------------------------ |
| if len(xs) < 1000: |
| return xs, ys |
| |
| axs = [xs[0]] |
| ays = [ys[0]] |
| _xs = [xs[0]] |
| _ys = [ys[0]] |
| |
| x_range = (xs[-1] - xs[0]) / 100 |
| for idx in range(1, len(ys)): |
| if (xs[idx] - _xs[0]) > x_range: |
| axs.append(_avg(_xs)) |
| ays.append(_avg(_ys)) |
| del _xs, _ys |
| |
| _xs = [xs[idx]] |
| _ys = [ys[idx]] |
| else: |
| _xs.append(xs[idx]) |
| _ys.append(ys[idx]) |
| |
| if len(_xs) > 1: |
| axs.append(_avg(_xs)) |
| ays.append(_avg(_ys)) |
| |
| return axs, ays |
| |
| #-------------------------------------------------------------- |
| global verbose |
| |
| db = {} |
| min_x = max_x = min_y = max_y = None |
| for file in files: |
| if not os.path.exists(file): |
| fatal('%s not found' % file) |
| elif verbose: |
| print 'Processing %s' % file |
| |
| xs = [] |
| ys = [] |
| for line in open(file, 'r'): |
| f = line.rstrip().split(None) |
| if line.find('#') == 0 or len(f) < 2: |
| continue |
| (min_x, max_x, x) = check(min_x, max_x, f[0]) |
| (min_y, max_y, y) = check(min_y, max_y, f[1]) |
| xs.append(x) |
| ys.append(y) |
| |
| db[file] = {'x':xs, 'y':ys} |
| if len(xs) > 10: |
| db[file]['ax'], db[file]['ay'] = avg(xs, ys) |
| else: |
| db[file]['ax'] = db[file]['ay'] = None |
| |
| db['min_x'] = min_x |
| db['max_x'] = max_x |
| db['min_y'] = min_y |
| db['max_y'] = max_y |
| return db |
| |
| #---------------------------------------------------------------------- |
| def parse_args(args): |
| """Parse command line arguments. |
| |
| Returns list of (data) files that need to be processed -- /unless/ |
| the -A (--generate-all) option is passed, in which case superfluous |
| data files are ignored... |
| """ |
| |
| global add_legend, output_file, title_str, type, verbose |
| global generate_all |
| |
| prog = args[0][args[0].rfind('/')+1:] |
| if prog == 'btt_plot.py': |
| pass |
| elif not prog in progs: |
| fatal('%s not a valid command name' % prog) |
| else: |
| type = prog[prog.rfind('_')+1:prog.rfind('.py')] |
| |
| s_opts = 'ALo:t:T:v' |
| l_opts = [ 'generate-all', 'type', 'no-legend', 'output', 'title', |
| 'verbose' ] |
| |
| try: |
| (opts, args) = getopt.getopt(args[1:], s_opts, l_opts) |
| except getopt.error, msg: |
| print >>sys.stderr, msg |
| fatal(__doc__) |
| |
| for (o, a) in opts: |
| if o in ('-A', '--generate-all'): |
| generate_all = True |
| elif o in ('-L', '--no-legend'): |
| add_legend = False |
| elif o in ('-o', '--output'): |
| output_file = a |
| elif o in ('-t', '--type'): |
| if not a in types: |
| fatal('Type %s not supported' % a) |
| type = a |
| elif o in ('-T', '--title'): |
| title_str = a |
| elif o in ('-v', '--verbose'): |
| verbose = True |
| |
| if type == None and not generate_all: |
| fatal('Need type of data files to process - (-t <type>)') |
| |
| return args |
| |
| #------------------------------------------------------------------------------ |
| def gen_title(fig, type, title_str): |
| """Sets the title for the figure based upon the type /or/ user title""" |
| |
| if title_str != None: |
| pass |
| elif type == 'aqd': |
| title_str = 'Average Queue Depth' |
| elif type == 'bnos': |
| title_str = 'Block Numbers Accessed' |
| elif type == 'q2d': |
| title_str = 'Queue (Q) To Issue (D) Average Latencies' |
| elif type == 'd2c': |
| title_str = 'Issue (D) To Complete (C) Average Latencies' |
| elif type == 'q2c': |
| title_str = 'Queue (Q) To Complete (C) Average Latencies' |
| |
| title = fig.text(.5, .95, title_str, horizontalalignment='center') |
| title.set_fontsize('large') |
| |
| #------------------------------------------------------------------------------ |
| def gen_labels(db, ax, type): |
| """Generate X & Y 'axis'""" |
| |
| #---------------------------------------------------------------------- |
| def gen_ylabel(ax, type): |
| """Set the Y axis label based upon the type""" |
| |
| if type == 'aqd': |
| str = 'Number of Requests Queued' |
| elif type == 'bnos': |
| str = 'Block Number' |
| else: |
| str = 'Seconds' |
| ax.set_ylabel(str) |
| |
| #---------------------------------------------------------------------- |
| xdelta = 0.1 * (db['max_x'] - db['min_x']) |
| ydelta = 0.1 * (db['max_y'] - db['min_y']) |
| |
| ax.set_xlim(db['min_x'] - xdelta, db['max_x'] + xdelta) |
| ax.set_ylim(db['min_y'] - ydelta, db['max_y'] + ydelta) |
| ax.set_xlabel('Runtime (seconds)') |
| ax.grid(True) |
| gen_ylabel(ax, type) |
| |
| #------------------------------------------------------------------------------ |
| def generate_output(type, db): |
| """Generate the output plot based upon the type and database""" |
| |
| #---------------------------------------------------------------------- |
| def color(idx, style): |
| """Returns a color/symbol type based upon the index passed.""" |
| |
| colors = [ 'b', 'g', 'r', 'c', 'm', 'y', 'k' ] |
| l_styles = [ '-', ':', '--', '-.' ] |
| m_styles = [ 'o', '+', '.', ',', 's', 'v', 'x', '<', '>' ] |
| |
| color = colors[idx % len(colors)] |
| if style == 'line': |
| style = l_styles[(idx / len(l_styles)) % len(l_styles)] |
| elif style == 'marker': |
| style = m_styles[(idx / len(m_styles)) % len(m_styles)] |
| |
| return '%s%s' % (color, style) |
| |
| #---------------------------------------------------------------------- |
| global add_legend, output_file, title_str, verbose |
| |
| if output_file != None: |
| ofile = output_file |
| else: |
| ofile = '%s.png' % type |
| |
| if verbose: |
| print 'Generating plot into %s' % ofile |
| |
| fig = plt.figure(figsize=plot_size) |
| ax = fig.add_subplot(111) |
| |
| gen_title(fig, type, title_str) |
| gen_labels(db, ax, type) |
| |
| idx = 0 |
| if add_legend: |
| legends = [] |
| else: |
| legends = None |
| |
| keys = [] |
| for file in db.iterkeys(): |
| if not file in ['min_x', 'max_x', 'min_y', 'max_y']: |
| keys.append(file) |
| |
| keys.sort() |
| for file in keys: |
| dat = db[file] |
| if type == 'bnos': |
| ax.plot(dat['x'], dat['y'], color(idx, 'marker'), |
| markersize=1) |
| elif dat['ax'] == None: |
| continue # Don't add legend |
| else: |
| ax.plot(dat['ax'], dat['ay'], color(idx, 'line'), |
| linewidth=1.0) |
| if add_legend: |
| legends.append(get_base(file)) |
| idx += 1 |
| |
| if add_legend and len(legends) > 0: |
| gen_legends(ax, legends) |
| plt.savefig(ofile) |
| |
| #------------------------------------------------------------------------------ |
| def get_files(type): |
| """Returns the list of files for the -A option based upon type""" |
| |
| if type == 'bnos': |
| files = [] |
| for fn in glob.glob('*c.dat'): |
| for t in [ 'q2q', 'd2d', 'q2c', 'd2c' ]: |
| if fn.find(t) >= 0: |
| break |
| else: |
| files.append(fn) |
| else: |
| files = glob.glob('*%s.dat' % type) |
| return files |
| |
| #------------------------------------------------------------------------------ |
| def do_bnos(files): |
| for file in files: |
| base = get_base(file) |
| title_str = 'Block Numbers Accessed: %s' % base |
| output_file = 'bnos_%s.png' % base |
| generate_output(t, get_data([file])) |
| |
| #------------------------------------------------------------------------------ |
| def do_live(files): |
| global plot_size |
| |
| #---------------------------------------------------------------------- |
| def get_live_data(fn): |
| xs = [] |
| ys = [] |
| for line in open(fn, 'r'): |
| f = line.rstrip().split() |
| if f[0] != '#' and len(f) == 2: |
| xs.append(float(f[0])) |
| ys.append(float(f[1])) |
| return xs, ys |
| |
| #---------------------------------------------------------------------- |
| def live_sort(a, b): |
| if a[0] == 'sys' and b[0] == 'sys': |
| return 0 |
| elif a[0] == 'sys' or a[2][0] < b[2][0]: |
| return -1 |
| elif b[0] == 'sys' or a[2][0] > b[2][0]: |
| return 1 |
| else: |
| return 0 |
| |
| #---------------------------------------------------------------------- |
| def turn_off_ticks(ax): |
| for tick in ax.xaxis.get_major_ticks(): |
| tick.tick1On = tick.tick2On = False |
| for tick in ax.yaxis.get_major_ticks(): |
| tick.tick1On = tick.tick2On = False |
| for tick in ax.xaxis.get_minor_ticks(): |
| tick.tick1On = tick.tick2On = False |
| for tick in ax.yaxis.get_minor_ticks(): |
| tick.tick1On = tick.tick2On = False |
| |
| #---------------------------------------------------------------------- |
| fig = plt.figure(figsize=plot_size) |
| ax = fig.add_subplot(111) |
| |
| db = [] |
| for fn in files: |
| if not os.path.exists(fn): |
| continue |
| (xs, ys) = get_live_data(fn) |
| db.append([fn[:fn.find('_live.dat')], xs, ys]) |
| db.sort(live_sort) |
| |
| for rec in db: |
| ax.plot(rec[1], rec[2]) |
| |
| gen_title(fig, 'live', 'Active I/O Per Device') |
| ax.set_xlabel('Runtime (seconds)') |
| ax.set_ylabel('Device') |
| ax.grid(False) |
| |
| ax.set_xlim(-0.1, db[0][1][-1]+1) |
| ax.set_yticks([idx for idx in range(0, len(db))]) |
| ax.yaxis.set_ticklabels([rec[0] for rec in db]) |
| turn_off_ticks(ax) |
| |
| plt.savefig('live.png') |
| plt.savefig('live.eps') |
| |
| #------------------------------------------------------------------------------ |
| if __name__ == '__main__': |
| files = parse_args(sys.argv) |
| |
| if generate_all: |
| output_file = title_str = type = None |
| for t in types: |
| files = get_files(t) |
| if len(files) == 0: |
| continue |
| elif t == 'bnos': |
| do_bnos(files) |
| elif t == 'live': |
| do_live(files) |
| else: |
| generate_output(t, get_data(files)) |
| continue |
| |
| elif len(files) < 1: |
| fatal('Need data files to process') |
| else: |
| generate_output(type, get_data(files)) |
| sys.exit(0) |