blob: 7eb29373a0b812278587c6bbe711045b0a987cc3 [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
Puthikorn Voravootivat16961362014-05-07 15:57:27 -070017import json, logging, re, utils
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -070018
Puthikorn Voravootivat16961362014-05-07 15:57:27 -070019class 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
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -070071 def _parse_log_file(cls, file_name, pass_index, pass_count, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -070072 """
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
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700115 def _gen_data_col(cls, pass_list, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700116 """
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'] + pass_list
129 else:
130 col_name_list = ['time'] + pass_list
131
132 return [{'label': name, 'type': 'number'} for name in col_name_list]
133
134 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700135 def _gen_data_row(cls, test_name, test_type, pass_list, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700136 """
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_name: name of current workload. i.e. randwrite
141 @param test_type: type of value collected for current test. i.e. IOPs
142 @param pass_list: list of run passes for current test
143 @param percentile: flag to use percentile as key instead of timestamp
144
145 @return: list of data rows in google.visualization.DataTable format
146 """
147 all_row = []
148 pass_count = len(pass_list)
149 for pass_index, pass_str in enumerate(pass_list):
150 log_file_name = str('%s_%s_%s.log' %
151 (test_name, pass_str, test_type))
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700152 all_row.extend(cls._parse_log_file(log_file_name, pass_index,
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700153 pass_count, percentile))
154 return all_row
155
156 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700157 def _write_data(cls, f, test_name, test_type, pass_list, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700158 """
159 Write google.visualization.DataTable object to output file.
160 https://developers.google.com/chart/interactive/docs/reference
161
162 @param test_name: name of current workload. i.e. randwrite
163 @param test_type: type of value collected for current test. i.e. IOPs
164 @param pass_list: list of run passes for current test
165 @param percentile: flag to use percentile as key instead of timestamp
166 """
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700167 col = cls._gen_data_col(pass_list, percentile)
168 row = cls._gen_data_row(test_name, test_type, pass_list, percentile)
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700169 data_dict = { 'cols' : col, 'rows' : row}
170
171 f.write('var data = new google.visualization.DataTable(')
172 json.dump(data_dict, f)
173 f.write(');\n')
174
175 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700176 def _write_option(cls, f, test_name, test_type, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700177 """
178 Write option to render scatter graph to output file.
179 https://google-developers.appspot.com/chart/interactive/docs/gallery/scatterchart
180
181 @param test_name: name of current workload. i.e. randwrite
182 @param test_type: type of value collected for current test. i.e. IOPs
183 @param percentile: flag to use percentile as key instead of timestamp
184 """
185 option = {'pointSize': 1 }
186 if percentile:
187 option['title'] = ('Percentile graph of %s for %s workload' %
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700188 (cls.graph_title[test_type], test_name))
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700189 else:
190 option['title'] = ('Graph of %s for %s workload over time' %
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700191 (cls.graph_title[test_type], test_name))
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700192
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700193 option['hAxis'] = { 'title': cls.h_title[percentile]}
194 option['vAxis'] = { 'title': cls.v_title[test_type]}
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700195
196 f.write('var options = ')
197 json.dump(option, f)
198 f.write(';\n')
199
200 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700201 def _write_graph(cls, test_name, test_type, pass_list, percentile=False):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700202 """
203 Generate graph for test name / test type
204
205 @param test_name: name of current workload. i.e. randwrite
206 @param test_type: type of value collected for current test. i.e. IOPs
207 @param pass_list: list of run passes for current test
208 @param percentile: flag to use percentile as key instead of timestamp
209 """
210 logging.info('fio_graph_generator._write_graph %s %s %s',
211 test_name, test_type, str(pass_list))
212
213
214 if percentile:
215 out_file_name = '%s_%s_percentile.html' % (test_name, test_type)
216 else:
217 out_file_name = '%s_%s.html' % (test_name, test_type)
218
219 with open(out_file_name, 'w') as f:
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700220 f.write(cls.html_head)
221 cls._write_data(f, test_name, test_type, pass_list, percentile)
222 cls._write_option(f, test_name, test_type, percentile)
223 f.write(cls.html_tail)
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700224
225 def __init__(self, test_name, test_type, pass_list):
226 """
227 @param test_name: name of current workload. i.e. randwrite
228 @param test_type: type of value collected for current test. i.e. IOPs
229 @param pass_list: list of run passes for current test
230 """
231 self.test_name = test_name
232 self.test_type = test_type
233 self.pass_list = pass_list
234
235 def run(self):
236 """
237 Run the graph generator.
238 """
239 self._write_graph(self.test_name, self.test_type, self.pass_list, False)
240 self._write_graph(self.test_name, self.test_type, self.pass_list, True)
241
242
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700243def fio_parse_dict(d, prefix):
244 """
245 Parse fio json dict
246
247 Recursively flaten json dict to generate autotest perf dict
248
249 @param d: input dict
250 @param prefix: name prefix of the key
251 """
252
253 # No need to parse something that didn't run such as read stat in write job.
254 if 'io_bytes' in d and d['io_bytes'] == 0:
255 return { }
256
257 results = { }
258 for k, v in d.items():
259
260 # remove >, >=, <, <=
261 for c in '>=<':
262 k = k.replace(c, '')
263
264 key = prefix + '_' + k
265
266 if type(v) is dict:
267 results.update(fio_parse_dict(v, key))
268 else:
269 results[key] = v
270 return results
271
272
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700273def fio_parser(lines, prefix=None):
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700274 """
275 Parse the json fio output
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700276
277 This collects all metrics given by fio and labels them according to unit
278 of measurement and test case name.
279
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700280 @param lines: text output of json fio output.
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700281 @param prefix: prefix for result keys.
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700282 """
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700283 results = { }
284 fio_dict = json.loads(lines)
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700285
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700286 if prefix:
287 prefix = prefix + '_'
288 else:
289 prefix = ''
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700290
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700291 results[prefix + 'fio_version'] = fio_dict['fio version']
292
293 if 'disk_util' in fio_dict:
294 results.update(fio_parse_dict(fio_dict['disk_util'][0],
295 prefix + 'disk'))
296
297 for job in fio_dict['jobs']:
298 job_prefix = '_' + prefix + job['jobname']
299 job.pop('jobname')
300
301
302 for k, v in job.iteritems():
303 results.update(fio_parse_dict({k:v}, job_prefix))
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700304
305 return results
306
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700307def fio_generate_graph():
308 """
309 Scan for fio log file in output directory and send data to generate each
310 graph to fio_graph_generator class.
311 """
312 log_types = ['bw', 'iops', 'lat', 'clat', 'slat']
313
314 # move fio log to result dir
315 for log_type in log_types:
316 logging.info('log_type %s', log_type)
317 logs = utils.system_output('ls *_%s.log' % log_type, ignore_status=True)
318 if not logs:
319 continue
320
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700321 pattern = r"""(?P<jobname>.*)_ # jobname
322 ((?P<runpass>p\d+)_) # pass
323 (?P<type>bw|iops|lat|clat|slat).log # type
324 """
325 matcher = re.compile(pattern, re.X)
326
327 pass_list = []
328 current_job = ''
329
330 for log in logs.split():
331 match = matcher.match(log)
332 if not match:
333 logging.warn('Unknown log file %s', log)
334 continue
335
336 jobname = match.group('jobname')
337 runpass = match.group('runpass')
338
339 # All files for particular job name are group together for create
340 # graph that can compare performance between result from each pass.
341 if jobname != current_job:
342 if pass_list:
343 fio_graph_generator(current_job, log_type, pass_list).run()
344 current_job = jobname
345 pass_list = []
346
347 pass_list.append(runpass)
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
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700358def fio_runner(test, job, env_vars,
359 name_prefix=None,
360 graph_prefix=None):
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700361 """
362 Runs fio.
363
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700364 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
Puthikorn Voravootivat425b1a72014-05-14 17:33:27 -0700372 @param test: test to upload perf value
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700373 @param job: fio config file to use
374 @param env_vars: environment variable fio will substituete in the fio
375 config file.
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700376 @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.
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700380 @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'
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700392 options = ['--output-format=json']
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700393 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)
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700399
400 fio_generate_graph()
401
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700402 filename = re.match('.*FILENAME=(?P<f>[^ ]*)', env_vars).group('f')
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700403 diskname = utils.get_disk_from_filename(filename)
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700404
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700405 if diskname:
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700406 model = utils.get_disk_model(diskname)
407 size = utils.get_disk_size_gb(diskname)
408 perfdb_name = '%s_%dG' % (model, size)
409 else:
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700410 perfdb_name = filename.replace('/', '_')
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700411
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700412 if name_prefix:
413 perfdb_name = name_prefix + '_' + perfdb_name
414
Gwendal Grignou51d50692014-06-20 11:42:18 -0700415 result = fio_parser(fio.stdout, prefix=name_prefix)
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700416 if not graph_prefix:
417 graph_prefix = ''
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700418
Puthikorn Voravootivat425b1a72014-05-14 17:33:27 -0700419 for k, v in result.iteritems():
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700420 # Remove the prefix for value, and replace it the graph prefix.
Gwendal Grignou51d50692014-06-20 11:42:18 -0700421 if name_prefix:
422 k = k.replace('_' + name_prefix, graph_prefix)
Puthikorn Voravootivat16dc0a22014-06-24 10:52:04 -0700423
424 # Make graph name to be same as the old code.
425 if k.endswith('bw'):
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700426 test.output_perf_value(description=perfdb_name, graph=k, value=v,
427 units='KB_per_sec', higher_is_better=True)
Puthikorn Voravootivatf5ebc662014-07-11 16:34:49 -0700428 elif k.rstrip('0').endswith('clat_percentile_99.'):
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700429 test.output_perf_value(description=perfdb_name, graph=k, value=v,
430 units='us', higher_is_better=False)
Puthikorn Voravootivat425b1a72014-05-14 17:33:27 -0700431 return result