blob: 6645b2d21bebcff8fda4a0a670794c8fe9b971c4 [file] [log] [blame]
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +01001# 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
Gwendal Grignou29d2af52014-05-02 11:32:27 -07005"""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
Allen Li2c32d6b2017-02-03 15:28:10 -080017import json
18import logging
19import re
20
21import common
22from autotest_lib.client.bin import utils
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -070023
Puthikorn Voravootivat16961362014-05-07 15:57:27 -070024class 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
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -070076 def _parse_log_file(cls, file_name, pass_index, pass_count, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -070077 """
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:
Gwendal Grignou08de2382015-02-06 17:42:16 -0800104 data.sort(key=lambda x: x[1])
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700105 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]}
Gwendal Grignou08de2382015-02-06 17:42:16 -0800114 row[pass_index + 1] = {'v': d[1]}
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700115 all_row.append({'c': row[:]})
116
117 return all_row
118
119 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700120 def _gen_data_col(cls, pass_list, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700121 """
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:
Gwendal Grignou08de2382015-02-06 17:42:16 -0800133 col_name_list = ['percentile'] + [p[0] for p in pass_list]
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700134 else:
Gwendal Grignou08de2382015-02-06 17:42:16 -0800135 col_name_list = ['time'] + [p[0] for p in pass_list]
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700136
137 return [{'label': name, 'type': 'number'} for name in col_name_list]
138
139 @classmethod
Gwendal Grignou08de2382015-02-06 17:42:16 -0800140 def _gen_data_row(cls, test_type, pass_list, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700141 """
142 Generate row for google.visualization.DataTable by generate all log
143 file name and call _parse_log_file for each file
144
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700145 @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)
Gwendal Grignou08de2382015-02-06 17:42:16 -0800153 for pass_index, log_file_name in enumerate([p[1] for p in pass_list]):
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700154 all_row.extend(cls._parse_log_file(log_file_name, pass_index,
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700155 pass_count, percentile))
156 return all_row
157
158 @classmethod
Gwendal Grignou08de2382015-02-06 17:42:16 -0800159 def _write_data(cls, f, test_type, pass_list, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700160 """
161 Write google.visualization.DataTable object to output file.
162 https://developers.google.com/chart/interactive/docs/reference
163
Gwendal Grignou08de2382015-02-06 17:42:16 -0800164 @param f: html file to update
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700165 @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 """
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700169 col = cls._gen_data_col(pass_list, percentile)
Gwendal Grignou08de2382015-02-06 17:42:16 -0800170 row = cls._gen_data_row(test_type, pass_list, percentile)
171 data_dict = {'cols' : col, 'rows' : row}
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700172
173 f.write('var data = new google.visualization.DataTable(')
174 json.dump(data_dict, f)
175 f.write(');\n')
176
177 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700178 def _write_option(cls, f, test_name, test_type, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700179 """
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 """
Gwendal Grignou08de2382015-02-06 17:42:16 -0800187 option = {'pointSize': 1}
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700188 if percentile:
189 option['title'] = ('Percentile graph of %s for %s workload' %
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700190 (cls.graph_title[test_type], test_name))
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700191 else:
192 option['title'] = ('Graph of %s for %s workload over time' %
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700193 (cls.graph_title[test_type], test_name))
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700194
Gwendal Grignou08de2382015-02-06 17:42:16 -0800195 option['hAxis'] = {'title': cls.h_title[percentile]}
196 option['vAxis'] = {'title': cls.v_title[test_type]}
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700197
198 f.write('var options = ')
199 json.dump(option, f)
200 f.write(';\n')
201
202 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700203 def _write_graph(cls, test_name, test_type, pass_list, percentile=False):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700204 """
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:
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700222 f.write(cls.html_head)
Gwendal Grignou08de2382015-02-06 17:42:16 -0800223 cls._write_data(f, test_type, pass_list, percentile)
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700224 cls._write_option(f, test_name, test_type, percentile)
225 f.write(cls.html_tail)
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700226
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
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700245def 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:
Gwendal Grignou08de2382015-02-06 17:42:16 -0800257 return {}
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700258
Gwendal Grignou08de2382015-02-06 17:42:16 -0800259 results = {}
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700260 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
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700275def fio_parser(lines, prefix=None):
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700276 """
277 Parse the json fio output
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700278
279 This collects all metrics given by fio and labels them according to unit
280 of measurement and test case name.
281
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700282 @param lines: text output of json fio output.
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700283 @param prefix: prefix for result keys.
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700284 """
Gwendal Grignou08de2382015-02-06 17:42:16 -0800285 results = {}
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700286 fio_dict = json.loads(lines)
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700287
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700288 if prefix:
289 prefix = prefix + '_'
290 else:
291 prefix = ''
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700292
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700293 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():
Gwendal Grignou98546312017-03-21 17:02:22 -0700305 # Igonre "job options", its alphanumerc keys confuses tko.
306 # Besides, these keys are redundant.
307 if k == 'job options':
308 continue
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700309 results.update(fio_parse_dict({k:v}, job_prefix))
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700310
311 return results
312
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700313def 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)
Gwendal Grignou08de2382015-02-06 17:42:16 -0800323 logs = utils.system_output('ls *_%s.*log' % log_type, ignore_status=True)
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700324 if not logs:
325 continue
326
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700327 pattern = r"""(?P<jobname>.*)_ # jobname
Gwendal Grignou08de2382015-02-06 17:42:16 -0800328 ((?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
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700332 """
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')
Gwendal Grignou08de2382015-02-06 17:42:16 -0800345 runpass = match.group('runpass') or '1'
346 if match.group('thread'):
347 runpass += '_' + match.group('thread')
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700348
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 = []
Gwendal Grignou08de2382015-02-06 17:42:16 -0800356 pass_list.append((runpass, log))
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700357
358 if pass_list:
359 fio_graph_generator(current_job, log_type, pass_list).run()
360
361
Gwendal Grignou08de2382015-02-06 17:42:16 -0800362 cmd = 'mv *_%s.*log results' % log_type
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700363 utils.run(cmd, ignore_status=True)
364 utils.run('mv *.html results', ignore_status=True)
365
366
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700367def fio_runner(test, job, env_vars,
368 name_prefix=None,
369 graph_prefix=None):
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700370 """
371 Runs fio.
372
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700373 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
Puthikorn Voravootivat425b1a72014-05-14 17:33:27 -0700381 @param test: test to upload perf value
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700382 @param job: fio config file to use
383 @param env_vars: environment variable fio will substituete in the fio
384 config file.
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700385 @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.
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700389 @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'
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700401 options = ['--output-format=json']
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700402 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)
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700408
409 fio_generate_graph()
410
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700411 filename = re.match('.*FILENAME=(?P<f>[^ ]*)', env_vars).group('f')
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700412 diskname = utils.get_disk_from_filename(filename)
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700413
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700414 if diskname:
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700415 model = utils.get_disk_model(diskname)
416 size = utils.get_disk_size_gb(diskname)
417 perfdb_name = '%s_%dG' % (model, size)
418 else:
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700419 perfdb_name = filename.replace('/', '_')
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700420
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700421 if name_prefix:
422 perfdb_name = name_prefix + '_' + perfdb_name
423
Gwendal Grignou51d50692014-06-20 11:42:18 -0700424 result = fio_parser(fio.stdout, prefix=name_prefix)
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700425 if not graph_prefix:
426 graph_prefix = ''
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700427
Puthikorn Voravootivat425b1a72014-05-14 17:33:27 -0700428 for k, v in result.iteritems():
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700429 # Remove the prefix for value, and replace it the graph prefix.
Gwendal Grignou51d50692014-06-20 11:42:18 -0700430 if name_prefix:
431 k = k.replace('_' + name_prefix, graph_prefix)
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700432
433 # Make graph name to be same as the old code.
434 if k.endswith('bw'):
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700435 test.output_perf_value(description=perfdb_name, graph=k, value=v,
436 units='KB_per_sec', higher_is_better=True)
Puthikorn Voravootivatf5ebc662014-07-11 16:34:49 -0700437 elif k.rstrip('0').endswith('clat_percentile_99.'):
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700438 test.output_perf_value(description=perfdb_name, graph=k, value=v,
439 units='us', higher_is_better=False)
Puthikorn Voravootivat425b1a72014-05-14 17:33:27 -0700440 return result