blob: ee34af42523d1962bbe5b72c38474b2e5938e977 [file] [log] [blame]
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +02001import array
koder aka kdanilovbc2c8982015-06-13 02:50:43 +03002import os.path
3import logging
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +02004from typing import cast, Dict
koder aka kdanilovbc2c8982015-06-13 02:50:43 +03005
koder aka kdanilov6ab4d432015-06-22 00:26:28 +03006import wally
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +03007
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +02008from ...utils import StopTestError, get_os, ssize2b
koder aka kdanilov70227062016-11-26 23:23:21 +02009from ...node_interfaces import IRPCNode
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +020010from ..itest import ThreadedTest, IterationConfig, NodeTestResults
11from ...result_classes import TimeSerie
12from .fio_task_parser import execution_time, fio_cfg_compile, FioJobSection, FioParams, get_log_files, get_test_summary
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +020013from . import rpc_plugin
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +020014from .fio_hist import expected_lat_bins
15from ...storage import Storage
koder aka kdanilovbc2c8982015-06-13 02:50:43 +030016
17logger = logging.getLogger("wally")
18
19
koder aka kdanilov70227062016-11-26 23:23:21 +020020class IOPerfTest(ThreadedTest):
koder aka kdanilovbc2c8982015-06-13 02:50:43 +030021 soft_runcycle = 5 * 60
Michael Semenov8ba6e232015-08-28 10:57:18 +000022 retry_time = 30
koder aka kdanilov70227062016-11-26 23:23:21 +020023 configs_dir = os.path.dirname(__file__) # type: str
koder aka kdanilovbc2c8982015-06-13 02:50:43 +030024
koder aka kdanilov70227062016-11-26 23:23:21 +020025 def __init__(self, *args, **kwargs) -> None:
26 super().__init__(*args, **kwargs)
koder aka kdanilovbc2c8982015-06-13 02:50:43 +030027
28 get = self.config.params.get
koder aka kdanilovbc2c8982015-06-13 02:50:43 +030029
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +020030 self.remote_task_file = self.join_remote("task.fio")
31 self.remote_output_file = self.join_remote("fio_result.json")
32 self.use_system_fio = get('use_system_fio', False) # type: bool
33 self.use_sudo = get("use_sudo", True) # type: bool
34 self.force_prefill = get('force_prefill', False) # type: bool
35
koder aka kdanilov70227062016-11-26 23:23:21 +020036 self.load_profile_name = self.config.params['load'] # type: str
37 self.name = "io." + self.load_profile_name
koder aka kdanilovbc2c8982015-06-13 02:50:43 +030038
koder aka kdanilov70227062016-11-26 23:23:21 +020039 if os.path.isfile(self.load_profile_name):
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +020040 self.load_profile_path = self.load_profile_name # type: str
koder aka kdanilov6ab4d432015-06-22 00:26:28 +030041 else:
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +020042 self.load_profile_path = os.path.join(self.configs_dir, self.load_profile_name+ '.cfg')
koder aka kdanilovbc2c8982015-06-13 02:50:43 +030043
koder aka kdanilov70227062016-11-26 23:23:21 +020044 self.load_profile = open(self.load_profile_path, 'rt').read() # type: str
koder aka kdanilovbc2c8982015-06-13 02:50:43 +030045
koder aka kdanilovbc2c8982015-06-13 02:50:43 +030046 if self.use_system_fio:
koder aka kdanilov70227062016-11-26 23:23:21 +020047 self.fio_path = "fio" # type: str
koder aka kdanilovbc2c8982015-06-13 02:50:43 +030048 else:
koder aka kdanilov70227062016-11-26 23:23:21 +020049 self.fio_path = os.path.join(self.config.remote_dir, "fio")
koder aka kdanilovbc2c8982015-06-13 02:50:43 +030050
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +020051 self.load_params = self.config.params['params']
52 self.file_name = self.load_params['FILENAME']
koder aka kdanilovbc2c8982015-06-13 02:50:43 +030053
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +020054 if 'FILESIZE' not in self.load_params:
55 logger.debug("Getting test file sizes on all nodes")
56 try:
57 sizes = {node.conn.fs.file_stat(self.file_name)['size']
58 for node in self.config.nodes}
59 except Exception:
60 logger.exception("FILESIZE is not set in config file and fail to detect it." +
61 "Set FILESIZE or fix error and rerun test")
62 raise StopTestError()
koder aka kdanilova94dfe12015-08-19 13:04:51 +030063
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +020064 if len(sizes) != 1:
65 logger.error("IO target file %r has different sizes on test nodes - %r",
66 self.file_name, sizes)
67 raise StopTestError()
koder aka kdanilova94dfe12015-08-19 13:04:51 +030068
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +020069 self.file_size = list(sizes)[0]
70 logger.info("Detected test file size is %s", self.file_size)
71 self.load_params['FILESIZE'] = self.file_size
72 else:
73 self.file_size = ssize2b(self.load_params['FILESIZE'])
koder aka kdanilov70227062016-11-26 23:23:21 +020074
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +020075 self.fio_configs = list(fio_cfg_compile(self.load_profile, self.load_profile_path,
76 cast(FioParams, self.load_params)))
koder aka kdanilov70227062016-11-26 23:23:21 +020077
78 if len(self.fio_configs) == 0:
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +020079 logger.error("Empty fio config provided")
80 raise StopTestError()
koder aka kdanilov70227062016-11-26 23:23:21 +020081
82 self.iterations_configs = self.fio_configs # type: ignore
koder aka kdanilov70227062016-11-26 23:23:21 +020083 self.exec_folder = self.config.remote_dir
koder aka kdanilovbc2c8982015-06-13 02:50:43 +030084
koder aka kdanilov70227062016-11-26 23:23:21 +020085 def config_node(self, node: IRPCNode) -> None:
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +020086 plugin_code = open(rpc_plugin.__file__.rsplit(".", 1)[0] + ".py", "rb").read() # type: bytes
87 node.upload_plugin("fio", plugin_code)
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +020088
koder aka kdanilovbc2c8982015-06-13 02:50:43 +030089 try:
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +020090 node.conn.fs.rmtree(self.config.remote_dir)
91 except Exception:
92 pass
93
94 try:
95 node.conn.fs.makedirs(self.config.remote_dir)
96 except Exception:
97 msg = "Failed to recreate folder {} on remote {}.".format(self.config.remote_dir, node)
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030098 logger.exception(msg)
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +020099 raise StopTestError()
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300100
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300101 self.install_utils(node)
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200102
103 mb = int(self.file_size / 1024 ** 2)
104 logger.info("Filling test file %s with %sMiB of random data", self.file_name, mb)
105 fill_bw = node.conn.fio.fill_file(self.file_name, mb, force=self.force_prefill, fio_path=self.fio_path)
koder aka kdanilov70227062016-11-26 23:23:21 +0200106 if fill_bw is not None:
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200107 logger.info("Initial fio fill bw is {} MiBps for {}".format(fill_bw, node))
108
koder aka kdanilov70227062016-11-26 23:23:21 +0200109 def install_utils(self, node: IRPCNode) -> None:
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200110 os_info = get_os(node)
koder aka kdanilov6ab4d432015-06-22 00:26:28 +0300111 if self.use_system_fio:
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200112 if os_info.distro != 'ubuntu':
113 logger.error("Only ubuntu supported on test VM")
114 raise StopTestError()
115 node.conn.fio.install('fio', binary='fio')
116 else:
117 node.conn.fio.install('bzip2', binary='bzip2')
koder aka kdanilov70227062016-11-26 23:23:21 +0200118 fio_dir = os.path.dirname(os.path.dirname(wally.__file__)) # type: str
119 fio_dir = os.path.join(os.getcwd(), fio_dir)
120 fio_dir = os.path.join(fio_dir, 'fio_binaries')
121 fname = 'fio_{0.release}_{0.arch}.bz2'.format(os_info)
122 fio_path = os.path.join(fio_dir, fname) # type: str
123
124 if not os.path.exists(fio_path):
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200125 logger.error("No prebuild fio binary available for {0}".format(os_info))
126 raise StopTestError()
koder aka kdanilov70227062016-11-26 23:23:21 +0200127
128 bz_dest = self.join_remote('fio.bz2') # type: str
129 node.copy_file(fio_path, bz_dest)
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200130 node.run("bzip2 --decompress {} ; chmod a+x {}".format(bz_dest, self.join_remote("fio")))
koder aka kdanilov70227062016-11-26 23:23:21 +0200131
132 def get_expected_runtime(self, iteration_info: IterationConfig) -> int:
133 return execution_time(cast(FioJobSection, iteration_info))
134
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200135 def prepare_iteration(self, node: IRPCNode, iter_config: IterationConfig) -> None:
136 node.put_to_file(self.remote_task_file, str(iter_config).encode("utf8"))
137
138 # TODO: get a link to substorage as a parameter
139 def run_iteration(self, node: IRPCNode, iter_config: IterationConfig, substorage: Storage) -> NodeTestResults:
koder aka kdanilov70227062016-11-26 23:23:21 +0200140 exec_time = execution_time(cast(FioJobSection, iter_config))
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200141
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200142 fio_cmd_templ = "cd {exec_folder}; " + \
143 "{fio_path} --output-format=json --output={out_file} --alloc-size=262144 {job_file}"
144
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200145 cmd = fio_cmd_templ.format(exec_folder=self.exec_folder,
146 fio_path=self.fio_path,
147 out_file=self.remote_output_file,
148 job_file=self.remote_task_file)
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200149 must_be_empty = node.run(cmd, timeout=exec_time + max(300, exec_time), check_timeout=1).strip()
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200150
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200151 if must_be_empty:
152 logger.error("Unexpected fio output: %r", must_be_empty)
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300153
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200154 res = NodeTestResults(self.__class__.__name__,
155 node.info.node_id(),
156 get_test_summary(cast(FioJobSection, iter_config)))
157
158 res.extra_logs['fio'] = node.get_file_content(self.remote_output_file)
159 substorage.store_raw(res.extra_logs['fio'], "fio_raw")
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
167 for name, path in get_log_files(cast(FioJobSection, iter_config)):
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
176 substorage.store_raw(raw_result, "{}_raw".format(name))
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
225 substorage.set_array(parsed, "{}_data".format(name)) # type: ignore
226 substorage["{}_meta".format(name)] = res.series[name].meta() # type: ignore
227
228 return res