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 # Igonre "job options", its alphanumerc keys confuses tko. 301 # Besides, these keys are redundant. 302 if k == 'job options': 303 continue 304 results.update(fio_parse_dict({k:v}, job_prefix)) 305 306 return results 307 308def fio_generate_graph(): 309 """ 310 Scan for fio log file in output directory and send data to generate each 311 graph to fio_graph_generator class. 312 """ 313 log_types = ['bw', 'iops', 'lat', 'clat', 'slat'] 314 315 # move fio log to result dir 316 for log_type in log_types: 317 logging.info('log_type %s', log_type) 318 logs = utils.system_output('ls *_%s.*log' % log_type, ignore_status=True) 319 if not logs: 320 continue 321 322 pattern = r"""(?P<jobname>.*)_ # jobname 323 ((?P<runpass>p\d+)_|) # pass 324 (?P<type>bw|iops|lat|clat|slat) # type 325 (.(?P<thread>\d+)|) # thread id for newer fio. 326 .log 327 """ 328 matcher = re.compile(pattern, re.X) 329 330 pass_list = [] 331 current_job = '' 332 333 for log in logs.split(): 334 match = matcher.match(log) 335 if not match: 336 logging.warn('Unknown log file %s', log) 337 continue 338 339 jobname = match.group('jobname') 340 runpass = match.group('runpass') or '1' 341 if match.group('thread'): 342 runpass += '_' + match.group('thread') 343 344 # All files for particular job name are group together for create 345 # graph that can compare performance between result from each pass. 346 if jobname != current_job: 347 if pass_list: 348 fio_graph_generator(current_job, log_type, pass_list).run() 349 current_job = jobname 350 pass_list = [] 351 pass_list.append((runpass, log)) 352 353 if pass_list: 354 fio_graph_generator(current_job, log_type, pass_list).run() 355 356 357 cmd = 'mv *_%s.*log results' % log_type 358 utils.run(cmd, ignore_status=True) 359 utils.run('mv *.html results', ignore_status=True) 360 361 362def fio_runner(test, job, env_vars, 363 name_prefix=None, 364 graph_prefix=None): 365 """ 366 Runs fio. 367 368 Build a result keyval and performence json. 369 The JSON would look like: 370 {"description": "<name_prefix>_<modle>_<size>G", 371 "graph": "<graph_prefix>_1m_write_wr_lat_99.00_percent_usec", 372 "higher_is_better": false, "units": "us", "value": "xxxx"} 373 {... 374 375 376 @param test: test to upload perf value 377 @param job: fio config file to use 378 @param env_vars: environment variable fio will substituete in the fio 379 config file. 380 @param name_prefix: prefix of the descriptions to use in chrome perfi 381 dashboard. 382 @param graph_prefix: prefix of the graph name in chrome perf dashboard 383 and result keyvals. 384 @return fio results. 385 386 """ 387 388 # running fio with ionice -c 3 so it doesn't lock out other 389 # processes from the disk while it is running. 390 # If you want to run the fio test for performance purposes, 391 # take out the ionice and disable hung process detection: 392 # "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" 393 # -c 3 = Idle 394 # Tried lowest priority for "best effort" but still failed 395 ionice = 'ionice -c 3' 396 options = ['--output-format=json'] 397 fio_cmd_line = ' '.join([env_vars, ionice, 'fio', 398 ' '.join(options), 399 '"' + job + '"']) 400 fio = utils.run(fio_cmd_line) 401 402 logging.debug(fio.stdout) 403 404 fio_generate_graph() 405 406 filename = re.match('.*FILENAME=(?P<f>[^ ]*)', env_vars).group('f') 407 diskname = utils.get_disk_from_filename(filename) 408 409 if diskname: 410 model = utils.get_disk_model(diskname) 411 size = utils.get_disk_size_gb(diskname) 412 perfdb_name = '%s_%dG' % (model, size) 413 else: 414 perfdb_name = filename.replace('/', '_') 415 416 if name_prefix: 417 perfdb_name = name_prefix + '_' + perfdb_name 418 419 result = fio_parser(fio.stdout, prefix=name_prefix) 420 if not graph_prefix: 421 graph_prefix = '' 422 423 for k, v in result.iteritems(): 424 # Remove the prefix for value, and replace it the graph prefix. 425 if name_prefix: 426 k = k.replace('_' + name_prefix, graph_prefix) 427 428 # Make graph name to be same as the old code. 429 if k.endswith('bw'): 430 test.output_perf_value(description=perfdb_name, graph=k, value=v, 431 units='KB_per_sec', higher_is_better=True) 432 elif k.rstrip('0').endswith('clat_percentile_99.'): 433 test.output_perf_value(description=perfdb_name, graph=k, value=v, 434 units='us', higher_is_better=False) 435 return result 436