• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
7fio_runner launch fio and collect results.
8The 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
13Decoding class can be invoked independently.
14
15"""
16
17import json, logging, re, utils
18
19class 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
240def 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
270def 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
304def 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
358def 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