blob: 4014151cb06da75b6a27635c03fc8c71d6a3d430 [file] [log] [blame]
Derek Beckette9b63ce2020-09-15 13:09:04 -07001# Lint as: python2, python3
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +01002# 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
Gwendal Grignou29d2af52014-05-02 11:32:27 -07006"""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
Derek Beckette9b63ce2020-09-15 13:09:04 -070018from __future__ import absolute_import
19from __future__ import division
20from __future__ import print_function
21
Allen Li2c32d6b2017-02-03 15:28:10 -080022import json
23import logging
24import re
25
Derek Beckette9b63ce2020-09-15 13:09:04 -070026import six
27from six.moves import range
28
Allen Li2c32d6b2017-02-03 15:28:10 -080029import common
30from autotest_lib.client.bin import utils
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -070031
Derek Beckette9b63ce2020-09-15 13:09:04 -070032
Puthikorn Voravootivat16961362014-05-07 15:57:27 -070033class 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
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -070085 def _parse_log_file(cls, file_name, pass_index, pass_count, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -070086 """
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:
Gwendal Grignou08de2382015-02-06 17:42:16 -0800113 data.sort(key=lambda x: x[1])
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700114 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]}
Gwendal Grignou08de2382015-02-06 17:42:16 -0800123 row[pass_index + 1] = {'v': d[1]}
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700124 all_row.append({'c': row[:]})
125
126 return all_row
127
128 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700129 def _gen_data_col(cls, pass_list, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700130 """
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:
Gwendal Grignou08de2382015-02-06 17:42:16 -0800142 col_name_list = ['percentile'] + [p[0] for p in pass_list]
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700143 else:
Gwendal Grignou08de2382015-02-06 17:42:16 -0800144 col_name_list = ['time'] + [p[0] for p in pass_list]
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700145
146 return [{'label': name, 'type': 'number'} for name in col_name_list]
147
148 @classmethod
Gwendal Grignou08de2382015-02-06 17:42:16 -0800149 def _gen_data_row(cls, test_type, pass_list, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700150 """
151 Generate row for google.visualization.DataTable by generate all log
152 file name and call _parse_log_file for each file
153
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700154 @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)
Gwendal Grignou08de2382015-02-06 17:42:16 -0800162 for pass_index, log_file_name in enumerate([p[1] for p in pass_list]):
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700163 all_row.extend(cls._parse_log_file(log_file_name, pass_index,
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700164 pass_count, percentile))
165 return all_row
166
167 @classmethod
Gwendal Grignou08de2382015-02-06 17:42:16 -0800168 def _write_data(cls, f, test_type, pass_list, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700169 """
170 Write google.visualization.DataTable object to output file.
171 https://developers.google.com/chart/interactive/docs/reference
172
Gwendal Grignou08de2382015-02-06 17:42:16 -0800173 @param f: html file to update
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700174 @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 """
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700178 col = cls._gen_data_col(pass_list, percentile)
Gwendal Grignou08de2382015-02-06 17:42:16 -0800179 row = cls._gen_data_row(test_type, pass_list, percentile)
180 data_dict = {'cols' : col, 'rows' : row}
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700181
182 f.write('var data = new google.visualization.DataTable(')
183 json.dump(data_dict, f)
184 f.write(');\n')
185
186 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700187 def _write_option(cls, f, test_name, test_type, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700188 """
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 """
Gwendal Grignou08de2382015-02-06 17:42:16 -0800196 option = {'pointSize': 1}
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700197 if percentile:
198 option['title'] = ('Percentile graph of %s for %s workload' %
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700199 (cls.graph_title[test_type], test_name))
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700200 else:
201 option['title'] = ('Graph of %s for %s workload over time' %
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700202 (cls.graph_title[test_type], test_name))
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700203
Gwendal Grignou08de2382015-02-06 17:42:16 -0800204 option['hAxis'] = {'title': cls.h_title[percentile]}
205 option['vAxis'] = {'title': cls.v_title[test_type]}
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700206
207 f.write('var options = ')
208 json.dump(option, f)
209 f.write(';\n')
210
211 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700212 def _write_graph(cls, test_name, test_type, pass_list, percentile=False):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700213 """
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:
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700231 f.write(cls.html_head)
Gwendal Grignou08de2382015-02-06 17:42:16 -0800232 cls._write_data(f, test_type, pass_list, percentile)
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700233 cls._write_option(f, test_name, test_type, percentile)
234 f.write(cls.html_tail)
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700235
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
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700254def 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:
Gwendal Grignou08de2382015-02-06 17:42:16 -0800266 return {}
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700267
Gwendal Grignou08de2382015-02-06 17:42:16 -0800268 results = {}
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700269 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
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700284def fio_parser(lines, prefix=None):
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700285 """
286 Parse the json fio output
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700287
288 This collects all metrics given by fio and labels them according to unit
289 of measurement and test case name.
290
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700291 @param lines: text output of json fio output.
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700292 @param prefix: prefix for result keys.
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700293 """
Gwendal Grignou08de2382015-02-06 17:42:16 -0800294 results = {}
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700295 fio_dict = json.loads(lines)
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700296
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700297 if prefix:
298 prefix = prefix + '_'
299 else:
300 prefix = ''
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700301
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700302 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
Derek Beckette9b63ce2020-09-15 13:09:04 -0700313 for k, v in six.iteritems(job):
Gwendal Grignou98546312017-03-21 17:02:22 -0700314 # Igonre "job options", its alphanumerc keys confuses tko.
315 # Besides, these keys are redundant.
316 if k == 'job options':
317 continue
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700318 results.update(fio_parse_dict({k:v}, job_prefix))
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700319
320 return results
321
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700322def 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)
Gwendal Grignou08de2382015-02-06 17:42:16 -0800332 logs = utils.system_output('ls *_%s.*log' % log_type, ignore_status=True)
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700333 if not logs:
334 continue
335
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700336 pattern = r"""(?P<jobname>.*)_ # jobname
Gwendal Grignou08de2382015-02-06 17:42:16 -0800337 ((?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
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700341 """
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.warn('Unknown log file %s', log)
351 continue
352
353 jobname = match.group('jobname')
Gwendal Grignou08de2382015-02-06 17:42:16 -0800354 runpass = match.group('runpass') or '1'
355 if match.group('thread'):
356 runpass += '_' + match.group('thread')
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700357
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 = []
Gwendal Grignou08de2382015-02-06 17:42:16 -0800365 pass_list.append((runpass, log))
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700366
367 if pass_list:
368 fio_graph_generator(current_job, log_type, pass_list).run()
369
370
Gwendal Grignou08de2382015-02-06 17:42:16 -0800371 cmd = 'mv *_%s.*log results' % log_type
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700372 utils.run(cmd, ignore_status=True)
373 utils.run('mv *.html results', ignore_status=True)
374
375
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700376def fio_runner(test, job, env_vars,
377 name_prefix=None,
378 graph_prefix=None):
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700379 """
380 Runs fio.
381
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700382 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
Puthikorn Voravootivat425b1a72014-05-14 17:33:27 -0700390 @param test: test to upload perf value
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700391 @param job: fio config file to use
392 @param env_vars: environment variable fio will substituete in the fio
393 config file.
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700394 @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.
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700398 @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'
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700410 options = ['--output-format=json']
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700411 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)
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700417
418 fio_generate_graph()
419
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700420 filename = re.match('.*FILENAME=(?P<f>[^ ]*)', env_vars).group('f')
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700421 diskname = utils.get_disk_from_filename(filename)
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700422
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700423 if diskname:
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700424 model = utils.get_disk_model(diskname)
425 size = utils.get_disk_size_gb(diskname)
426 perfdb_name = '%s_%dG' % (model, size)
427 else:
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700428 perfdb_name = filename.replace('/', '_')
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700429
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700430 if name_prefix:
431 perfdb_name = name_prefix + '_' + perfdb_name
432
Gwendal Grignou51d50692014-06-20 11:42:18 -0700433 result = fio_parser(fio.stdout, prefix=name_prefix)
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700434 if not graph_prefix:
435 graph_prefix = ''
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700436
Derek Beckette9b63ce2020-09-15 13:09:04 -0700437 for k, v in six.iteritems(result):
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700438 # Remove the prefix for value, and replace it the graph prefix.
Gwendal Grignou51d50692014-06-20 11:42:18 -0700439 if name_prefix:
440 k = k.replace('_' + name_prefix, graph_prefix)
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700441
442 # Make graph name to be same as the old code.
443 if k.endswith('bw'):
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700444 test.output_perf_value(description=perfdb_name, graph=k, value=v,
445 units='KB_per_sec', higher_is_better=True)
David Jimenezf6b5f512020-08-20 15:59:23 +1000446 elif 'clat_percentile_' in k:
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700447 test.output_perf_value(description=perfdb_name, graph=k, value=v,
448 units='us', higher_is_better=False)
David Jimenezf6b5f512020-08-20 15:59:23 +1000449 elif 'clat_ns_percentile_' in k:
Alexis Savery959cc6d2018-08-13 11:39:31 -0700450 test.output_perf_value(description=perfdb_name, graph=k, value=v,
451 units='ns', higher_is_better=False)
Puthikorn Voravootivat425b1a72014-05-14 17:33:27 -0700452 return result