blob: 62e74f07c472dc41d3f8a3e3389673ca7cea66b4 [file] [log] [blame]
koder aka kdanilov108ac362017-01-19 20:17:16 +02001import abc
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +02002import array
koder aka kdanilov108ac362017-01-19 20:17:16 +02003from typing import Dict, List, Any, Optional, Tuple, cast, Type, Iterator
4from collections import OrderedDict
koder aka kdanilovf2865172016-12-30 03:35:11 +02005
koder aka kdanilovffaf48d2016-12-27 02:25:29 +02006
7import numpy
koder aka kdanilovf2865172016-12-30 03:35:11 +02008from scipy.stats.mstats_basic import NormaltestResult
koder aka kdanilovffaf48d2016-12-27 02:25:29 +02009
10
koder aka kdanilovf90de852017-01-20 18:12:27 +020011from .suits.job import JobConfig
koder aka kdanilovf2865172016-12-30 03:35:11 +020012from .node_interfaces import IRPCNode
koder aka kdanilovf90de852017-01-20 18:12:27 +020013from .common_types import Storable, IStorable
koder aka kdanilovf2865172016-12-30 03:35:11 +020014from .utils import round_digits, Number
koder aka kdanilov70227062016-11-26 23:23:21 +020015
16
koder aka kdanilovf90de852017-01-20 18:12:27 +020017class SuiteConfig(Storable):
koder aka kdanilovf2865172016-12-30 03:35:11 +020018 """
19 Test suite input configuration.
20
21 test_type - test type name
22 params - parameters from yaml file for this test
23 run_uuid - UUID to be used to create file names & Co
24 nodes - nodes to run tests on
25 remote_dir - directory on nodes to be used for local files
26 """
koder aka kdanilovf90de852017-01-20 18:12:27 +020027 __ignore_fields__ = ['nodes', 'run_uuid', 'remote_dir']
28
koder aka kdanilovf2865172016-12-30 03:35:11 +020029 def __init__(self,
30 test_type: str,
31 params: Dict[str, Any],
32 run_uuid: str,
33 nodes: List[IRPCNode],
koder aka kdanilov108ac362017-01-19 20:17:16 +020034 remote_dir: str,
35 idx: int) -> None:
koder aka kdanilovf2865172016-12-30 03:35:11 +020036 self.test_type = test_type
37 self.params = params
38 self.run_uuid = run_uuid
39 self.nodes = nodes
koder aka kdanilov108ac362017-01-19 20:17:16 +020040 self.nodes_ids = [node.node_id for node in nodes]
koder aka kdanilovf2865172016-12-30 03:35:11 +020041 self.remote_dir = remote_dir
koder aka kdanilov108ac362017-01-19 20:17:16 +020042 self.storage_id = "{}_{}".format(self.test_type, idx)
koder aka kdanilovf2865172016-12-30 03:35:11 +020043
koder aka kdanilov108ac362017-01-19 20:17:16 +020044 def __eq__(self, o: object) -> bool:
45 if type(o) is not self.__class__:
46 return False
47
koder aka kdanilovf90de852017-01-20 18:12:27 +020048 other = cast(SuiteConfig, o)
koder aka kdanilov108ac362017-01-19 20:17:16 +020049
koder aka kdanilovf2865172016-12-30 03:35:11 +020050 return (self.test_type == other.test_type and
51 self.params == other.params and
52 set(self.nodes_ids) == set(other.nodes_ids))
53
koder aka kdanilovf2865172016-12-30 03:35:11 +020054
koder aka kdanilov108ac362017-01-19 20:17:16 +020055class DataSource:
56 def __init__(self,
57 suite_id: str = None,
58 job_id: str = None,
59 node_id: str = None,
60 dev: str = None,
61 sensor: str = None,
62 tag: str = None) -> None:
63 self.suite_id = suite_id
64 self.job_id = job_id
65 self.node_id = node_id
66 self.dev = dev
67 self.sensor = sensor
68 self.tag = tag
69
70 def __call__(self, **kwargs) -> 'DataSource':
71 dct = self.__dict__.copy()
72 dct.update(kwargs)
73 return self.__class__(**dct)
74
75 def __str__(self) -> str:
76 return "{0.suite_id}.{0.job_id}/{0.node_id}/{0.dev}.{0.sensor}.{0.tag}".format(self)
77
78 def __repr__(self) -> str:
79 return str(self)
80
81
koder aka kdanilovf2865172016-12-30 03:35:11 +020082class TimeSeries:
83 """Data series from sensor - either system sensor or from load generator tool (e.g. fio)"""
84
85 def __init__(self,
86 name: str,
87 raw: Optional[bytes],
koder aka kdanilov108ac362017-01-19 20:17:16 +020088 data: numpy.array,
89 times: numpy.array,
koder aka kdanilovf90de852017-01-20 18:12:27 +020090 units: str,
91 time_units: str = 'us',
koder aka kdanilovf2865172016-12-30 03:35:11 +020092 second_axis_size: int = 1,
koder aka kdanilov108ac362017-01-19 20:17:16 +020093 source: DataSource = None) -> None:
koder aka kdanilovf2865172016-12-30 03:35:11 +020094
95 # Sensor name. Typically DEV_NAME.METRIC
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +020096 self.name = name
koder aka kdanilovf2865172016-12-30 03:35:11 +020097
koder aka kdanilovf90de852017-01-20 18:12:27 +020098 # units for data
99 self.units = units
100
101 # units for time
102 self.time_units = time_units
103
koder aka kdanilovf2865172016-12-30 03:35:11 +0200104 # Time series times and values. Time in ms from Unix epoch.
koder aka kdanilov108ac362017-01-19 20:17:16 +0200105 self.times = times
106 self.data = data
koder aka kdanilovf2865172016-12-30 03:35:11 +0200107
108 # Not equal to 1 in case of 2d sensors, like latency, when each measurement is a histogram.
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200109 self.second_axis_size = second_axis_size
koder aka kdanilovf2865172016-12-30 03:35:11 +0200110
111 # Raw sensor data (is provided). Like log file for fio iops/bw/lat.
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200112 self.raw = raw
koder aka kdanilov70227062016-11-26 23:23:21 +0200113
koder aka kdanilov108ac362017-01-19 20:17:16 +0200114 self.source = source
115
116 def __str__(self) -> str:
117 res = "TS({}):\n".format(self.name)
118 res += " source={}\n".format(self.source)
119 res += " times_size={}\n".format(len(self.times))
120 res += " data_size={}\n".format(len(self.data))
121 res += " data_shape={}x{}\n".format(len(self.data) // self.second_axis_size, self.second_axis_size)
122 return res
123
124 def __repr__(self) -> str:
125 return str(self)
koder aka kdanilov70227062016-11-26 23:23:21 +0200126
127
koder aka kdanilovf2865172016-12-30 03:35:11 +0200128# (node_name, source_dev, metric_name) => metric_results
129JobMetrics = Dict[Tuple[str, str, str], TimeSeries]
koder aka kdanilov70227062016-11-26 23:23:21 +0200130
131
koder aka kdanilovf90de852017-01-20 18:12:27 +0200132class StatProps(Storable):
koder aka kdanilovf2865172016-12-30 03:35:11 +0200133 "Statistic properties for timeseries with unknown data distribution"
koder aka kdanilovf90de852017-01-20 18:12:27 +0200134
135 __ignore_fields__ = ['data']
136
koder aka kdanilov108ac362017-01-19 20:17:16 +0200137 def __init__(self, data: numpy.array) -> None:
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200138 self.perc_99 = None # type: float
139 self.perc_95 = None # type: float
140 self.perc_90 = None # type: float
141 self.perc_50 = None # type: float
koder aka kdanilov70227062016-11-26 23:23:21 +0200142
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200143 self.min = None # type: Number
144 self.max = None # type: Number
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200145
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200146 # bin_center: bin_count
koder aka kdanilov108ac362017-01-19 20:17:16 +0200147 self.bins_populations = None # type: numpy.array
148 self.bins_mids = None # type: numpy.array
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200149 self.data = data
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200150
koder aka kdanilovf2865172016-12-30 03:35:11 +0200151 def __str__(self) -> str:
152 res = ["{}(size = {}):".format(self.__class__.__name__, len(self.data))]
153 for name in ["perc_50", "perc_90", "perc_95", "perc_99"]:
154 res.append(" {} = {}".format(name, round_digits(getattr(self, name))))
155 res.append(" range {} {}".format(round_digits(self.min), round_digits(self.max)))
156 return "\n".join(res)
157
158 def __repr__(self) -> str:
159 return str(self)
160
161 def raw(self) -> Dict[str, Any]:
koder aka kdanilovf90de852017-01-20 18:12:27 +0200162 data = super().raw()
163 data['bins_mids'] = list(data['bins_mids'])
164 data['bins_populations'] = list(data['bins_populations'])
koder aka kdanilovf2865172016-12-30 03:35:11 +0200165 return data
166
167 @classmethod
168 def fromraw(cls, data: Dict[str, Any]) -> 'StatProps':
koder aka kdanilov108ac362017-01-19 20:17:16 +0200169 data['bins_mids'] = numpy.array(data['bins_mids'])
koder aka kdanilovf2865172016-12-30 03:35:11 +0200170 data['bins_populations'] = numpy.array(data['bins_populations'])
koder aka kdanilovf90de852017-01-20 18:12:27 +0200171 return cast(StatProps, super().fromraw(data))
koder aka kdanilovf2865172016-12-30 03:35:11 +0200172
173
174class HistoStatProps(StatProps):
175 """Statistic properties for 2D timeseries with unknown data distribution and histogram as input value.
176 Used for latency"""
koder aka kdanilov108ac362017-01-19 20:17:16 +0200177 def __init__(self, data: numpy.array, second_axis_size: int) -> None:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200178 self.second_axis_size = second_axis_size
179 StatProps.__init__(self, data)
180
181
182class NormStatProps(StatProps):
183 "Statistic properties for timeseries with normal data distribution. Used for iops/bw"
koder aka kdanilov108ac362017-01-19 20:17:16 +0200184 def __init__(self, data: numpy.array) -> None:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200185 StatProps.__init__(self, data)
186
187 self.average = None # type: float
188 self.deviation = None # type: float
189 self.confidence = None # type: float
190 self.confidence_level = None # type: float
191 self.normtest = None # type: NormaltestResult
koder aka kdanilov108ac362017-01-19 20:17:16 +0200192 self.skew = None # type: float
193 self.kurt = None # type: float
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200194
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200195 def __str__(self) -> str:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200196 res = ["NormStatProps(size = {}):".format(len(self.data)),
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200197 " distr = {} ~ {}".format(round_digits(self.average), round_digits(self.deviation)),
198 " confidence({0.confidence_level}) = {1}".format(self, round_digits(self.confidence)),
199 " perc_50 = {}".format(round_digits(self.perc_50)),
200 " perc_90 = {}".format(round_digits(self.perc_90)),
201 " perc_95 = {}".format(round_digits(self.perc_95)),
202 " perc_99 = {}".format(round_digits(self.perc_99)),
203 " range {} {}".format(round_digits(self.min), round_digits(self.max)),
koder aka kdanilov108ac362017-01-19 20:17:16 +0200204 " normtest = {0.normtest}".format(self),
205 " skew ~ kurt = {0.skew} ~ {0.kurt}".format(self)]
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200206 return "\n".join(res)
207
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200208 def raw(self) -> Dict[str, Any]:
koder aka kdanilovf90de852017-01-20 18:12:27 +0200209 data = super().raw()
koder aka kdanilovf2865172016-12-30 03:35:11 +0200210 data['normtest'] = (data['nortest'].statistic, data['nortest'].pvalue)
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200211 return data
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200212
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200213 @classmethod
214 def fromraw(cls, data: Dict[str, Any]) -> 'NormStatProps':
koder aka kdanilovf2865172016-12-30 03:35:11 +0200215 data['normtest'] = NormaltestResult(*data['normtest'])
koder aka kdanilovf90de852017-01-20 18:12:27 +0200216 return cast(NormStatProps, super().fromraw(data))
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200217
218
koder aka kdanilovf2865172016-12-30 03:35:11 +0200219JobStatMetrics = Dict[Tuple[str, str, str], StatProps]
220
221
koder aka kdanilovf90de852017-01-20 18:12:27 +0200222class JobResult:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200223 """Contains done test job information"""
224
225 def __init__(self,
koder aka kdanilovf90de852017-01-20 18:12:27 +0200226 info: JobConfig,
koder aka kdanilovf2865172016-12-30 03:35:11 +0200227 begin_time: int,
228 end_time: int,
229 raw: JobMetrics) -> None:
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200230 self.info = info
koder aka kdanilovf2865172016-12-30 03:35:11 +0200231 self.run_interval = (begin_time, end_time)
232 self.raw = raw # type: JobMetrics
233 self.processed = None # type: JobStatMetrics
koder aka kdanilov108ac362017-01-19 20:17:16 +0200234
235
236class IResultStorage(metaclass=abc.ABCMeta):
237
238 @abc.abstractmethod
239 def sync(self) -> None:
240 pass
241
242 @abc.abstractmethod
koder aka kdanilovf90de852017-01-20 18:12:27 +0200243 def put_or_check_suite(self, suite: SuiteConfig) -> None:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200244 pass
245
246 @abc.abstractmethod
koder aka kdanilovf90de852017-01-20 18:12:27 +0200247 def put_job(self, suite: SuiteConfig, job: JobConfig) -> None:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200248 pass
249
250 @abc.abstractmethod
251 def put_ts(self, ts: TimeSeries) -> None:
252 pass
253
254 @abc.abstractmethod
255 def put_extra(self, data: bytes, source: DataSource) -> None:
256 pass
257
258 @abc.abstractmethod
259 def put_stat(self, data: StatProps, source: DataSource) -> None:
260 pass
261
262 @abc.abstractmethod
263 def get_stat(self, stat_cls: Type[StatProps], source: DataSource) -> StatProps:
264 pass
265
266 @abc.abstractmethod
koder aka kdanilovf90de852017-01-20 18:12:27 +0200267 def iter_suite(self, suite_type: str = None) -> Iterator[SuiteConfig]:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200268 pass
269
270 @abc.abstractmethod
koder aka kdanilovf90de852017-01-20 18:12:27 +0200271 def iter_job(self, suite: SuiteConfig) -> Iterator[JobConfig]:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200272 pass
273
274 @abc.abstractmethod
koder aka kdanilovf90de852017-01-20 18:12:27 +0200275 def iter_ts(self, suite: SuiteConfig, job: JobConfig) -> Iterator[TimeSeries]:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200276 pass
277
278 # return path to file to be inserted into report
279 @abc.abstractmethod
280 def put_plot_file(self, data: bytes, source: DataSource) -> str:
281 pass