koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 1 | import array |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 2 | import os.path |
| 3 | import logging |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame^] | 4 | from typing import cast, Any |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 5 | |
koder aka kdanilov | 6ab4d43 | 2015-06-22 00:26:28 +0300 | [diff] [blame] | 6 | import wally |
koder aka kdanilov | 3b4da8b | 2016-10-17 00:17:53 +0300 | [diff] [blame] | 7 | |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 8 | from ...utils import StopTestError, get_os, ssize2b |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 9 | from ...node_interfaces import IRPCNode |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 10 | from ..itest import ThreadedTest, IterationConfig, NodeTestResults |
| 11 | from ...result_classes import TimeSerie |
| 12 | from .fio_task_parser import execution_time, fio_cfg_compile, FioJobSection, FioParams, get_log_files, get_test_summary |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 13 | from . import rpc_plugin |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 14 | from .fio_hist import expected_lat_bins |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame^] | 15 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 16 | |
| 17 | logger = logging.getLogger("wally") |
| 18 | |
| 19 | |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 20 | class IOPerfTest(ThreadedTest): |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 21 | soft_runcycle = 5 * 60 |
Michael Semenov | 8ba6e23 | 2015-08-28 10:57:18 +0000 | [diff] [blame] | 22 | retry_time = 30 |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 23 | configs_dir = os.path.dirname(__file__) # type: str |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame^] | 24 | name = 'fio' |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 25 | |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 26 | def __init__(self, *args, **kwargs) -> None: |
| 27 | super().__init__(*args, **kwargs) |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 28 | |
| 29 | get = self.config.params.get |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 30 | |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 31 | self.remote_task_file = self.join_remote("task.fio") |
| 32 | self.remote_output_file = self.join_remote("fio_result.json") |
| 33 | self.use_system_fio = get('use_system_fio', False) # type: bool |
| 34 | self.use_sudo = get("use_sudo", True) # type: bool |
| 35 | self.force_prefill = get('force_prefill', False) # type: bool |
| 36 | |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 37 | self.load_profile_name = self.config.params['load'] # type: str |
| 38 | self.name = "io." + self.load_profile_name |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 39 | |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 40 | if os.path.isfile(self.load_profile_name): |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 41 | self.load_profile_path = self.load_profile_name # type: str |
koder aka kdanilov | 6ab4d43 | 2015-06-22 00:26:28 +0300 | [diff] [blame] | 42 | else: |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 43 | self.load_profile_path = os.path.join(self.configs_dir, self.load_profile_name+ '.cfg') |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 44 | |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 45 | self.load_profile = open(self.load_profile_path, 'rt').read() # type: str |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 46 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 47 | if self.use_system_fio: |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 48 | self.fio_path = "fio" # type: str |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 49 | else: |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 50 | self.fio_path = os.path.join(self.config.remote_dir, "fio") |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 51 | |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 52 | self.load_params = self.config.params['params'] |
| 53 | self.file_name = self.load_params['FILENAME'] |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 54 | |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 55 | if 'FILESIZE' not in self.load_params: |
| 56 | logger.debug("Getting test file sizes on all nodes") |
| 57 | try: |
| 58 | sizes = {node.conn.fs.file_stat(self.file_name)['size'] |
| 59 | for node in self.config.nodes} |
| 60 | except Exception: |
| 61 | logger.exception("FILESIZE is not set in config file and fail to detect it." + |
| 62 | "Set FILESIZE or fix error and rerun test") |
| 63 | raise StopTestError() |
koder aka kdanilov | a94dfe1 | 2015-08-19 13:04:51 +0300 | [diff] [blame] | 64 | |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 65 | if len(sizes) != 1: |
| 66 | logger.error("IO target file %r has different sizes on test nodes - %r", |
| 67 | self.file_name, sizes) |
| 68 | raise StopTestError() |
koder aka kdanilov | a94dfe1 | 2015-08-19 13:04:51 +0300 | [diff] [blame] | 69 | |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 70 | self.file_size = list(sizes)[0] |
| 71 | logger.info("Detected test file size is %s", self.file_size) |
| 72 | self.load_params['FILESIZE'] = self.file_size |
| 73 | else: |
| 74 | self.file_size = ssize2b(self.load_params['FILESIZE']) |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 75 | |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 76 | self.fio_configs = list(fio_cfg_compile(self.load_profile, self.load_profile_path, |
| 77 | cast(FioParams, self.load_params))) |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 78 | |
| 79 | if len(self.fio_configs) == 0: |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 80 | logger.error("Empty fio config provided") |
| 81 | raise StopTestError() |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 82 | |
| 83 | self.iterations_configs = self.fio_configs # type: ignore |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 84 | self.exec_folder = self.config.remote_dir |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 85 | |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 86 | def config_node(self, node: IRPCNode) -> None: |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 87 | plugin_code = open(rpc_plugin.__file__.rsplit(".", 1)[0] + ".py", "rb").read() # type: bytes |
| 88 | node.upload_plugin("fio", plugin_code) |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 89 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 90 | try: |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 91 | node.conn.fs.rmtree(self.config.remote_dir) |
| 92 | except Exception: |
| 93 | pass |
| 94 | |
| 95 | try: |
| 96 | node.conn.fs.makedirs(self.config.remote_dir) |
| 97 | except Exception: |
| 98 | msg = "Failed to recreate folder {} on remote {}.".format(self.config.remote_dir, node) |
koder aka kdanilov | 3b4da8b | 2016-10-17 00:17:53 +0300 | [diff] [blame] | 99 | logger.exception(msg) |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 100 | raise StopTestError() |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 101 | |
koder aka kdanilov | 3b4da8b | 2016-10-17 00:17:53 +0300 | [diff] [blame] | 102 | self.install_utils(node) |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 103 | |
| 104 | mb = int(self.file_size / 1024 ** 2) |
| 105 | logger.info("Filling test file %s with %sMiB of random data", self.file_name, mb) |
| 106 | fill_bw = node.conn.fio.fill_file(self.file_name, mb, force=self.force_prefill, fio_path=self.fio_path) |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 107 | if fill_bw is not None: |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 108 | logger.info("Initial fio fill bw is {} MiBps for {}".format(fill_bw, node)) |
| 109 | |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 110 | def install_utils(self, node: IRPCNode) -> None: |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 111 | os_info = get_os(node) |
koder aka kdanilov | 6ab4d43 | 2015-06-22 00:26:28 +0300 | [diff] [blame] | 112 | if self.use_system_fio: |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 113 | if os_info.distro != 'ubuntu': |
| 114 | logger.error("Only ubuntu supported on test VM") |
| 115 | raise StopTestError() |
| 116 | node.conn.fio.install('fio', binary='fio') |
| 117 | else: |
| 118 | node.conn.fio.install('bzip2', binary='bzip2') |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 119 | fio_dir = os.path.dirname(os.path.dirname(wally.__file__)) # type: str |
| 120 | fio_dir = os.path.join(os.getcwd(), fio_dir) |
| 121 | fio_dir = os.path.join(fio_dir, 'fio_binaries') |
| 122 | fname = 'fio_{0.release}_{0.arch}.bz2'.format(os_info) |
| 123 | fio_path = os.path.join(fio_dir, fname) # type: str |
| 124 | |
| 125 | if not os.path.exists(fio_path): |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 126 | logger.error("No prebuild fio binary available for {0}".format(os_info)) |
| 127 | raise StopTestError() |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 128 | |
| 129 | bz_dest = self.join_remote('fio.bz2') # type: str |
| 130 | node.copy_file(fio_path, bz_dest) |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 131 | node.run("bzip2 --decompress {} ; chmod a+x {}".format(bz_dest, self.join_remote("fio"))) |
koder aka kdanilov | 7022706 | 2016-11-26 23:23:21 +0200 | [diff] [blame] | 132 | |
| 133 | def get_expected_runtime(self, iteration_info: IterationConfig) -> int: |
| 134 | return execution_time(cast(FioJobSection, iteration_info)) |
| 135 | |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 136 | def prepare_iteration(self, node: IRPCNode, iter_config: IterationConfig) -> None: |
| 137 | node.put_to_file(self.remote_task_file, str(iter_config).encode("utf8")) |
| 138 | |
| 139 | # TODO: get a link to substorage as a parameter |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame^] | 140 | def run_iteration(self, node: IRPCNode, iter_config: IterationConfig, stor_prefix: str) -> NodeTestResults: |
| 141 | f_iter_config = cast(FioJobSection, iter_config) |
| 142 | exec_time = execution_time(f_iter_config) |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 143 | |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 144 | fio_cmd_templ = "cd {exec_folder}; " + \ |
| 145 | "{fio_path} --output-format=json --output={out_file} --alloc-size=262144 {job_file}" |
| 146 | |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 147 | cmd = fio_cmd_templ.format(exec_folder=self.exec_folder, |
| 148 | fio_path=self.fio_path, |
| 149 | out_file=self.remote_output_file, |
| 150 | job_file=self.remote_task_file) |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 151 | must_be_empty = node.run(cmd, timeout=exec_time + max(300, exec_time), check_timeout=1).strip() |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 152 | |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 153 | if must_be_empty: |
| 154 | logger.error("Unexpected fio output: %r", must_be_empty) |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 155 | |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame^] | 156 | res = NodeTestResults(self.__class__.__name__, node.info.node_id(), get_test_summary(f_iter_config)) |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 157 | |
| 158 | res.extra_logs['fio'] = node.get_file_content(self.remote_output_file) |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame^] | 159 | self.store_data(res.extra_logs['fio'], "raw", stor_prefix, "fio_raw") |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 160 | node.conn.fs.unlink(self.remote_output_file) |
| 161 | |
| 162 | files = [name for name in node.conn.fs.listdir(self.exec_folder)] |
| 163 | |
| 164 | expected_time_delta = 1000 # 1000ms == 1s |
| 165 | max_time_diff = 50 # 50ms - 5% |
| 166 | |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame^] | 167 | for name, path in get_log_files(f_iter_config): |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 168 | log_files = [fname for fname in files if fname.startswith(path)] |
| 169 | if len(log_files) != 1: |
| 170 | logger.error("Found %s files, match log pattern %s(%s) - %s", |
| 171 | len(log_files), path, name, ",".join(log_files[10:])) |
| 172 | raise StopTestError() |
| 173 | |
| 174 | fname = os.path.join(self.exec_folder, log_files[0]) |
| 175 | raw_result = node.get_file_content(fname) # type: bytes |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame^] | 176 | self.store_data(raw_result, "raw", stor_prefix, "{}_raw".format(name)) |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 177 | node.conn.fs.unlink(fname) |
| 178 | |
| 179 | try: |
| 180 | log_data = raw_result.decode("utf8").split("\n") |
| 181 | except UnicodeEncodeError: |
| 182 | logger.exception("Error during parse %s fio log file - can't decode usint UTF8", name) |
| 183 | raise StopTestError() |
| 184 | |
| 185 | parsed = array.array('L' if name == 'lat' else 'Q') |
| 186 | prev_ts = None |
| 187 | load_start_at = None |
| 188 | |
| 189 | # TODO: need to adjust vals for timedelta |
| 190 | for idx, line in enumerate(log_data): |
| 191 | line = line.strip() |
| 192 | if line: |
| 193 | try: |
| 194 | time_ms_s, val_s, _, *rest = line.split(",") |
| 195 | time_ms = int(time_ms_s.strip()) |
| 196 | if prev_ts and abs(time_ms - prev_ts - expected_time_delta) > max_time_diff: |
| 197 | logger.warning("Too large gap in {} log at {} - {}ms" |
| 198 | .format(time_ms, name, time_ms - prev_ts)) |
| 199 | else: |
| 200 | prev_ts = time_ms - expected_time_delta |
| 201 | load_start_at = time_ms |
| 202 | if name == 'lat': |
| 203 | vals = [int(i.strip()) for i in rest] |
| 204 | |
| 205 | if len(vals) != expected_lat_bins: |
| 206 | logger.error("Expect {} bins in latency histogram, but found {} at time {}" |
| 207 | .format(expected_lat_bins, len(vals), time_ms_s)) |
| 208 | raise StopTestError() |
| 209 | |
| 210 | parsed.extend(vals) |
| 211 | else: |
| 212 | parsed.append(int(val_s.strip())) |
| 213 | except ValueError: |
| 214 | logger.exception("Error during parse %s fio log file in line %s: %r", name, idx, line) |
| 215 | raise StopTestError() |
| 216 | prev_ts += expected_time_delta |
| 217 | |
| 218 | res.series[name] = TimeSerie(name=name, |
| 219 | raw=raw_result, |
| 220 | second_axis_size=expected_lat_bins if name == 'lat' else 1, |
| 221 | start_at=load_start_at, |
| 222 | step=expected_time_delta, |
| 223 | data=parsed) |
| 224 | |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame^] | 225 | self.store_data(parsed, "array", stor_prefix, "{}_data".format(name)) |
| 226 | self.store_data(res.series[name].meta(), "yaml", stor_prefix, "{}_meta".format(name)) |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 227 | |
| 228 | return res |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame^] | 229 | |
| 230 | def format_for_console(self, data: Any) -> str: |
| 231 | raise NotImplementedError() |