only thing that strikes me is whether the gnuplot support should be abstracted out a bit. See tko/plotgraph.py ? On Mon, May 3, 2010 at 2:52 PM, Lucas Meneghel Rodrigues <lmr@xxxxxxxxxx> wrote: > This module contains code to postprocess IOzone data > in a convenient way so we can generate performance graphs > and condensed data. The graph generation part depends > on gnuplot, but if the utility is not present, > functionality will gracefully degrade. > > Use the postprocessing module introduced on the previous > patch, use it to analyze results and write performance > graphs and performance tables. > > Also, in order for other tests to be able to use the > postprocessing code, added the right __init__.py > files, so a simple > > from autotest_lib.client.tests.iozone import postprocessing > > will work > > Note: Martin, as patch will ignore and not create the > zero-sized files (high time we move to git), if the changes > look good to you I can commit them all at once, making sure > all files are created. > > Signed-off-by: Lucas Meneghel Rodrigues <lmr@xxxxxxxxxx> > --- > client/tests/iozone/common.py | 8 + > client/tests/iozone/iozone.py | 25 ++- > client/tests/iozone/postprocessing.py | 487 +++++++++++++++++++++++++++++++++ > 3 files changed, 515 insertions(+), 5 deletions(-) > create mode 100644 client/tests/__init__.py > create mode 100644 client/tests/iozone/__init__.py > create mode 100644 client/tests/iozone/common.py > create mode 100755 client/tests/iozone/postprocessing.py > > diff --git a/client/tests/__init__.py b/client/tests/__init__.py > new file mode 100644 > index 0000000..e69de29 > diff --git a/client/tests/iozone/__init__.py b/client/tests/iozone/__init__.py > new file mode 100644 > index 0000000..e69de29 > diff --git a/client/tests/iozone/common.py b/client/tests/iozone/common.py > new file mode 100644 > index 0000000..ce78b85 > --- /dev/null > +++ b/client/tests/iozone/common.py > @@ -0,0 +1,8 @@ > +import os, sys > +dirname = os.path.dirname(sys.modules[__name__].__file__) > +client_dir = os.path.abspath(os.path.join(dirname, "..", "..")) > +sys.path.insert(0, client_dir) > +import setup_modules > +sys.path.pop(0) > +setup_modules.setup(base_path=client_dir, > + root_module_name="autotest_lib.client") > diff --git a/client/tests/iozone/iozone.py b/client/tests/iozone/iozone.py > index fa3fba4..03c2c04 100755 > --- a/client/tests/iozone/iozone.py > +++ b/client/tests/iozone/iozone.py > @@ -1,5 +1,6 @@ > import os, re > from autotest_lib.client.bin import test, utils > +import postprocessing > > > class iozone(test.test): > @@ -63,17 +64,19 @@ class iozone(test.test): > self.results = utils.system_output('%s %s' % (cmd, args)) > self.auto_mode = ("-a" in args) > > - path = os.path.join(self.resultsdir, 'raw_output_%s' % self.iteration) > - raw_output_file = open(path, 'w') > - raw_output_file.write(self.results) > - raw_output_file.close() > + self.results_path = os.path.join(self.resultsdir, > + 'raw_output_%s' % self.iteration) > + self.analysisdir = os.path.join(self.resultsdir, > + 'analysis_%s' % self.iteration) > + > + utils.open_write_close(self.results_path, self.results) > > > def __get_section_name(self, desc): > return desc.strip().replace(' ', '_') > > > - def postprocess_iteration(self): > + def generate_keyval(self): > keylist = {} > > if self.auto_mode: > @@ -150,3 +153,15 @@ class iozone(test.test): > keylist[key_name] = result > > self.write_perf_keyval(keylist) > + > + > + def postprocess_iteration(self): > + self.generate_keyval() > + if self.auto_mode: > + a = postprocessing.IOzoneAnalyzer(list_files=[self.results_path], > + output_dir=self.analysisdir) > + a.analyze() > + p = postprocessing.IOzonePlotter(results_file=self.results_path, > + output_dir=self.analysisdir) > + p.plot_all() > + > diff --git a/client/tests/iozone/postprocessing.py b/client/tests/iozone/postprocessing.py > new file mode 100755 > index 0000000..c995aea > --- /dev/null > +++ b/client/tests/iozone/postprocessing.py > @@ -0,0 +1,487 @@ > +#!/usr/bin/python > +""" > +Postprocessing module for IOzone. It is capable to pick results from an > +IOzone run, calculate the geometric mean for all throughput results for > +a given file size or record size, and then generate a series of 2D and 3D > +graphs. The graph generation functionality depends on gnuplot, and if it > +is not present, functionality degrates gracefully. > + > +@copyright: Red Hat 2010 > +""" > +import os, sys, optparse, logging, math, time > +import common > +from autotest_lib.client.common_lib import logging_config, logging_manager > +from autotest_lib.client.common_lib import error > +from autotest_lib.client.bin import utils, os_dep > + > + > +_LABELS = ['file_size', 'record_size', 'write', 'rewrite', 'read', 'reread', > + 'randread', 'randwrite', 'bkwdread', 'recordrewrite', 'strideread', > + 'fwrite', 'frewrite', 'fread', 'freread'] > + > + > +def unique(list): > + """ > + Return a list of the elements in list, but without duplicates. > + > + @param list: List with values. > + @return: List with non duplicate elements. > + """ > + n = len(list) > + if n == 0: > + return [] > + u = {} > + try: > + for x in list: > + u[x] = 1 > + except TypeError: > + return None > + else: > + return u.keys() > + > + > +def geometric_mean(values): > + """ > + Evaluates the geometric mean for a list of numeric values. > + > + @param values: List with values. > + @return: Single value representing the geometric mean for the list values. > + @see: http://en.wikipedia.org/wiki/Geometric_mean > + """ > + try: > + values = [int(value) for value in values] > + except ValueError: > + return None > + product = 1 > + n = len(values) > + if n == 0: > + return None > + return math.exp(sum([math.log(x) for x in values])/n) > + > + > +def compare_matrices(matrix1, matrix2, treshold=0.05): > + """ > + Compare 2 matrices nxm and return a matrix nxm with comparison data > + > + @param matrix1: Reference Matrix with numeric data > + @param matrix2: Matrix that will be compared > + @param treshold: Any difference bigger than this percent treshold will be > + reported. > + """ > + improvements = 0 > + regressions = 0 > + same = 0 > + comparison_matrix = [] > + > + new_matrix = [] > + for line1, line2 in zip(matrix1, matrix2): > + new_line = [] > + for element1, element2 in zip(line1, line2): > + ratio = float(element2) / float(element1) > + if ratio < (1 - treshold): > + regressions += 1 > + new_line.append((100 * ratio - 1) - 100) > + elif ratio > (1 + treshold): > + improvements += 1 > + new_line.append("+" + str((100 * ratio - 1) - 100)) > + else: > + same + 1 > + if line1.index(element1) == 0: > + new_line.append(element1) > + else: > + new_line.append(".") > + new_matrix.append(new_line) > + > + total = improvements + regressions + same > + > + return (new_matrix, improvements, regressions, total) > + > + > +class IOzoneAnalyzer(object): > + """ > + Analyze an unprocessed IOzone file, and generate the following types of > + report: > + > + * Summary of throughput for all file and record sizes combined > + * Summary of throughput for all file sizes > + * Summary of throughput for all record sizes > + > + If more than one file is provided to the analyzer object, a comparison > + between the two runs is made, searching for regressions in performance. > + """ > + def __init__(self, list_files, output_dir): > + self.list_files = list_files > + if not os.path.isdir(output_dir): > + os.makedirs(output_dir) > + self.output_dir = output_dir > + logging.info("Results will be stored in %s", output_dir) > + > + > + def average_performance(self, results, size=None): > + """ > + Flattens a list containing performance results. > + > + @param results: List of n lists containing data from performance runs. > + @param size: Numerical value of a size (say, file_size) that was used > + to filter the original results list. > + @return: List with 1 list containing average data from the performance > + run. > + """ > + average_line = [] > + if size is not None: > + average_line.append(size) > + for i in range(2, 15): > + average = geometric_mean([line[i] for line in results]) / 1024.0 > + average = int(average) > + average_line.append(average) > + return average_line > + > + > + def process_results(self, results, label=None): > + """ > + Process a list of IOzone results according to label. > + > + @label: IOzone column label that we'll use to filter and compute > + geometric mean results, in practical term either 'file_size' > + or 'record_size'. > + @result: A list of n x m columns with original iozone results. > + @return: A list of n-? x (m-1) columns with geometric averages for > + values of each label (ex, average for all file_sizes). > + """ > + performance = [] > + if label is not None: > + index = _LABELS.index(label) > + sizes = unique([line[index] for line in results]) > + sizes.sort() > + for size in sizes: > + r_results = [line for line in results if line[index] == size] > + performance.append(self.average_performance(r_results, size)) > + else: > + performance.append(self.average_performance(results)) > + > + return performance > + > + > + def parse_file(self, file): > + """ > + Parse an IOzone results file. > + > + @param file: File object that will be parsed. > + @return: Matrix containing IOzone results extracted from the file. > + """ > + lines = [] > + for line in file.readlines(): > + fields = line.split() > + if len(fields) != 15: > + continue > + try: > + lines.append([int(i) for i in fields]) > + except ValueError: > + continue > + return lines > + > + > + def report(self, overall_results, record_size_results, file_size_results): > + """ > + Generates analysis data for IOZone run. > + > + Generates a report to both logs (where it goes with nice headers) and > + output files for further processing (graph generation). > + > + @param overall_results: 1x15 Matrix containing IOzone results for all > + file sizes > + @param record_size_results: nx15 Matrix containing IOzone results for > + each record size tested. > + @param file_size_results: nx15 Matrix containing file size results > + for each file size tested. > + """ > + # Here we'll use the logging system to put the output of our analysis > + # to files > + logger = logging.getLogger() > + formatter = logging.Formatter("") > + > + logging.info("") > + logging.info("TABLE: SUMMARY of ALL FILE and RECORD SIZES Results in MB/sec") > + logging.info("") > + logging.info("FILE & RECORD INIT RE RE RANDOM RANDOM BACKWD RECRE STRIDE F FRE F FRE") > + logging.info("SIZES (KB) WRITE WRITE READ READ READ WRITE READ WRITE READ WRITE WRITE READ READ") > + logging.info("-------------------------------------------------------------------------------------------------------------------") > + for result_line in overall_results: > + logging.info("ALL %-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s" % tuple(result_line)) > + logging.info("") > + > + logging.info("DRILLED DATA:") > + > + logging.info("") > + logging.info("TABLE: RECORD Size against all FILE Sizes Results in MB/sec") > + logging.info("") > + logging.info("RECORD INIT RE RE RANDOM RANDOM BACKWD RECRE STRIDE F FRE F FRE ") > + logging.info("SIZE (KB) WRITE WRITE READ READ READ WRITE READ WRITE READ WRITE WRITE READ READ") > + logging.info("--------------------------------------------------------------------------------------------------------------") > + > + foutput_path = os.path.join(self.output_dir, '2d-datasource-file') > + if os.path.isfile(foutput_path): > + os.unlink(foutput_path) > + foutput = logging.FileHandler(foutput_path) > + foutput.setFormatter(formatter) > + logger.addHandler(foutput) > + for result_line in record_size_results: > + logging.info("%-10s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s" % tuple(result_line)) > + logger.removeHandler(foutput) > + > + logging.info("") > + > + logging.info("") > + logging.info("TABLE: FILE Size against all RECORD Sizes Results in MB/sec") > + logging.info("") > + logging.info("RECORD INIT RE RE RANDOM RANDOM BACKWD RECRE STRIDE F FRE F FRE ") > + logging.info("SIZE (KB) WRITE WRITE READ READ READ WRITE READ WRITE READ WRITE WRITE READ READ") > + logging.info("--------------------------------------------------------------------------------------------------------------") > + > + routput_path = os.path.join(self.output_dir, '2d-datasource-record') > + if os.path.isfile(routput_path): > + os.unlink(routput_path) > + routput = logging.FileHandler(routput_path) > + routput.setFormatter(formatter) > + logger.addHandler(routput) > + for result_line in file_size_results: > + logging.info("%-10s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s" % tuple(result_line)) > + logger.removeHandler(routput) > + > + logging.info("") > + > + > + def report_comparison(self, record, file): > + """ > + Generates comparison data for 2 IOZone runs. > + > + It compares 2 sets of nxm results and outputs a table with differences. > + If a difference higher or smaller than 5% is found, a warning is > + triggered. > + > + @param record: Tuple with 4 elements containing results for record size. > + @param file: Tuple with 4 elements containing results for file size. > + """ > + (record_size, record_improvements, record_regressions, > + record_total) = record > + (file_size, file_improvements, file_regressions, > + file_total) = file > + logging.info("ANALYSIS of DRILLED DATA:") > + > + logging.info("") > + logging.info("TABLE: RECsize Difference between runs Results are % DIFF") > + logging.info("") > + logging.info("RECORD INIT RE RE RANDOM RANDOM BACKWD RECRE STRIDE F FRE F FRE ") > + logging.info("SIZE (KB) WRITE WRITE READ READ READ WRITE READ WRITE READ WRITE WRITE READ READ") > + logging.info("--------------------------------------------------------------------------------------------------------------") > + for result_line in record_size: > + logging.info("%-10s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s" % tuple(result_line)) > + logging.info("REGRESSIONS: %d (%.2f%%) Improvements: %d (%.2f%%)", > + record_regressions, > + (100 * record_regressions/float(record_total)), > + record_improvements, > + (100 * record_improvements/float(record_total))) > + logging.info("") > + > + logging.info("") > + logging.info("TABLE: FILEsize Difference between runs Results are % DIFF") > + logging.info("") > + logging.info("RECORD INIT RE RE RANDOM RANDOM BACKWD RECRE STRIDE F FRE F FRE ") > + logging.info("SIZE (KB) WRITE WRITE READ READ READ WRITE READ WRITE READ WRITE WRITE READ READ") > + logging.info("--------------------------------------------------------------------------------------------------------------") > + for result_line in file_size: > + logging.info("%-10s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s%-8.6s" % tuple(result_line)) > + logging.info("REGRESSIONS: %d (%.2f%%) Improvements: %d (%.2f%%)", > + file_regressions, > + (100 * file_regressions/float(file_total)), > + file_improvements, > + (100 * file_improvements/float(file_total))) > + logging.info("") > + > + > + def analyze(self): > + """ > + Analyzes and eventually compares sets of IOzone data. > + """ > + overall = [] > + record_size = [] > + file_size = [] > + for path in self.list_files: > + file = open(path, 'r') > + logging.info('FILE: %s', path) > + > + results = self.parse_file(file) > + > + overall_results = self.process_results(results) > + record_size_results = self.process_results(results, 'record_size') > + file_size_results = self.process_results(results, 'file_size') > + self.report(overall_results, record_size_results, file_size_results) > + > + if len(self.list_files) == 2: > + overall.append(overall_results) > + record_size.append(record_size_results) > + file_size.append(file_size_results) > + > + if len(self.list_files) == 2: > + record_comparison = compare_matrices(*record_size) > + file_comparison = compare_matrices(*file_size) > + self.report_comparison(record_comparison, file_comparison) > + > + > +class IOzonePlotter(object): > + """ > + Plots graphs based on the results of an IOzone run. > + > + Plots graphs based on the results of an IOzone run. Uses gnuplot to > + generate the graphs. > + """ > + def __init__(self, results_file, output_dir): > + self.active = True > + try: > + self.gnuplot = os_dep.command("gnuplot") > + except: > + logging.error("Command gnuplot not found, disabling graph " > + "generation") > + self.active = False > + > + if not os.path.isdir(output_dir): > + os.makedirs(output_dir) > + self.output_dir = output_dir > + > + if not os.path.isfile(results_file): > + logging.error("Invalid file %s provided, disabling graph " > + "generation", results_file) > + self.active = False > + self.results_file = None > + else: > + self.results_file = results_file > + self.generate_data_source() > + > + > + def generate_data_source(self): > + """ > + Creates data file without headers for gnuplot consumption. > + """ > + results_file = open(self.results_file, 'r') > + self.datasource = os.path.join(self.output_dir, '3d-datasource') > + datasource = open(self.datasource, 'w') > + for line in results_file.readlines(): > + fields = line.split() > + if len(fields) != 15: > + continue > + try: > + values = [int(i) for i in fields] > + datasource.write(line) > + except ValueError: > + continue > + datasource.close() > + > + > + def plot_2d_graphs(self): > + """ > + For each one of the throughput parameters, generate a set of gnuplot > + commands that will create a parametric surface with file size vs. > + record size vs. throughput. > + """ > + datasource_2d = os.path.join(self.output_dir, '2d-datasource-file') > + for index, label in zip(range(1, 14), _LABELS[2:]): > + commands_path = os.path.join(self.output_dir, '2d-%s.do' % label) > + commands = "" > + commands += "set title 'Iozone performance: %s'\n" % label > + commands += "set logscale x\n" > + commands += "set xlabel 'File size (KB)'\n" > + commands += "set ylabel 'Througput (MB/s)'\n" > + commands += "set terminal png small size 450 350\n" > + commands += "set output '%s'\n" % os.path.join(self.output_dir, > + '2d-%s.png' % label) > + commands += ("plot '%s' using 1:%s title '%s' with lines \n" % > + (datasource_2d, index, label)) > + commands_file = open(commands_path, 'w') > + commands_file.write(commands) > + commands_file.close() > + try: > + utils.run("%s %s" % (self.gnuplot, commands_path)) > + except error.CmdError, e: > + logging.error("Problem plotting from commands file %s: %s", > + commands_file, str(e)) > + > + > + def plot_3d_graphs(self): > + """ > + For each one of the throughput parameters, generate a set of gnuplot > + commands that will create a parametric surface with file size vs. > + record size vs. throughput. > + """ > + for index, label in zip(range(1, 14), _LABELS[2:]): > + commands_path = os.path.join(self.output_dir, '%s.do' % label) > + commands = "" > + commands += "set title 'Iozone performance: %s'\n" % label > + commands += "set grid lt 2 lw 1\n" > + commands += "set surface\n" > + commands += "set parametric\n" > + commands += "set xtics\n" > + commands += "set ytics\n" > + commands += "set logscale x 2\n" > + commands += "set logscale y 2\n" > + commands += "set logscale z\n" > + commands += "set xrange [2.**5:2.**24]\n" > + commands += "set xlabel 'File size (KB)'\n" > + commands += "set ylabel 'Record size (KB)'\n" > + commands += "set zlabel 'Througput (KB/s)'\n" > + commands += "set data style lines\n" > + commands += "set dgrid3d 80,80, 3\n" > + commands += "set terminal png small size 900 700\n" > + commands += "set output '%s'\n" % os.path.join(self.output_dir, > + '%s.png' % label) > + commands += ("splot '%s' using 1:2:%s title '%s'\n" % > + (self.datasource, index, label)) > + commands_file = open(commands_path, 'w') > + commands_file.write(commands) > + commands_file.close() > + try: > + utils.run("%s %s" % (self.gnuplot, commands_path)) > + except error.CmdError, e: > + logging.error("Problem plotting from commands file %s: %s", > + commands_file, str(e)) > + > + > + def plot_all(self): > + """ > + Plot all graphs that are to be plotted, provided that we have gnuplot. > + """ > + if self.active: > + self.plot_2d_graphs() > + self.plot_3d_graphs() > + > + > +class AnalyzerLoggingConfig(logging_config.LoggingConfig): > + def configure_logging(self, results_dir=None, verbose=False): > + super(AnalyzerLoggingConfig, self).configure_logging(use_console=True, > + verbose=verbose) > + > + > +if __name__ == "__main__": > + parser = optparse.OptionParser("usage: %prog [options] [filenames]") > + options, args = parser.parse_args() > + > + logging_manager.configure_logging(AnalyzerLoggingConfig()) > + > + if args: > + filenames = args > + else: > + parser.print_help() > + sys.exit(1) > + > + if len(args) > 2: > + parser.print_help() > + sys.exit(1) > + > + o = os.path.join(os.getcwd(), > + "iozone-graphs-%s" % time.strftime('%Y-%m-%d-%H.%M.%S')) > + if not os.path.isdir(o): > + os.makedirs(o) > + > + a = IOzoneAnalyzer(list_files=filenames, output_dir=o) > + a.analyze() > + p = IOzonePlotter(results_file=filenames[0], output_dir=o) > + p.plot_all() > -- > 1.7.0.1 > > -- To unsubscribe from this list: send the line "unsubscribe kvm" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html