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 results.update(fio_parse_dict({k:v}, job_prefix)) 301 302 return results 303 304def fio_generate_graph(): 305 """ 306 Scan for fio log file in output directory and send data to generate each 307 graph to fio_graph_generator class. 308 """ 309 log_types = ['bw', 'iops', 'lat', 'clat', 'slat'] 310 311 # move fio log to result dir 312 for log_type in log_types: 313 logging.info('log_type %s', log_type) 314 logs = utils.system_output('ls *_%s.*log' % log_type, ignore_status=True) 315 if not logs: 316 continue 317 318 pattern = r"""(?P<jobname>.*)_ # jobname 319 ((?P<runpass>p\d+)_|) # pass 320 (?P<type>bw|iops|lat|clat|slat) # type 321 (.(?P<thread>\d+)|) # thread id for newer fio. 322 .log 323 """ 324 matcher = re.compile(pattern, re.X) 325 326 pass_list = [] 327 current_job = '' 328 329 for log in logs.split(): 330 match = matcher.match(log) 331 if not match: 332 logging.warn('Unknown log file %s', log) 333 continue 334 335 jobname = match.group('jobname') 336 runpass = match.group('runpass') or '1' 337 if match.group('thread'): 338 runpass += '_' + match.group('thread') 339 340 # All files for particular job name are group together for create 341 # graph that can compare performance between result from each pass. 342 if jobname != current_job: 343 if pass_list: 344 fio_graph_generator(current_job, log_type, pass_list).run() 345 current_job = jobname 346 pass_list = [] 347 pass_list.append((runpass, log)) 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 358def fio_runner(test, job, env_vars, 359 name_prefix=None, 360 graph_prefix=None): 361 """ 362 Runs fio. 363 364 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 372 @param test: test to upload perf value 373 @param job: fio config file to use 374 @param env_vars: environment variable fio will substituete in the fio 375 config file. 376 @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. 380 @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' 392 options = ['--output-format=json'] 393 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) 399 400 fio_generate_graph() 401 402 filename = re.match('.*FILENAME=(?P<f>[^ ]*)', env_vars).group('f') 403 diskname = utils.get_disk_from_filename(filename) 404 405 if diskname: 406 model = utils.get_disk_model(diskname) 407 size = utils.get_disk_size_gb(diskname) 408 perfdb_name = '%s_%dG' % (model, size) 409 else: 410 perfdb_name = filename.replace('/', '_') 411 412 if name_prefix: 413 perfdb_name = name_prefix + '_' + perfdb_name 414 415 result = fio_parser(fio.stdout, prefix=name_prefix) 416 if not graph_prefix: 417 graph_prefix = '' 418 419 for k, v in result.iteritems(): 420 # Remove the prefix for value, and replace it the graph prefix. 421 if name_prefix: 422 k = k.replace('_' + name_prefix, graph_prefix) 423 424 # Make graph name to be same as the old code. 425 if k.endswith('bw'): 426 test.output_perf_value(description=perfdb_name, graph=k, value=v, 427 units='KB_per_sec', higher_is_better=True) 428 elif k.rstrip('0').endswith('clat_percentile_99.'): 429 test.output_perf_value(description=perfdb_name, graph=k, value=v, 430 units='us', higher_is_better=False) 431 return result 432