btt_plot.py: Fix pylint: wrong-import-order
[blktrace.git] / btt / btt_plot.py
CommitLineData
726d2aee
AB
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"""
23btt_plot.py: Generate matplotlib plots for BTT generated data files
24
25Files 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
34Usage:
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
41Arguments:
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
70d5ca2d
ES
58from __future__ import absolute_import
59from __future__ import print_function
fd3766eb 60import getopt, glob, os, sys
70d5ca2d
ES
61import six
62from six.moves import range
726d2aee
AB
63__author__ = 'Alan D. Brunelle <alan.brunelle@hp.com>'
64
65#------------------------------------------------------------------------------
66
67import matplotlib
68matplotlib.use('Agg')
726d2aee
AB
69import matplotlib.pyplot as plt
70
71plot_size = [10.9, 8.4] # inches...
72
73add_legend = True
74generate_all = False
75output_file = None
76title_str = None
77type = None
78verbose = False
79
2e37a10e 80types = [ 'aqd', 'q2d', 'd2c', 'q2c', 'live', 'bnos' ]
726d2aee
AB
81progs = [ 'btt_plot_%s.py' % t for t in types ]
82
83get_base = lambda file: file[file.find('_')+1:file.rfind('_')]
84
85#------------------------------------------------------------------------------
86def fatal(msg):
87 """Generate fatal error message and exit"""
88
70d5ca2d 89 print('FATAL: %s' % msg, file=sys.stderr)
726d2aee
AB
90 sys.exit(1)
91
2e37a10e
AB
92#------------------------------------------------------------------------------
93def gen_legends(ax, legends):
94 leg = ax.legend(legends, 'best', shadow=True)
95 frame = leg.get_frame()
96 frame.set_facecolor('0.80')
97 for t in leg.get_texts():
98 t.set_fontsize('xx-small')
99
726d2aee
AB
100#----------------------------------------------------------------------
101def get_data(files):
102 """Retrieve data from files provided.
103
104 Returns a database containing:
105 'min_x', 'max_x' - Minimum and maximum X values found
106 'min_y', 'max_y' - Minimum and maximum Y values found
107 'x', 'y' - X & Y value arrays
108 'ax', 'ay' - Running average over X & Y --
109 if > 10 values provided...
110 """
111 #--------------------------------------------------------------
112 def check(mn, mx, v):
113 """Returns new min, max, and float value for those passed in"""
114
115 v = float(v)
116 if mn == None or v < mn: mn = v
117 if mx == None or v > mx: mx = v
118 return mn, mx, v
119
120 #--------------------------------------------------------------
121 def avg(xs, ys):
122 """Computes running average for Xs and Ys"""
123
124 #------------------------------------------------------
125 def _avg(vals):
126 """Computes average for array of values passed"""
127
86c6ec28 128 return sum(vals) / len(vals)
726d2aee
AB
129
130 #------------------------------------------------------
131 if len(xs) < 1000:
132 return xs, ys
133
134 axs = [xs[0]]
135 ays = [ys[0]]
136 _xs = [xs[0]]
137 _ys = [ys[0]]
138
139 x_range = (xs[-1] - xs[0]) / 100
140 for idx in range(1, len(ys)):
141 if (xs[idx] - _xs[0]) > x_range:
142 axs.append(_avg(_xs))
143 ays.append(_avg(_ys))
144 del _xs, _ys
145
146 _xs = [xs[idx]]
147 _ys = [ys[idx]]
148 else:
149 _xs.append(xs[idx])
150 _ys.append(ys[idx])
151
152 if len(_xs) > 1:
153 axs.append(_avg(_xs))
154 ays.append(_avg(_ys))
155
156 return axs, ays
157
158 #--------------------------------------------------------------
159 global verbose
160
161 db = {}
162 min_x = max_x = min_y = max_y = None
163 for file in files:
164 if not os.path.exists(file):
165 fatal('%s not found' % file)
166 elif verbose:
70d5ca2d 167 print('Processing %s' % file)
726d2aee
AB
168
169 xs = []
170 ys = []
171 for line in open(file, 'r'):
172 f = line.rstrip().split(None)
173 if line.find('#') == 0 or len(f) < 2:
174 continue
175 (min_x, max_x, x) = check(min_x, max_x, f[0])
176 (min_y, max_y, y) = check(min_y, max_y, f[1])
177 xs.append(x)
178 ys.append(y)
179
180 db[file] = {'x':xs, 'y':ys}
181 if len(xs) > 10:
182 db[file]['ax'], db[file]['ay'] = avg(xs, ys)
183 else:
184 db[file]['ax'] = db[file]['ay'] = None
185
186 db['min_x'] = min_x
187 db['max_x'] = max_x
188 db['min_y'] = min_y
189 db['max_y'] = max_y
190 return db
191
192#----------------------------------------------------------------------
193def parse_args(args):
194 """Parse command line arguments.
195
196 Returns list of (data) files that need to be processed -- /unless/
197 the -A (--generate-all) option is passed, in which case superfluous
198 data files are ignored...
199 """
200
201 global add_legend, output_file, title_str, type, verbose
202 global generate_all
203
204 prog = args[0][args[0].rfind('/')+1:]
205 if prog == 'btt_plot.py':
206 pass
207 elif not prog in progs:
208 fatal('%s not a valid command name' % prog)
209 else:
210 type = prog[prog.rfind('_')+1:prog.rfind('.py')]
211
212 s_opts = 'ALo:t:T:v'
213 l_opts = [ 'generate-all', 'type', 'no-legend', 'output', 'title',
214 'verbose' ]
215
216 try:
217 (opts, args) = getopt.getopt(args[1:], s_opts, l_opts)
70d5ca2d
ES
218 except getopt.error as msg:
219 print(msg, file=sys.stderr)
726d2aee
AB
220 fatal(__doc__)
221
222 for (o, a) in opts:
223 if o in ('-A', '--generate-all'):
224 generate_all = True
225 elif o in ('-L', '--no-legend'):
226 add_legend = False
227 elif o in ('-o', '--output'):
228 output_file = a
229 elif o in ('-t', '--type'):
230 if not a in types:
231 fatal('Type %s not supported' % a)
232 type = a
233 elif o in ('-T', '--title'):
234 title_str = a
235 elif o in ('-v', '--verbose'):
236 verbose = True
237
238 if type == None and not generate_all:
239 fatal('Need type of data files to process - (-t <type>)')
240
241 return args
242
243#------------------------------------------------------------------------------
244def gen_title(fig, type, title_str):
245 """Sets the title for the figure based upon the type /or/ user title"""
246
247 if title_str != None:
248 pass
249 elif type == 'aqd':
250 title_str = 'Average Queue Depth'
251 elif type == 'bnos':
252 title_str = 'Block Numbers Accessed'
253 elif type == 'q2d':
254 title_str = 'Queue (Q) To Issue (D) Average Latencies'
255 elif type == 'd2c':
256 title_str = 'Issue (D) To Complete (C) Average Latencies'
257 elif type == 'q2c':
258 title_str = 'Queue (Q) To Complete (C) Average Latencies'
259
260 title = fig.text(.5, .95, title_str, horizontalalignment='center')
261 title.set_fontsize('large')
262
263#------------------------------------------------------------------------------
264def gen_labels(db, ax, type):
265 """Generate X & Y 'axis'"""
266
267 #----------------------------------------------------------------------
268 def gen_ylabel(ax, type):
269 """Set the Y axis label based upon the type"""
270
271 if type == 'aqd':
272 str = 'Number of Requests Queued'
273 elif type == 'bnos':
274 str = 'Block Number'
275 else:
276 str = 'Seconds'
277 ax.set_ylabel(str)
278
279 #----------------------------------------------------------------------
280 xdelta = 0.1 * (db['max_x'] - db['min_x'])
281 ydelta = 0.1 * (db['max_y'] - db['min_y'])
282
283 ax.set_xlim(db['min_x'] - xdelta, db['max_x'] + xdelta)
284 ax.set_ylim(db['min_y'] - ydelta, db['max_y'] + ydelta)
285 ax.set_xlabel('Runtime (seconds)')
286 ax.grid(True)
287 gen_ylabel(ax, type)
288
289#------------------------------------------------------------------------------
290def generate_output(type, db):
291 """Generate the output plot based upon the type and database"""
292
293 #----------------------------------------------------------------------
294 def color(idx, style):
295 """Returns a color/symbol type based upon the index passed."""
296
70d5ca2d 297 colors = [ 'b', 'g', 'r', 'c', 'm', 'y', 'k' ]
726d2aee
AB
298 l_styles = [ '-', ':', '--', '-.' ]
299 m_styles = [ 'o', '+', '.', ',', 's', 'v', 'x', '<', '>' ]
300
301 color = colors[idx % len(colors)]
302 if style == 'line':
70d5ca2d 303 style = l_styles[int((idx / len(l_styles)) % len(l_styles))]
726d2aee 304 elif style == 'marker':
70d5ca2d 305 style = m_styles[int((idx / len(m_styles)) % len(m_styles))]
726d2aee
AB
306
307 return '%s%s' % (color, style)
308
726d2aee
AB
309 #----------------------------------------------------------------------
310 global add_legend, output_file, title_str, verbose
311
312 if output_file != None:
313 ofile = output_file
314 else:
315 ofile = '%s.png' % type
316
317 if verbose:
70d5ca2d 318 print('Generating plot into %s' % ofile)
726d2aee
AB
319
320 fig = plt.figure(figsize=plot_size)
321 ax = fig.add_subplot(111)
322
323 gen_title(fig, type, title_str)
324 gen_labels(db, ax, type)
325
326 idx = 0
327 if add_legend:
328 legends = []
329 else:
330 legends = None
331
332 keys = []
70d5ca2d 333 for file in six.iterkeys(db):
726d2aee
AB
334 if not file in ['min_x', 'max_x', 'min_y', 'max_y']:
335 keys.append(file)
336
337 keys.sort()
338 for file in keys:
339 dat = db[file]
340 if type == 'bnos':
341 ax.plot(dat['x'], dat['y'], color(idx, 'marker'),
342 markersize=1)
343 elif dat['ax'] == None:
344 continue # Don't add legend
345 else:
346 ax.plot(dat['ax'], dat['ay'], color(idx, 'line'),
347 linewidth=1.0)
348 if add_legend:
349 legends.append(get_base(file))
350 idx += 1
351
352 if add_legend and len(legends) > 0:
353 gen_legends(ax, legends)
354 plt.savefig(ofile)
355
356#------------------------------------------------------------------------------
357def get_files(type):
358 """Returns the list of files for the -A option based upon type"""
359
360 if type == 'bnos':
361 files = []
362 for fn in glob.glob('*c.dat'):
363 for t in [ 'q2q', 'd2d', 'q2c', 'd2c' ]:
364 if fn.find(t) >= 0:
365 break
366 else:
367 files.append(fn)
368 else:
369 files = glob.glob('*%s.dat' % type)
370 return files
371
2e37a10e
AB
372#------------------------------------------------------------------------------
373def do_bnos(files):
374 for file in files:
375 base = get_base(file)
376 title_str = 'Block Numbers Accessed: %s' % base
377 output_file = 'bnos_%s.png' % base
378 generate_output(t, get_data([file]))
379
380#------------------------------------------------------------------------------
381def do_live(files):
382 global plot_size
383
384 #----------------------------------------------------------------------
385 def get_live_data(fn):
386 xs = []
387 ys = []
388 for line in open(fn, 'r'):
389 f = line.rstrip().split()
390 if f[0] != '#' and len(f) == 2:
391 xs.append(float(f[0]))
392 ys.append(float(f[1]))
393 return xs, ys
394
395 #----------------------------------------------------------------------
396 def live_sort(a, b):
397 if a[0] == 'sys' and b[0] == 'sys':
398 return 0
399 elif a[0] == 'sys' or a[2][0] < b[2][0]:
400 return -1
401 elif b[0] == 'sys' or a[2][0] > b[2][0]:
402 return 1
403 else:
404 return 0
405
406 #----------------------------------------------------------------------
407 def turn_off_ticks(ax):
408 for tick in ax.xaxis.get_major_ticks():
409 tick.tick1On = tick.tick2On = False
410 for tick in ax.yaxis.get_major_ticks():
411 tick.tick1On = tick.tick2On = False
412 for tick in ax.xaxis.get_minor_ticks():
413 tick.tick1On = tick.tick2On = False
414 for tick in ax.yaxis.get_minor_ticks():
415 tick.tick1On = tick.tick2On = False
416
417 #----------------------------------------------------------------------
418 fig = plt.figure(figsize=plot_size)
419 ax = fig.add_subplot(111)
420
421 db = []
422 for fn in files:
423 if not os.path.exists(fn):
424 continue
425 (xs, ys) = get_live_data(fn)
426 db.append([fn[:fn.find('_live.dat')], xs, ys])
427 db.sort(live_sort)
428
429 for rec in db:
430 ax.plot(rec[1], rec[2])
431
432 gen_title(fig, 'live', 'Active I/O Per Device')
433 ax.set_xlabel('Runtime (seconds)')
434 ax.set_ylabel('Device')
435 ax.grid(False)
436
437 ax.set_xlim(-0.1, db[0][1][-1]+1)
438 ax.set_yticks([idx for idx in range(0, len(db))])
439 ax.yaxis.set_ticklabels([rec[0] for rec in db])
440 turn_off_ticks(ax)
441
442 plt.savefig('live.png')
443 plt.savefig('live.eps')
444
726d2aee
AB
445#------------------------------------------------------------------------------
446if __name__ == '__main__':
447 files = parse_args(sys.argv)
448
449 if generate_all:
450 output_file = title_str = type = None
451 for t in types:
452 files = get_files(t)
453 if len(files) == 0:
454 continue
2e37a10e
AB
455 elif t == 'bnos':
456 do_bnos(files)
457 elif t == 'live':
458 do_live(files)
459 else:
726d2aee
AB
460 generate_output(t, get_data(files))
461 continue
462
726d2aee
AB
463 elif len(files) < 1:
464 fatal('Need data files to process')
465 else:
466 generate_output(type, get_data(files))
467 sys.exit(0)