Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 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 | |
Gwendal Grignou | 29d2af5 | 2014-05-02 11:32:27 -0700 | [diff] [blame] | 5 | """Library to run fio scripts. |
| 6 | |
| 7 | fio_runner launch fio and collect results. |
| 8 | The 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 | |
| 13 | Decoding class can be invoked independently. |
| 14 | |
| 15 | """ |
| 16 | |
Puthikorn Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 17 | import json, logging, re, utils |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 18 | |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 19 | from UserDict import UserDict |
| 20 | |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 21 | class 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 Grundler | b628ac9 | 2013-10-11 15:28:12 -0700 | [diff] [blame] | 33 | def 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 Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 48 | class 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 Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 56 | 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 Voravootivat | 40bb809 | 2013-12-18 18:01:34 -0800 | [diff] [blame] | 93 | fields = ['_%s_' + '%s_min_%s_lat_usec' % (io, typ), |
| 94 | '_%s_' + '%s_max_%s_lat_usec' % (io, typ), |
Gwendal Grignou | 1b89695 | 2013-10-25 12:27:39 -0700 | [diff] [blame] | 95 | '_%s_' + '%s_mean_%s_lat_usec' % (io, typ), |
| 96 | '_%s_' + '%s_stdv_%s_lat_usec' % (io, typ)] |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 97 | 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 Grignou | 1b89695 | 2013-10-25 12:27:39 -0700 | [diff] [blame] | 111 | field = '_%s_' + '%s_lat_' % io + '%.2f_percent_usec' |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 112 | self._fio_table.append((field, i, self._parse_percentile)) |
| 113 | |
| 114 | |
Puthikorn Voravootivat | 2e26596 | 2014-02-21 14:39:17 -0800 | [diff] [blame] | 115 | def _build_fio_terse_4_table(self): |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 116 | """ |
| 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 Voravootivat | 40bb809 | 2013-12-18 18:01:34 -0800 | [diff] [blame] | 123 | ('_%s_fio_version' , 1, self._parse_gen), |
| 124 | ('_%s_groupid' , 3, self._parse_gen), |
| 125 | ('_%s_error' , 4, self._parse_gen)]) |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 126 | |
| 127 | # Results of READ Status: |
| 128 | self._fio_table.extend([ |
Puthikorn Voravootivat | 40bb809 | 2013-12-18 18:01:34 -0800 | [diff] [blame] | 129 | ('_%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 Grundler | a520132 | 2013-10-08 16:17:47 -0700 | [diff] [blame] | 134 | self._append_stats(range(13, 17), 'rd', 'completed') |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 135 | self._append_percentiles(range(17, 37), 'rd') |
Puthikorn Voravootivat | 40bb809 | 2013-12-18 18:01:34 -0800 | [diff] [blame] | 136 | self._append_stats(range(37, 41), 'rd', 'total') |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 137 | self._fio_table.extend([ |
Puthikorn Voravootivat | 40bb809 | 2013-12-18 18:01:34 -0800 | [diff] [blame] | 138 | ('_%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 Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 143 | |
| 144 | # Results of WRITE Status: |
| 145 | self._fio_table.extend([ |
Puthikorn Voravootivat | 40bb809 | 2013-12-18 18:01:34 -0800 | [diff] [blame] | 146 | ('_%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 Grundler | a520132 | 2013-10-08 16:17:47 -0700 | [diff] [blame] | 150 | self._append_stats(range(50, 54), 'wr', 'submitted') |
| 151 | self._append_stats(range(54, 58), 'wr', 'completed') |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 152 | self._append_percentiles(range(58, 78), 'wr') |
Puthikorn Voravootivat | 40bb809 | 2013-12-18 18:01:34 -0800 | [diff] [blame] | 153 | self._append_stats(range(78, 82), 'wr', 'total') |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 154 | self._fio_table.extend([ |
Puthikorn Voravootivat | 40bb809 | 2013-12-18 18:01:34 -0800 | [diff] [blame] | 155 | ('_%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 Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 177 | |
| 178 | # Other Results: |
| 179 | self._fio_table.extend([ |
Puthikorn Voravootivat | 40bb809 | 2013-12-18 18:01:34 -0800 | [diff] [blame] | 180 | ('_%s_cpu_usg_usr_percent' , 128, self._parse_gen), |
| 181 | ('_%s_cpu_usg_sys_percent' , 129, self._parse_gen), |
Puthikorn Voravootivat | f505a7f | 2013-12-20 17:21:18 -0800 | [diff] [blame] | 182 | ('_%s_cpu_context_count' , 130, self._parse_gen), |
Puthikorn Voravootivat | 40bb809 | 2013-12-18 18:01:34 -0800 | [diff] [blame] | 183 | ('_%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 Grundler | b628ac9 | 2013-10-11 15:28:12 -0700 | [diff] [blame] | 214 | |
| 215 | # Disk Utilization: only boot disk is tested |
Puthikorn Voravootivat | 40bb809 | 2013-12-18 18:01:34 -0800 | [diff] [blame] | 216 | ('_%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 Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 225 | |
| 226 | |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 227 | def __init__(self, data, prefix=None): |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 228 | """ |
| 229 | Fills the dictionary object with the fio output upon instantiation. |
| 230 | |
Grant Grundler | b628ac9 | 2013-10-11 15:28:12 -0700 | [diff] [blame] | 231 | @param data: fio HOWTO documents list of values from fio output. |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 232 | @param prefix: name to append for the result key value pair. |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 233 | |
| 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 Voravootivat | 2e26596 | 2014-02-21 14:39:17 -0800 | [diff] [blame] | 247 | terse_version = int(data[0]) |
| 248 | fio_terse_parser = { 4 : self._build_fio_terse_4_table } |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 249 | |
Puthikorn Voravootivat | 2e26596 | 2014-02-21 14:39:17 -0800 | [diff] [blame] | 250 | if terse_version in fio_terse_parser: |
| 251 | fio_terse_parser[terse_version]() |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 252 | else: |
Puthikorn Voravootivat | 2e26596 | 2014-02-21 14:39:17 -0800 | [diff] [blame] | 253 | raise fio_parser_exception('fio terse version %s unsupported.' |
| 254 | 'fio_parser supports terse version %s' % |
| 255 | (terse_version, fio_terse_parser.keys())) |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 256 | |
| 257 | # Fill dictionary object. |
| 258 | self._job_name = data[2] |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 259 | if prefix: |
| 260 | self._job_name = prefix + '_' + self._job_name |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 261 | for field, idx, parser in self._fio_table: |
Puthikorn Voravootivat | 2e26596 | 2014-02-21 14:39:17 -0800 | [diff] [blame] | 262 | # Field 162-170 only reported when we test on block device. |
| 263 | if len(data) <= idx: |
| 264 | break |
Vivia Nikolaidou | 26bc65a | 2012-07-17 15:32:13 +0100 | [diff] [blame] | 265 | parser(self._job_name, field, data[idx]) |
Gwendal Grignou | 29d2af5 | 2014-05-02 11:32:27 -0700 | [diff] [blame] | 266 | |
| 267 | |
Puthikorn Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 268 | class 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 Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 320 | def _parse_log_file(cls, file_name, pass_index, pass_count, percentile): |
Puthikorn Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 321 | """ |
| 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 Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 364 | def _gen_data_col(cls, pass_list, percentile): |
Puthikorn Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 365 | """ |
| 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 Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 384 | def _gen_data_row(cls, test_name, test_type, pass_list, percentile): |
Puthikorn Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 385 | """ |
| 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 Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 401 | all_row.extend(cls._parse_log_file(log_file_name, pass_index, |
Puthikorn Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 402 | pass_count, percentile)) |
| 403 | return all_row |
| 404 | |
| 405 | @classmethod |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 406 | def _write_data(cls, f, test_name, test_type, pass_list, percentile): |
Puthikorn Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 407 | """ |
| 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 Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 416 | col = cls._gen_data_col(pass_list, percentile) |
| 417 | row = cls._gen_data_row(test_name, test_type, pass_list, percentile) |
Puthikorn Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 418 | 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 Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 425 | def _write_option(cls, f, test_name, test_type, percentile): |
Puthikorn Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 426 | """ |
| 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 Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 437 | (cls.graph_title[test_type], test_name)) |
Puthikorn Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 438 | else: |
| 439 | option['title'] = ('Graph of %s for %s workload over time' % |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 440 | (cls.graph_title[test_type], test_name)) |
Puthikorn Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 441 | |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 442 | option['hAxis'] = { 'title': cls.h_title[percentile]} |
| 443 | option['vAxis'] = { 'title': cls.v_title[test_type]} |
Puthikorn Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 444 | |
| 445 | f.write('var options = ') |
| 446 | json.dump(option, f) |
| 447 | f.write(';\n') |
| 448 | |
| 449 | @classmethod |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 450 | def _write_graph(cls, test_name, test_type, pass_list, percentile=False): |
Puthikorn Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 451 | """ |
| 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 Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 469 | 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 Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 473 | |
| 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 Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 492 | def fio_parser(lines, prefix=None): |
Gwendal Grignou | 29d2af5 | 2014-05-02 11:32:27 -0700 | [diff] [blame] | 493 | """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 Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 499 | @param prefix: prefix for result keys. |
Gwendal Grignou | 29d2af5 | 2014-05-02 11:32:27 -0700 | [diff] [blame] | 500 | |
| 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 Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 521 | results.update(fio_job_output(values, prefix)) |
Gwendal Grignou | 29d2af5 | 2014-05-02 11:32:27 -0700 | [diff] [blame] | 522 | |
| 523 | return results |
| 524 | |
| 525 | |
Puthikorn Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 526 | def 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 Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 540 | 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 Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 577 | def fio_runner(test, job, env_vars, |
| 578 | name_prefix=None, |
| 579 | graph_prefix=None): |
Gwendal Grignou | 29d2af5 | 2014-05-02 11:32:27 -0700 | [diff] [blame] | 580 | """ |
| 581 | Runs fio. |
| 582 | |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 583 | 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 Voravootivat | 425b1a7 | 2014-05-14 17:33:27 -0700 | [diff] [blame] | 591 | @param test: test to upload perf value |
Gwendal Grignou | 29d2af5 | 2014-05-02 11:32:27 -0700 | [diff] [blame] | 592 | @param job: fio config file to use |
| 593 | @param env_vars: environment variable fio will substituete in the fio |
| 594 | config file. |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 595 | @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 Grignou | 29d2af5 | 2014-05-02 11:32:27 -0700 | [diff] [blame] | 599 | @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 Voravootivat | 1696136 | 2014-05-07 15:57:27 -0700 | [diff] [blame] | 622 | |
| 623 | fio_generate_graph() |
| 624 | |
Puthikorn Voravootivat | 8b811e0 | 2014-06-02 14:13:45 -0700 | [diff] [blame] | 625 | filename = re.match('.*FILENAME=(?P<f>[^ ]*)', env_vars).group('f') |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 626 | diskname = utils.get_disk_from_filename(filename) |
Puthikorn Voravootivat | 8b811e0 | 2014-06-02 14:13:45 -0700 | [diff] [blame] | 627 | |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 628 | if diskname: |
Puthikorn Voravootivat | 8b811e0 | 2014-06-02 14:13:45 -0700 | [diff] [blame] | 629 | model = utils.get_disk_model(diskname) |
| 630 | size = utils.get_disk_size_gb(diskname) |
| 631 | perfdb_name = '%s_%dG' % (model, size) |
| 632 | else: |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 633 | perfdb_name = filename.replace('/', '_') |
Puthikorn Voravootivat | 8b811e0 | 2014-06-02 14:13:45 -0700 | [diff] [blame] | 634 | |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 635 | if name_prefix: |
| 636 | perfdb_name = name_prefix + '_' + perfdb_name |
| 637 | |
Gwendal Grignou | 51d5069 | 2014-06-20 11:42:18 -0700 | [diff] [blame] | 638 | result = fio_parser(fio.stdout, prefix=name_prefix) |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 639 | if not graph_prefix: |
| 640 | graph_prefix = '' |
Puthikorn Voravootivat | 8b811e0 | 2014-06-02 14:13:45 -0700 | [diff] [blame] | 641 | |
Puthikorn Voravootivat | 425b1a7 | 2014-05-14 17:33:27 -0700 | [diff] [blame] | 642 | # 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 Voravootivat | 9867b76 | 2014-05-22 12:09:20 -0700 | [diff] [blame] | 645 | # don't log useless stat like 16k_randwrite_rd_bw_KB_sec |
| 646 | skip_matcher = re.compile('.*(trim.*|write.*_rd|read.*_wr)_.*') |
Puthikorn Voravootivat | 425b1a7 | 2014-05-14 17:33:27 -0700 | [diff] [blame] | 647 | for k, v in result.iteritems(): |
Gwendal Grignou | 2f16f2f | 2014-06-12 11:28:05 -0700 | [diff] [blame] | 648 | # Remove the prefix for value, and replace it the graph prefix. |
Gwendal Grignou | 51d5069 | 2014-06-20 11:42:18 -0700 | [diff] [blame] | 649 | if name_prefix: |
| 650 | k = k.replace('_' + name_prefix, graph_prefix) |
Puthikorn Voravootivat | 9867b76 | 2014-05-22 12:09:20 -0700 | [diff] [blame] | 651 | if skip_matcher.match(k): |
| 652 | continue |
Puthikorn Voravootivat | 425b1a7 | 2014-05-14 17:33:27 -0700 | [diff] [blame] | 653 | if bw_matcher.match(k): |
Puthikorn Voravootivat | 8b811e0 | 2014-06-02 14:13:45 -0700 | [diff] [blame] | 654 | test.output_perf_value(description=perfdb_name, graph=k, value=v, |
| 655 | units='KB_per_sec', higher_is_better=True) |
Puthikorn Voravootivat | 425b1a7 | 2014-05-14 17:33:27 -0700 | [diff] [blame] | 656 | elif lat_matcher.match(k): |
Puthikorn Voravootivat | 8b811e0 | 2014-06-02 14:13:45 -0700 | [diff] [blame] | 657 | test.output_perf_value(description=perfdb_name, graph=k, value=v, |
| 658 | units='us', higher_is_better=False) |
Puthikorn Voravootivat | 425b1a7 | 2014-05-14 17:33:27 -0700 | [diff] [blame] | 659 | return result |