blob: 5fdef0f3d4f0bfb92fcfc941c5d8421d916f1631 [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
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +010019from UserDict import UserDict
20
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +010021class fio_parser_exception(Exception):
22 """
23 Exception class for fio_job_output.
24
25 """
26
27 def __init__(self, value):
28 self.value = value
29 def __str__(self):
30 return repr(self.value)
31
32
Grant Grundlerb628ac92013-10-11 15:28:12 -070033def fio_version(version_line):
34 """
35 Strip 'fio-' prefix from self-reported version
36
37 @param version_line: first line of fio output should be version.
38
39 @raises fio_parser_exception when prefix isn't "fio-" as expected.
40
41 """
42
43 if version_line[0:4] == "fio-":
44 return version_line[4:]
45 raise fio_parser_exception('fio version not found: %s' % version_line)
46
47
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +010048class fio_job_output(UserDict):
49 """
50 Dictionary class to hold the fio output.
51
52 This class accepts fio output as a list of values.
53
54 """
55
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +010056 def _parse_gen(self, job, field, val):
57 """
58 Parses a regular field and adds it to the dictionary.
59
60 @param job: fio job name.
61 @param field: fio output field name.
62 @param val: fio output field value.
63
64 """
65
66 self[field % job] = val
67
68
69 def _parse_percentile(self, job, field, val):
70 """
71 Parses a percentile field and adds it to the dictionary.
72
73 @param job: fio job name.
74 @param field: fio output field name.
75 @param val: fio output field value.
76
77 """
78
79 prc = float(val.split('=')[0].strip('%'))
80 self[field % (job, prc)] = val.split('=')[1]
81
82
83 def _append_stats(self, idxs, io, typ):
84 """
85 Appends repetitive statistics fields to self._fio_table.
86
87 @param idxs: range of field indexes to use for the map.
88 @param io: I/O type: rd or wr
89 @param typ: latency type: submission or completion.
90
91 """
92
Puthikorn Voravootivat40bb8092013-12-18 18:01:34 -080093 fields = ['_%s_' + '%s_min_%s_lat_usec' % (io, typ),
94 '_%s_' + '%s_max_%s_lat_usec' % (io, typ),
Gwendal Grignou1b896952013-10-25 12:27:39 -070095 '_%s_' + '%s_mean_%s_lat_usec' % (io, typ),
96 '_%s_' + '%s_stdv_%s_lat_usec' % (io, typ)]
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +010097 for field, idx in zip(fields, idxs):
98 self._fio_table.append((field, idx, self._parse_gen))
99
100
101 def _append_percentiles(self, idxs, io):
102 """
103 Appends repetitive percentile fields to self._fio_table.
104
105 @param idxs: range of field indexes to use for the map.
106 @param io: I/O type: rd or wr
107
108 """
109
110 for i in idxs:
Gwendal Grignou1b896952013-10-25 12:27:39 -0700111 field = '_%s_' + '%s_lat_' % io + '%.2f_percent_usec'
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100112 self._fio_table.append((field, i, self._parse_percentile))
113
114
Puthikorn Voravootivat2e265962014-02-21 14:39:17 -0800115 def _build_fio_terse_4_table(self):
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100116 """
117 Creates map from field name to fio output index and parse function.
118
119 """
120
121 # General fio Job Info:
122 self._fio_table.extend([
Puthikorn Voravootivat40bb8092013-12-18 18:01:34 -0800123 ('_%s_fio_version' , 1, self._parse_gen),
124 ('_%s_groupid' , 3, self._parse_gen),
125 ('_%s_error' , 4, self._parse_gen)])
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100126
127 # Results of READ Status:
128 self._fio_table.extend([
Puthikorn Voravootivat40bb8092013-12-18 18:01:34 -0800129 ('_%s_rd_total_io_KB' , 5, self._parse_gen),
130 ('_%s_rd_bw_KB_sec' , 6, self._parse_gen),
131 ('_%s_rd_IOPS' , 7, self._parse_gen),
132 ('_%s_rd_runtime_msec' , 8, self._parse_gen)])
133 self._append_stats(range(9, 13), 'rd', 'submitted')
Grant Grundlera5201322013-10-08 16:17:47 -0700134 self._append_stats(range(13, 17), 'rd', 'completed')
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100135 self._append_percentiles(range(17, 37), 'rd')
Puthikorn Voravootivat40bb8092013-12-18 18:01:34 -0800136 self._append_stats(range(37, 41), 'rd', 'total')
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100137 self._fio_table.extend([
Puthikorn Voravootivat40bb8092013-12-18 18:01:34 -0800138 ('_%s_rd_min_bw_KB_sec' , 41, self._parse_gen),
139 ('_%s_rd_max_bw_KB_sec' , 42, self._parse_gen),
140 ('_%s_rd_percent' , 43, self._parse_gen),
141 ('_%s_rd_mean_bw_KB_sec' , 44, self._parse_gen),
142 ('_%s_rd_stdev_bw_KB_sec' , 45, self._parse_gen)])
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100143
144 # Results of WRITE Status:
145 self._fio_table.extend([
Puthikorn Voravootivat40bb8092013-12-18 18:01:34 -0800146 ('_%s_wr_total_io_KB' , 46, self._parse_gen),
147 ('_%s_wr_bw_KB_sec' , 47, self._parse_gen),
148 ('_%s_wr_IOPS' , 48, self._parse_gen),
149 ('_%s_wr_runtime_msec' , 49, self._parse_gen)])
Grant Grundlera5201322013-10-08 16:17:47 -0700150 self._append_stats(range(50, 54), 'wr', 'submitted')
151 self._append_stats(range(54, 58), 'wr', 'completed')
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100152 self._append_percentiles(range(58, 78), 'wr')
Puthikorn Voravootivat40bb8092013-12-18 18:01:34 -0800153 self._append_stats(range(78, 82), 'wr', 'total')
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100154 self._fio_table.extend([
Puthikorn Voravootivat40bb8092013-12-18 18:01:34 -0800155 ('_%s_wr_min_bw_KB_sec' , 82, self._parse_gen),
156 ('_%s_wr_max_bw_KB_sec' , 83, self._parse_gen),
157 ('_%s_wr_percent' , 84, self._parse_gen),
158 ('_%s_wr_mean_bw_KB_sec' , 85, self._parse_gen),
159 ('_%s_wr_stdv_bw_KB_sec' , 86, self._parse_gen)])
160
161 # Results of TRIM Status:
162 self._fio_table.extend([
163 ('_%s_tr_total_io_KB' , 87, self._parse_gen),
164 ('_%s_tr_bw_KB_sec' , 88, self._parse_gen),
165 ('_%s_tr_IOPS' , 89, self._parse_gen),
166 ('_%s_tr_runtime_msec' , 90, self._parse_gen)])
167 self._append_stats(range(91, 95), 'tr', 'submitted')
168 self._append_stats(range(95, 99), 'tr', 'completed')
169 self._append_percentiles(range(99, 119), 'tr')
170 self._append_stats(range(119, 123), 'tr', 'total')
171 self._fio_table.extend([
172 ('_%s_tr_min_bw_KB_sec' , 123, self._parse_gen),
173 ('_%s_tr_max_bw_KB_sec' , 124, self._parse_gen),
174 ('_%s_tr_percent' , 125, self._parse_gen),
175 ('_%s_tr_mean_bw_KB_sec' , 126, self._parse_gen),
176 ('_%s_tr_stdv_bw_KB_sec' , 127, self._parse_gen)])
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100177
178 # Other Results:
179 self._fio_table.extend([
Puthikorn Voravootivat40bb8092013-12-18 18:01:34 -0800180 ('_%s_cpu_usg_usr_percent' , 128, self._parse_gen),
181 ('_%s_cpu_usg_sys_percent' , 129, self._parse_gen),
Puthikorn Voravootivatf505a7f2013-12-20 17:21:18 -0800182 ('_%s_cpu_context_count' , 130, self._parse_gen),
Puthikorn Voravootivat40bb8092013-12-18 18:01:34 -0800183 ('_%s_major_page_faults' , 131, self._parse_gen),
184 ('_%s_minor_page_faults' , 132, self._parse_gen),
185 ('_%s_io_depth_le_1_percent' , 133, self._parse_gen),
186 ('_%s_io_depth_2_percent' , 134, self._parse_gen),
187 ('_%s_io_depth_4_percent' , 135, self._parse_gen),
188 ('_%s_io_depth_8_percent' , 136, self._parse_gen),
189 ('_%s_io_depth_16_percent' , 137, self._parse_gen),
190 ('_%s_io_depth_32_percent' , 138, self._parse_gen),
191 ('_%s_io_depth_ge_64_percent' , 139, self._parse_gen),
192 ('_%s_io_lats_le_2_usec_percent' , 140, self._parse_gen),
193 ('_%s_io_lats_4_usec_percent' , 141, self._parse_gen),
194 ('_%s_io_lats_10_usec_percent' , 142, self._parse_gen),
195 ('_%s_io_lats_20_usec_percent' , 143, self._parse_gen),
196 ('_%s_io_lats_50_usec_percent' , 144, self._parse_gen),
197 ('_%s_io_lats_100_usec_percent' , 145, self._parse_gen),
198 ('_%s_io_lats_250_usec_percent' , 146, self._parse_gen),
199 ('_%s_io_lats_500_usec_percent' , 147, self._parse_gen),
200 ('_%s_io_lats_750_usec_percent' , 148, self._parse_gen),
201 ('_%s_io_lats_1000_usec_percent' , 149, self._parse_gen),
202 ('_%s_io_lats_le_2_msec_percent' , 150, self._parse_gen),
203 ('_%s_io_lats_4_msec_percent' , 151, self._parse_gen),
204 ('_%s_io_lats_10_msec_percent' , 152, self._parse_gen),
205 ('_%s_io_lats_20_msec_percent' , 153, self._parse_gen),
206 ('_%s_io_lats_50_msec_percent' , 154, self._parse_gen),
207 ('_%s_io_lats_100_msec_percent' , 155, self._parse_gen),
208 ('_%s_io_lats_250_msec_percent' , 156, self._parse_gen),
209 ('_%s_io_lats_500_msec_percent' , 157, self._parse_gen),
210 ('_%s_io_lats_750_msec_percent' , 158, self._parse_gen),
211 ('_%s_io_lats_1000_msec_percent' , 159, self._parse_gen),
212 ('_%s_io_lats_2000_msec_percent' , 160, self._parse_gen),
213 ('_%s_io_lats_gt_2000_msec_percent' , 161, self._parse_gen),
Grant Grundlerb628ac92013-10-11 15:28:12 -0700214
215 # Disk Utilization: only boot disk is tested
Puthikorn Voravootivat40bb8092013-12-18 18:01:34 -0800216 ('_%s_disk_name' , 162, self._parse_gen),
217 ('_%s_rd_ios' , 163, self._parse_gen),
218 ('_%s_wr_ios' , 164, self._parse_gen),
219 ('_%s_rd_merges' , 165, self._parse_gen),
220 ('_%s_wr_merges' , 166, self._parse_gen),
221 ('_%s_rd_ticks' , 167, self._parse_gen),
222 ('_%s_wr_ticks' , 168, self._parse_gen),
223 ('_%s_time_in_queue' , 169, self._parse_gen),
224 ('_%s_disk_util_percent' , 170, self._parse_gen)])
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100225
226
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700227 def __init__(self, data, prefix=None):
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100228 """
229 Fills the dictionary object with the fio output upon instantiation.
230
Grant Grundlerb628ac92013-10-11 15:28:12 -0700231 @param data: fio HOWTO documents list of values from fio output.
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700232 @param prefix: name to append for the result key value pair.
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100233
234 @raises fio_parser_exception.
235
236 """
237
238 UserDict.__init__(self)
239
240 # Check that data parameter.
241 if len(data) == 0:
242 raise fio_parser_exception('No fio output supplied.')
243
244 # Create table that relates field name to fio output index and
245 # parsing function to be used for the field.
246 self._fio_table = []
Puthikorn Voravootivat2e265962014-02-21 14:39:17 -0800247 terse_version = int(data[0])
248 fio_terse_parser = { 4 : self._build_fio_terse_4_table }
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100249
Puthikorn Voravootivat2e265962014-02-21 14:39:17 -0800250 if terse_version in fio_terse_parser:
251 fio_terse_parser[terse_version]()
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100252 else:
Puthikorn Voravootivat2e265962014-02-21 14:39:17 -0800253 raise fio_parser_exception('fio terse version %s unsupported.'
254 'fio_parser supports terse version %s' %
255 (terse_version, fio_terse_parser.keys()))
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100256
257 # Fill dictionary object.
258 self._job_name = data[2]
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700259 if prefix:
260 self._job_name = prefix + '_' + self._job_name
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100261 for field, idx, parser in self._fio_table:
Puthikorn Voravootivat2e265962014-02-21 14:39:17 -0800262 # Field 162-170 only reported when we test on block device.
263 if len(data) <= idx:
264 break
Vivia Nikolaidou26bc65a2012-07-17 15:32:13 +0100265 parser(self._job_name, field, data[idx])
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700266
267
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700268class fio_graph_generator():
269 """
270 Generate graph from fio log that created when specified these options.
271 - write_bw_log
272 - write_iops_log
273 - write_lat_log
274
275 The following limitations apply
276 - Log file name must be in format jobname_testpass
277 - Graph is generate using Google graph api -> Internet require to view.
278 """
279
280 html_head = """
281<html>
282 <head>
283 <script type="text/javascript" src="https://www.google.com/jsapi"></script>
284 <script type="text/javascript">
285 google.load("visualization", "1", {packages:["corechart"]});
286 google.setOnLoadCallback(drawChart);
287 function drawChart() {
288"""
289
290 html_tail = """
291 var chart_div = document.getElementById('chart_div');
292 var chart = new google.visualization.ScatterChart(chart_div);
293 chart.draw(data, options);
294 }
295 </script>
296 </head>
297 <body>
298 <div id="chart_div" style="width: 100%; height: 100%;"></div>
299 </body>
300</html>
301"""
302
303 h_title = { True: 'Percentile', False: 'Time (s)' }
304 v_title = { 'bw' : 'Bandwidth (KB/s)',
305 'iops': 'IOPs',
306 'lat' : 'Total latency (us)',
307 'clat': 'Completion latency (us)',
308 'slat': 'Submission latency (us)' }
309 graph_title = { 'bw' : 'bandwidth',
310 'iops': 'IOPs',
311 'lat' : 'total latency',
312 'clat': 'completion latency',
313 'slat': 'submission latency' }
314
315 test_name = ''
316 test_type = ''
317 pass_list = ''
318
319 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700320 def _parse_log_file(cls, file_name, pass_index, pass_count, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700321 """
322 Generate row for google.visualization.DataTable from one log file.
323 Log file is the one that generated using write_{bw,lat,iops}_log
324 option in the FIO job file.
325
326 The fio log file format is timestamp, value, direction, blocksize
327 The output format for each row is { c: list of { v: value} }
328
329 @param file_name: log file name to read data from
330 @param pass_index: index of current run pass
331 @param pass_count: number of all test run passes
332 @param percentile: flag to use percentile as key instead of timestamp
333
334 @return: list of data rows in google.visualization.DataTable format
335 """
336 # Read data from log
337 with open(file_name, 'r') as f:
338 data = []
339
340 for line in f.readlines():
341 if not line:
342 break
343 t, v, _, _ = [int(x) for x in line.split(', ')]
344 data.append([t / 1000.0, v])
345
346 # Sort & calculate percentile
347 if percentile:
348 data.sort(key=lambda x:x[1])
349 l = len(data)
350 for i in range(l):
351 data[i][0] = 100 * (i + 0.5) / l
352
353 # Generate the data row
354 all_row = []
355 row = [None] * (pass_count + 1)
356 for d in data:
357 row[0] = {'v' : '%.3f' % d[0]}
358 row[pass_index + 1] = {'v': d[1] }
359 all_row.append({'c': row[:]})
360
361 return all_row
362
363 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700364 def _gen_data_col(cls, pass_list, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700365 """
366 Generate col for google.visualization.DataTable
367
368 The output format is list of dict of label and type. In this case,
369 type is always number.
370
371 @param pass_list: list of test run passes
372 @param percentile: flag to use percentile as key instead of timestamp
373
374 @return: list of column in google.visualization.DataTable format
375 """
376 if percentile:
377 col_name_list = ['percentile'] + pass_list
378 else:
379 col_name_list = ['time'] + pass_list
380
381 return [{'label': name, 'type': 'number'} for name in col_name_list]
382
383 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700384 def _gen_data_row(cls, test_name, test_type, pass_list, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700385 """
386 Generate row for google.visualization.DataTable by generate all log
387 file name and call _parse_log_file for each file
388
389 @param test_name: name of current workload. i.e. randwrite
390 @param test_type: type of value collected for current test. i.e. IOPs
391 @param pass_list: list of run passes for current test
392 @param percentile: flag to use percentile as key instead of timestamp
393
394 @return: list of data rows in google.visualization.DataTable format
395 """
396 all_row = []
397 pass_count = len(pass_list)
398 for pass_index, pass_str in enumerate(pass_list):
399 log_file_name = str('%s_%s_%s.log' %
400 (test_name, pass_str, test_type))
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700401 all_row.extend(cls._parse_log_file(log_file_name, pass_index,
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700402 pass_count, percentile))
403 return all_row
404
405 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700406 def _write_data(cls, f, test_name, test_type, pass_list, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700407 """
408 Write google.visualization.DataTable object to output file.
409 https://developers.google.com/chart/interactive/docs/reference
410
411 @param test_name: name of current workload. i.e. randwrite
412 @param test_type: type of value collected for current test. i.e. IOPs
413 @param pass_list: list of run passes for current test
414 @param percentile: flag to use percentile as key instead of timestamp
415 """
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700416 col = cls._gen_data_col(pass_list, percentile)
417 row = cls._gen_data_row(test_name, test_type, pass_list, percentile)
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700418 data_dict = { 'cols' : col, 'rows' : row}
419
420 f.write('var data = new google.visualization.DataTable(')
421 json.dump(data_dict, f)
422 f.write(');\n')
423
424 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700425 def _write_option(cls, f, test_name, test_type, percentile):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700426 """
427 Write option to render scatter graph to output file.
428 https://google-developers.appspot.com/chart/interactive/docs/gallery/scatterchart
429
430 @param test_name: name of current workload. i.e. randwrite
431 @param test_type: type of value collected for current test. i.e. IOPs
432 @param percentile: flag to use percentile as key instead of timestamp
433 """
434 option = {'pointSize': 1 }
435 if percentile:
436 option['title'] = ('Percentile graph of %s for %s workload' %
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700437 (cls.graph_title[test_type], test_name))
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700438 else:
439 option['title'] = ('Graph of %s for %s workload over time' %
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700440 (cls.graph_title[test_type], test_name))
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700441
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700442 option['hAxis'] = { 'title': cls.h_title[percentile]}
443 option['vAxis'] = { 'title': cls.v_title[test_type]}
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700444
445 f.write('var options = ')
446 json.dump(option, f)
447 f.write(';\n')
448
449 @classmethod
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700450 def _write_graph(cls, test_name, test_type, pass_list, percentile=False):
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700451 """
452 Generate graph for test name / test type
453
454 @param test_name: name of current workload. i.e. randwrite
455 @param test_type: type of value collected for current test. i.e. IOPs
456 @param pass_list: list of run passes for current test
457 @param percentile: flag to use percentile as key instead of timestamp
458 """
459 logging.info('fio_graph_generator._write_graph %s %s %s',
460 test_name, test_type, str(pass_list))
461
462
463 if percentile:
464 out_file_name = '%s_%s_percentile.html' % (test_name, test_type)
465 else:
466 out_file_name = '%s_%s.html' % (test_name, test_type)
467
468 with open(out_file_name, 'w') as f:
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700469 f.write(cls.html_head)
470 cls._write_data(f, test_name, test_type, pass_list, percentile)
471 cls._write_option(f, test_name, test_type, percentile)
472 f.write(cls.html_tail)
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700473
474 def __init__(self, test_name, test_type, pass_list):
475 """
476 @param test_name: name of current workload. i.e. randwrite
477 @param test_type: type of value collected for current test. i.e. IOPs
478 @param pass_list: list of run passes for current test
479 """
480 self.test_name = test_name
481 self.test_type = test_type
482 self.pass_list = pass_list
483
484 def run(self):
485 """
486 Run the graph generator.
487 """
488 self._write_graph(self.test_name, self.test_type, self.pass_list, False)
489 self._write_graph(self.test_name, self.test_type, self.pass_list, True)
490
491
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700492def fio_parser(lines, prefix=None):
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700493 """Parse the terse fio output
494
495 This collects all metrics given by fio and labels them according to unit
496 of measurement and test case name.
497
498 @param lines: text output of terse fio output.
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700499 @param prefix: prefix for result keys.
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700500
501 """
502 # fio version 2.0.8+ outputs all needed information with --minimal
503 # Using that instead of the human-readable version, since it's easier
504 # to parse.
505 # Following is a partial example of the semicolon-delimited output.
506 # 3;fio-2.1;quick_write;0;0;0;0;0;0;0;0;0.000000;0.000000;0;0;0.000000;
507 # 0.000000;1.000000%=0;5.000000%=0;10.000000%=0;20.000000%=0;
508 # ...
509 # Refer to the HOWTO file of the fio package for more information.
510
511 results = {}
512
513 # Extract the values from the test.
514 for line in lines.splitlines():
515 # Put the values from the output into an array.
516 values = line.split(';')
517 # This check makes sure that we are parsing the actual values
518 # instead of the job description or possible blank lines.
519 if len(values) <= 128:
520 continue
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700521 results.update(fio_job_output(values, prefix))
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700522
523 return results
524
525
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700526def fio_generate_graph():
527 """
528 Scan for fio log file in output directory and send data to generate each
529 graph to fio_graph_generator class.
530 """
531 log_types = ['bw', 'iops', 'lat', 'clat', 'slat']
532
533 # move fio log to result dir
534 for log_type in log_types:
535 logging.info('log_type %s', log_type)
536 logs = utils.system_output('ls *_%s.log' % log_type, ignore_status=True)
537 if not logs:
538 continue
539
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700540 pattern = r"""(?P<jobname>.*)_ # jobname
541 ((?P<runpass>p\d+)_) # pass
542 (?P<type>bw|iops|lat|clat|slat).log # type
543 """
544 matcher = re.compile(pattern, re.X)
545
546 pass_list = []
547 current_job = ''
548
549 for log in logs.split():
550 match = matcher.match(log)
551 if not match:
552 logging.warn('Unknown log file %s', log)
553 continue
554
555 jobname = match.group('jobname')
556 runpass = match.group('runpass')
557
558 # All files for particular job name are group together for create
559 # graph that can compare performance between result from each pass.
560 if jobname != current_job:
561 if pass_list:
562 fio_graph_generator(current_job, log_type, pass_list).run()
563 current_job = jobname
564 pass_list = []
565
566 pass_list.append(runpass)
567
568 if pass_list:
569 fio_graph_generator(current_job, log_type, pass_list).run()
570
571
572 cmd = 'mv *_%s.log results' % log_type
573 utils.run(cmd, ignore_status=True)
574 utils.run('mv *.html results', ignore_status=True)
575
576
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700577def fio_runner(test, job, env_vars,
578 name_prefix=None,
579 graph_prefix=None):
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700580 """
581 Runs fio.
582
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700583 Build a result keyval and performence json.
584 The JSON would look like:
585 {"description": "<name_prefix>_<modle>_<size>G",
586 "graph": "<graph_prefix>_1m_write_wr_lat_99.00_percent_usec",
587 "higher_is_better": false, "units": "us", "value": "xxxx"}
588 {...
589
590
Puthikorn Voravootivat425b1a72014-05-14 17:33:27 -0700591 @param test: test to upload perf value
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700592 @param job: fio config file to use
593 @param env_vars: environment variable fio will substituete in the fio
594 config file.
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700595 @param name_prefix: prefix of the descriptions to use in chrome perfi
596 dashboard.
597 @param graph_prefix: prefix of the graph name in chrome perf dashboard
598 and result keyvals.
Gwendal Grignou29d2af52014-05-02 11:32:27 -0700599 @return fio results.
600
601 """
602
603 # running fio with ionice -c 3 so it doesn't lock out other
604 # processes from the disk while it is running.
605 # If you want to run the fio test for performance purposes,
606 # take out the ionice and disable hung process detection:
607 # "echo 0 > /proc/sys/kernel/hung_task_timeout_secs"
608 # -c 3 = Idle
609 # Tried lowest priority for "best effort" but still failed
610 ionice = 'ionice -c 3'
611
612 # Using the --minimal flag for easier results parsing
613 # Newest fio doesn't omit any information in --minimal
614 # Need to set terse-version to 4 for trim related output
615 options = ['--minimal', '--terse-version=4']
616 fio_cmd_line = ' '.join([env_vars, ionice, 'fio',
617 ' '.join(options),
618 '"' + job + '"'])
619 fio = utils.run(fio_cmd_line)
620
621 logging.debug(fio.stdout)
Puthikorn Voravootivat16961362014-05-07 15:57:27 -0700622
623 fio_generate_graph()
624
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700625 filename = re.match('.*FILENAME=(?P<f>[^ ]*)', env_vars).group('f')
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700626 diskname = utils.get_disk_from_filename(filename)
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700627
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700628 if diskname:
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700629 model = utils.get_disk_model(diskname)
630 size = utils.get_disk_size_gb(diskname)
631 perfdb_name = '%s_%dG' % (model, size)
632 else:
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700633 perfdb_name = filename.replace('/', '_')
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700634
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700635 if name_prefix:
636 perfdb_name = name_prefix + '_' + perfdb_name
637
Gwendal Grignou51d50692014-06-20 11:42:18 -0700638 result = fio_parser(fio.stdout, prefix=name_prefix)
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700639 if not graph_prefix:
640 graph_prefix = ''
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700641
Puthikorn Voravootivat425b1a72014-05-14 17:33:27 -0700642 # Upload bw / 99% lat to dashboard
643 bw_matcher = re.compile('.*(rd|wr)_bw_KB_sec')
644 lat_matcher = re.compile('.*(rd|wr)_lat_99.00_percent_usec')
Puthikorn Voravootivat9867b762014-05-22 12:09:20 -0700645 # don't log useless stat like 16k_randwrite_rd_bw_KB_sec
646 skip_matcher = re.compile('.*(trim.*|write.*_rd|read.*_wr)_.*')
Puthikorn Voravootivat425b1a72014-05-14 17:33:27 -0700647 for k, v in result.iteritems():
Gwendal Grignou2f16f2f2014-06-12 11:28:05 -0700648 # Remove the prefix for value, and replace it the graph prefix.
Gwendal Grignou51d50692014-06-20 11:42:18 -0700649 if name_prefix:
650 k = k.replace('_' + name_prefix, graph_prefix)
Puthikorn Voravootivat9867b762014-05-22 12:09:20 -0700651 if skip_matcher.match(k):
652 continue
Puthikorn Voravootivat425b1a72014-05-14 17:33:27 -0700653 if bw_matcher.match(k):
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700654 test.output_perf_value(description=perfdb_name, graph=k, value=v,
655 units='KB_per_sec', higher_is_better=True)
Puthikorn Voravootivat425b1a72014-05-14 17:33:27 -0700656 elif lat_matcher.match(k):
Puthikorn Voravootivat8b811e02014-06-02 14:13:45 -0700657 test.output_perf_value(description=perfdb_name, graph=k, value=v,
658 units='us', higher_is_better=False)
Puthikorn Voravootivat425b1a72014-05-14 17:33:27 -0700659 return result