blob: 60a0a5519ae1a2b207a4379a214f8be6901bf9c8 [file] [log] [blame]
koder aka kdanilov108ac362017-01-19 20:17:16 +02001import abc
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +03002import copy
3from typing import Dict, List, Any, Optional, Tuple, cast, Type, Iterator, NamedTuple
koder aka kdanilovf2865172016-12-30 03:35:11 +02004
koder aka kdanilovffaf48d2016-12-27 02:25:29 +02005
6import numpy
koder aka kdanilovf2865172016-12-30 03:35:11 +02007from scipy.stats.mstats_basic import NormaltestResult
koder aka kdanilovffaf48d2016-12-27 02:25:29 +02008
koder aka kdanilovf90de852017-01-20 18:12:27 +02009from .suits.job import JobConfig
koder aka kdanilovf2865172016-12-30 03:35:11 +020010from .node_interfaces import IRPCNode
koder aka kdanilova732a602017-02-01 20:29:56 +020011from .common_types import Storable
koder aka kdanilovf2865172016-12-30 03:35:11 +020012from .utils import round_digits, Number
koder aka kdanilov70227062016-11-26 23:23:21 +020013
14
koder aka kdanilovf90de852017-01-20 18:12:27 +020015class SuiteConfig(Storable):
koder aka kdanilovf2865172016-12-30 03:35:11 +020016 """
17 Test suite input configuration.
18
19 test_type - test type name
20 params - parameters from yaml file for this test
21 run_uuid - UUID to be used to create file names & Co
22 nodes - nodes to run tests on
23 remote_dir - directory on nodes to be used for local files
24 """
koder aka kdanilovf90de852017-01-20 18:12:27 +020025 __ignore_fields__ = ['nodes', 'run_uuid', 'remote_dir']
26
koder aka kdanilovf2865172016-12-30 03:35:11 +020027 def __init__(self,
28 test_type: str,
29 params: Dict[str, Any],
30 run_uuid: str,
31 nodes: List[IRPCNode],
koder aka kdanilov108ac362017-01-19 20:17:16 +020032 remote_dir: str,
koder aka kdanilova732a602017-02-01 20:29:56 +020033 idx: int,
34 keep_raw_files: bool) -> None:
koder aka kdanilovf2865172016-12-30 03:35:11 +020035 self.test_type = test_type
36 self.params = params
37 self.run_uuid = run_uuid
38 self.nodes = nodes
koder aka kdanilov108ac362017-01-19 20:17:16 +020039 self.nodes_ids = [node.node_id for node in nodes]
koder aka kdanilovf2865172016-12-30 03:35:11 +020040 self.remote_dir = remote_dir
koder aka kdanilova732a602017-02-01 20:29:56 +020041 self.keep_raw_files = keep_raw_files
42
43 if 'load' in self.params:
44 self.storage_id = "{}_{}_{}".format(self.test_type, self.params['load'], idx)
45 else:
46 self.storage_id = "{}_{}".format(self.test_type, idx)
koder aka kdanilovf2865172016-12-30 03:35:11 +020047
koder aka kdanilov108ac362017-01-19 20:17:16 +020048 def __eq__(self, o: object) -> bool:
49 if type(o) is not self.__class__:
50 return False
51
koder aka kdanilovf90de852017-01-20 18:12:27 +020052 other = cast(SuiteConfig, o)
koder aka kdanilov108ac362017-01-19 20:17:16 +020053
koder aka kdanilovf2865172016-12-30 03:35:11 +020054 return (self.test_type == other.test_type and
55 self.params == other.params and
56 set(self.nodes_ids) == set(other.nodes_ids))
57
koder aka kdanilovf2865172016-12-30 03:35:11 +020058
koder aka kdanilov108ac362017-01-19 20:17:16 +020059class DataSource:
60 def __init__(self,
61 suite_id: str = None,
62 job_id: str = None,
63 node_id: str = None,
koder aka kdanilov108ac362017-01-19 20:17:16 +020064 sensor: str = None,
koder aka kdanilova732a602017-02-01 20:29:56 +020065 dev: str = None,
66 metric: str = None,
koder aka kdanilov108ac362017-01-19 20:17:16 +020067 tag: str = None) -> None:
68 self.suite_id = suite_id
69 self.job_id = job_id
70 self.node_id = node_id
koder aka kdanilov108ac362017-01-19 20:17:16 +020071 self.sensor = sensor
koder aka kdanilova732a602017-02-01 20:29:56 +020072 self.dev = dev
73 self.metric = metric
koder aka kdanilov108ac362017-01-19 20:17:16 +020074 self.tag = tag
75
koder aka kdanilova732a602017-02-01 20:29:56 +020076 @property
77 def metric_fqdn(self) -> str:
78 return "{0.sensor}.{0.dev}.{0.metric}".format(self)
79
koder aka kdanilov108ac362017-01-19 20:17:16 +020080 def __call__(self, **kwargs) -> 'DataSource':
81 dct = self.__dict__.copy()
82 dct.update(kwargs)
83 return self.__class__(**dct)
84
85 def __str__(self) -> str:
koder aka kdanilova732a602017-02-01 20:29:56 +020086 return ("suite={0.suite_id},job={0.job_id},node={0.node_id}," +
87 "path={0.sensor}.{0.dev}.{0.metric},tag={0.tag}").format(self)
koder aka kdanilov108ac362017-01-19 20:17:16 +020088
89 def __repr__(self) -> str:
90 return str(self)
91
koder aka kdanilova732a602017-02-01 20:29:56 +020092 @property
93 def tpl(self) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str],
94 Optional[str], Optional[str], Optional[str]]:
95 return self.suite_id, self.job_id, self.node_id, self.sensor, self.dev, self.metric, self.tag
96
97 def __eq__(self, o: object) -> bool:
98 return self.tpl == cast(DataSource, o).tpl
99
100 def __hash__(self) -> int:
101 return hash(self.tpl)
102
koder aka kdanilov108ac362017-01-19 20:17:16 +0200103
koder aka kdanilovf2865172016-12-30 03:35:11 +0200104class TimeSeries:
105 """Data series from sensor - either system sensor or from load generator tool (e.g. fio)"""
106
107 def __init__(self,
108 name: str,
109 raw: Optional[bytes],
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300110 data: numpy.ndarray,
111 times: numpy.ndarray,
koder aka kdanilovf90de852017-01-20 18:12:27 +0200112 units: str,
koder aka kdanilova732a602017-02-01 20:29:56 +0200113 source: DataSource,
koder aka kdanilovf90de852017-01-20 18:12:27 +0200114 time_units: str = 'us',
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300115 raw_tag: str = 'txt',
116 histo_bins: numpy.ndarray = None) -> None:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200117
118 # Sensor name. Typically DEV_NAME.METRIC
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200119 self.name = name
koder aka kdanilovf2865172016-12-30 03:35:11 +0200120
koder aka kdanilovf90de852017-01-20 18:12:27 +0200121 # units for data
122 self.units = units
123
124 # units for time
125 self.time_units = time_units
126
koder aka kdanilovf2865172016-12-30 03:35:11 +0200127 # Time series times and values. Time in ms from Unix epoch.
koder aka kdanilov108ac362017-01-19 20:17:16 +0200128 self.times = times
129 self.data = data
koder aka kdanilovf2865172016-12-30 03:35:11 +0200130
koder aka kdanilovf2865172016-12-30 03:35:11 +0200131 # Raw sensor data (is provided). Like log file for fio iops/bw/lat.
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200132 self.raw = raw
koder aka kdanilova732a602017-02-01 20:29:56 +0200133 self.raw_tag = raw_tag
koder aka kdanilov108ac362017-01-19 20:17:16 +0200134 self.source = source
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300135 self.histo_bins = histo_bins
koder aka kdanilov108ac362017-01-19 20:17:16 +0200136
137 def __str__(self) -> str:
138 res = "TS({}):\n".format(self.name)
139 res += " source={}\n".format(self.source)
140 res += " times_size={}\n".format(len(self.times))
koder aka kdanilova732a602017-02-01 20:29:56 +0200141 res += " data_shape={}\n".format(*self.data.shape)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200142 return res
143
144 def __repr__(self) -> str:
145 return str(self)
koder aka kdanilov70227062016-11-26 23:23:21 +0200146
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300147 def copy(self, no_data: bool = False) -> 'TimeSeries':
kdanylov aka koder45183182017-04-30 23:55:40 +0300148 cp = copy.copy(self)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300149
150 if not no_data:
151 cp.times = self.times.copy()
152 cp.data = self.data.copy()
153
kdanylov aka koder45183182017-04-30 23:55:40 +0300154 cp.source = self.source()
155 return cp
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300156
koder aka kdanilov70227062016-11-26 23:23:21 +0200157
koder aka kdanilovf2865172016-12-30 03:35:11 +0200158# (node_name, source_dev, metric_name) => metric_results
159JobMetrics = Dict[Tuple[str, str, str], TimeSeries]
koder aka kdanilov70227062016-11-26 23:23:21 +0200160
161
koder aka kdanilovf90de852017-01-20 18:12:27 +0200162class StatProps(Storable):
koder aka kdanilovf2865172016-12-30 03:35:11 +0200163 "Statistic properties for timeseries with unknown data distribution"
koder aka kdanilovf90de852017-01-20 18:12:27 +0200164
165 __ignore_fields__ = ['data']
166
kdanylov aka koder45183182017-04-30 23:55:40 +0300167 def __init__(self, data: numpy.array, units: str) -> None:
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200168 self.perc_99 = None # type: float
169 self.perc_95 = None # type: float
170 self.perc_90 = None # type: float
171 self.perc_50 = None # type: float
koder aka kdanilova732a602017-02-01 20:29:56 +0200172 self.perc_10 = None # type: float
173 self.perc_5 = None # type: float
174 self.perc_1 = None # type: float
koder aka kdanilov70227062016-11-26 23:23:21 +0200175
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200176 self.min = None # type: Number
177 self.max = None # type: Number
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200178
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200179 # bin_center: bin_count
koder aka kdanilova732a602017-02-01 20:29:56 +0200180 self.log_bins = False
koder aka kdanilov108ac362017-01-19 20:17:16 +0200181 self.bins_populations = None # type: numpy.array
koder aka kdanilova732a602017-02-01 20:29:56 +0200182
183 # bin edges, one more element that in bins_populations
184 self.bins_edges = None # type: numpy.array
185
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200186 self.data = data
kdanylov aka koder45183182017-04-30 23:55:40 +0300187 self.units = units
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200188
koder aka kdanilovf2865172016-12-30 03:35:11 +0200189 def __str__(self) -> str:
190 res = ["{}(size = {}):".format(self.__class__.__name__, len(self.data))]
koder aka kdanilova732a602017-02-01 20:29:56 +0200191 for name in ["perc_1", "perc_5", "perc_10", "perc_50", "perc_90", "perc_95", "perc_99"]:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200192 res.append(" {} = {}".format(name, round_digits(getattr(self, name))))
193 res.append(" range {} {}".format(round_digits(self.min), round_digits(self.max)))
194 return "\n".join(res)
195
196 def __repr__(self) -> str:
197 return str(self)
198
199 def raw(self) -> Dict[str, Any]:
koder aka kdanilovf90de852017-01-20 18:12:27 +0200200 data = super().raw()
201 data['bins_mids'] = list(data['bins_mids'])
202 data['bins_populations'] = list(data['bins_populations'])
koder aka kdanilovf2865172016-12-30 03:35:11 +0200203 return data
204
205 @classmethod
206 def fromraw(cls, data: Dict[str, Any]) -> 'StatProps':
koder aka kdanilov108ac362017-01-19 20:17:16 +0200207 data['bins_mids'] = numpy.array(data['bins_mids'])
koder aka kdanilovf2865172016-12-30 03:35:11 +0200208 data['bins_populations'] = numpy.array(data['bins_populations'])
koder aka kdanilovf90de852017-01-20 18:12:27 +0200209 return cast(StatProps, super().fromraw(data))
koder aka kdanilovf2865172016-12-30 03:35:11 +0200210
211
212class HistoStatProps(StatProps):
213 """Statistic properties for 2D timeseries with unknown data distribution and histogram as input value.
214 Used for latency"""
kdanylov aka koder45183182017-04-30 23:55:40 +0300215 def __init__(self, data: numpy.array, units: str) -> None:
216 StatProps.__init__(self, data, units)
koder aka kdanilovf2865172016-12-30 03:35:11 +0200217
218
219class NormStatProps(StatProps):
220 "Statistic properties for timeseries with normal data distribution. Used for iops/bw"
kdanylov aka koder45183182017-04-30 23:55:40 +0300221 def __init__(self, data: numpy.array, units: str) -> None:
222 StatProps.__init__(self, data, units)
koder aka kdanilovf2865172016-12-30 03:35:11 +0200223
224 self.average = None # type: float
225 self.deviation = None # type: float
226 self.confidence = None # type: float
227 self.confidence_level = None # type: float
228 self.normtest = None # type: NormaltestResult
koder aka kdanilov108ac362017-01-19 20:17:16 +0200229 self.skew = None # type: float
230 self.kurt = None # type: float
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200231
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200232 def __str__(self) -> str:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200233 res = ["NormStatProps(size = {}):".format(len(self.data)),
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200234 " distr = {} ~ {}".format(round_digits(self.average), round_digits(self.deviation)),
235 " confidence({0.confidence_level}) = {1}".format(self, round_digits(self.confidence)),
koder aka kdanilova732a602017-02-01 20:29:56 +0200236 " perc_1 = {}".format(round_digits(self.perc_1)),
237 " perc_5 = {}".format(round_digits(self.perc_5)),
238 " perc_10 = {}".format(round_digits(self.perc_10)),
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200239 " perc_50 = {}".format(round_digits(self.perc_50)),
240 " perc_90 = {}".format(round_digits(self.perc_90)),
241 " perc_95 = {}".format(round_digits(self.perc_95)),
242 " perc_99 = {}".format(round_digits(self.perc_99)),
243 " range {} {}".format(round_digits(self.min), round_digits(self.max)),
koder aka kdanilov108ac362017-01-19 20:17:16 +0200244 " normtest = {0.normtest}".format(self),
245 " skew ~ kurt = {0.skew} ~ {0.kurt}".format(self)]
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200246 return "\n".join(res)
247
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200248 def raw(self) -> Dict[str, Any]:
koder aka kdanilovf90de852017-01-20 18:12:27 +0200249 data = super().raw()
koder aka kdanilovf2865172016-12-30 03:35:11 +0200250 data['normtest'] = (data['nortest'].statistic, data['nortest'].pvalue)
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200251 return data
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200252
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200253 @classmethod
254 def fromraw(cls, data: Dict[str, Any]) -> 'NormStatProps':
koder aka kdanilovf2865172016-12-30 03:35:11 +0200255 data['normtest'] = NormaltestResult(*data['normtest'])
koder aka kdanilovf90de852017-01-20 18:12:27 +0200256 return cast(NormStatProps, super().fromraw(data))
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200257
258
koder aka kdanilovf2865172016-12-30 03:35:11 +0200259JobStatMetrics = Dict[Tuple[str, str, str], StatProps]
260
261
koder aka kdanilovf90de852017-01-20 18:12:27 +0200262class JobResult:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200263 """Contains done test job information"""
264
265 def __init__(self,
koder aka kdanilovf90de852017-01-20 18:12:27 +0200266 info: JobConfig,
koder aka kdanilovf2865172016-12-30 03:35:11 +0200267 begin_time: int,
268 end_time: int,
269 raw: JobMetrics) -> None:
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200270 self.info = info
koder aka kdanilovf2865172016-12-30 03:35:11 +0200271 self.run_interval = (begin_time, end_time)
272 self.raw = raw # type: JobMetrics
273 self.processed = None # type: JobStatMetrics
koder aka kdanilov108ac362017-01-19 20:17:16 +0200274
275
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300276ArrayData = NamedTuple("ArrayData",
277 [('header', List[str]),
278 ('histo_bins', Optional[numpy.ndarray]),
279 ('data', Optional[numpy.ndarray])])
280
281
koder aka kdanilov108ac362017-01-19 20:17:16 +0200282class IResultStorage(metaclass=abc.ABCMeta):
283
284 @abc.abstractmethod
285 def sync(self) -> None:
286 pass
287
288 @abc.abstractmethod
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300289 def append_sensor(self, data: numpy.array, ds: DataSource, units: str, histo_bins: numpy.ndarray = None) -> None:
290 pass
291
292 @abc.abstractmethod
koder aka kdanilova732a602017-02-01 20:29:56 +0200293 def load_sensor(self, ds: DataSource) -> TimeSeries:
294 pass
295
296 @abc.abstractmethod
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300297 def iter_sensors(self, ds: DataSource) -> Iterator[TimeSeries]:
298 pass
299
300 @abc.abstractmethod
koder aka kdanilovf90de852017-01-20 18:12:27 +0200301 def put_or_check_suite(self, suite: SuiteConfig) -> None:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200302 pass
303
304 @abc.abstractmethod
koder aka kdanilovf90de852017-01-20 18:12:27 +0200305 def put_job(self, suite: SuiteConfig, job: JobConfig) -> None:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200306 pass
307
308 @abc.abstractmethod
309 def put_ts(self, ts: TimeSeries) -> None:
310 pass
311
312 @abc.abstractmethod
313 def put_extra(self, data: bytes, source: DataSource) -> None:
314 pass
315
316 @abc.abstractmethod
317 def put_stat(self, data: StatProps, source: DataSource) -> None:
318 pass
319
320 @abc.abstractmethod
321 def get_stat(self, stat_cls: Type[StatProps], source: DataSource) -> StatProps:
322 pass
323
324 @abc.abstractmethod
koder aka kdanilovf90de852017-01-20 18:12:27 +0200325 def iter_suite(self, suite_type: str = None) -> Iterator[SuiteConfig]:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200326 pass
327
328 @abc.abstractmethod
koder aka kdanilovf90de852017-01-20 18:12:27 +0200329 def iter_job(self, suite: SuiteConfig) -> Iterator[JobConfig]:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200330 pass
331
332 @abc.abstractmethod
koder aka kdanilovf90de852017-01-20 18:12:27 +0200333 def iter_ts(self, suite: SuiteConfig, job: JobConfig) -> Iterator[TimeSeries]:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200334 pass
335
336 # return path to file to be inserted into report
337 @abc.abstractmethod
338 def put_plot_file(self, data: bytes, source: DataSource) -> str:
339 pass