blob: 222589b2dde2dcc87a41fee6d5f3db3c656a91de [file] [log] [blame]
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +03001#!/usr/bin/env python3
2
3import re
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +03004import os
5import sys
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +03006import os.path
7import argparse
8import itertools
koder aka kdanilovf90de852017-01-20 18:12:27 +02009from typing import Optional, Iterator, Union, Dict, Iterable, List, Tuple, NamedTuple, Any
koder aka kdanilov70227062016-11-26 23:23:21 +020010from collections import OrderedDict
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030011
12
koder aka kdanilovf90de852017-01-20 18:12:27 +020013from ...utils import sec_to_str, ssize2b, flatmap
14from .fio_job import Var, FioJobConfig
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030015
16SECTION = 0
17SETTING = 1
18INCLUDE = 2
19
20
koder aka kdanilov70227062016-11-26 23:23:21 +020021CfgLine = NamedTuple('CfgLine',
22 [('fname', str),
23 ('lineno', int),
24 ('oline', str),
25 ('tp', int),
26 ('name', str),
27 ('val', Any)])
koder aka kdanilov7f59d562016-12-26 01:34:23 +020028
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030029
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030030class ParseError(ValueError):
koder aka kdanilov70227062016-11-26 23:23:21 +020031 def __init__(self, msg: str, fname: str, lineno: int, line_cont:Optional[str] = "") -> None:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030032 ValueError.__init__(self, msg)
33 self.file_name = fname
34 self.lineno = lineno
35 self.line_cont = line_cont
36
koder aka kdanilov70227062016-11-26 23:23:21 +020037 def __str__(self) -> str:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030038 msg = "In {0}:{1} ({2}) : {3}"
39 return msg.format(self.file_name,
40 self.lineno,
41 self.line_cont,
42 super(ParseError, self).__str__())
43
44
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030045def is_name(name: str) -> bool:
koder aka kdanilov70227062016-11-26 23:23:21 +020046 return re.match("[a-zA-Z_][a-zA-Z_0-9]*", name) is not None
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030047
48
koder aka kdanilov70227062016-11-26 23:23:21 +020049def parse_value(val: str) -> Union[int, str, float, List, Var]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030050 try:
51 return int(val)
52 except ValueError:
53 pass
54
55 try:
56 return float(val)
57 except ValueError:
58 pass
59
60 if val.startswith('{%'):
61 assert val.endswith("%}")
62 content = val[2:-2]
63 vals = list(i.strip() for i in content.split(','))
koder aka kdanilov70227062016-11-26 23:23:21 +020064 return list(map(parse_value, vals))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030065
66 if val.startswith('{'):
67 assert val.endswith("}")
68 assert is_name(val[1:-1])
69 return Var(val[1:-1])
koder aka kdanilov70227062016-11-26 23:23:21 +020070
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030071 return val
72
73
koder aka kdanilov22d134e2016-11-08 11:33:19 +020074def fio_config_lexer(fio_cfg: str, fname: str) -> Iterator[CfgLine]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030075 for lineno, oline in enumerate(fio_cfg.split("\n")):
76 try:
77 line = oline.strip()
78
79 if line.startswith("#") or line.startswith(";"):
80 continue
81
82 if line == "":
83 continue
84
85 if '#' in line:
86 raise ParseError("# isn't allowed inside line",
87 fname, lineno, oline)
88
89 if line.startswith('['):
90 yield CfgLine(fname, lineno, oline, SECTION,
91 line[1:-1].strip(), None)
92 elif '=' in line:
93 opt_name, opt_val = line.split('=', 1)
94 yield CfgLine(fname, lineno, oline, SETTING,
95 opt_name.strip(),
96 parse_value(opt_val.strip()))
97 elif line.startswith("include "):
98 yield CfgLine(fname, lineno, oline, INCLUDE,
99 line.split(" ", 1)[1], None)
100 else:
101 yield CfgLine(fname, lineno, oline, SETTING, line, '1')
102
103 except Exception as exc:
104 raise ParseError(str(exc), fname, lineno, oline)
105
106
koder aka kdanilovf2865172016-12-30 03:35:11 +0200107def fio_config_parse(lexer_iter: Iterable[CfgLine]) -> Iterator[FioJobConfig]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300108 in_globals = False
109 curr_section = None
koder aka kdanilov70227062016-11-26 23:23:21 +0200110 glob_vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300111 sections_count = 0
112
koder aka kdanilov70227062016-11-26 23:23:21 +0200113 lexed_lines = list(lexer_iter) # type: List[CfgLine]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300114 one_more = True
115 includes = {}
116
117 while one_more:
koder aka kdanilov70227062016-11-26 23:23:21 +0200118 new_lines = [] # type: List[CfgLine]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300119 one_more = False
120 for line in lexed_lines:
121 fname, lineno, oline, tp, name, val = line
122
123 if INCLUDE == tp:
124 if not os.path.exists(fname):
125 dirname = '.'
126 else:
127 dirname = os.path.dirname(fname)
128
129 new_fname = os.path.join(dirname, name)
130 includes[new_fname] = (fname, lineno)
131
132 try:
133 cont = open(new_fname).read()
134 except IOError as err:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200135 raise ParseError("Error while including file {}: {}".format(new_fname, err), fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300136
137 new_lines.extend(fio_config_lexer(cont, new_fname))
138 one_more = True
139 else:
140 new_lines.append(line)
141
142 lexed_lines = new_lines
143
koder aka kdanilov108ac362017-01-19 20:17:16 +0200144 suite_section_idx = 0
145
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300146 for fname, lineno, oline, tp, name, val in lexed_lines:
147 if tp == SECTION:
148 if curr_section is not None:
149 yield curr_section
150 curr_section = None
151
152 if name == 'global':
153 if sections_count != 0:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200154 raise ParseError("[global] section should be only one and first", fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300155 in_globals = True
156 else:
157 in_globals = False
koder aka kdanilov108ac362017-01-19 20:17:16 +0200158 curr_section = FioJobConfig(name, idx=suite_section_idx)
159 suite_section_idx += 1
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300160 curr_section.vals = glob_vals.copy()
161 sections_count += 1
162 else:
163 assert tp == SETTING
164 if in_globals:
165 glob_vals[name] = val
166 elif name == name.upper():
koder aka kdanilovf2865172016-12-30 03:35:11 +0200167 raise ParseError("Param {!r} not in [global] section".format(name), fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300168 elif curr_section is None:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200169 raise ParseError("Data outside section", fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300170 else:
171 curr_section.vals[name] = val
172
173 if curr_section is not None:
174 yield curr_section
175
176
koder aka kdanilovf2865172016-12-30 03:35:11 +0200177def process_cycles(sec: FioJobConfig) -> Iterator[FioJobConfig]:
koder aka kdanilov70227062016-11-26 23:23:21 +0200178 cycles = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300179
180 for name, val in sec.vals.items():
181 if isinstance(val, list) and name.upper() != name:
182 cycles[name] = val
183
184 if len(cycles) == 0:
185 yield sec
186 else:
koder aka kdanilov70227062016-11-26 23:23:21 +0200187 # iodepth should changes faster
188 numjobs = cycles.pop('iodepth', None)
189 items = list(cycles.items())
koder aka kdanilov170936a2015-06-27 22:51:17 +0300190
koder aka kdanilov70227062016-11-26 23:23:21 +0200191 if items:
koder aka kdanilov170936a2015-06-27 22:51:17 +0300192 keys, vals = zip(*items)
193 keys = list(keys)
194 vals = list(vals)
195 else:
196 keys = []
197 vals = []
198
199 if numjobs is not None:
200 vals.append(numjobs)
koder aka kdanilov70227062016-11-26 23:23:21 +0200201 keys.append('iodepth')
koder aka kdanilov170936a2015-06-27 22:51:17 +0300202
203 for combination in itertools.product(*vals):
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300204 new_sec = sec.copy()
koder aka kdanilov170936a2015-06-27 22:51:17 +0300205 new_sec.vals.update(zip(keys, combination))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300206 yield new_sec
207
208
koder aka kdanilov70227062016-11-26 23:23:21 +0200209FioParamsVal = Union[str, Var]
210FioParams = Dict[str, FioParamsVal]
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300211
212
koder aka kdanilovf2865172016-12-30 03:35:11 +0200213def apply_params(sec: FioJobConfig, params: FioParams) -> FioJobConfig:
koder aka kdanilov70227062016-11-26 23:23:21 +0200214 processed_vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300215 processed_vals.update(params)
216 for name, val in sec.vals.items():
217 if name in params:
218 continue
219
220 if isinstance(val, Var):
221 if val.name in params:
222 val = params[val.name]
223 elif val.name in processed_vals:
224 val = processed_vals[val.name]
225 processed_vals[name] = val
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300226
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300227 sec = sec.copy()
228 sec.vals = processed_vals
229 return sec
230
231
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300232def abbv_name_to_full(name: str) -> str:
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300233 assert len(name) == 3
234
235 smode = {
236 'a': 'async',
237 's': 'sync',
238 'd': 'direct',
239 'x': 'sync direct'
240 }
241 off_mode = {'s': 'sequential', 'r': 'random'}
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300242 oper = {'r': 'read', 'w': 'write', 'm': 'mixed'}
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300243 return smode[name[2]] + " " + \
244 off_mode[name[0]] + " " + oper[name[1]]
245
246
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300247MAGIC_OFFSET = 0.1885
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300248
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300249
koder aka kdanilovf2865172016-12-30 03:35:11 +0200250def final_process(sec: FioJobConfig, counter: List[int] = [0]) -> FioJobConfig:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300251 sec = sec.copy()
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300252
253 sec.vals['unified_rw_reporting'] = '1'
254
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300255 if isinstance(sec.vals['size'], Var):
256 raise ValueError("Variable {0} isn't provided".format(
257 sec.vals['size'].name))
258
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300259 sz = ssize2b(sec.vals['size'])
260 offset = sz * ((MAGIC_OFFSET * counter[0]) % 1.0)
261 offset = int(offset) // 1024 ** 2
262 new_vars = {'UNIQ_OFFSET': str(offset) + "m"}
263
264 for name, val in sec.vals.items():
265 if isinstance(val, Var):
266 if val.name in new_vars:
267 sec.vals[name] = new_vars[val.name]
268
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300269 for vl in sec.vals.values():
270 if isinstance(vl, Var):
271 raise ValueError("Variable {0} isn't provided".format(vl.name))
272
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300273 params = sec.vals.copy()
274 params['UNIQ'] = 'UN{0}'.format(counter[0])
275 params['COUNTER'] = str(counter[0])
koder aka kdanilov108ac362017-01-19 20:17:16 +0200276 params['TEST_SUMM'] = sec.summary
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300277 sec.name = sec.name.format(**params)
278 counter[0] += 1
279
280 return sec
281
282
koder aka kdanilovf2865172016-12-30 03:35:11 +0200283def execution_time(sec: FioJobConfig) -> int:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300284 return sec.vals.get('ramp_time', 0) + sec.vals.get('runtime', 0)
285
286
koder aka kdanilovf2865172016-12-30 03:35:11 +0200287def parse_all_in_1(source:str, fname: str = None) -> Iterator[FioJobConfig]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300288 return fio_config_parse(fio_config_lexer(source, fname))
289
290
koder aka kdanilova732a602017-02-01 20:29:56 +0200291def get_log_files(sec: FioJobConfig, iops: bool = False) -> Iterator[Tuple[str, str, str]]:
292 res = [] # type: List[Tuple[str, str, str]]
koder aka kdanilov108ac362017-01-19 20:17:16 +0200293
kdanylov aka koder45183182017-04-30 23:55:40 +0300294 keys = [('write_bw_log', 'bw', 'KiBps'),
koder aka kdanilova732a602017-02-01 20:29:56 +0200295 ('write_hist_log', 'lat', 'us')]
koder aka kdanilov108ac362017-01-19 20:17:16 +0200296 if iops:
kdanylov aka koder45183182017-04-30 23:55:40 +0300297 keys.append(('write_iops_log', 'iops', 'IOPS'))
koder aka kdanilov108ac362017-01-19 20:17:16 +0200298
koder aka kdanilova732a602017-02-01 20:29:56 +0200299 for key, name, units in keys:
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200300 log = sec.vals.get(key)
301 if log is not None:
koder aka kdanilova732a602017-02-01 20:29:56 +0200302 yield (name, log, units)
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200303
304
koder aka kdanilovf2865172016-12-30 03:35:11 +0200305def fio_cfg_compile(source: str, fname: str, test_params: FioParams) -> Iterator[FioJobConfig]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300306 it = parse_all_in_1(source, fname)
307 it = (apply_params(sec, test_params) for sec in it)
308 it = flatmap(process_cycles, it)
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200309 for sec in map(final_process, it):
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200310 yield sec
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300311
312
313def parse_args(argv):
314 parser = argparse.ArgumentParser(
315 description="Run fio' and return result")
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300316 parser.add_argument("-p", "--params", nargs="*", metavar="PARAM=VAL",
317 default=[],
318 help="Provide set of pairs PARAM=VAL to" +
319 "format into job description")
320 parser.add_argument("action", choices=['estimate', 'compile', 'num_tests'])
321 parser.add_argument("jobfile")
322 return parser.parse_args(argv)
323
324
325def main(argv):
326 argv_obj = parse_args(argv)
327
328 if argv_obj.jobfile == '-':
329 job_cfg = sys.stdin.read()
330 else:
331 job_cfg = open(argv_obj.jobfile).read()
332
333 params = {}
334 for param_val in argv_obj.params:
335 assert '=' in param_val
336 name, val = param_val.split("=", 1)
337 params[name] = parse_value(val)
338
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300339 sec_it = fio_cfg_compile(job_cfg, argv_obj.jobfile, params)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300340
341 if argv_obj.action == 'estimate':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300342 print(sec_to_str(sum(map(execution_time, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300343 elif argv_obj.action == 'num_tests':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300344 print(sum(map(len, map(list, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300345 elif argv_obj.action == 'compile':
346 splitter = "\n#" + "-" * 70 + "\n\n"
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300347 print(splitter.join(map(str, sec_it)))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300348
349 return 0
350
351
352if __name__ == '__main__':
353 exit(main(sys.argv[1:]))