blob: 9615461ad01175215f040474957fb00ad0819f95 [file] [log] [blame]
koder aka kdanilov108ac362017-01-19 20:17:16 +02001import os
koder aka kdanilov7f59d562016-12-26 01:34:23 +02002import abc
koder aka kdanilova047e1b2015-04-21 23:16:59 +03003import logging
koder aka kdanilov108ac362017-01-19 20:17:16 +02004from collections import defaultdict
kdanylov aka koder026e5f22017-05-15 01:04:39 +03005from typing import Dict, Any, Iterator, Tuple, cast, List, Set, Optional, Union, Type
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +03006
koder aka kdanilovffaf48d2016-12-27 02:25:29 +02007import numpy
kdanylov aka koder736e5c12017-05-07 17:27:14 +03008from statsmodels.tsa.stattools import adfuller
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +03009
kdanylov aka koder736e5c12017-05-07 17:27:14 +030010import xmlbuilder3
koder aka kdanilovbe8f89f2015-04-28 14:51:51 +030011
koder aka kdanilov108ac362017-01-19 20:17:16 +020012import wally
koder aka kdanilovffaf48d2016-12-27 02:25:29 +020013
kdanylov aka koderb0833332017-05-13 20:39:17 +030014from cephlib import html
15from cephlib.units import b2ssize, b2ssize_10, unit_conversion_coef, unit_conversion_coef_f
16from cephlib.statistic import calc_norm_stat_props
17from cephlib.storage_selectors import summ_sensors, find_sensors_to_2d
18from cephlib.wally_storage import find_nodes_by_roles
kdanylov aka koder026e5f22017-05-15 01:04:39 +030019from cephlib.plot import (plot_simple_bars, plot_hmap_from_2d, plot_lat_over_time, plot_simple_over_time,
20 plot_histo_heatmap, plot_v_over_time, plot_hist)
kdanylov aka koderb0833332017-05-13 20:39:17 +030021
22from .utils import STORAGE_ROLES
koder aka kdanilov39e449e2016-12-17 15:15:26 +020023from .stage import Stage, StepOrder
24from .test_run_class import TestRun
kdanylov aka koder026e5f22017-05-15 01:04:39 +030025from .result_classes import IWallyStorage
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +030026from .result_classes import DataSource, TimeSeries, SuiteConfig
koder aka kdanilov108ac362017-01-19 20:17:16 +020027from .suits.io.fio import FioTest, FioJobConfig
koder aka kdanilova732a602017-02-01 20:29:56 +020028from .suits.io.fio_job import FioJobParams
29from .suits.job import JobConfig
kdanylov aka koderb0833332017-05-13 20:39:17 +030030from .data_selectors import get_aggregated, AGG_TAG
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +030031from .report_profiles import (DefStyleProfile, DefColorProfile, StyleProfile, ColorProfile,
32 default_format, io_chart_format)
kdanylov aka koder026e5f22017-05-15 01:04:39 +030033from .plot import io_chart
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +030034from .resources import ResourceNames, get_resources_usage, make_iosum, IOSummary, get_cluster_cpu_load
kdanylov aka koder026e5f22017-05-15 01:04:39 +030035
36
koder aka kdanilov962ee5f2016-12-19 02:40:08 +020037logger = logging.getLogger("wally")
koder aka kdanilova047e1b2015-04-21 23:16:59 +030038
39
koder aka kdanilov108ac362017-01-19 20:17:16 +020040# ---------------- CONSTS ---------------------------------------------------------------------------------------------
koder aka kdanilov39e449e2016-12-17 15:15:26 +020041
koder aka kdanilov7f59d562016-12-26 01:34:23 +020042
koder aka kdanilov108ac362017-01-19 20:17:16 +020043DEBUG = False
koder aka kdanilov39e449e2016-12-17 15:15:26 +020044
koder aka kdanilov39e449e2016-12-17 15:15:26 +020045
koder aka kdanilov108ac362017-01-19 20:17:16 +020046# ---------------- STRUCTS -------------------------------------------------------------------------------------------
koder aka kdanilov39e449e2016-12-17 15:15:26 +020047
koder aka kdanilov7f59d562016-12-26 01:34:23 +020048
49# TODO: need to be revised, have to user StatProps fields instead
50class StoragePerfSummary:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +030051 def __init__(self) -> None:
koder aka kdanilov7f59d562016-12-26 01:34:23 +020052 self.direct_iops_r_max = 0 # type: int
53 self.direct_iops_w_max = 0 # type: int
54
55 # 64 used instead of 4k to faster feed caches
56 self.direct_iops_w64_max = 0 # type: int
57
58 self.rws4k_10ms = 0 # type: int
59 self.rws4k_30ms = 0 # type: int
60 self.rws4k_100ms = 0 # type: int
61 self.bw_write_max = 0 # type: int
62 self.bw_read_max = 0 # type: int
63
64 self.bw = None # type: float
65 self.iops = None # type: float
66 self.lat = None # type: float
67 self.lat_50 = None # type: float
68 self.lat_95 = None # type: float
69
70
koder aka kdanilov108ac362017-01-19 20:17:16 +020071# -------------- AGGREGATION AND STAT FUNCTIONS ----------------------------------------------------------------------
koder aka kdanilov108ac362017-01-19 20:17:16 +020072
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +030073LEVEL_SENSORS = {("block-io", "io_queue"), ("system-cpu", "procs_blocked"), ("system-cpu", "procs_queue")}
koder aka kdanilova732a602017-02-01 20:29:56 +020074
75
76def is_level_sensor(sensor: str, metric: str) -> bool:
77 """Returns True if sensor measure level of any kind, E.g. queue depth."""
78 return (sensor, metric) in LEVEL_SENSORS
79
80
81def is_delta_sensor(sensor: str, metric: str) -> bool:
82 """Returns True if sensor provides deltas for cumulative value. E.g. io completed in given period"""
83 return not is_level_sensor(sensor, metric)
84
kdanylov aka koder736e5c12017-05-07 17:27:14 +030085
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +030086# def get_idle_load(rstorage: ResultStorage, *args, **kwargs) -> float:
87# if 'idle' not in rstorage.storage:
88# return 0.0
89# idle_time = rstorage.storage.get('idle')
90# ssum = summ_sensors(rstorage, time_range=idle_time, *args, **kwargs)
91# return numpy.average(ssum)
kdanylov aka koder736e5c12017-05-07 17:27:14 +030092
koder aka kdanilov108ac362017-01-19 20:17:16 +020093
94# -------------------- REPORT HELPERS --------------------------------------------------------------------------------
95
96
koder aka kdanilov7f59d562016-12-26 01:34:23 +020097class HTMLBlock:
98 data = None # type: str
99 js_links = [] # type: List[str]
100 css_links = [] # type: List[str]
koder aka kdanilova732a602017-02-01 20:29:56 +0200101 order_attr = None # type: Any
102
103 def __init__(self, data: str, order_attr: Any = None) -> None:
104 self.data = data
105 self.order_attr = order_attr
106
kdanylov aka koder45183182017-04-30 23:55:40 +0300107 def __eq__(self, o: Any) -> bool:
koder aka kdanilova732a602017-02-01 20:29:56 +0200108 return o.order_attr == self.order_attr # type: ignore
109
kdanylov aka koder45183182017-04-30 23:55:40 +0300110 def __lt__(self, o: Any) -> bool:
koder aka kdanilova732a602017-02-01 20:29:56 +0200111 return o.order_attr > self.order_attr # type: ignore
112
113
114class Table:
115 def __init__(self, header: List[str]) -> None:
116 self.header = header
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300117 self.data = [] # type: List[List[str]]
koder aka kdanilova732a602017-02-01 20:29:56 +0200118
119 def add_line(self, values: List[str]) -> None:
120 self.data.append(values)
121
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300122 def html(self) -> str:
koder aka kdanilova732a602017-02-01 20:29:56 +0200123 return html.table("", self.header, self.data)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200124
125
koder aka kdanilov108ac362017-01-19 20:17:16 +0200126class Menu1st:
127 engineering = "Engineering"
128 summary = "Summary"
koder aka kdanilova732a602017-02-01 20:29:56 +0200129 per_job = "Per Job"
koder aka kdanilov108ac362017-01-19 20:17:16 +0200130
131
132class Menu2ndEng:
133 iops_time = "IOPS(time)"
134 hist = "IOPS/lat overall histogram"
135 lat_time = "Lat(time)"
136
137
138class Menu2ndSumm:
139 io_lat_qd = "IO & Lat vs QD"
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300140 cpu_usage_qd = "CPU usage"
koder aka kdanilov108ac362017-01-19 20:17:16 +0200141
142
koder aka kdanilova732a602017-02-01 20:29:56 +0200143menu_1st_order = [Menu1st.summary, Menu1st.engineering, Menu1st.per_job]
koder aka kdanilov108ac362017-01-19 20:17:16 +0200144
145
146# -------------------- REPORTS --------------------------------------------------------------------------------------
147
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300148class ReporterBase:
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300149 def __init__(self, rstorage: IWallyStorage, style: StyleProfile, colors: ColorProfile) -> None:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300150 self.style = style
151 self.colors = colors
152 self.rstorage = rstorage
koder aka kdanilov108ac362017-01-19 20:17:16 +0200153
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300154 def plt(self, func, ds: DataSource, *args, **kwargs) -> str:
155 return func(self.rstorage, self.style, self.colors, ds, *args, **kwargs)
156
157
158class SuiteReporter(ReporterBase, metaclass=abc.ABCMeta):
159 suite_types = set() # type: Set[str]
koder aka kdanilova732a602017-02-01 20:29:56 +0200160
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200161 @abc.abstractmethod
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300162 def get_divs(self, suite: SuiteConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
koder aka kdanilova732a602017-02-01 20:29:56 +0200163 pass
164
165
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300166class JobReporter(ReporterBase, metaclass=abc.ABCMeta):
koder aka kdanilova732a602017-02-01 20:29:56 +0200167 suite_type = set() # type: Set[str]
168
169 @abc.abstractmethod
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300170 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200171 pass
172
173
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300174# # Linearization report
175# class IOPSBsize(SuiteReporter):
176# """Creates graphs, which show how IOPS and Latency depend on block size"""
177#
178#
179# # Main performance report
180# class PerformanceSummary(SuiteReporter):
181# """Aggregated summary fro storage"""
182
183# # Node load over test time
184# class NodeLoad(SuiteReporter):
185# """IOPS/latency during test"""
186
187# # Ceph operation breakout report
188# class CephClusterSummary(SuiteReporter):
189# """IOPS/latency during test"""
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200190
191
192# Main performance report
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300193class IOQD(SuiteReporter):
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200194 """Creates graph, which show how IOPS and Latency depend on QD"""
koder aka kdanilova732a602017-02-01 20:29:56 +0200195 suite_types = {'fio'}
196
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300197 def get_divs(self, suite: SuiteConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
koder aka kdanilova732a602017-02-01 20:29:56 +0200198 ts_map = defaultdict(list) # type: Dict[FioJobParams, List[Tuple[SuiteConfig, FioJobConfig]]]
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300199 str_summary = {} # type: Dict[FioJobParams, Tuple[str, str]]
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300200
201 for job in self.rstorage.iter_job(suite):
koder aka kdanilov108ac362017-01-19 20:17:16 +0200202 fjob = cast(FioJobConfig, job)
koder aka kdanilova732a602017-02-01 20:29:56 +0200203 fjob_no_qd = cast(FioJobParams, fjob.params.copy(qd=None))
204 str_summary[fjob_no_qd] = (fjob_no_qd.summary, fjob_no_qd.long_summary)
205 ts_map[fjob_no_qd].append((suite, fjob))
koder aka kdanilov108ac362017-01-19 20:17:16 +0200206
koder aka kdanilova732a602017-02-01 20:29:56 +0200207 for tpl, suites_jobs in ts_map.items():
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300208 if len(suites_jobs) >= self.style.min_iops_vs_qd_jobs:
209 iosums = [make_iosum(self.rstorage, suite, job, self.style.hist_boxes) for suite, job in suites_jobs]
koder aka kdanilova732a602017-02-01 20:29:56 +0200210 iosums.sort(key=lambda x: x.qd)
211 summary, summary_long = str_summary[tpl]
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300212
213 yield Menu1st.summary, Menu2ndSumm.io_lat_qd, \
214 HTMLBlock(html.H2(html.center("IOPS, BW, Lat = func(QD). " + summary_long)))
215
koder aka kdanilova732a602017-02-01 20:29:56 +0200216 ds = DataSource(suite_id=suite.storage_id,
217 job_id=summary,
218 node_id=AGG_TAG,
219 sensor="fio",
220 dev=AGG_TAG,
221 metric="io_over_qd",
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300222 tag=io_chart_format)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200223
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300224 fpath = self.plt(io_chart, ds, title="", legend="IOPS/BW", iosums=iosums)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300225 yield Menu1st.summary, Menu2ndSumm.io_lat_qd, HTMLBlock(html.center(html.img(fpath)))
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200226
227
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300228class ResourceQD(SuiteReporter):
229 suite_types = {'fio'}
230
231 def get_divs(self, suite: SuiteConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
232 qd_grouped_jobs = {} # type: Dict[FioJobParams, List[FioJobConfig]]
kdanylov aka koderb0833332017-05-13 20:39:17 +0300233 test_nc = len(list(find_nodes_by_roles(self.rstorage.storage, ['testnode'])))
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300234 for job in self.rstorage.iter_job(suite):
235 fjob = cast(FioJobConfig, job)
236 if fjob.bsize != 4:
237 continue
238
239 fjob_no_qd = cast(FioJobParams, fjob.params.copy(qd=None))
240 qd_grouped_jobs.setdefault(fjob_no_qd, []).append(fjob)
241
242 for jc_no_qd, jobs in sorted(qd_grouped_jobs.items()):
243 cpu_usage2qd = {}
244 for job in jobs:
245 usage, iops_ok = get_resources_usage(suite, job, self.rstorage, hist_boxes=self.style.hist_boxes,
246 large_block=self.style.large_blocks)
247
248 if iops_ok:
249 cpu_usage2qd[job.qd] = usage[ResourceNames.storage_cpu_s]
250
251 if len(cpu_usage2qd) < StyleProfile.min_iops_vs_qd_jobs:
252 continue
253
254 labels, vals, errs = zip(*((l, avg, dev) for l, (_, avg, dev) in sorted(cpu_usage2qd.items())))
255
256 if test_nc == 1:
257 labels = list(map(str, labels))
258 else:
259 labels = ["{} * {}".format(label, test_nc) for label in labels]
260
261 ds = DataSource(suite_id=suite.storage_id,
262 job_id=jc_no_qd.summary,
263 node_id="cluster",
264 sensor=AGG_TAG,
265 dev='cpu',
266 metric="cpu_for_iop",
267 tag=io_chart_format)
268
269 fpath = self.plt(plot_simple_bars, ds, jc_no_qd.long_summary, labels, vals, errs,
kdanylov aka koderb0833332017-05-13 20:39:17 +0300270 xlabel="CPU core time per IOP", ylabel="QD * Test nodes" if test_nc != 1 else "QD",
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300271 x_formatter=(lambda x, pos: b2ssize_10(x) + 's'),
272 one_point_zero_line=False)
273
274 yield Menu1st.summary, Menu2ndSumm.cpu_usage_qd, HTMLBlock(html.center(html.img(fpath)))
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200275
276
koder aka kdanilova732a602017-02-01 20:29:56 +0200277class StatInfo(JobReporter):
278 """Statistic info for job results"""
279 suite_types = {'fio'}
280
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300281 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
koder aka kdanilova732a602017-02-01 20:29:56 +0200282
283 fjob = cast(FioJobConfig, job)
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300284 io_sum = make_iosum(self.rstorage, suite, fjob, self.style.hist_boxes)
koder aka kdanilova732a602017-02-01 20:29:56 +0200285
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300286 caption = "Test summary - " + job.params.long_summary
kdanylov aka koderb0833332017-05-13 20:39:17 +0300287 test_nc = len(list(find_nodes_by_roles(self.rstorage.storage, ['testnode'])))
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300288 if test_nc > 1:
289 caption += " * {} nodes".format(test_nc)
290
291 res = html.H2(html.center(caption))
292 stat_data_headers = ["Name",
293 "Total done",
294 "Average ~ Dev",
295 "Conf interval",
296 "Mediana",
297 "Mode",
298 "Kurt / Skew",
299 "95%",
300 "99%",
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300301 "ADF test"]
koder aka kdanilova732a602017-02-01 20:29:56 +0200302
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300303 align = ['left'] + ['right'] * (len(stat_data_headers) - 1)
304
305 bw_units = "B"
306 bw_target_units = bw_units + 'ps'
kdanylov aka koderb0833332017-05-13 20:39:17 +0300307 bw_coef = unit_conversion_coef_f(io_sum.bw.units, bw_target_units)
kdanylov aka koder45183182017-04-30 23:55:40 +0300308
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300309 adf_v, *_1, stats, _2 = adfuller(io_sum.bw.data)
310
311 for v in ("1%", "5%", "10%"):
312 if adf_v <= stats[v]:
313 ad_test = v
314 break
315 else:
316 ad_test = "Failed"
317
koder aka kdanilova732a602017-02-01 20:29:56 +0200318 bw_data = ["Bandwidth",
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300319 b2ssize(io_sum.bw.data.sum() * bw_coef) + bw_units,
kdanylov aka koder45183182017-04-30 23:55:40 +0300320 "{}{} ~ {}{}".format(b2ssize(io_sum.bw.average * bw_coef), bw_target_units,
321 b2ssize(io_sum.bw.deviation * bw_coef), bw_target_units),
322 b2ssize(io_sum.bw.confidence * bw_coef) + bw_target_units,
323 b2ssize(io_sum.bw.perc_50 * bw_coef) + bw_target_units,
koder aka kdanilova732a602017-02-01 20:29:56 +0200324 "-",
325 "{:.2f} / {:.2f}".format(io_sum.bw.kurt, io_sum.bw.skew),
kdanylov aka koder45183182017-04-30 23:55:40 +0300326 b2ssize(io_sum.bw.perc_5 * bw_coef) + bw_target_units,
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300327 b2ssize(io_sum.bw.perc_1 * bw_coef) + bw_target_units,
328 ad_test]
koder aka kdanilova732a602017-02-01 20:29:56 +0200329
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300330 stat_data = [bw_data]
koder aka kdanilova732a602017-02-01 20:29:56 +0200331
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300332 if fjob.bsize < StyleProfile.large_blocks:
kdanylov aka koderb0833332017-05-13 20:39:17 +0300333 iops_coef = unit_conversion_coef_f(io_sum.bw.units, 'KiBps') / fjob.bsize
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300334 iops_data = ["IOPS",
335 b2ssize_10(io_sum.bw.data.sum() * iops_coef),
336 "{}IOPS ~ {}IOPS".format(b2ssize_10(io_sum.bw.average * iops_coef),
337 b2ssize_10(io_sum.bw.deviation * iops_coef)),
338 b2ssize_10(io_sum.bw.confidence * iops_coef) + "IOPS",
339 b2ssize_10(io_sum.bw.perc_50 * iops_coef) + "IOPS",
340 "-",
341 "{:.2f} / {:.2f}".format(io_sum.bw.kurt, io_sum.bw.skew),
342 b2ssize_10(io_sum.bw.perc_5 * iops_coef) + "IOPS",
343 b2ssize_10(io_sum.bw.perc_1 * iops_coef) + "IOPS",
344 ad_test]
koder aka kdanilova732a602017-02-01 20:29:56 +0200345
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300346 lat_target_unit = 's'
kdanylov aka koderb0833332017-05-13 20:39:17 +0300347 lat_coef = unit_conversion_coef_f(io_sum.lat.units, lat_target_unit)
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300348 # latency
349 lat_data = ["Latency",
350 "-",
351 "-",
352 "-",
353 b2ssize_10(io_sum.lat.perc_50 * lat_coef) + lat_target_unit,
354 "-",
355 "-",
356 b2ssize_10(io_sum.lat.perc_95 * lat_coef) + lat_target_unit,
357 b2ssize_10(io_sum.lat.perc_99 * lat_coef) + lat_target_unit,
358 '-']
359
360 # sensor usage
361 stat_data.extend([iops_data, lat_data])
362
363 res += html.center(html.table("Load stats info", stat_data_headers, stat_data, align=align))
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300364 yield Menu1st.per_job, job.summary, HTMLBlock(res)
koder aka kdanilova732a602017-02-01 20:29:56 +0200365
koder aka kdanilova732a602017-02-01 20:29:56 +0200366
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300367class Resources(JobReporter):
368 """Statistic info for job results"""
369 suite_types = {'fio'}
370
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300371 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300372
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300373 records, iops_ok = get_resources_usage(suite, job, self.rstorage,
374 large_block=self.style.large_blocks,
375 hist_boxes=self.style.hist_boxes)
koder aka kdanilova732a602017-02-01 20:29:56 +0200376
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300377 table_structure = [
378 "Service provided",
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300379 (ResourceNames.io_made, ResourceNames.data_tr),
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300380 "Test nodes total load",
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300381 (ResourceNames.test_send_pkt, ResourceNames.test_send),
382 (ResourceNames.test_recv_pkt, ResourceNames.test_recv),
383 (ResourceNames.test_net_pkt, ResourceNames.test_net),
384 (ResourceNames.test_write_iop, ResourceNames.test_write),
385 (ResourceNames.test_read_iop, ResourceNames.test_read),
386 (ResourceNames.test_iop, ResourceNames.test_rw),
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300387 "Storage nodes resource consumed",
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300388 (ResourceNames.storage_send_pkt, ResourceNames.storage_send),
389 (ResourceNames.storage_recv_pkt, ResourceNames.storage_recv),
390 (ResourceNames.storage_net_pkt, ResourceNames.storage_net),
391 (ResourceNames.storage_write_iop, ResourceNames.storage_write),
392 (ResourceNames.storage_read_iop, ResourceNames.storage_read),
393 (ResourceNames.storage_iop, ResourceNames.storage_rw),
394 (ResourceNames.storage_cpu_s, ResourceNames.storage_cpu_s_b),
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300395 ] # type: List[Union[str, Tuple[Optional[str], ...]]]
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300396
397 if not iops_ok:
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300398 table_structure2 = [] # type: List[Union[Tuple[str, ...], str]]
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300399 for line in table_structure:
400 if isinstance(line, str):
401 table_structure2.append(line)
402 else:
403 assert len(line) == 2
404 table_structure2.append((line[1],))
405 table_structure = table_structure2
koder aka kdanilova732a602017-02-01 20:29:56 +0200406
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300407 yield Menu1st.per_job, job.summary, HTMLBlock(html.H2(html.center("Resources usage")))
408
409 doc = xmlbuilder3.XMLBuilder("table",
410 **{"class": "table table-bordered table-striped table-condensed table-hover",
411 "style": "width: auto;"})
412
413 with doc.thead:
414 with doc.tr:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300415 [doc.th(header) for header in ["Resource", "Usage count", "To service"] * (2 if iops_ok else 1)]
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300416
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300417 cols = 6 if iops_ok else 3
418 col_per_tp = 3
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300419
420 short_name = {
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300421 name: (name if name in {ResourceNames.io_made, ResourceNames.data_tr}
422 else " ".join(name.split()[2:]).capitalize())
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300423 for name in records.keys()
424 }
425
kdanylov aka koderb0833332017-05-13 20:39:17 +0300426 short_name[ResourceNames.storage_cpu_s] = "CPU core (s/IOP)"
427 short_name[ResourceNames.storage_cpu_s_b] = "CPU core (s/B)"
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300428
429 with doc.tbody:
430 with doc.tr:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300431 if iops_ok:
432 doc.td(colspan=str(col_per_tp)).center.b("Operations")
433 doc.td(colspan=str(col_per_tp)).center.b("Bytes")
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300434
435 for line in table_structure:
436 with doc.tr:
437 if isinstance(line, str):
438 with doc.td(colspan=str(cols)):
439 doc.center.b(line)
440 else:
441 for name in line:
442 if name is None:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300443 doc.td("-", colspan=str(col_per_tp))
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300444 continue
445
446 amount_s, avg, dev = records[name]
447
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300448 if name in (ResourceNames.storage_cpu_s, ResourceNames.storage_cpu_s_b) and avg is not None:
449 if dev is None:
450 rel_val_s = b2ssize_10(avg) + 's'
451 else:
452 dev_s = str(int(dev * 100 / avg)) + "%" if avg > 1E-9 else b2ssize_10(dev) + 's'
453 rel_val_s = "{}s ~ {}".format(b2ssize_10(avg), dev_s)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300454 else:
455 if avg is None:
456 rel_val_s = '-'
457 else:
458 avg_s = int(avg) if avg > 10 else '{:.1f}'.format(avg)
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300459 if dev is None:
460 rel_val_s = avg_s
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300461 else:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300462 if avg > 1E-5:
463 dev_s = str(int(dev * 100 / avg)) + "%"
464 else:
465 dev_s = int(dev) if dev > 10 else '{:.1f}'.format(dev)
466 rel_val_s = "{} ~ {}".format(avg_s, dev_s)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300467
468 doc.td(short_name[name], align="left")
469 doc.td(amount_s, align="right")
470
471 if avg is None or avg < 0.9:
472 doc.td(rel_val_s, align="right")
473 elif avg < 2.0:
474 doc.td(align="right").font(rel_val_s, color='green')
475 elif avg < 5.0:
476 doc.td(align="right").font(rel_val_s, color='orange')
477 else:
478 doc.td(align="right").font(rel_val_s, color='red')
479
480 res = xmlbuilder3.tostr(doc).split("\n", 1)[1]
481 yield Menu1st.per_job, job.summary, HTMLBlock(html.center(res))
482
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300483 iop_names = [ResourceNames.test_write_iop, ResourceNames.test_read_iop, ResourceNames.test_iop,
484 ResourceNames.storage_write_iop, ResourceNames.storage_read_iop, ResourceNames.storage_iop]
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300485
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300486 bytes_names = [ResourceNames.test_write, ResourceNames.test_read, ResourceNames.test_rw,
487 ResourceNames.test_send, ResourceNames.test_recv, ResourceNames.test_net,
488 ResourceNames.storage_write, ResourceNames.storage_read, ResourceNames.storage_rw,
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300489 ResourceNames.storage_send, ResourceNames.storage_recv,
490 ResourceNames.storage_net] # type: List[str]
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300491
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300492 net_pkt_names = [ResourceNames.test_send_pkt, ResourceNames.test_recv_pkt, ResourceNames.test_net_pkt,
493 ResourceNames.storage_send_pkt, ResourceNames.storage_recv_pkt, ResourceNames.storage_net_pkt]
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300494
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300495 pairs = [("bytes", bytes_names)]
496 if iops_ok:
497 pairs.insert(0, ('iop', iop_names))
498 pairs.append(('Net packets per IOP', net_pkt_names))
499
500 yield Menu1st.per_job, job.summary, \
501 HTMLBlock(html.H2(html.center("Resource consumption per service provided")))
502
503 for tp, names in pairs:
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300504 vals = [] # type: List[float]
505 devs = [] # type: List[float]
506 avail_names = [] # type: List[str]
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300507 for name in names:
508 if name in records:
509 avail_names.append(name)
510 _, avg, dev = records[name]
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300511
512 if dev is None:
513 dev = 0
514
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300515 vals.append(avg)
516 devs.append(dev)
517
518 # synchronously sort values and names, values is a key
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300519 vals, names, devs = map(list, zip(*sorted(zip(vals, names, devs)))) # type: ignore
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300520
521 ds = DataSource(suite_id=suite.storage_id,
522 job_id=job.storage_id,
523 node_id=AGG_TAG,
524 sensor='resources',
525 dev=AGG_TAG,
526 metric=tp.replace(' ', "_") + '2service_bar',
527 tag=default_format)
528
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300529 fname = self.plt(plot_simple_bars, ds, tp.capitalize(),
530 [name.replace(" nodes", "") for name in names],
531 vals, devs)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300532
533 yield Menu1st.per_job, job.summary, HTMLBlock(html.img(fname))
534
535
536class BottleNeck(JobReporter):
537 """Statistic info for job results"""
538 suite_types = {'fio'}
539
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300540 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300541
kdanylov aka koderb0833332017-05-13 20:39:17 +0300542 nodes = list(find_nodes_by_roles(self.rstorage.storage, STORAGE_ROLES))
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300543
544 sensor = 'block-io'
545 metric = 'io_queue'
546 bn_val = 16
547
kdanylov aka koderb0833332017-05-13 20:39:17 +0300548 for node_id in nodes:
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300549 bn = 0
550 tot = 0
kdanylov aka koderb0833332017-05-13 20:39:17 +0300551 for _, ds in self.rstorage.iter_sensors(node_id=node_id, sensor=sensor, metric=metric):
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300552 if ds.dev in ('sdb', 'sdc', 'sdd', 'sde'):
kdanylov aka koderb0833332017-05-13 20:39:17 +0300553 ts = self.rstorage.get_sensor(ds, job.reliable_info_range_s)
554 bn += (ts.data > bn_val).sum()
555 tot += len(ts.data)
556 print(node_id, bn, tot)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300557
558 yield Menu1st.per_job, job.summary, HTMLBlock("")
koder aka kdanilova732a602017-02-01 20:29:56 +0200559
560
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300561# CPU load
562class CPULoadPlot(JobReporter):
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300563 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300564
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300565 # plot CPU time
566 for rt, roles in [('storage', STORAGE_ROLES), ('test', ['testnode'])]:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300567 cpu_ts = get_cluster_cpu_load(self.rstorage, roles, job.reliable_info_range_s)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300568 tss = [(name, ts.data * 100 / cpu_ts['total'].data)
569 for name, ts in cpu_ts.items()
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300570 if name in {'user', 'sys', 'idle', 'iowait'}]
571
572
573 ds = cpu_ts['idle'].source(job_id=job.storage_id, suite_id=suite.storage_id,
574 node_id=AGG_TAG, metric='allcpu', tag=rt + '.plt.' + default_format)
575
576 fname = self.plt(plot_simple_over_time, ds, tss=tss, average=True, ylabel="CPU time %",
577 title="{} nodes CPU usage".format(rt.capitalize()),
578 xlabel="Time from test begin")
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300579
580 yield Menu1st.per_job, job.summary, HTMLBlock(html.img(fname))
581
582
583# IO time and QD
584class QDIOTimeHeatmap(JobReporter):
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300585 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300586
587 # TODO: fix this hardcode, need to track what devices are actually used on test and storage nodes
588 # use saved storage info in nodes
589
590 journal_devs = None
591 storage_devs = None
592 test_nodes_devs = ['rbd0']
593
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300594 for node in self.rstorage.load_nodes():
595 if node.roles.intersection(STORAGE_ROLES):
596 cjd = set(node.params['ceph_journal_devs'])
597 if journal_devs is None:
598 journal_devs = cjd
599 else:
600 assert journal_devs == cjd, "{!r} != {!r}".format(journal_devs, cjd)
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300601
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300602 csd = set(node.params['ceph_storage_devs'])
603 if storage_devs is None:
604 storage_devs = csd
605 else:
606 assert storage_devs == csd, "{!r} != {!r}".format(storage_devs, csd)
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300607
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300608 trange = (job.reliable_info_range[0] // 1000, job.reliable_info_range[1] // 1000)
609
610 for name, devs, roles in [('storage', storage_devs, STORAGE_ROLES),
611 ('journal', journal_devs, STORAGE_ROLES),
612 ('test', test_nodes_devs, ['testnode'])]:
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300613
614 yield Menu1st.per_job, job.summary, \
615 HTMLBlock(html.H2(html.center("{} IO heatmaps".format(name.capitalize()))))
616
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300617 # QD heatmap
kdanylov aka koderb0833332017-05-13 20:39:17 +0300618 nodes = find_nodes_by_roles(self.rstorage.storage, roles)
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300619 ioq2d = find_sensors_to_2d(self.rstorage, trange, sensor='block-io', dev=devs,
620 node_id=nodes, metric='io_queue')
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300621
622 ds = DataSource(suite.storage_id, job.storage_id, AGG_TAG, 'block-io', name, tag="hmap." + default_format)
623
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300624 fname = self.plt(plot_hmap_from_2d, ds(metric='io_queue'), data2d=ioq2d, xlabel='Time', ylabel="IO QD",
625 title=name.capitalize() + " devs QD", bins=StyleProfile.qd_bins)
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300626 yield Menu1st.per_job, job.summary, HTMLBlock(html.img(fname))
627
628 # Block size heatmap
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300629 wc2d = find_sensors_to_2d(self.rstorage, trange, node_id=nodes, sensor='block-io', dev=devs,
kdanylov aka koderb0833332017-05-13 20:39:17 +0300630 metric='writes_completed')
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300631 wc2d[wc2d < 1E-3] = 1
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300632 sw2d = find_sensors_to_2d(self.rstorage, trange, node_id=nodes, sensor='block-io', dev=devs,
kdanylov aka koderb0833332017-05-13 20:39:17 +0300633 metric='sectors_written')
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300634 data2d = sw2d / wc2d / 1024
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300635 fname = self.plt(plot_hmap_from_2d, ds(metric='wr_block_size'),
636 data2d=data2d, title=name.capitalize() + " write block size",
637 ylabel="IO bsize, KiB", xlabel='Time', bins=StyleProfile.block_size_bins)
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300638 yield Menu1st.per_job, job.summary, HTMLBlock(html.img(fname))
639
640 # iotime heatmap
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300641 wtime2d = find_sensors_to_2d(self.rstorage, trange, node_id=nodes, sensor='block-io', dev=devs,
kdanylov aka koderb0833332017-05-13 20:39:17 +0300642 metric='io_time')
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300643 fname = self.plt(plot_hmap_from_2d, ds(metric='io_time'), data2d=wtime2d,
644 xlabel='Time', ylabel="IO time (ms) per second",
645 title=name.capitalize() + " iotime", bins=StyleProfile.iotime_bins)
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300646 yield Menu1st.per_job, job.summary, HTMLBlock(html.img(fname))
647
648
koder aka kdanilov108ac362017-01-19 20:17:16 +0200649# IOPS/latency over test time for each job
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300650class LoadToolResults(JobReporter):
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200651 """IOPS/latency during test"""
koder aka kdanilova732a602017-02-01 20:29:56 +0200652 suite_types = {'fio'}
koder aka kdanilov108ac362017-01-19 20:17:16 +0200653
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300654 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200655
koder aka kdanilova732a602017-02-01 20:29:56 +0200656 fjob = cast(FioJobConfig, job)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200657
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300658 yield Menu1st.per_job, job.summary, HTMLBlock(html.H2(html.center("Load tool results")))
659
kdanylov aka koderb0833332017-05-13 20:39:17 +0300660 agg_io = get_aggregated(self.rstorage, suite.storage_id, fjob.storage_id, "bw", job.reliable_info_range_s)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300661 if fjob.bsize >= DefStyleProfile.large_blocks:
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300662 title = "Fio measured Bandwidth over time"
koder aka kdanilova732a602017-02-01 20:29:56 +0200663 units = "MiBps"
kdanylov aka koderb0833332017-05-13 20:39:17 +0300664 agg_io.data //= int(unit_conversion_coef_f(units, agg_io.units))
koder aka kdanilova732a602017-02-01 20:29:56 +0200665 else:
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300666 title = "Fio measured IOPS over time"
kdanylov aka koderb0833332017-05-13 20:39:17 +0300667 agg_io.data //= (int(unit_conversion_coef_f("KiBps", agg_io.units)) * fjob.bsize)
koder aka kdanilova732a602017-02-01 20:29:56 +0200668 units = "IOPS"
koder aka kdanilov108ac362017-01-19 20:17:16 +0200669
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300670 fpath = self.plt(plot_v_over_time, agg_io.source(tag='ts.' + default_format), title, units, agg_io)
koder aka kdanilova732a602017-02-01 20:29:56 +0200671 yield Menu1st.per_job, fjob.summary, HTMLBlock(html.img(fpath))
koder aka kdanilov108ac362017-01-19 20:17:16 +0200672
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300673 if fjob.bsize < DefStyleProfile.large_blocks:
kdanylov aka koderb0833332017-05-13 20:39:17 +0300674 agg_lat = get_aggregated(self.rstorage, suite.storage_id, fjob.storage_id, "lat",
675 job.reliable_info_range_s)
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300676 TARGET_UNITS = 'ms'
kdanylov aka koderb0833332017-05-13 20:39:17 +0300677 coef = unit_conversion_coef_f(agg_lat.units, TARGET_UNITS)
678 agg_lat.histo_bins = agg_lat.histo_bins.copy() * coef
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300679 agg_lat.units = TARGET_UNITS
koder aka kdanilov108ac362017-01-19 20:17:16 +0200680
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300681 fpath = self.plt(plot_lat_over_time, agg_lat.source(tag='ts.' + default_format), "Latency", agg_lat,
682 ylabel="Latency, " + agg_lat.units)
683 yield Menu1st.per_job, fjob.summary, HTMLBlock(html.img(fpath))
koder aka kdanilov108ac362017-01-19 20:17:16 +0200684
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300685 fpath = self.plt(plot_histo_heatmap, agg_lat.source(tag='hmap.' + default_format),
686 "Latency heatmap", agg_lat, ylabel="Latency, " + agg_lat.units, xlabel='Test time')
koder aka kdanilov108ac362017-01-19 20:17:16 +0200687
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300688 yield Menu1st.per_job, fjob.summary, HTMLBlock(html.img(fpath))
koder aka kdanilov108ac362017-01-19 20:17:16 +0200689
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300690 fjob = cast(FioJobConfig, job)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200691
kdanylov aka koderb0833332017-05-13 20:39:17 +0300692 agg_io = get_aggregated(self.rstorage, suite.storage_id, fjob.storage_id, "bw", job.reliable_info_range_s)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200693
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300694 if fjob.bsize >= DefStyleProfile.large_blocks:
695 title = "BW distribution"
696 units = "MiBps"
kdanylov aka koderb0833332017-05-13 20:39:17 +0300697 agg_io.data //= int(unit_conversion_coef_f(units, agg_io.units))
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300698 else:
699 title = "IOPS distribution"
kdanylov aka koderb0833332017-05-13 20:39:17 +0300700 agg_io.data //= (int(unit_conversion_coef_f("KiBps", agg_io.units)) * fjob.bsize)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300701 units = "IOPS"
702
703 io_stat_prop = calc_norm_stat_props(agg_io, bins_count=StyleProfile.hist_boxes)
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300704 fpath = self.plt(plot_hist, agg_io.source(tag='hist.' + default_format), title, units, io_stat_prop)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300705 yield Menu1st.per_job, fjob.summary, HTMLBlock(html.img(fpath))
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200706
707
708# Cluster load over test time
koder aka kdanilova732a602017-02-01 20:29:56 +0200709class ClusterLoad(JobReporter):
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200710 """IOPS/latency during test"""
711
koder aka kdanilova732a602017-02-01 20:29:56 +0200712 # TODO: units should came from sensor
koder aka kdanilov108ac362017-01-19 20:17:16 +0200713 storage_sensors = [
kdanylov aka koder45183182017-04-30 23:55:40 +0300714 ('block-io', 'reads_completed', "Read", 'iop'),
715 ('block-io', 'writes_completed', "Write", 'iop'),
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300716 ('block-io', 'sectors_read', "Read", 'MiB'),
717 ('block-io', 'sectors_written', "Write", 'MiB'),
koder aka kdanilov108ac362017-01-19 20:17:16 +0200718 ]
719
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300720 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
721
koder aka kdanilova732a602017-02-01 20:29:56 +0200722 yield Menu1st.per_job, job.summary, HTMLBlock(html.H2(html.center("Cluster load")))
koder aka kdanilov108ac362017-01-19 20:17:16 +0200723
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300724 sensors = []
725 max_iop = 0
726 max_bytes = 0
kdanylov aka koderb0833332017-05-13 20:39:17 +0300727 stor_nodes = find_nodes_by_roles(self.rstorage.storage, STORAGE_ROLES)
kdanylov aka koder45183182017-04-30 23:55:40 +0300728 for sensor, metric, op, units in self.storage_sensors:
kdanylov aka koderb0833332017-05-13 20:39:17 +0300729 ts = summ_sensors(self.rstorage, job.reliable_info_range_s, node_id=stor_nodes, sensor=sensor,
730 metric=metric)
731 if ts is not None:
732 ds = DataSource(suite_id=suite.storage_id,
733 job_id=job.storage_id,
734 node_id="storage",
735 sensor=sensor,
736 dev=AGG_TAG,
737 metric=metric,
738 tag="ts." + default_format)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200739
kdanylov aka koderb0833332017-05-13 20:39:17 +0300740 data = ts.data if units != 'MiB' else ts.data * unit_conversion_coef_f(ts.units, 'MiB')
741 ts = TimeSeries(times=numpy.arange(*job.reliable_info_range_s),
742 data=data,
743 units=units if ts.units is None else ts.units,
744 time_units=ts.time_units,
745 source=ds,
746 histo_bins=ts.histo_bins)
kdanylov aka koder0e0cfcb2017-03-27 22:19:09 +0300747
kdanylov aka koderb0833332017-05-13 20:39:17 +0300748 sensors.append(("{} {}".format(op, units), ds, ts, units))
koder aka kdanilov108ac362017-01-19 20:17:16 +0200749
kdanylov aka koderb0833332017-05-13 20:39:17 +0300750 if units == 'iop':
751 max_iop = max(max_iop, data.sum())
752 else:
753 assert units == 'MiB'
754 max_bytes = max(max_bytes, data.sum())
koder aka kdanilov108ac362017-01-19 20:17:16 +0200755
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300756 for title, ds, ts, units in sensors:
757 if ts.data.sum() >= (max_iop if units == 'iop' else max_bytes) * DefStyleProfile.min_load_diff:
758 fpath = self.plt(plot_v_over_time, ds, title, units, ts=ts)
759 yield Menu1st.per_job, job.summary, HTMLBlock(html.img(fpath))
760 else:
761 logger.info("Hide '%s' plot for %s, as it's cum load is less then %s%%",
762 title, job.summary, int(DefStyleProfile.min_load_diff * 100))
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200763
764
koder aka kdanilov108ac362017-01-19 20:17:16 +0200765# ------------------------------------------ REPORT STAGES -----------------------------------------------------------
766
767
768class HtmlReportStage(Stage):
769 priority = StepOrder.REPORT
770
771 def run(self, ctx: TestRun) -> None:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300772 job_reporters_cls = [StatInfo, Resources, LoadToolResults, ClusterLoad, CPULoadPlot, QDIOTimeHeatmap]
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300773 job_reporters = [rcls(ctx.rstorage, DefStyleProfile, DefColorProfile)
774 for rcls in job_reporters_cls] # type: ignore
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300775
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300776 suite_reporters_cls = [IOQD, ResourceQD] # type: List[Type[SuiteReporter]]
777 suite_reporters = [rcls(ctx.rstorage, DefStyleProfile, DefColorProfile)
778 for rcls in suite_reporters_cls] # type: ignore
koder aka kdanilov108ac362017-01-19 20:17:16 +0200779
780 root_dir = os.path.dirname(os.path.dirname(wally.__file__))
781 doc_templ_path = os.path.join(root_dir, "report_templates/index.html")
782 report_template = open(doc_templ_path, "rt").read()
783 css_file_src = os.path.join(root_dir, "report_templates/main.css")
784 css_file = open(css_file_src, "rt").read()
785
786 menu_block = []
787 content_block = []
788 link_idx = 0
789
koder aka kdanilova732a602017-02-01 20:29:56 +0200790 # matplotlib.rcParams.update(ctx.config.reporting.matplotlib_params.raw())
791 # ColorProfile.__dict__.update(ctx.config.reporting.colors.raw())
792 # StyleProfile.__dict__.update(ctx.config.reporting.style.raw())
koder aka kdanilov108ac362017-01-19 20:17:16 +0200793
koder aka kdanilova732a602017-02-01 20:29:56 +0200794 items = defaultdict(lambda: defaultdict(list)) # type: Dict[str, Dict[str, List[HTMLBlock]]]
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300795 DEBUG = False
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300796 job_summ_sort_order = []
797
koder aka kdanilova732a602017-02-01 20:29:56 +0200798 # TODO: filter reporters
kdanylov aka koderb0833332017-05-13 20:39:17 +0300799 for suite in ctx.rstorage.iter_suite(FioTest.name):
800 all_jobs = list(ctx.rstorage.iter_job(suite))
koder aka kdanilova732a602017-02-01 20:29:56 +0200801 all_jobs.sort(key=lambda job: job.params)
koder aka kdanilova732a602017-02-01 20:29:56 +0200802
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300803 new_jobs_in_order = [job.summary for job in all_jobs]
804 same = set(new_jobs_in_order).intersection(set(job_summ_sort_order))
805 assert not same, "Job with same names in different suits found: " + ",".join(same)
806 job_summ_sort_order.extend(new_jobs_in_order)
807
kdanylov aka koderb0833332017-05-13 20:39:17 +0300808 for job in all_jobs:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300809 try:
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300810 for reporter in job_reporters: # type: JobReporter
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300811 logger.debug("Start reporter %s on job %s suite %s",
812 reporter.__class__.__name__, job.summary, suite.test_type)
813 for block, item, html in reporter.get_divs(suite, job):
814 items[block][item].append(html)
815 if DEBUG:
816 break
817 except Exception:
818 logger.exception("Failed to generate report for %s", job.summary)
819
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300820 for sreporter in suite_reporters: # type: SuiteReporter
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300821 try:
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300822 logger.debug("Start reporter %s on suite %s", sreporter.__class__.__name__, suite.test_type)
823 for block, item, html in sreporter.get_divs(suite):
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300824 items[block][item].append(html)
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300825 except Exception:
826 logger.exception("Failed to generate report for suite %s", suite)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200827
koder aka kdanilova732a602017-02-01 20:29:56 +0200828 if DEBUG:
829 break
830
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300831 logger.debug("Generating result html")
832
koder aka kdanilov108ac362017-01-19 20:17:16 +0200833 for idx_1st, menu_1st in enumerate(sorted(items, key=lambda x: menu_1st_order.index(x))):
834 menu_block.append(
835 '<a href="#item{}" class="nav-group" data-toggle="collapse" data-parent="#MainMenu">{}</a>'
836 .format(idx_1st, menu_1st)
837 )
838 menu_block.append('<div class="collapse" id="item{}">'.format(idx_1st))
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300839
840 if menu_1st == Menu1st.per_job:
841 in_order = sorted(items[menu_1st], key=job_summ_sort_order.index)
842 else:
843 in_order = sorted(items[menu_1st])
844
845 for menu_2nd in in_order:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200846 menu_block.append(' <a href="#content{}" class="nav-group-item">{}</a>'
847 .format(link_idx, menu_2nd))
848 content_block.append('<div id="content{}">'.format(link_idx))
koder aka kdanilova732a602017-02-01 20:29:56 +0200849 content_block.extend(" " + x.data for x in items[menu_1st][menu_2nd])
koder aka kdanilov108ac362017-01-19 20:17:16 +0200850 content_block.append('</div>')
851 link_idx += 1
852 menu_block.append('</div>')
853
854 report = report_template.replace("{{{menu}}}", ("\n" + " " * 16).join(menu_block))
855 report = report.replace("{{{content}}}", ("\n" + " " * 16).join(content_block))
kdanylov aka koderb0833332017-05-13 20:39:17 +0300856 report_path = ctx.rstorage.put_report(report, "index.html")
857 ctx.rstorage.put_report(css_file, "main.css")
koder aka kdanilov108ac362017-01-19 20:17:16 +0200858 logger.info("Report is stored into %r", report_path)