Home | History | Annotate | Download | only in bin
      1 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Library to run fio scripts.
      6 
      7 fio_runner launch fio and collect results.
      8 The output dictionary can be add to autotest keyval:
      9         results = {}
     10         results.update(fio_util.fio_runner(job_file, env_vars))
     11         self.write_perf_keyval(results)
     12 
     13 Decoding class can be invoked independently.
     14 
     15 """
     16 
     17 import json, logging, re, utils
     18 
     19 class fio_graph_generator():
     20     """
     21     Generate graph from fio log that created when specified these options.
     22     - write_bw_log
     23     - write_iops_log
     24     - write_lat_log
     25 
     26     The following limitations apply
     27     - Log file name must be in format jobname_testpass
     28     - Graph is generate using Google graph api -> Internet require to view.
     29     """
     30 
     31     html_head = """
     32 <html>
     33   <head>
     34     <script type="text/javascript" src="https://www.google.com/jsapi"></script>
     35     <script type="text/javascript">
     36       google.load("visualization", "1", {packages:["corechart"]});
     37       google.setOnLoadCallback(drawChart);
     38       function drawChart() {
     39 """
     40 
     41     html_tail = """
     42         var chart_div = document.getElementById('chart_div');
     43         var chart = new google.visualization.ScatterChart(chart_div);
     44         chart.draw(data, options);
     45       }
     46     </script>
     47   </head>
     48   <body>
     49     <div id="chart_div" style="width: 100%; height: 100%;"></div>
     50   </body>
     51 </html>
     52 """
     53 
     54     h_title = { True: 'Percentile', False: 'Time (s)' }
     55     v_title = { 'bw'  : 'Bandwidth (KB/s)',
     56                 'iops': 'IOPs',
     57                 'lat' : 'Total latency (us)',
     58                 'clat': 'Completion latency (us)',
     59                 'slat': 'Submission latency (us)' }
     60     graph_title = { 'bw'  : 'bandwidth',
     61                     'iops': 'IOPs',
     62                     'lat' : 'total latency',
     63                     'clat': 'completion latency',
     64                     'slat': 'submission latency' }
     65 
     66     test_name = ''
     67     test_type = ''
     68     pass_list = ''
     69 
     70     @classmethod
     71     def _parse_log_file(cls, file_name, pass_index, pass_count, percentile):
     72         """
     73         Generate row for google.visualization.DataTable from one log file.
     74         Log file is the one that generated using write_{bw,lat,iops}_log
     75         option in the FIO job file.
     76 
     77         The fio log file format is  timestamp, value, direction, blocksize
     78         The output format for each row is { c: list of { v: value} }
     79 
     80         @param file_name:  log file name to read data from
     81         @param pass_index: index of current run pass
     82         @param pass_count: number of all test run passes
     83         @param percentile: flag to use percentile as key instead of timestamp
     84 
     85         @return: list of data rows in google.visualization.DataTable format
     86         """
     87         # Read data from log
     88         with open(file_name, 'r') as f:
     89             data = []
     90 
     91             for line in f.readlines():
     92                 if not line:
     93                     break
     94                 t, v, _, _ = [int(x) for x in line.split(', ')]
     95                 data.append([t / 1000.0, v])
     96 
     97         # Sort & calculate percentile
     98         if percentile:
     99             data.sort(key=lambda x: x[1])
    100             l = len(data)
    101             for i in range(l):
    102                 data[i][0] = 100 * (i + 0.5) / l
    103 
    104         # Generate the data row
    105         all_row = []
    106         row = [None] * (pass_count + 1)
    107         for d in data:
    108             row[0] = {'v' : '%.3f' % d[0]}
    109             row[pass_index + 1] = {'v': d[1]}
    110             all_row.append({'c': row[:]})
    111 
    112         return all_row
    113 
    114     @classmethod
    115     def _gen_data_col(cls, pass_list, percentile):
    116         """
    117         Generate col for google.visualization.DataTable
    118 
    119         The output format is list of dict of label and type. In this case,
    120         type is always number.
    121 
    122         @param pass_list:  list of test run passes
    123         @param percentile: flag to use percentile as key instead of timestamp
    124 
    125         @return: list of column in google.visualization.DataTable format
    126         """
    127         if percentile:
    128             col_name_list = ['percentile'] + [p[0] for p in pass_list]
    129         else:
    130             col_name_list = ['time'] + [p[0] for p in pass_list]
    131 
    132         return [{'label': name, 'type': 'number'} for name in col_name_list]
    133 
    134     @classmethod
    135     def _gen_data_row(cls, test_type, pass_list, percentile):
    136         """
    137         Generate row for google.visualization.DataTable by generate all log
    138         file name and call _parse_log_file for each file
    139 
    140         @param test_type: type of value collected for current test. i.e. IOPs
    141         @param pass_list: list of run passes for current test
    142         @param percentile: flag to use percentile as key instead of timestamp
    143 
    144         @return: list of data rows in google.visualization.DataTable format
    145         """
    146         all_row = []
    147         pass_count = len(pass_list)
    148         for pass_index, log_file_name in enumerate([p[1] for p in pass_list]):
    149             all_row.extend(cls._parse_log_file(log_file_name, pass_index,
    150                                                 pass_count, percentile))
    151         return all_row
    152 
    153     @classmethod
    154     def _write_data(cls, f, test_type, pass_list, percentile):
    155         """
    156         Write google.visualization.DataTable object to output file.
    157         https://developers.google.com/chart/interactive/docs/reference
    158 
    159         @param f: html file to update
    160         @param test_type: type of value collected for current test. i.e. IOPs
    161         @param pass_list: list of run passes for current test
    162         @param percentile: flag to use percentile as key instead of timestamp
    163         """
    164         col = cls._gen_data_col(pass_list, percentile)
    165         row = cls._gen_data_row(test_type, pass_list, percentile)
    166         data_dict = {'cols' : col, 'rows' : row}
    167 
    168         f.write('var data = new google.visualization.DataTable(')
    169         json.dump(data_dict, f)
    170         f.write(');\n')
    171 
    172     @classmethod
    173     def _write_option(cls, f, test_name, test_type, percentile):
    174         """
    175         Write option to render scatter graph to output file.
    176         https://google-developers.appspot.com/chart/interactive/docs/gallery/scatterchart
    177 
    178         @param test_name: name of current workload. i.e. randwrite
    179         @param test_type: type of value collected for current test. i.e. IOPs
    180         @param percentile: flag to use percentile as key instead of timestamp
    181         """
    182         option = {'pointSize': 1}
    183         if percentile:
    184             option['title'] = ('Percentile graph of %s for %s workload' %
    185                                (cls.graph_title[test_type], test_name))
    186         else:
    187             option['title'] = ('Graph of %s for %s workload over time' %
    188                                (cls.graph_title[test_type], test_name))
    189 
    190         option['hAxis'] = {'title': cls.h_title[percentile]}
    191         option['vAxis'] = {'title': cls.v_title[test_type]}
    192 
    193         f.write('var options = ')
    194         json.dump(option, f)
    195         f.write(';\n')
    196 
    197     @classmethod
    198     def _write_graph(cls, test_name, test_type, pass_list, percentile=False):
    199         """
    200         Generate graph for test name / test type
    201 
    202         @param test_name: name of current workload. i.e. randwrite
    203         @param test_type: type of value collected for current test. i.e. IOPs
    204         @param pass_list: list of run passes for current test
    205         @param percentile: flag to use percentile as key instead of timestamp
    206         """
    207         logging.info('fio_graph_generator._write_graph %s %s %s',
    208                      test_name, test_type, str(pass_list))
    209 
    210 
    211         if percentile:
    212             out_file_name = '%s_%s_percentile.html' % (test_name, test_type)
    213         else:
    214             out_file_name = '%s_%s.html' % (test_name, test_type)
    215 
    216         with open(out_file_name, 'w') as f:
    217             f.write(cls.html_head)
    218             cls._write_data(f, test_type, pass_list, percentile)
    219             cls._write_option(f, test_name, test_type, percentile)
    220             f.write(cls.html_tail)
    221 
    222     def __init__(self, test_name, test_type, pass_list):
    223         """
    224         @param test_name: name of current workload. i.e. randwrite
    225         @param test_type: type of value collected for current test. i.e. IOPs
    226         @param pass_list: list of run passes for current test
    227         """
    228         self.test_name = test_name
    229         self.test_type = test_type
    230         self.pass_list = pass_list
    231 
    232     def run(self):
    233         """
    234         Run the graph generator.
    235         """
    236         self._write_graph(self.test_name, self.test_type, self.pass_list, False)
    237         self._write_graph(self.test_name, self.test_type, self.pass_list, True)
    238 
    239 
    240 def fio_parse_dict(d, prefix):
    241     """
    242     Parse fio json dict
    243 
    244     Recursively flaten json dict to generate autotest perf dict
    245 
    246     @param d: input dict
    247     @param prefix: name prefix of the key
    248     """
    249 
    250     # No need to parse something that didn't run such as read stat in write job.
    251     if 'io_bytes' in d and d['io_bytes'] == 0:
    252         return {}
    253 
    254     results = {}
    255     for k, v in d.items():
    256 
    257         # remove >, >=, <, <=
    258         for c in '>=<':
    259             k = k.replace(c, '')
    260 
    261         key = prefix + '_' + k
    262 
    263         if type(v) is dict:
    264             results.update(fio_parse_dict(v, key))
    265         else:
    266             results[key] = v
    267     return results
    268 
    269 
    270 def fio_parser(lines, prefix=None):
    271     """
    272     Parse the json fio output
    273 
    274     This collects all metrics given by fio and labels them according to unit
    275     of measurement and test case name.
    276 
    277     @param lines: text output of json fio output.
    278     @param prefix: prefix for result keys.
    279     """
    280     results = {}
    281     fio_dict = json.loads(lines)
    282 
    283     if prefix:
    284         prefix = prefix + '_'
    285     else:
    286         prefix = ''
    287 
    288     results[prefix + 'fio_version'] = fio_dict['fio version']
    289 
    290     if 'disk_util' in fio_dict:
    291         results.update(fio_parse_dict(fio_dict['disk_util'][0],
    292                                       prefix + 'disk'))
    293 
    294     for job in fio_dict['jobs']:
    295         job_prefix = '_' + prefix + job['jobname']
    296         job.pop('jobname')
    297 
    298 
    299         for k, v in job.iteritems():
    300             results.update(fio_parse_dict({k:v}, job_prefix))
    301 
    302     return results
    303 
    304 def fio_generate_graph():
    305     """
    306     Scan for fio log file in output directory and send data to generate each
    307     graph to fio_graph_generator class.
    308     """
    309     log_types = ['bw', 'iops', 'lat', 'clat', 'slat']
    310 
    311     # move fio log to result dir
    312     for log_type in log_types:
    313         logging.info('log_type %s', log_type)
    314         logs = utils.system_output('ls *_%s.*log' % log_type, ignore_status=True)
    315         if not logs:
    316             continue
    317 
    318         pattern = r"""(?P<jobname>.*)_                    # jobname
    319                       ((?P<runpass>p\d+)_|)               # pass
    320                       (?P<type>bw|iops|lat|clat|slat)     # type
    321                       (.(?P<thread>\d+)|)                 # thread id for newer fio.
    322                       .log
    323                    """
    324         matcher = re.compile(pattern, re.X)
    325 
    326         pass_list = []
    327         current_job = ''
    328 
    329         for log in logs.split():
    330             match = matcher.match(log)
    331             if not match:
    332                 logging.warn('Unknown log file %s', log)
    333                 continue
    334 
    335             jobname = match.group('jobname')
    336             runpass = match.group('runpass') or '1'
    337             if match.group('thread'):
    338                 runpass += '_' +  match.group('thread')
    339 
    340             # All files for particular job name are group together for create
    341             # graph that can compare performance between result from each pass.
    342             if jobname != current_job:
    343                 if pass_list:
    344                     fio_graph_generator(current_job, log_type, pass_list).run()
    345                 current_job = jobname
    346                 pass_list = []
    347             pass_list.append((runpass, log))
    348 
    349         if pass_list:
    350             fio_graph_generator(current_job, log_type, pass_list).run()
    351 
    352 
    353         cmd = 'mv *_%s.*log results' % log_type
    354         utils.run(cmd, ignore_status=True)
    355         utils.run('mv *.html results', ignore_status=True)
    356 
    357 
    358 def fio_runner(test, job, env_vars,
    359                name_prefix=None,
    360                graph_prefix=None):
    361     """
    362     Runs fio.
    363 
    364     Build a result keyval and performence json.
    365     The JSON would look like:
    366     {"description": "<name_prefix>_<modle>_<size>G",
    367      "graph": "<graph_prefix>_1m_write_wr_lat_99.00_percent_usec",
    368      "higher_is_better": false, "units": "us", "value": "xxxx"}
    369     {...
    370 
    371 
    372     @param test: test to upload perf value
    373     @param job: fio config file to use
    374     @param env_vars: environment variable fio will substituete in the fio
    375         config file.
    376     @param name_prefix: prefix of the descriptions to use in chrome perfi
    377         dashboard.
    378     @param graph_prefix: prefix of the graph name in chrome perf dashboard
    379         and result keyvals.
    380     @return fio results.
    381 
    382     """
    383 
    384     # running fio with ionice -c 3 so it doesn't lock out other
    385     # processes from the disk while it is running.
    386     # If you want to run the fio test for performance purposes,
    387     # take out the ionice and disable hung process detection:
    388     # "echo 0 > /proc/sys/kernel/hung_task_timeout_secs"
    389     # -c 3 = Idle
    390     # Tried lowest priority for "best effort" but still failed
    391     ionice = 'ionice -c 3'
    392     options = ['--output-format=json']
    393     fio_cmd_line = ' '.join([env_vars, ionice, 'fio',
    394                              ' '.join(options),
    395                              '"' + job + '"'])
    396     fio = utils.run(fio_cmd_line)
    397 
    398     logging.debug(fio.stdout)
    399 
    400     fio_generate_graph()
    401 
    402     filename = re.match('.*FILENAME=(?P<f>[^ ]*)', env_vars).group('f')
    403     diskname = utils.get_disk_from_filename(filename)
    404 
    405     if diskname:
    406         model = utils.get_disk_model(diskname)
    407         size = utils.get_disk_size_gb(diskname)
    408         perfdb_name = '%s_%dG' % (model, size)
    409     else:
    410         perfdb_name = filename.replace('/', '_')
    411 
    412     if name_prefix:
    413         perfdb_name = name_prefix + '_' + perfdb_name
    414 
    415     result = fio_parser(fio.stdout, prefix=name_prefix)
    416     if not graph_prefix:
    417         graph_prefix = ''
    418 
    419     for k, v in result.iteritems():
    420         # Remove the prefix for value, and replace it the graph prefix.
    421         if name_prefix:
    422             k = k.replace('_' + name_prefix, graph_prefix)
    423 
    424         # Make graph name to be same as the old code.
    425         if k.endswith('bw'):
    426             test.output_perf_value(description=perfdb_name, graph=k, value=v,
    427                                    units='KB_per_sec', higher_is_better=True)
    428         elif k.rstrip('0').endswith('clat_percentile_99.'):
    429             test.output_perf_value(description=perfdb_name, graph=k, value=v,
    430                                    units='us', higher_is_better=False)
    431     return result
    432