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