Home | History | Annotate | Download | only in btt
      1 #! /usr/bin/env python
      2 #
      3 # btt_plot.py: Generate matplotlib plots for BTT generate data files
      4 #
      5 #  (C) Copyright 2009 Hewlett-Packard Development Company, L.P.
      6 #
      7 #  This program is free software; you can redistribute it and/or modify
      8 #  it under the terms of the GNU General Public License as published by
      9 #  the Free Software Foundation; either version 2 of the License, or
     10 #  (at your option) any later version.
     11 #
     12 #  This program is distributed in the hope that it will be useful,
     13 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
     14 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     15 #  GNU General Public License for more details.
     16 #
     17 #  You should have received a copy of the GNU General Public License
     18 #  along with this program; if not, write to the Free Software
     19 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
     20 #
     21 
     22 """
     23 btt_plot.py: Generate matplotlib plots for BTT generated data files
     24 
     25 Files handled:
     26   AQD	- Average Queue Depth		Running average of queue depths
     27 
     28   BNOS	- Block numbers accessed	Markers for each block
     29 
     30   Q2D	- Queue to Issue latencies	Running averages
     31   D2C	- Issue to Complete latencies	Running averages
     32   Q2C	- Queue to Complete latencies	Running averages
     33 
     34 Usage:
     35   btt_plot_aqd.py	equivalent to: btt_plot.py -t aqd	<type>=aqd
     36   btt_plot_bnos.py	equivalent to: btt_plot.py -t bnos	<type>=bnos
     37   btt_plot_q2d.py	equivalent to: btt_plot.py -t q2d	<type>=q2d
     38   btt_plot_d2c.py	equivalent to: btt_plot.py -t d2c	<type>=d2c
     39   btt_plot_q2c.py	equivalent to: btt_plot.py -t q2c	<type>=q2c
     40 
     41 Arguments:
     42   [ -A          | --generate-all   ] Default: False
     43   [ -L          | --no-legend      ] Default: Legend table produced
     44   [ -o <file>   | --output=<file>  ] Default: <type>.png
     45   [ -T <string> | --title=<string> ] Default: Based upon <type>
     46   [ -v          | --verbose        ] Default: False
     47   <data-files...>
     48 
     49   The -A (--generate-all) argument is different: when this is specified,
     50   an attempt is made to generate default plots for all 5 types (aqd, bnos,
     51   q2d, d2c and q2c). It will find files with the appropriate suffix for
     52   each type ('aqd.dat' for example). If such files are found, a plot for
     53   that type will be made. The output file name will be the default for
     54   each type. The -L (--no-legend) option will be obeyed for all plots,
     55   but the -o (--output) and -T (--title) options will be ignored.
     56 """
     57 
     58 __author__ = 'Alan D. Brunelle <alan.brunelle (at] hp.com>'
     59 
     60 #------------------------------------------------------------------------------
     61 
     62 import matplotlib
     63 matplotlib.use('Agg')
     64 import getopt, glob, os, sys
     65 import matplotlib.pyplot as plt
     66 
     67 plot_size	= [10.9, 8.4]	# inches...
     68 
     69 add_legend	= True
     70 generate_all	= False
     71 output_file	= None
     72 title_str	= None
     73 type		= None
     74 verbose		= False
     75 
     76 types		= [ 'aqd', 'q2d', 'd2c', 'q2c', 'bnos' ]
     77 progs		= [ 'btt_plot_%s.py' % t for t in types ]
     78 
     79 get_base 	= lambda file: file[file.find('_')+1:file.rfind('_')]
     80 
     81 #------------------------------------------------------------------------------
     82 def fatal(msg):
     83 	"""Generate fatal error message and exit"""
     84 
     85 	print >>sys.stderr, 'FATAL: %s' % msg
     86 	sys.exit(1)
     87 
     88 #----------------------------------------------------------------------
     89 def get_data(files):
     90 	"""Retrieve data from files provided.
     91 
     92 	Returns a database containing:
     93 		'min_x', 'max_x' 	- Minimum and maximum X values found
     94 		'min_y', 'max_y' 	- Minimum and maximum Y values found
     95 		'x', 'y'		- X & Y value arrays
     96 		'ax', 'ay'		- Running average over X & Y --
     97 					  if > 10 values provided...
     98 	"""
     99 	#--------------------------------------------------------------
    100 	def check(mn, mx, v):
    101 		"""Returns new min, max, and float value for those passed in"""
    102 
    103 		v = float(v)
    104 		if mn == None or v < mn: mn = v
    105 		if mx == None or v > mx: mx = v
    106 		return mn, mx, v
    107 
    108 	#--------------------------------------------------------------
    109 	def avg(xs, ys):
    110 		"""Computes running average for Xs and Ys"""
    111 
    112 		#------------------------------------------------------
    113 		def _avg(vals):
    114 			"""Computes average for array of values passed"""
    115 
    116 			total = 0.0
    117 			for val in vals:
    118 				total += val
    119 			return total / len(vals)
    120 
    121 		#------------------------------------------------------
    122 		if len(xs) < 1000:
    123 			return xs, ys
    124 
    125 		axs = [xs[0]]
    126 		ays = [ys[0]]
    127 		_xs = [xs[0]]
    128 		_ys = [ys[0]]
    129 
    130 		x_range = (xs[-1] - xs[0]) / 100
    131 		for idx in range(1, len(ys)):
    132 			if (xs[idx] - _xs[0]) > x_range:
    133 				axs.append(_avg(_xs))
    134 				ays.append(_avg(_ys))
    135 				del _xs, _ys
    136 
    137 				_xs = [xs[idx]]
    138 				_ys = [ys[idx]]
    139 			else:
    140 				_xs.append(xs[idx])
    141 				_ys.append(ys[idx])
    142 
    143 		if len(_xs) > 1:
    144 			axs.append(_avg(_xs))
    145 			ays.append(_avg(_ys))
    146 
    147 		return axs, ays
    148 
    149 	#--------------------------------------------------------------
    150 	global verbose
    151 
    152 	db = {}
    153 	min_x = max_x = min_y = max_y = None
    154 	for file in files:
    155 		if not os.path.exists(file):
    156 			fatal('%s not found' % file)
    157 		elif verbose:
    158 			print 'Processing %s' % file
    159 
    160 		xs = []
    161 		ys = []
    162 		for line in open(file, 'r'):
    163 			f = line.rstrip().split(None)
    164 			if line.find('#') == 0 or len(f) < 2:
    165 				continue
    166 			(min_x, max_x, x) = check(min_x, max_x, f[0])
    167 			(min_y, max_y, y) = check(min_y, max_y, f[1])
    168 			xs.append(x)
    169 			ys.append(y)
    170 
    171 		db[file] = {'x':xs, 'y':ys}
    172 		if len(xs) > 10:
    173 			db[file]['ax'], db[file]['ay'] = avg(xs, ys)
    174 		else:
    175 			db[file]['ax'] = db[file]['ay'] = None
    176 
    177 	db['min_x'] = min_x
    178 	db['max_x'] = max_x
    179 	db['min_y'] = min_y
    180 	db['max_y'] = max_y
    181 	return db
    182 
    183 #----------------------------------------------------------------------
    184 def parse_args(args):
    185 	"""Parse command line arguments.
    186 
    187 	Returns list of (data) files that need to be processed -- /unless/
    188 	the -A (--generate-all) option is passed, in which case superfluous
    189 	data files are ignored...
    190 	"""
    191 
    192 	global add_legend, output_file, title_str, type, verbose
    193 	global generate_all
    194 
    195 	prog = args[0][args[0].rfind('/')+1:]
    196 	if prog == 'btt_plot.py':
    197 		pass
    198 	elif not prog in progs:
    199 		fatal('%s not a valid command name' % prog)
    200 	else:
    201 		type = prog[prog.rfind('_')+1:prog.rfind('.py')]
    202 
    203 	s_opts = 'ALo:t:T:v'
    204 	l_opts = [ 'generate-all', 'type', 'no-legend', 'output', 'title',
    205 		   'verbose' ]
    206 
    207 	try:
    208 		(opts, args) = getopt.getopt(args[1:], s_opts, l_opts)
    209 	except getopt.error, msg:
    210 		print >>sys.stderr, msg
    211 		fatal(__doc__)
    212 
    213 	for (o, a) in opts:
    214 		if o in ('-A', '--generate-all'):
    215 			generate_all = True
    216 		elif o in ('-L', '--no-legend'):
    217 			add_legend = False
    218 		elif o in ('-o', '--output'):
    219 			output_file = a
    220 		elif o in ('-t', '--type'):
    221 			if not a in types:
    222 				fatal('Type %s not supported' % a)
    223 			type = a
    224 		elif o in ('-T', '--title'):
    225 			title_str = a
    226 		elif o in ('-v', '--verbose'):
    227 			verbose = True
    228 
    229 	if type == None and not generate_all:
    230 		fatal('Need type of data files to process - (-t <type>)')
    231 
    232 	return args
    233 
    234 #------------------------------------------------------------------------------
    235 def gen_title(fig, type, title_str):
    236 	"""Sets the title for the figure based upon the type /or/ user title"""
    237 
    238 	if title_str != None:
    239 		pass
    240 	elif type == 'aqd':
    241 		title_str = 'Average Queue Depth'
    242 	elif type == 'bnos':
    243 		title_str = 'Block Numbers Accessed'
    244 	elif type == 'q2d':
    245 		title_str = 'Queue (Q) To Issue (D) Average Latencies'
    246 	elif type == 'd2c':
    247 		title_str = 'Issue (D) To Complete (C) Average Latencies'
    248 	elif type == 'q2c':
    249 		title_str = 'Queue (Q) To Complete (C) Average Latencies'
    250 
    251 	title = fig.text(.5, .95, title_str, horizontalalignment='center')
    252 	title.set_fontsize('large')
    253 
    254 #------------------------------------------------------------------------------
    255 def gen_labels(db, ax, type):
    256 	"""Generate X & Y 'axis'"""
    257 
    258 	#----------------------------------------------------------------------
    259 	def gen_ylabel(ax, type):
    260 		"""Set the Y axis label based upon the type"""
    261 
    262 		if type == 'aqd':
    263 			str = 'Number of Requests Queued'
    264 		elif type == 'bnos':
    265 			str = 'Block Number'
    266 		else:
    267 			str = 'Seconds'
    268 		ax.set_ylabel(str)
    269 
    270 	#----------------------------------------------------------------------
    271 	xdelta = 0.1 * (db['max_x'] - db['min_x'])
    272 	ydelta = 0.1 * (db['max_y'] - db['min_y'])
    273 
    274 	ax.set_xlim(db['min_x'] - xdelta, db['max_x'] + xdelta)
    275 	ax.set_ylim(db['min_y'] - ydelta, db['max_y'] + ydelta)
    276 	ax.set_xlabel('Runtime (seconds)')
    277 	ax.grid(True)
    278 	gen_ylabel(ax, type)
    279 
    280 #------------------------------------------------------------------------------
    281 def generate_output(type, db):
    282 	"""Generate the output plot based upon the type and database"""
    283 
    284 	#----------------------------------------------------------------------
    285 	def color(idx, style):
    286 		"""Returns a color/symbol type based upon the index passed."""
    287 
    288                 colors = [ 'b', 'g', 'r', 'c', 'm', 'y', 'k' ]
    289 		l_styles = [ '-', ':', '--', '-.' ]
    290 		m_styles = [ 'o', '+', '.', ',', 's', 'v', 'x', '<', '>' ]
    291 
    292 		color = colors[idx % len(colors)]
    293 		if style == 'line':
    294 			style = l_styles[(idx / len(l_styles)) % len(l_styles)]
    295 		elif style == 'marker':
    296 			style = m_styles[(idx / len(m_styles)) % len(m_styles)]
    297 
    298 		return '%s%s' % (color, style)
    299 
    300 	#----------------------------------------------------------------------
    301 	def gen_legends(a, legends):
    302 		leg = ax.legend(legends, 'best', shadow=True)
    303 		frame = leg.get_frame()
    304 		frame.set_facecolor('0.80')
    305 		for t in leg.get_texts():
    306 			t.set_fontsize('xx-small')
    307 
    308 	#----------------------------------------------------------------------
    309 	global add_legend, output_file, title_str, verbose
    310 
    311 	if output_file != None:
    312 		ofile = output_file
    313 	else:
    314 		ofile = '%s.png' % type
    315 
    316 	if verbose:
    317 		print 'Generating plot into %s' % ofile
    318 
    319 	fig = plt.figure(figsize=plot_size)
    320 	ax = fig.add_subplot(111)
    321 
    322 	gen_title(fig, type, title_str)
    323 	gen_labels(db, ax, type)
    324 
    325 	idx = 0
    326 	if add_legend:
    327 		legends = []
    328 	else:
    329 		legends = None
    330 
    331 	keys = []
    332 	for file in db.iterkeys():
    333 		if not file in ['min_x', 'max_x', 'min_y', 'max_y']:
    334 			keys.append(file)
    335 
    336 	keys.sort()
    337 	for file in keys:
    338 		dat = db[file]
    339 		if type == 'bnos':
    340 			ax.plot(dat['x'], dat['y'], color(idx, 'marker'),
    341 				markersize=1)
    342 		elif dat['ax'] == None:
    343 			continue	# Don't add legend
    344 		else:
    345 			ax.plot(dat['ax'], dat['ay'], color(idx, 'line'),
    346 				linewidth=1.0)
    347 		if add_legend:
    348 			legends.append(get_base(file))
    349 		idx += 1
    350 
    351 	if add_legend and len(legends) > 0:
    352 		gen_legends(ax, legends)
    353 	plt.savefig(ofile)
    354 
    355 #------------------------------------------------------------------------------
    356 def get_files(type):
    357 	"""Returns the list of files for the -A option based upon type"""
    358 
    359 	if type == 'bnos':
    360 		files = []
    361 		for fn in glob.glob('*c.dat'):
    362 			for t in [ 'q2q', 'd2d', 'q2c', 'd2c' ]:
    363 				if fn.find(t) >= 0:
    364 					break
    365 			else:
    366 				files.append(fn)
    367 	else:
    368 		files = glob.glob('*%s.dat' % type)
    369 	return files
    370 
    371 #------------------------------------------------------------------------------
    372 if __name__ == '__main__':
    373 	files = parse_args(sys.argv)
    374 
    375 	if generate_all:
    376 		output_file = title_str = type = None
    377 		for t in types:
    378 			files = get_files(t)
    379 			if len(files) == 0:
    380 				continue
    381 			elif t != 'bnos':
    382 				generate_output(t, get_data(files))
    383 				continue
    384 
    385 			for file in files:
    386 				base = get_base(file)
    387 				title_str = 'Block Numbers Accessed: %s' % base
    388 				output_file = 'bnos_%s.png' % base
    389 				generate_output(t, get_data([file]))
    390 	elif len(files) < 1:
    391 		fatal('Need data files to process')
    392 	else:
    393 		generate_output(type, get_data(files))
    394 	sys.exit(0)
    395