blob: d6a42ee19ada0c7baf7d1041a15756406316f300 [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
kdanylov aka koder84de1e42017-05-22 14:00:07 +03004import collections
koder aka kdanilov108ac362017-01-19 20:17:16 +02005from collections import defaultdict
kdanylov aka koder84de1e42017-05-22 14:00:07 +03006from typing import Dict, Any, Iterator, Tuple, cast, List, Set, Optional, Union, Type, Iterable
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +03007
koder aka kdanilovffaf48d2016-12-27 02:25:29 +02008import numpy
kdanylov aka koder84de1e42017-05-22 14:00:07 +03009import scipy.stats
kdanylov aka koder736e5c12017-05-07 17:27:14 +030010from statsmodels.tsa.stattools import adfuller
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +030011
kdanylov aka koder736e5c12017-05-07 17:27:14 +030012import xmlbuilder3
koder aka kdanilovbe8f89f2015-04-28 14:51:51 +030013
koder aka kdanilov108ac362017-01-19 20:17:16 +020014import wally
koder aka kdanilovffaf48d2016-12-27 02:25:29 +020015
kdanylov aka koderb0833332017-05-13 20:39:17 +030016from cephlib import html
17from cephlib.units import b2ssize, b2ssize_10, unit_conversion_coef, unit_conversion_coef_f
18from cephlib.statistic import calc_norm_stat_props
kdanylov aka koder84de1e42017-05-22 14:00:07 +030019from cephlib.storage_selectors import sum_sensors, find_sensors_to_2d, update_storage_selector, DevRoles
kdanylov aka koderb0833332017-05-13 20:39:17 +030020from cephlib.wally_storage import find_nodes_by_roles
kdanylov aka koder026e5f22017-05-15 01:04:39 +030021from cephlib.plot import (plot_simple_bars, plot_hmap_from_2d, plot_lat_over_time, plot_simple_over_time,
kdanylov aka koder84de1e42017-05-22 14:00:07 +030022 plot_histo_heatmap, plot_v_over_time, plot_hist, plot_dots_with_regression)
23from cephlib.numeric_types import ndarray2d
24from cephlib.node import NodeRole
kdanylov aka koderb0833332017-05-13 20:39:17 +030025
26from .utils import STORAGE_ROLES
koder aka kdanilov39e449e2016-12-17 15:15:26 +020027from .stage import Stage, StepOrder
28from .test_run_class import TestRun
kdanylov aka koder026e5f22017-05-15 01:04:39 +030029from .result_classes import IWallyStorage
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +030030from .result_classes import DataSource, TimeSeries, SuiteConfig
koder aka kdanilov108ac362017-01-19 20:17:16 +020031from .suits.io.fio import FioTest, FioJobConfig
koder aka kdanilova732a602017-02-01 20:29:56 +020032from .suits.io.fio_job import FioJobParams
33from .suits.job import JobConfig
kdanylov aka koderb0833332017-05-13 20:39:17 +030034from .data_selectors import get_aggregated, AGG_TAG
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +030035from .report_profiles import (DefStyleProfile, DefColorProfile, StyleProfile, ColorProfile,
36 default_format, io_chart_format)
kdanylov aka koder026e5f22017-05-15 01:04:39 +030037from .plot import io_chart
kdanylov aka koder84de1e42017-05-22 14:00:07 +030038from .resources import ResourceNames, get_resources_usage, make_iosum, get_cluster_cpu_load
39from .console_report import get_console_report_table, console_report_headers, console_report_align, Texttable
kdanylov aka koder026e5f22017-05-15 01:04:39 +030040
41
koder aka kdanilov962ee5f2016-12-19 02:40:08 +020042logger = logging.getLogger("wally")
koder aka kdanilova047e1b2015-04-21 23:16:59 +030043
44
koder aka kdanilov108ac362017-01-19 20:17:16 +020045# ---------------- CONSTS ---------------------------------------------------------------------------------------------
koder aka kdanilov39e449e2016-12-17 15:15:26 +020046
koder aka kdanilov7f59d562016-12-26 01:34:23 +020047
koder aka kdanilov108ac362017-01-19 20:17:16 +020048DEBUG = False
koder aka kdanilov39e449e2016-12-17 15:15:26 +020049
koder aka kdanilov39e449e2016-12-17 15:15:26 +020050
koder aka kdanilov108ac362017-01-19 20:17:16 +020051# -------------- AGGREGATION AND STAT FUNCTIONS ----------------------------------------------------------------------
koder aka kdanilov108ac362017-01-19 20:17:16 +020052
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +030053LEVEL_SENSORS = {("block-io", "io_queue"), ("system-cpu", "procs_blocked"), ("system-cpu", "procs_queue")}
koder aka kdanilova732a602017-02-01 20:29:56 +020054
55
56def is_level_sensor(sensor: str, metric: str) -> bool:
57 """Returns True if sensor measure level of any kind, E.g. queue depth."""
58 return (sensor, metric) in LEVEL_SENSORS
59
60
61def is_delta_sensor(sensor: str, metric: str) -> bool:
62 """Returns True if sensor provides deltas for cumulative value. E.g. io completed in given period"""
63 return not is_level_sensor(sensor, metric)
64
kdanylov aka koder736e5c12017-05-07 17:27:14 +030065
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +030066# def get_idle_load(rstorage: ResultStorage, *args, **kwargs) -> float:
67# if 'idle' not in rstorage.storage:
68# return 0.0
69# idle_time = rstorage.storage.get('idle')
70# ssum = summ_sensors(rstorage, time_range=idle_time, *args, **kwargs)
71# return numpy.average(ssum)
kdanylov aka koder736e5c12017-05-07 17:27:14 +030072
koder aka kdanilov108ac362017-01-19 20:17:16 +020073
74# -------------------- REPORT HELPERS --------------------------------------------------------------------------------
75
76
koder aka kdanilov7f59d562016-12-26 01:34:23 +020077class HTMLBlock:
78 data = None # type: str
79 js_links = [] # type: List[str]
80 css_links = [] # type: List[str]
koder aka kdanilova732a602017-02-01 20:29:56 +020081 order_attr = None # type: Any
82
83 def __init__(self, data: str, order_attr: Any = None) -> None:
84 self.data = data
85 self.order_attr = order_attr
86
kdanylov aka koder45183182017-04-30 23:55:40 +030087 def __eq__(self, o: Any) -> bool:
koder aka kdanilova732a602017-02-01 20:29:56 +020088 return o.order_attr == self.order_attr # type: ignore
89
kdanylov aka koder45183182017-04-30 23:55:40 +030090 def __lt__(self, o: Any) -> bool:
koder aka kdanilova732a602017-02-01 20:29:56 +020091 return o.order_attr > self.order_attr # type: ignore
92
93
94class Table:
95 def __init__(self, header: List[str]) -> None:
96 self.header = header
kdanylov aka koder026e5f22017-05-15 01:04:39 +030097 self.data = [] # type: List[List[str]]
koder aka kdanilova732a602017-02-01 20:29:56 +020098
99 def add_line(self, values: List[str]) -> None:
100 self.data.append(values)
101
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300102 def html(self) -> str:
koder aka kdanilova732a602017-02-01 20:29:56 +0200103 return html.table("", self.header, self.data)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200104
105
koder aka kdanilov108ac362017-01-19 20:17:16 +0200106class Menu1st:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200107 summary = "Summary"
koder aka kdanilova732a602017-02-01 20:29:56 +0200108 per_job = "Per Job"
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300109 engineering = "Engineering"
110 engineering_per_job = "Engineering per job"
111 order = [summary, per_job, engineering, engineering_per_job]
koder aka kdanilov108ac362017-01-19 20:17:16 +0200112
113
114class Menu2ndEng:
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300115 summary = "Summary"
koder aka kdanilov108ac362017-01-19 20:17:16 +0200116 iops_time = "IOPS(time)"
117 hist = "IOPS/lat overall histogram"
118 lat_time = "Lat(time)"
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300119 resource_regression = "Resource usage LR"
120 order = [summary, iops_time, hist, lat_time, resource_regression]
koder aka kdanilov108ac362017-01-19 20:17:16 +0200121
122
123class Menu2ndSumm:
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300124 summary = "Summary"
koder aka kdanilov108ac362017-01-19 20:17:16 +0200125 io_lat_qd = "IO & Lat vs QD"
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300126 resources_usage_qd = "Resource usage"
127 order = [summary, io_lat_qd, resources_usage_qd]
koder aka kdanilov108ac362017-01-19 20:17:16 +0200128
129
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300130menu_1st_order = [Menu1st.summary, Menu1st.engineering, Menu1st.per_job, Menu1st.engineering_per_job]
koder aka kdanilov108ac362017-01-19 20:17:16 +0200131
132
133# -------------------- REPORTS --------------------------------------------------------------------------------------
134
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300135class ReporterBase:
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300136 def __init__(self, rstorage: IWallyStorage, style: StyleProfile, colors: ColorProfile) -> None:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300137 self.style = style
138 self.colors = colors
139 self.rstorage = rstorage
koder aka kdanilov108ac362017-01-19 20:17:16 +0200140
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300141 def plt(self, func, ds: DataSource, *args, **kwargs) -> str:
142 return func(self.rstorage, self.style, self.colors, ds, *args, **kwargs)
143
144
145class SuiteReporter(ReporterBase, metaclass=abc.ABCMeta):
146 suite_types = set() # type: Set[str]
koder aka kdanilova732a602017-02-01 20:29:56 +0200147
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200148 @abc.abstractmethod
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300149 def get_divs(self, suite: SuiteConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
koder aka kdanilova732a602017-02-01 20:29:56 +0200150 pass
151
152
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300153class JobReporter(ReporterBase, metaclass=abc.ABCMeta):
koder aka kdanilova732a602017-02-01 20:29:56 +0200154 suite_type = set() # type: Set[str]
155
156 @abc.abstractmethod
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300157 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200158 pass
159
160
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300161# # Linearization report
162# class IOPSBsize(SuiteReporter):
163# """Creates graphs, which show how IOPS and Latency depend on block size"""
164#
165#
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300166
167
168class StoragePerfSummary:
169 iops_units = "KiBps"
170 bw_units = "Bps"
171 NO_VAL = -1
172
173 def __init__(self) -> None:
kdanylov aka koder13e58452018-07-15 02:51:51 +0300174 self.rw_iops_10ms = self.NO_VAL
175 self.rw_iops_30ms = self.NO_VAL
176 self.rw_iops_100ms = self.NO_VAL
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300177
kdanylov aka koder13e58452018-07-15 02:51:51 +0300178 self.rr_iops_10ms = self.NO_VAL
179 self.rr_iops_30ms = self.NO_VAL
180 self.rr_iops_100ms = self.NO_VAL
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300181
kdanylov aka koder13e58452018-07-15 02:51:51 +0300182 self.bw_write_max = self.NO_VAL
183 self.bw_read_max = self.NO_VAL
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300184
kdanylov aka koder13e58452018-07-15 02:51:51 +0300185 self.bw: Optional[float] = None
186 self.read_iops: Optional[float] = None
187 self.write_iops: Optional[float] = None
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300188
189
190def get_performance_summary(storage: IWallyStorage, suite: SuiteConfig,
191 hboxes: int, large_blocks: int) -> Tuple[StoragePerfSummary, StoragePerfSummary]:
192
193 psum95 = StoragePerfSummary()
194 psum50 = StoragePerfSummary()
195
196 for job in storage.iter_job(suite):
197 if isinstance(job, FioJobConfig):
198 fjob = cast(FioJobConfig, job)
199 io_sum = make_iosum(storage, suite, job, hboxes)
200
201 bw_avg = io_sum.bw.average * unit_conversion_coef(io_sum.bw.units, StoragePerfSummary.bw_units)
202
203 if fjob.bsize < large_blocks:
204 lat_95_ms = io_sum.lat.perc_95 * unit_conversion_coef(io_sum.lat.units, 'ms')
205 lat_50_ms = io_sum.lat.perc_50 * unit_conversion_coef(io_sum.lat.units, 'ms')
206
207 iops_avg = io_sum.bw.average * unit_conversion_coef(io_sum.bw.units, StoragePerfSummary.iops_units)
208 iops_avg /= fjob.bsize
209
210 if fjob.oper == 'randwrite' and fjob.sync_mode == 'd':
211 for lat, field in [(10, 'rw_iops_10ms'), (30, 'rw_iops_30ms'), (100, 'rw_iops_100ms')]:
212 if lat_95_ms <= lat:
213 setattr(psum95, field, max(getattr(psum95, field), iops_avg))
214 if lat_50_ms <= lat:
215 setattr(psum50, field, max(getattr(psum50, field), iops_avg))
216
217 if fjob.oper == 'randread' and fjob.sync_mode == 'd':
218 for lat, field in [(10, 'rr_iops_10ms'), (30, 'rr_iops_30ms'), (100, 'rr_iops_100ms')]:
219 if lat_95_ms <= lat:
220 setattr(psum95, field, max(getattr(psum95, field), iops_avg))
221 if lat_50_ms <= lat:
222 setattr(psum50, field, max(getattr(psum50, field), iops_avg))
223 elif fjob.sync_mode == 'd':
224 if fjob.oper in ('randwrite', 'write'):
225 psum50.bw_write_max = max(psum50.bw_write_max, bw_avg)
226 elif fjob.oper in ('randread', 'read'):
227 psum50.bw_read_max = max(psum50.bw_read_max, bw_avg)
228
229 return psum50, psum95
230
231
232# Main performance report
233class PerformanceSummary(SuiteReporter):
234 """Aggregated summary for storage"""
235 def get_divs(self, suite: SuiteConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
236 psum50, psum95 = get_performance_summary(self.rstorage, suite, self.style.hist_boxes, self.style.large_blocks)
237
238 caption = "Storage summary report"
239 res = html.H3(html.center(caption))
240
241 headers = ["Mode", "Stats", "Explanation"]
242 align = ['left', 'right', "left"]
kdanylov aka koder13e58452018-07-15 02:51:51 +0300243 data: List[Union[str, Tuple[str, str, str]]] = []
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300244
245 if psum95.rr_iops_10ms != psum95.NO_VAL or psum95.rr_iops_30ms != psum95.NO_VAL or \
246 psum95.rr_iops_100ms != psum95.NO_VAL:
247 data.append("Average random read IOPS for small blocks")
248
249 if psum95.rr_iops_10ms != psum95.NO_VAL:
250 data.append(("Database", b2ssize_10(psum95.rr_iops_10ms), "Latency 95th percentile < 10ms"))
251 if psum95.rr_iops_30ms != psum95.NO_VAL:
252 data.append(("File system", b2ssize_10(psum95.rr_iops_30ms), "Latency 95th percentile < 30ms"))
253 if psum95.rr_iops_100ms != psum95.NO_VAL:
254 data.append(("File server", b2ssize_10(psum95.rr_iops_100ms), "Latency 95th percentile < 100ms"))
255
256 if psum95.rw_iops_10ms != psum95.NO_VAL or psum95.rw_iops_30ms != psum95.NO_VAL or \
257 psum95.rw_iops_100ms != psum95.NO_VAL:
258 data.append("Average random write IOPS for small blocks")
259
260 if psum95.rw_iops_10ms != psum95.NO_VAL:
261 data.append(("Database", b2ssize_10(psum95.rw_iops_10ms), "Latency 95th percentile < 10ms"))
262 if psum95.rw_iops_30ms != psum95.NO_VAL:
263 data.append(("File system", b2ssize_10(psum95.rw_iops_30ms), "Latency 95th percentile < 30ms"))
264 if psum95.rw_iops_100ms != psum95.NO_VAL:
265 data.append(("File server", b2ssize_10(psum95.rw_iops_100ms), "Latency 95th percentile < 100ms"))
266
267 if psum50.bw_write_max != psum50.NO_VAL or psum50.bw_read_max != psum50.NO_VAL:
268 data.append("Average sequention IO")
269
270 if psum50.bw_write_max != psum95.NO_VAL:
271 data.append(("Write", b2ssize(psum50.bw_write_max) + psum50.bw_units,
272 "Large blocks (>={}KiB)".format(self.style.large_blocks)))
273 if psum50.bw_read_max != psum95.NO_VAL:
274 data.append(("Read", b2ssize(psum50.bw_read_max) + psum50.bw_units,
275 "Large blocks (>={}KiB)".format(self.style.large_blocks)))
276
kdanylov aka koder13e58452018-07-15 02:51:51 +0300277 if data:
278 res += html.center(html.table("Performance", headers, data, align=align))
279 yield Menu1st.summary, Menu2ndSumm.summary, HTMLBlock(res)
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300280
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300281
282# # Node load over test time
283# class NodeLoad(SuiteReporter):
284# """IOPS/latency during test"""
285
286# # Ceph operation breakout report
287# class CephClusterSummary(SuiteReporter):
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200288
289
290# Main performance report
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300291class IOQD(SuiteReporter):
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200292 """Creates graph, which show how IOPS and Latency depend on QD"""
koder aka kdanilova732a602017-02-01 20:29:56 +0200293 suite_types = {'fio'}
294
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300295 def get_divs(self, suite: SuiteConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
kdanylov aka koder13e58452018-07-15 02:51:51 +0300296 ts_map: Dict[FioJobParams, List[Tuple[SuiteConfig, FioJobConfig]]] = defaultdict(list)
297 str_summary: Dict[FioJobParams, Tuple[str, str]] = {}
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300298
299 for job in self.rstorage.iter_job(suite):
koder aka kdanilov108ac362017-01-19 20:17:16 +0200300 fjob = cast(FioJobConfig, job)
koder aka kdanilova732a602017-02-01 20:29:56 +0200301 fjob_no_qd = cast(FioJobParams, fjob.params.copy(qd=None))
302 str_summary[fjob_no_qd] = (fjob_no_qd.summary, fjob_no_qd.long_summary)
303 ts_map[fjob_no_qd].append((suite, fjob))
koder aka kdanilov108ac362017-01-19 20:17:16 +0200304
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300305 caption = "IOPS, bandwith, and latency as function of parallel IO request count (QD)"
306 yield Menu1st.summary, Menu2ndSumm.io_lat_qd, HTMLBlock(html.H3(html.center(caption)))
307
koder aka kdanilova732a602017-02-01 20:29:56 +0200308 for tpl, suites_jobs in ts_map.items():
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300309 if len(suites_jobs) >= self.style.min_iops_vs_qd_jobs:
310 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 +0200311 iosums.sort(key=lambda x: x.qd)
312 summary, summary_long = str_summary[tpl]
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300313
koder aka kdanilova732a602017-02-01 20:29:56 +0200314 ds = DataSource(suite_id=suite.storage_id,
315 job_id=summary,
316 node_id=AGG_TAG,
317 sensor="fio",
318 dev=AGG_TAG,
319 metric="io_over_qd",
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300320 tag=io_chart_format)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200321
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300322 fpath = self.plt(io_chart, ds, title=summary_long, legend="IOPS/BW", iosums=iosums)
323 yield Menu1st.summary, Menu2ndSumm.io_lat_qd, HTMLBlock(html.img(fpath))
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200324
325
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300326class ResourceQD(SuiteReporter):
327 suite_types = {'fio'}
328
329 def get_divs(self, suite: SuiteConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
kdanylov aka koder13e58452018-07-15 02:51:51 +0300330 qd_grouped_jobs: Dict[FioJobParams, List[FioJobConfig]] = {}
kdanylov aka koderb0833332017-05-13 20:39:17 +0300331 test_nc = len(list(find_nodes_by_roles(self.rstorage.storage, ['testnode'])))
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300332 for job in self.rstorage.iter_job(suite):
333 fjob = cast(FioJobConfig, job)
334 if fjob.bsize != 4:
335 continue
336
337 fjob_no_qd = cast(FioJobParams, fjob.params.copy(qd=None))
338 qd_grouped_jobs.setdefault(fjob_no_qd, []).append(fjob)
339
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300340 yield Menu1st.summary, Menu2ndSumm.resources_usage_qd, HTMLBlock(html.center(html.H3("Resource usage summary")))
341
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300342 for jc_no_qd, jobs in sorted(qd_grouped_jobs.items()):
343 cpu_usage2qd = {}
344 for job in jobs:
345 usage, iops_ok = get_resources_usage(suite, job, self.rstorage, hist_boxes=self.style.hist_boxes,
346 large_block=self.style.large_blocks)
347
348 if iops_ok:
349 cpu_usage2qd[job.qd] = usage[ResourceNames.storage_cpu_s]
350
351 if len(cpu_usage2qd) < StyleProfile.min_iops_vs_qd_jobs:
352 continue
353
kdanylov aka koder13e58452018-07-15 02:51:51 +0300354 labels, vals, errs = zip(*((l, avg, dev)
355 for l, (_, avg, dev) in sorted(cpu_usage2qd.items()))) # type: ignore
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300356
357 if test_nc == 1:
358 labels = list(map(str, labels))
359 else:
360 labels = ["{} * {}".format(label, test_nc) for label in labels]
361
362 ds = DataSource(suite_id=suite.storage_id,
363 job_id=jc_no_qd.summary,
364 node_id="cluster",
365 sensor=AGG_TAG,
366 dev='cpu',
367 metric="cpu_for_iop",
368 tag=io_chart_format)
369
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300370 title = "CPU time per IOP, " + jc_no_qd.long_summary
371 fpath = self.plt(plot_simple_bars, ds, title, labels, vals, errs,
372 xlabel="CPU core time per IOP",
373 ylabel="QD * Test nodes" if test_nc != 1 else "QD",
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300374 x_formatter=(lambda x, pos: b2ssize_10(x) + 's'),
375 one_point_zero_line=False)
376
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300377 yield Menu1st.summary, Menu2ndSumm.resources_usage_qd, HTMLBlock(html.img(fpath))
378
379
380def get_resources_usage2(suite: SuiteConfig, job: JobConfig, rstorage: IWallyStorage,
381 roles, sensor, metric, test_metric, agg_window: int = 5) -> ndarray2d:
382 assert test_metric == 'iops'
383 fjob = cast(FioJobConfig, job)
384 bw = get_aggregated(rstorage, suite.storage_id, job.storage_id, "bw", job.reliable_info_range_s)
385 io_transfered = bw.data * unit_conversion_coef_f(bw.units, "Bps")
386 ops_done = io_transfered / (fjob.bsize * unit_conversion_coef_f("KiBps", "Bps"))
387 nodes = [node for node in rstorage.load_nodes() if node.roles.intersection(STORAGE_ROLES)]
388
389 if sensor == 'system-cpu':
390 assert metric == 'used'
391 core_count = None
392 for node in nodes:
393 if core_count is None:
394 core_count = sum(cores for _, cores in node.hw_info.cpus)
395 else:
396 assert core_count == sum(cores for _, cores in node.hw_info.cpus)
397 cpu_ts = get_cluster_cpu_load(rstorage, roles, job.reliable_info_range_s)
398 metric_data = (1.0 - (cpu_ts['idle'].data + cpu_ts['iowait'].data) / cpu_ts['total'].data) * core_count
399 else:
400 metric_data = sum_sensors(rstorage, job.reliable_info_range_s,
401 node_id=[node.node_id for node in nodes], sensor=sensor, metric=metric)
402
403 res = []
404 for pos in range(0, len(ops_done) - agg_window, agg_window):
405 pe = pos + agg_window
406 res.append((numpy.average(ops_done[pos: pe]), numpy.average(metric_data.data[pos: pe])))
407
408 return res
409
410
411class ResourceConsumptionSummary(SuiteReporter):
412 suite_types = {'fio'}
413
414 def get_divs(self, suite: SuiteConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
415 vs = 'iops'
416 for job_tp in ('rwd4', 'rrd4'):
417 for sensor_metric in ('net-io.send_packets', 'system-cpu.used'):
418 sensor, metric = sensor_metric.split(".")
419 usage = []
420 for job in self.rstorage.iter_job(suite):
421 if job_tp in job.summary:
422 usage.extend(get_resources_usage2(suite, job, self.rstorage, STORAGE_ROLES,
423 sensor=sensor, metric=metric, test_metric=vs))
424
425 if not usage:
426 continue
427
428 iops, cpu = zip(*usage)
429 slope, intercept, r_value, p_value, std_err = scipy.stats.linregress(iops, cpu)
430 x = numpy.array([0.0, max(iops) * 1.1])
431
432 ds = DataSource(suite_id=suite.storage_id,
433 job_id=job_tp,
434 node_id="storage",
435 sensor='usage-regression',
436 dev=AGG_TAG,
437 metric=sensor_metric + '.VS.' + vs,
438 tag=default_format)
439
440 fname = self.plt(plot_dots_with_regression, ds,
441 "{}::{}.{}".format(job_tp, sensor_metric, vs),
442 x=iops, y=cpu,
443 xlabel=vs,
444 ylabel=sensor_metric,
445 x_approx=x, y_approx=intercept + slope * x)
446
447 yield Menu1st.engineering, Menu2ndEng.resource_regression, HTMLBlock(html.img(fname))
448
449
450class EngineeringSummary(SuiteReporter):
451 suite_types = {'fio'}
452
453 def get_divs(self, suite: SuiteConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
454 tbl = [line for line in get_console_report_table(suite, self.rstorage) if line is not Texttable.HLINE]
455 align = [{'l': 'left', 'r': 'right'}[al] for al in console_report_align]
456 res = html.center(html.table("Test results", console_report_headers, tbl, align=align))
457 yield Menu1st.engineering, Menu2ndEng.summary, HTMLBlock(res)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200458
459
koder aka kdanilova732a602017-02-01 20:29:56 +0200460class StatInfo(JobReporter):
461 """Statistic info for job results"""
462 suite_types = {'fio'}
463
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300464 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
koder aka kdanilova732a602017-02-01 20:29:56 +0200465
466 fjob = cast(FioJobConfig, job)
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300467 io_sum = make_iosum(self.rstorage, suite, fjob, self.style.hist_boxes)
koder aka kdanilova732a602017-02-01 20:29:56 +0200468
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300469 caption = "Test summary - " + job.params.long_summary
kdanylov aka koderb0833332017-05-13 20:39:17 +0300470 test_nc = len(list(find_nodes_by_roles(self.rstorage.storage, ['testnode'])))
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300471 if test_nc > 1:
472 caption += " * {} nodes".format(test_nc)
473
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300474 res = html.H3(html.center(caption))
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300475 stat_data_headers = ["Name",
476 "Total done",
477 "Average ~ Dev",
478 "Conf interval",
479 "Mediana",
480 "Mode",
481 "Kurt / Skew",
482 "95%",
483 "99%",
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300484 "ADF test"]
koder aka kdanilova732a602017-02-01 20:29:56 +0200485
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300486 align = ['left'] + ['right'] * (len(stat_data_headers) - 1)
487
488 bw_units = "B"
489 bw_target_units = bw_units + 'ps'
kdanylov aka koderb0833332017-05-13 20:39:17 +0300490 bw_coef = unit_conversion_coef_f(io_sum.bw.units, bw_target_units)
kdanylov aka koder45183182017-04-30 23:55:40 +0300491
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300492 adf_v, *_1, stats, _2 = adfuller(io_sum.bw.data)
493
494 for v in ("1%", "5%", "10%"):
495 if adf_v <= stats[v]:
496 ad_test = v
497 break
498 else:
499 ad_test = "Failed"
500
koder aka kdanilova732a602017-02-01 20:29:56 +0200501 bw_data = ["Bandwidth",
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300502 b2ssize(io_sum.bw.data.sum() * bw_coef) + bw_units,
kdanylov aka koder45183182017-04-30 23:55:40 +0300503 "{}{} ~ {}{}".format(b2ssize(io_sum.bw.average * bw_coef), bw_target_units,
504 b2ssize(io_sum.bw.deviation * bw_coef), bw_target_units),
505 b2ssize(io_sum.bw.confidence * bw_coef) + bw_target_units,
506 b2ssize(io_sum.bw.perc_50 * bw_coef) + bw_target_units,
koder aka kdanilova732a602017-02-01 20:29:56 +0200507 "-",
508 "{:.2f} / {:.2f}".format(io_sum.bw.kurt, io_sum.bw.skew),
kdanylov aka koder45183182017-04-30 23:55:40 +0300509 b2ssize(io_sum.bw.perc_5 * bw_coef) + bw_target_units,
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300510 b2ssize(io_sum.bw.perc_1 * bw_coef) + bw_target_units,
511 ad_test]
koder aka kdanilova732a602017-02-01 20:29:56 +0200512
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300513 stat_data = [bw_data]
koder aka kdanilova732a602017-02-01 20:29:56 +0200514
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300515 if fjob.bsize < StyleProfile.large_blocks:
kdanylov aka koderb0833332017-05-13 20:39:17 +0300516 iops_coef = unit_conversion_coef_f(io_sum.bw.units, 'KiBps') / fjob.bsize
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300517 iops_data = ["IOPS",
518 b2ssize_10(io_sum.bw.data.sum() * iops_coef),
519 "{}IOPS ~ {}IOPS".format(b2ssize_10(io_sum.bw.average * iops_coef),
520 b2ssize_10(io_sum.bw.deviation * iops_coef)),
521 b2ssize_10(io_sum.bw.confidence * iops_coef) + "IOPS",
522 b2ssize_10(io_sum.bw.perc_50 * iops_coef) + "IOPS",
523 "-",
524 "{:.2f} / {:.2f}".format(io_sum.bw.kurt, io_sum.bw.skew),
525 b2ssize_10(io_sum.bw.perc_5 * iops_coef) + "IOPS",
526 b2ssize_10(io_sum.bw.perc_1 * iops_coef) + "IOPS",
527 ad_test]
koder aka kdanilova732a602017-02-01 20:29:56 +0200528
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300529 lat_target_unit = 's'
kdanylov aka koderb0833332017-05-13 20:39:17 +0300530 lat_coef = unit_conversion_coef_f(io_sum.lat.units, lat_target_unit)
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300531 # latency
532 lat_data = ["Latency",
533 "-",
534 "-",
535 "-",
536 b2ssize_10(io_sum.lat.perc_50 * lat_coef) + lat_target_unit,
537 "-",
538 "-",
539 b2ssize_10(io_sum.lat.perc_95 * lat_coef) + lat_target_unit,
540 b2ssize_10(io_sum.lat.perc_99 * lat_coef) + lat_target_unit,
541 '-']
542
543 # sensor usage
544 stat_data.extend([iops_data, lat_data])
545
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300546 res += html.center(html.table("Test results", stat_data_headers, stat_data, align=align))
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300547 yield Menu1st.per_job, job.summary, HTMLBlock(res)
koder aka kdanilova732a602017-02-01 20:29:56 +0200548
koder aka kdanilova732a602017-02-01 20:29:56 +0200549
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300550class Resources(JobReporter):
551 """Statistic info for job results"""
552 suite_types = {'fio'}
553
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300554 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300555
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300556 records, iops_ok = get_resources_usage(suite, job, self.rstorage,
557 large_block=self.style.large_blocks,
558 hist_boxes=self.style.hist_boxes)
koder aka kdanilova732a602017-02-01 20:29:56 +0200559
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300560 table_structure = [
561 "Service provided",
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300562 (ResourceNames.io_made, ResourceNames.data_tr),
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300563 "Test nodes total load",
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300564 (ResourceNames.test_send_pkt, ResourceNames.test_send),
565 (ResourceNames.test_recv_pkt, ResourceNames.test_recv),
566 (ResourceNames.test_net_pkt, ResourceNames.test_net),
567 (ResourceNames.test_write_iop, ResourceNames.test_write),
568 (ResourceNames.test_read_iop, ResourceNames.test_read),
569 (ResourceNames.test_iop, ResourceNames.test_rw),
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300570 "Storage nodes resource consumed",
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300571 (ResourceNames.storage_send_pkt, ResourceNames.storage_send),
572 (ResourceNames.storage_recv_pkt, ResourceNames.storage_recv),
573 (ResourceNames.storage_net_pkt, ResourceNames.storage_net),
574 (ResourceNames.storage_write_iop, ResourceNames.storage_write),
575 (ResourceNames.storage_read_iop, ResourceNames.storage_read),
576 (ResourceNames.storage_iop, ResourceNames.storage_rw),
577 (ResourceNames.storage_cpu_s, ResourceNames.storage_cpu_s_b),
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300578 ] # type: List[Union[str, Tuple[Optional[str], ...]]]
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300579
580 if not iops_ok:
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300581 table_structure2 = [] # type: List[Union[Tuple[str, ...], str]]
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300582 for line in table_structure:
583 if isinstance(line, str):
584 table_structure2.append(line)
585 else:
586 assert len(line) == 2
587 table_structure2.append((line[1],))
588 table_structure = table_structure2
koder aka kdanilova732a602017-02-01 20:29:56 +0200589
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300590 yield Menu1st.per_job, job.summary, HTMLBlock(html.H3(html.center("Resources usage")))
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300591
592 doc = xmlbuilder3.XMLBuilder("table",
593 **{"class": "table table-bordered table-striped table-condensed table-hover",
594 "style": "width: auto;"})
595
596 with doc.thead:
597 with doc.tr:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300598 [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 +0300599
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300600 cols = 6 if iops_ok else 3
601 col_per_tp = 3
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300602
603 short_name = {
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300604 name: (name if name in {ResourceNames.io_made, ResourceNames.data_tr}
605 else " ".join(name.split()[2:]).capitalize())
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300606 for name in records.keys()
607 }
608
kdanylov aka koderb0833332017-05-13 20:39:17 +0300609 short_name[ResourceNames.storage_cpu_s] = "CPU core (s/IOP)"
610 short_name[ResourceNames.storage_cpu_s_b] = "CPU core (s/B)"
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300611
612 with doc.tbody:
613 with doc.tr:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300614 if iops_ok:
615 doc.td(colspan=str(col_per_tp)).center.b("Operations")
616 doc.td(colspan=str(col_per_tp)).center.b("Bytes")
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300617
618 for line in table_structure:
619 with doc.tr:
620 if isinstance(line, str):
621 with doc.td(colspan=str(cols)):
622 doc.center.b(line)
623 else:
624 for name in line:
625 if name is None:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300626 doc.td("-", colspan=str(col_per_tp))
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300627 continue
628
629 amount_s, avg, dev = records[name]
630
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300631 if name in (ResourceNames.storage_cpu_s, ResourceNames.storage_cpu_s_b) and avg is not None:
632 if dev is None:
633 rel_val_s = b2ssize_10(avg) + 's'
634 else:
635 dev_s = str(int(dev * 100 / avg)) + "%" if avg > 1E-9 else b2ssize_10(dev) + 's'
636 rel_val_s = "{}s ~ {}".format(b2ssize_10(avg), dev_s)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300637 else:
638 if avg is None:
639 rel_val_s = '-'
640 else:
641 avg_s = int(avg) if avg > 10 else '{:.1f}'.format(avg)
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300642 if dev is None:
643 rel_val_s = avg_s
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300644 else:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300645 if avg > 1E-5:
646 dev_s = str(int(dev * 100 / avg)) + "%"
647 else:
648 dev_s = int(dev) if dev > 10 else '{:.1f}'.format(dev)
649 rel_val_s = "{} ~ {}".format(avg_s, dev_s)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300650
651 doc.td(short_name[name], align="left")
652 doc.td(amount_s, align="right")
653
654 if avg is None or avg < 0.9:
655 doc.td(rel_val_s, align="right")
656 elif avg < 2.0:
657 doc.td(align="right").font(rel_val_s, color='green')
658 elif avg < 5.0:
659 doc.td(align="right").font(rel_val_s, color='orange')
660 else:
661 doc.td(align="right").font(rel_val_s, color='red')
662
663 res = xmlbuilder3.tostr(doc).split("\n", 1)[1]
664 yield Menu1st.per_job, job.summary, HTMLBlock(html.center(res))
665
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300666 iop_names = [ResourceNames.test_write_iop, ResourceNames.test_read_iop, ResourceNames.test_iop,
667 ResourceNames.storage_write_iop, ResourceNames.storage_read_iop, ResourceNames.storage_iop]
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300668
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300669 bytes_names = [ResourceNames.test_write, ResourceNames.test_read, ResourceNames.test_rw,
670 ResourceNames.test_send, ResourceNames.test_recv, ResourceNames.test_net,
671 ResourceNames.storage_write, ResourceNames.storage_read, ResourceNames.storage_rw,
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300672 ResourceNames.storage_send, ResourceNames.storage_recv,
673 ResourceNames.storage_net] # type: List[str]
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300674
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300675 net_pkt_names = [ResourceNames.test_send_pkt, ResourceNames.test_recv_pkt, ResourceNames.test_net_pkt,
676 ResourceNames.storage_send_pkt, ResourceNames.storage_recv_pkt, ResourceNames.storage_net_pkt]
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300677
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300678 pairs = [("bytes", bytes_names)]
679 if iops_ok:
680 pairs.insert(0, ('iop', iop_names))
681 pairs.append(('Net packets per IOP', net_pkt_names))
682
683 yield Menu1st.per_job, job.summary, \
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300684 HTMLBlock(html.H3(html.center("Resource consumption per service provided")))
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300685
686 for tp, names in pairs:
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300687 vals = [] # type: List[float]
688 devs = [] # type: List[float]
689 avail_names = [] # type: List[str]
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300690 for name in names:
691 if name in records:
692 avail_names.append(name)
693 _, avg, dev = records[name]
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300694
695 if dev is None:
696 dev = 0
697
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300698 vals.append(avg)
699 devs.append(dev)
700
701 # synchronously sort values and names, values is a key
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300702 vals, names, devs = map(list, zip(*sorted(zip(vals, names, devs)))) # type: ignore
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300703
704 ds = DataSource(suite_id=suite.storage_id,
705 job_id=job.storage_id,
706 node_id=AGG_TAG,
707 sensor='resources',
708 dev=AGG_TAG,
709 metric=tp.replace(' ', "_") + '2service_bar',
710 tag=default_format)
711
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300712 fname = self.plt(plot_simple_bars, ds, tp.capitalize(),
713 [name.replace(" nodes", "") for name in names],
714 vals, devs)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300715
716 yield Menu1st.per_job, job.summary, HTMLBlock(html.img(fname))
717
718
719class BottleNeck(JobReporter):
720 """Statistic info for job results"""
721 suite_types = {'fio'}
722
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300723 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300724
kdanylov aka koderb0833332017-05-13 20:39:17 +0300725 nodes = list(find_nodes_by_roles(self.rstorage.storage, STORAGE_ROLES))
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300726
727 sensor = 'block-io'
728 metric = 'io_queue'
729 bn_val = 16
730
kdanylov aka koderb0833332017-05-13 20:39:17 +0300731 for node_id in nodes:
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300732 bn = 0
733 tot = 0
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300734 for ds in self.rstorage.iter_sensors(node_id=node_id, sensor=sensor, metric=metric):
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300735 if ds.dev in ('sdb', 'sdc', 'sdd', 'sde'):
kdanylov aka koderb0833332017-05-13 20:39:17 +0300736 ts = self.rstorage.get_sensor(ds, job.reliable_info_range_s)
737 bn += (ts.data > bn_val).sum()
738 tot += len(ts.data)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300739
740 yield Menu1st.per_job, job.summary, HTMLBlock("")
koder aka kdanilova732a602017-02-01 20:29:56 +0200741
742
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300743# CPU load
744class CPULoadPlot(JobReporter):
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300745 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300746
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300747 # plot CPU time
748 for rt, roles in [('storage', STORAGE_ROLES), ('test', ['testnode'])]:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300749 cpu_ts = get_cluster_cpu_load(self.rstorage, roles, job.reliable_info_range_s)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300750 tss = [(name, ts.data * 100 / cpu_ts['total'].data)
751 for name, ts in cpu_ts.items()
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300752 if name in {'user', 'sys', 'idle', 'iowait'}]
753
754
755 ds = cpu_ts['idle'].source(job_id=job.storage_id, suite_id=suite.storage_id,
756 node_id=AGG_TAG, metric='allcpu', tag=rt + '.plt.' + default_format)
757
758 fname = self.plt(plot_simple_over_time, ds, tss=tss, average=True, ylabel="CPU time %",
759 title="{} nodes CPU usage".format(rt.capitalize()),
760 xlabel="Time from test begin")
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300761
762 yield Menu1st.per_job, job.summary, HTMLBlock(html.img(fname))
763
764
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300765def roles_for_sensors(storage: IWallyStorage) -> Dict[str, List[DataSource]]:
766 role2ds = defaultdict(list)
767
768 for node in storage.load_nodes():
769 ds = DataSource(node_id=node.node_id)
770 if 'ceph-osd' in node.roles:
771 for jdev in node.params.get('ceph_journal_devs', []):
kdanylov aka koder13e58452018-07-15 02:51:51 +0300772 role2ds[DevRoles.osd_journal].append(ds(dev=jdev))
773 role2ds[DevRoles.storage_block].append(ds(dev=jdev))
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300774
775 for sdev in node.params.get('ceph_storage_devs', []):
kdanylov aka koder13e58452018-07-15 02:51:51 +0300776 role2ds[DevRoles.osd_storage].append(ds(dev=sdev))
777 role2ds[DevRoles.storage_block].append(ds(dev=sdev))
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300778
779 if node.hw_info:
780 for dev in node.hw_info.disks_info:
kdanylov aka koder13e58452018-07-15 02:51:51 +0300781 role2ds[DevRoles.storage_block].append(ds(dev=dev))
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300782
783 if 'testnode' in node.roles:
kdanylov aka koder13e58452018-07-15 02:51:51 +0300784 role2ds[DevRoles.client_block].append(ds(dev='rbd0'))
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300785
786 return role2ds
787
788
789def get_sources_for_roles(roles: Iterable[str]) -> List[DataSource]:
790 return []
791
792
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300793# IO time and QD
794class QDIOTimeHeatmap(JobReporter):
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300795 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300796 trange = (job.reliable_info_range[0] // 1000, job.reliable_info_range[1] // 1000)
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300797 test_nc = len(list(find_nodes_by_roles(self.rstorage.storage, ['testnode'])))
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300798
kdanylov aka koder13e58452018-07-15 02:51:51 +0300799 for dev_role in (DevRoles.osd_storage, DevRoles.osd_journal, DevRoles.client_block):
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300800
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300801 caption = "{} IO heatmaps - {}".format(dev_role.capitalize(), cast(FioJobParams, job).params.long_summary)
802 if test_nc != 1:
803 caption += " * {} nodes".format(test_nc)
804
805 yield Menu1st.engineering_per_job, job.summary, HTMLBlock(html.H3(html.center(caption)))
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300806
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300807 # QD heatmap
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300808 ioq2d = find_sensors_to_2d(self.rstorage, trange, dev_role=dev_role, sensor='block-io', metric='io_queue')
809
810 ds = DataSource(suite.storage_id, job.storage_id, AGG_TAG, 'block-io', dev_role,
811 tag="hmap." + default_format)
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300812
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300813 fname = self.plt(plot_hmap_from_2d, ds(metric='io_queue'), data2d=ioq2d, xlabel='Time', ylabel="IO QD",
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300814 title=dev_role.capitalize() + " devs QD", bins=StyleProfile.qd_bins)
815 yield Menu1st.engineering_per_job, job.summary, HTMLBlock(html.img(fname))
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300816
817 # Block size heatmap
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300818 wc2d = find_sensors_to_2d(self.rstorage, trange, dev_role=dev_role, sensor='block-io',
kdanylov aka koderb0833332017-05-13 20:39:17 +0300819 metric='writes_completed')
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300820 wc2d[wc2d < 1E-3] = 1
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300821 sw2d = find_sensors_to_2d(self.rstorage, trange, dev_role=dev_role, sensor='block-io',
kdanylov aka koderb0833332017-05-13 20:39:17 +0300822 metric='sectors_written')
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300823 data2d = sw2d / wc2d / 1024
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300824 fname = self.plt(plot_hmap_from_2d, ds(metric='wr_block_size'),
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300825 data2d=data2d, title=dev_role.capitalize() + " write block size",
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300826 ylabel="IO bsize, KiB", xlabel='Time', bins=StyleProfile.block_size_bins)
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300827 yield Menu1st.engineering_per_job, job.summary, HTMLBlock(html.img(fname))
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300828
829 # iotime heatmap
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300830 wtime2d = find_sensors_to_2d(self.rstorage, trange, dev_role=dev_role, sensor='block-io',
kdanylov aka koderb0833332017-05-13 20:39:17 +0300831 metric='io_time')
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300832 fname = self.plt(plot_hmap_from_2d, ds(metric='io_time'), data2d=wtime2d,
833 xlabel='Time', ylabel="IO time (ms) per second",
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300834 title=dev_role.capitalize() + " iotime", bins=StyleProfile.iotime_bins)
835 yield Menu1st.engineering_per_job, job.summary, HTMLBlock(html.img(fname))
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300836
837
koder aka kdanilov108ac362017-01-19 20:17:16 +0200838# IOPS/latency over test time for each job
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300839class LoadToolResults(JobReporter):
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200840 """IOPS/latency during test"""
koder aka kdanilova732a602017-02-01 20:29:56 +0200841 suite_types = {'fio'}
koder aka kdanilov108ac362017-01-19 20:17:16 +0200842
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300843 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
koder aka kdanilova732a602017-02-01 20:29:56 +0200844 fjob = cast(FioJobConfig, job)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200845
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300846 # caption = "Load tool results, " + job.params.long_summary
847 caption = "Load tool results"
848 yield Menu1st.per_job, job.summary, HTMLBlock(html.H3(html.center(caption)))
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300849
kdanylov aka koderb0833332017-05-13 20:39:17 +0300850 agg_io = get_aggregated(self.rstorage, suite.storage_id, fjob.storage_id, "bw", job.reliable_info_range_s)
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300851
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300852 if fjob.bsize >= DefStyleProfile.large_blocks:
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300853 title = "Fio measured bandwidth over time"
koder aka kdanilova732a602017-02-01 20:29:56 +0200854 units = "MiBps"
kdanylov aka koderb0833332017-05-13 20:39:17 +0300855 agg_io.data //= int(unit_conversion_coef_f(units, agg_io.units))
koder aka kdanilova732a602017-02-01 20:29:56 +0200856 else:
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300857 title = "Fio measured IOPS over time"
kdanylov aka koderb0833332017-05-13 20:39:17 +0300858 agg_io.data //= (int(unit_conversion_coef_f("KiBps", agg_io.units)) * fjob.bsize)
koder aka kdanilova732a602017-02-01 20:29:56 +0200859 units = "IOPS"
koder aka kdanilov108ac362017-01-19 20:17:16 +0200860
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300861 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 +0200862 yield Menu1st.per_job, fjob.summary, HTMLBlock(html.img(fpath))
koder aka kdanilov108ac362017-01-19 20:17:16 +0200863
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300864 title = "BW distribution" if fjob.bsize >= DefStyleProfile.large_blocks else "IOPS distribution"
865 io_stat_prop = calc_norm_stat_props(agg_io, bins_count=StyleProfile.hist_boxes)
866 fpath = self.plt(plot_hist, agg_io.source(tag='hist.' + default_format), title, units, io_stat_prop)
867 yield Menu1st.per_job, fjob.summary, HTMLBlock(html.img(fpath))
868
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300869 if fjob.bsize < DefStyleProfile.large_blocks:
kdanylov aka koderb0833332017-05-13 20:39:17 +0300870 agg_lat = get_aggregated(self.rstorage, suite.storage_id, fjob.storage_id, "lat",
871 job.reliable_info_range_s)
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300872 TARGET_UNITS = 'ms'
kdanylov aka koderb0833332017-05-13 20:39:17 +0300873 coef = unit_conversion_coef_f(agg_lat.units, TARGET_UNITS)
874 agg_lat.histo_bins = agg_lat.histo_bins.copy() * coef
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300875 agg_lat.units = TARGET_UNITS
koder aka kdanilov108ac362017-01-19 20:17:16 +0200876
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300877 fpath = self.plt(plot_lat_over_time, agg_lat.source(tag='ts.' + default_format), "Latency", agg_lat,
878 ylabel="Latency, " + agg_lat.units)
879 yield Menu1st.per_job, fjob.summary, HTMLBlock(html.img(fpath))
koder aka kdanilov108ac362017-01-19 20:17:16 +0200880
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300881 fpath = self.plt(plot_histo_heatmap, agg_lat.source(tag='hmap.' + default_format),
882 "Latency heatmap", agg_lat, ylabel="Latency, " + agg_lat.units, xlabel='Test time')
koder aka kdanilov108ac362017-01-19 20:17:16 +0200883
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300884 yield Menu1st.per_job, fjob.summary, HTMLBlock(html.img(fpath))
koder aka kdanilov108ac362017-01-19 20:17:16 +0200885
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200886
887# Cluster load over test time
koder aka kdanilova732a602017-02-01 20:29:56 +0200888class ClusterLoad(JobReporter):
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200889 """IOPS/latency during test"""
890
koder aka kdanilova732a602017-02-01 20:29:56 +0200891 # TODO: units should came from sensor
koder aka kdanilov108ac362017-01-19 20:17:16 +0200892 storage_sensors = [
kdanylov aka koder45183182017-04-30 23:55:40 +0300893 ('block-io', 'reads_completed', "Read", 'iop'),
894 ('block-io', 'writes_completed', "Write", 'iop'),
kdanylov aka koder736e5c12017-05-07 17:27:14 +0300895 ('block-io', 'sectors_read', "Read", 'MiB'),
896 ('block-io', 'sectors_written', "Write", 'MiB'),
koder aka kdanilov108ac362017-01-19 20:17:16 +0200897 ]
898
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300899 def get_divs(self, suite: SuiteConfig, job: JobConfig) -> Iterator[Tuple[str, str, HTMLBlock]]:
900
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300901 yield Menu1st.per_job, job.summary, HTMLBlock(html.H3(html.center("Cluster load")))
koder aka kdanilov108ac362017-01-19 20:17:16 +0200902
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300903 sensors = []
904 max_iop = 0
905 max_bytes = 0
kdanylov aka koderb0833332017-05-13 20:39:17 +0300906 stor_nodes = find_nodes_by_roles(self.rstorage.storage, STORAGE_ROLES)
kdanylov aka koder45183182017-04-30 23:55:40 +0300907 for sensor, metric, op, units in self.storage_sensors:
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300908 ts = sum_sensors(self.rstorage, job.reliable_info_range_s, node_id=stor_nodes, sensor=sensor, metric=metric)
kdanylov aka koderb0833332017-05-13 20:39:17 +0300909 if ts is not None:
910 ds = DataSource(suite_id=suite.storage_id,
911 job_id=job.storage_id,
912 node_id="storage",
913 sensor=sensor,
914 dev=AGG_TAG,
915 metric=metric,
916 tag="ts." + default_format)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200917
kdanylov aka koderb0833332017-05-13 20:39:17 +0300918 data = ts.data if units != 'MiB' else ts.data * unit_conversion_coef_f(ts.units, 'MiB')
919 ts = TimeSeries(times=numpy.arange(*job.reliable_info_range_s),
920 data=data,
921 units=units if ts.units is None else ts.units,
922 time_units=ts.time_units,
923 source=ds,
924 histo_bins=ts.histo_bins)
kdanylov aka koder0e0cfcb2017-03-27 22:19:09 +0300925
kdanylov aka koderb0833332017-05-13 20:39:17 +0300926 sensors.append(("{} {}".format(op, units), ds, ts, units))
koder aka kdanilov108ac362017-01-19 20:17:16 +0200927
kdanylov aka koderb0833332017-05-13 20:39:17 +0300928 if units == 'iop':
929 max_iop = max(max_iop, data.sum())
930 else:
931 assert units == 'MiB'
932 max_bytes = max(max_bytes, data.sum())
koder aka kdanilov108ac362017-01-19 20:17:16 +0200933
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300934 for title, ds, ts, units in sensors:
935 if ts.data.sum() >= (max_iop if units == 'iop' else max_bytes) * DefStyleProfile.min_load_diff:
936 fpath = self.plt(plot_v_over_time, ds, title, units, ts=ts)
937 yield Menu1st.per_job, job.summary, HTMLBlock(html.img(fpath))
938 else:
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300939 logger.info("Hide '%s' plot for %s, as it's load is less then %s%% from maximum",
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300940 title, job.summary, int(DefStyleProfile.min_load_diff * 100))
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200941
942
koder aka kdanilov108ac362017-01-19 20:17:16 +0200943# ------------------------------------------ REPORT STAGES -----------------------------------------------------------
944
945
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300946def add_devroles(ctx: TestRun):
947 # TODO: need to detect all devices for node on this stage using hw info
948 detected_selectors = collections.defaultdict(
949 lambda: collections.defaultdict(list)) # type: Dict[str, Dict[str, List[str]]]
950
951 for node in ctx.nodes:
952 if NodeRole.osd in node.info.roles:
953 all_devs = set()
954
955 jdevs = node.info.params.get('ceph_journal_devs')
956 if jdevs:
957 all_devs.update(jdevs)
958 detected_selectors[node.info.hostname]["|".join(jdevs)].append(DevRoles.osd_journal)
959
960 sdevs = node.info.params.get('ceph_storage_devs')
961 if sdevs:
962 all_devs.update(sdevs)
963 detected_selectors[node.info.hostname]["|".join(sdevs)].append(DevRoles.osd_storage)
964
965 if all_devs:
966 detected_selectors[node.info.hostname]["|".join(all_devs)].append(DevRoles.storage_block)
967
968 for hostname, dev_rules in detected_selectors.items():
969 dev_locs = [] # type: List[Dict[str, List[str]]]
970 ctx.devs_locator.append({hostname: dev_locs})
971 for dev_names, roles in dev_rules.items():
972 dev_locs.append({dev_names: roles})
973
974
koder aka kdanilov108ac362017-01-19 20:17:16 +0200975class HtmlReportStage(Stage):
976 priority = StepOrder.REPORT
977
978 def run(self, ctx: TestRun) -> None:
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300979 nodes = ctx.rstorage.load_nodes()
980 update_storage_selector(ctx.rstorage, ctx.devs_locator, nodes)
981
kdanylov aka koder470a8fa2017-07-14 21:07:58 +0300982 # role2ds = roles_for_sensors(ctx.rstorage)
983
984 job_reporters_cls = [StatInfo, LoadToolResults, Resources, ClusterLoad, CPULoadPlot] #, QDIOTimeHeatmap]
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300985 # job_reporters_cls = [QDIOTimeHeatmap]
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300986 job_reporters = [rcls(ctx.rstorage, DefStyleProfile, DefColorProfile)
987 for rcls in job_reporters_cls] # type: ignore
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +0300988
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300989 suite_reporters_cls = [IOQD,
990 ResourceQD,
991 PerformanceSummary,
992 EngineeringSummary,
993 ResourceConsumptionSummary] # type: List[Type[SuiteReporter]]
994 # suite_reporters_cls = [] # type: List[Type[SuiteReporter]]
kdanylov aka koder026e5f22017-05-15 01:04:39 +0300995 suite_reporters = [rcls(ctx.rstorage, DefStyleProfile, DefColorProfile)
996 for rcls in suite_reporters_cls] # type: ignore
koder aka kdanilov108ac362017-01-19 20:17:16 +0200997
998 root_dir = os.path.dirname(os.path.dirname(wally.__file__))
999 doc_templ_path = os.path.join(root_dir, "report_templates/index.html")
1000 report_template = open(doc_templ_path, "rt").read()
1001 css_file_src = os.path.join(root_dir, "report_templates/main.css")
1002 css_file = open(css_file_src, "rt").read()
1003
1004 menu_block = []
1005 content_block = []
1006 link_idx = 0
1007
koder aka kdanilova732a602017-02-01 20:29:56 +02001008 items = defaultdict(lambda: defaultdict(list)) # type: Dict[str, Dict[str, List[HTMLBlock]]]
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +03001009 DEBUG = False
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +03001010 job_summ_sort_order = []
1011
koder aka kdanilova732a602017-02-01 20:29:56 +02001012 # TODO: filter reporters
kdanylov aka koderb0833332017-05-13 20:39:17 +03001013 for suite in ctx.rstorage.iter_suite(FioTest.name):
1014 all_jobs = list(ctx.rstorage.iter_job(suite))
koder aka kdanilova732a602017-02-01 20:29:56 +02001015 all_jobs.sort(key=lambda job: job.params)
koder aka kdanilova732a602017-02-01 20:29:56 +02001016
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +03001017 new_jobs_in_order = [job.summary for job in all_jobs]
1018 same = set(new_jobs_in_order).intersection(set(job_summ_sort_order))
1019 assert not same, "Job with same names in different suits found: " + ",".join(same)
1020 job_summ_sort_order.extend(new_jobs_in_order)
1021
kdanylov aka koderb0833332017-05-13 20:39:17 +03001022 for job in all_jobs:
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +03001023 try:
kdanylov aka koder026e5f22017-05-15 01:04:39 +03001024 for reporter in job_reporters: # type: JobReporter
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +03001025 logger.debug("Start reporter %s on job %s suite %s",
1026 reporter.__class__.__name__, job.summary, suite.test_type)
1027 for block, item, html in reporter.get_divs(suite, job):
1028 items[block][item].append(html)
1029 if DEBUG:
1030 break
1031 except Exception:
1032 logger.exception("Failed to generate report for %s", job.summary)
1033
kdanylov aka koder026e5f22017-05-15 01:04:39 +03001034 for sreporter in suite_reporters: # type: SuiteReporter
kdanylov aka koder736e5c12017-05-07 17:27:14 +03001035 try:
kdanylov aka koder026e5f22017-05-15 01:04:39 +03001036 logger.debug("Start reporter %s on suite %s", sreporter.__class__.__name__, suite.test_type)
1037 for block, item, html in sreporter.get_divs(suite):
kdanylov aka koder736e5c12017-05-07 17:27:14 +03001038 items[block][item].append(html)
kdanylov aka koder026e5f22017-05-15 01:04:39 +03001039 except Exception:
kdanylov aka koder84de1e42017-05-22 14:00:07 +03001040 logger.exception("Failed to generate report for suite %s", suite.storage_id)
koder aka kdanilov108ac362017-01-19 20:17:16 +02001041
koder aka kdanilova732a602017-02-01 20:29:56 +02001042 if DEBUG:
1043 break
1044
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +03001045 logger.debug("Generating result html")
1046
koder aka kdanilov108ac362017-01-19 20:17:16 +02001047 for idx_1st, menu_1st in enumerate(sorted(items, key=lambda x: menu_1st_order.index(x))):
1048 menu_block.append(
1049 '<a href="#item{}" class="nav-group" data-toggle="collapse" data-parent="#MainMenu">{}</a>'
1050 .format(idx_1st, menu_1st)
1051 )
1052 menu_block.append('<div class="collapse" id="item{}">'.format(idx_1st))
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +03001053
kdanylov aka koder84de1e42017-05-22 14:00:07 +03001054 if menu_1st in (Menu1st.per_job, Menu1st.engineering_per_job):
1055 key = job_summ_sort_order.index
1056 elif menu_1st == Menu1st.engineering:
1057 key = Menu2ndEng.order.index
1058 elif menu_1st == Menu1st.summary:
1059 key = Menu2ndSumm.order.index
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +03001060 else:
kdanylov aka koder84de1e42017-05-22 14:00:07 +03001061 key = lambda x: x
1062
1063 in_order = sorted(items[menu_1st], key=key)
kdanylov aka koder3a9e5db2017-05-09 20:00:44 +03001064
1065 for menu_2nd in in_order:
koder aka kdanilov108ac362017-01-19 20:17:16 +02001066 menu_block.append(' <a href="#content{}" class="nav-group-item">{}</a>'
1067 .format(link_idx, menu_2nd))
1068 content_block.append('<div id="content{}">'.format(link_idx))
koder aka kdanilova732a602017-02-01 20:29:56 +02001069 content_block.extend(" " + x.data for x in items[menu_1st][menu_2nd])
koder aka kdanilov108ac362017-01-19 20:17:16 +02001070 content_block.append('</div>')
1071 link_idx += 1
1072 menu_block.append('</div>')
1073
1074 report = report_template.replace("{{{menu}}}", ("\n" + " " * 16).join(menu_block))
1075 report = report.replace("{{{content}}}", ("\n" + " " * 16).join(content_block))
kdanylov aka koderb0833332017-05-13 20:39:17 +03001076 report_path = ctx.rstorage.put_report(report, "index.html")
1077 ctx.rstorage.put_report(css_file, "main.css")
koder aka kdanilov108ac362017-01-19 20:17:16 +02001078 logger.info("Report is stored into %r", report_path)