1# Lint as: python2, python3 2# 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 6"""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 18from __future__ import absolute_import 19from __future__ import division 20from __future__ import print_function 21 22import json 23import logging 24import re 25 26import six 27from six.moves import range 28 29import common 30from autotest_lib.client.bin import utils 31 32 33class 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 85 def _parse_log_file(cls, file_name, pass_index, pass_count, percentile): 86 """ 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: 113 data.sort(key=lambda x: x[1]) 114 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]} 123 row[pass_index + 1] = {'v': d[1]} 124 all_row.append({'c': row[:]}) 125 126 return all_row 127 128 @classmethod 129 def _gen_data_col(cls, pass_list, percentile): 130 """ 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: 142 col_name_list = ['percentile'] + [p[0] for p in pass_list] 143 else: 144 col_name_list = ['time'] + [p[0] for p in pass_list] 145 146 return [{'label': name, 'type': 'number'} for name in col_name_list] 147 148 @classmethod 149 def _gen_data_row(cls, test_type, pass_list, percentile): 150 """ 151 Generate row for google.visualization.DataTable by generate all log 152 file name and call _parse_log_file for each file 153 154 @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) 162 for pass_index, log_file_name in enumerate([p[1] for p in pass_list]): 163 all_row.extend(cls._parse_log_file(log_file_name, pass_index, 164 pass_count, percentile)) 165 return all_row 166 167 @classmethod 168 def _write_data(cls, f, test_type, pass_list, percentile): 169 """ 170 Write google.visualization.DataTable object to output file. 171 https://developers.google.com/chart/interactive/docs/reference 172 173 @param f: html file to update 174 @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 """ 178 col = cls._gen_data_col(pass_list, percentile) 179 row = cls._gen_data_row(test_type, pass_list, percentile) 180 data_dict = {'cols' : col, 'rows' : row} 181 182 f.write('var data = new google.visualization.DataTable(') 183 json.dump(data_dict, f) 184 f.write(');\n') 185 186 @classmethod 187 def _write_option(cls, f, test_name, test_type, percentile): 188 """ 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 """ 196 option = {'pointSize': 1} 197 if percentile: 198 option['title'] = ('Percentile graph of %s for %s workload' % 199 (cls.graph_title[test_type], test_name)) 200 else: 201 option['title'] = ('Graph of %s for %s workload over time' % 202 (cls.graph_title[test_type], test_name)) 203 204 option['hAxis'] = {'title': cls.h_title[percentile]} 205 option['vAxis'] = {'title': cls.v_title[test_type]} 206 207 f.write('var options = ') 208 json.dump(option, f) 209 f.write(';\n') 210 211 @classmethod 212 def _write_graph(cls, test_name, test_type, pass_list, percentile=False): 213 """ 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: 231 f.write(cls.html_head) 232 cls._write_data(f, test_type, pass_list, percentile) 233 cls._write_option(f, test_name, test_type, percentile) 234 f.write(cls.html_tail) 235 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 254def 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: 266 return {} 267 268 results = {} 269 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 284def fio_parser(lines, prefix=None): 285 """ 286 Parse the json fio output 287 288 This collects all metrics given by fio and labels them according to unit 289 of measurement and test case name. 290 291 @param lines: text output of json fio output. 292 @param prefix: prefix for result keys. 293 """ 294 results = {} 295 fio_dict = json.loads(lines) 296 297 if prefix: 298 prefix = prefix + '_' 299 else: 300 prefix = '' 301 302 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 313 for k, v in six.iteritems(job): 314 # Igonre "job options", its alphanumerc keys confuses tko. 315 # Besides, these keys are redundant. 316 if k == 'job options': 317 continue 318 results.update(fio_parse_dict({k:v}, job_prefix)) 319 320 return results 321 322def 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) 332 logs = utils.system_output('ls *_%s.*log' % log_type, ignore_status=True) 333 if not logs: 334 continue 335 336 pattern = r"""(?P<jobname>.*)_ # jobname 337 ((?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 341 """ 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.warning('Unknown log file %s', log) 351 continue 352 353 jobname = match.group('jobname') 354 runpass = match.group('runpass') or '1' 355 if match.group('thread'): 356 runpass += '_' + match.group('thread') 357 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 = [] 365 pass_list.append((runpass, log)) 366 367 if pass_list: 368 fio_graph_generator(current_job, log_type, pass_list).run() 369 370 371 cmd = 'mv *_%s.*log results' % log_type 372 utils.run(cmd, ignore_status=True) 373 utils.run('mv *.html results', ignore_status=True) 374 375 376def fio_runner(test, job, env_vars, 377 name_prefix=None, 378 graph_prefix=None): 379 """ 380 Runs fio. 381 382 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 390 @param test: test to upload perf value 391 @param job: fio config file to use 392 @param env_vars: environment variable fio will substituete in the fio 393 config file. 394 @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. 398 @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' 410 options = ['--output-format=json'] 411 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) 417 418 fio_generate_graph() 419 420 filename = re.match('.*FILENAME=(?P<f>[^ ]*)', env_vars).group('f') 421 diskname = utils.get_disk_from_filename(filename) 422 423 if diskname: 424 model = utils.get_disk_model(diskname) 425 size = utils.get_disk_size_gb(diskname) 426 perfdb_name = '%s_%dG' % (model, size) 427 else: 428 perfdb_name = filename.replace('/', '_') 429 430 if name_prefix: 431 perfdb_name = name_prefix + '_' + perfdb_name 432 433 result = fio_parser(fio.stdout, prefix=name_prefix) 434 if not graph_prefix: 435 graph_prefix = '' 436 437 for k, v in six.iteritems(result): 438 # Remove the prefix for value, and replace it the graph prefix. 439 if name_prefix: 440 k = k.replace('_' + name_prefix, graph_prefix) 441 442 # Make graph name to be same as the old code. 443 if k.endswith('bw'): 444 test.output_perf_value(description=perfdb_name, graph=k, value=v, 445 units='KB_per_sec', higher_is_better=True) 446 elif 'clat_percentile_' in k: 447 test.output_perf_value(description=perfdb_name, graph=k, value=v, 448 units='us', higher_is_better=False) 449 elif 'clat_ns_percentile_' in k: 450 test.output_perf_value(description=perfdb_name, graph=k, value=v, 451 units='ns', higher_is_better=False) 452 return result 453