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