Move shared code to testlib
Signed-off-by: Andrey Berezin <a.berezin@yadro.com>
This commit is contained in:
parent
d97a02d1d3
commit
997e768e92
69 changed files with 9213 additions and 64 deletions
265
src/frostfs_testlib/load/load_report.py
Normal file
265
src/frostfs_testlib/load/load_report.py
Normal file
|
@ -0,0 +1,265 @@
|
|||
from datetime import datetime
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import yaml
|
||||
|
||||
from frostfs_testlib.load.load_config import K6ProcessAllocationStrategy, LoadParams, LoadScenario
|
||||
from frostfs_testlib.load.load_metrics import get_metrics_object
|
||||
|
||||
|
||||
class LoadReport:
|
||||
def __init__(self, load_test) -> None:
|
||||
self.load_test = load_test
|
||||
self.load_summaries: Optional[dict] = None
|
||||
self.load_params: Optional[LoadParams] = None
|
||||
self.start_time: Optional[datetime] = None
|
||||
self.end_time: Optional[datetime] = None
|
||||
|
||||
def set_start_time(self):
|
||||
self.start_time = datetime.utcnow()
|
||||
|
||||
def set_end_time(self):
|
||||
self.end_time = datetime.utcnow()
|
||||
|
||||
def set_summaries(self, load_summaries: dict):
|
||||
self.load_summaries = load_summaries
|
||||
|
||||
def set_load_params(self, load_params: LoadParams):
|
||||
self.load_params = load_params
|
||||
|
||||
def get_report_html(self):
|
||||
report_sections = [
|
||||
[self.load_test, self._get_load_params_section_html],
|
||||
[self.load_summaries, self._get_totals_section_html],
|
||||
[self.end_time, self._get_test_time_html],
|
||||
]
|
||||
|
||||
html = ""
|
||||
for section in report_sections:
|
||||
if section[0] is not None:
|
||||
html += section[1]()
|
||||
|
||||
return html
|
||||
|
||||
def _get_load_params_section_html(self) -> str:
|
||||
params: str = yaml.safe_dump(self.load_test, sort_keys=False)
|
||||
params = params.replace("\n", "<br>")
|
||||
section_html = f"""<h3>Scenario params</h3>
|
||||
|
||||
<pre>{params}</pre>
|
||||
<hr>"""
|
||||
|
||||
return section_html
|
||||
|
||||
def _get_test_time_html(self) -> str:
|
||||
html = f"""<h3>Scenario duration in UTC time (from agent)</h3>
|
||||
{self.start_time} - {self.end_time}<br>
|
||||
<hr>
|
||||
"""
|
||||
|
||||
return html
|
||||
|
||||
def _calc_unit(self, value: float, skip_units: int = 0) -> Tuple[float, str]:
|
||||
units = ["B", "KB", "MB", "GB", "TB"]
|
||||
|
||||
for unit in units[skip_units:]:
|
||||
if value < 1024:
|
||||
return value, unit
|
||||
|
||||
value = value / 1024.0
|
||||
|
||||
return value, unit
|
||||
|
||||
def _seconds_to_formatted_duration(self, seconds: int) -> str:
|
||||
"""Converts N number of seconds to formatted output ignoring zeroes.
|
||||
Examples:
|
||||
186399 -> "2d3h46m39s"
|
||||
86399 -> "23h59m59s"
|
||||
86399 -> "23h59m59s"
|
||||
3605 -> "1h5s"
|
||||
123 -> "2m3s"
|
||||
"""
|
||||
units = {"d": 86400, "h": 3600, "m": 60, "s": 1}
|
||||
parts = []
|
||||
remaining = seconds
|
||||
for divisor in units.values():
|
||||
part = remaining // divisor
|
||||
remaining -= divisor * part
|
||||
parts.append(part)
|
||||
|
||||
return "".join([f"{val}{unit}" for unit, val in zip(units, parts) if val > 0])
|
||||
|
||||
def _row(self, caption: str, value: str) -> str:
|
||||
return f"<tr><th>{caption}</th><td>{value}</td></tr>"
|
||||
|
||||
def _get_model_string(self):
|
||||
if self.load_params.min_iteration_duration is not None:
|
||||
return f"min_iteration_duration={self.load_params.min_iteration_duration}"
|
||||
|
||||
model_map = {
|
||||
LoadScenario.gRPC: "closed model",
|
||||
LoadScenario.S3: "closed model",
|
||||
LoadScenario.HTTP: "closed model",
|
||||
LoadScenario.gRPC_CAR: "open model",
|
||||
LoadScenario.S3_CAR: "open model",
|
||||
}
|
||||
|
||||
return model_map[self.load_params.scenario]
|
||||
|
||||
def _get_oprations_sub_section_html(
|
||||
self,
|
||||
operation_type: str,
|
||||
total_operations: int,
|
||||
requested_rate_str: str,
|
||||
vus_str: str,
|
||||
total_rate: float,
|
||||
throughput: float,
|
||||
errors: dict[str, int],
|
||||
):
|
||||
throughput_html = ""
|
||||
if throughput > 0:
|
||||
throughput, unit = self._calc_unit(throughput)
|
||||
throughput_html = self._row("Throughput", f"{throughput:.2f} {unit}/sec")
|
||||
|
||||
per_node_errors_html = ""
|
||||
total_errors = 0
|
||||
if errors:
|
||||
total_errors: int = 0
|
||||
for node_key, errors in errors.items():
|
||||
total_errors += errors
|
||||
if (
|
||||
self.load_params.k6_process_allocation_strategy
|
||||
== K6ProcessAllocationStrategy.PER_ENDPOINT
|
||||
):
|
||||
per_node_errors_html += self._row(f"At {node_key}", errors)
|
||||
|
||||
object_size, object_size_unit = self._calc_unit(self.load_params.object_size, 1)
|
||||
duration = self._seconds_to_formatted_duration(self.load_params.load_time)
|
||||
model = self._get_model_string()
|
||||
# write 8KB 15h49m 50op/sec 50th open model/closed model/min_iteration duration=1s - 1.636MB/s 199.57451/s
|
||||
short_summary = f"{operation_type} {object_size}{object_size_unit} {duration} {requested_rate_str} {vus_str} {model} - {throughput:.2f}{unit} {total_rate:.2f}/s"
|
||||
|
||||
html = f"""
|
||||
<table border="1" cellpadding="5px"><tbody>
|
||||
<th colspan="2" bgcolor="gainsboro">{short_summary}</th>
|
||||
<th colspan="2" bgcolor="gainsboro">Metrics</th>
|
||||
{self._row("Total operations", total_operations)}
|
||||
{self._row("OP/sec", f"{total_rate:.2f}")}
|
||||
{throughput_html}
|
||||
|
||||
<th colspan="2" bgcolor="gainsboro">Errors</th>
|
||||
{per_node_errors_html}
|
||||
{self._row("Total", f"{total_errors} ({total_errors/total_operations*100.0:.2f}%)")}
|
||||
</tbody></table><br><hr>
|
||||
"""
|
||||
|
||||
return html
|
||||
|
||||
def _get_totals_section_html(self):
|
||||
|
||||
html = "<h3>Load Results</h3>"
|
||||
|
||||
write_operations = 0
|
||||
write_op_sec = 0
|
||||
write_throughput = 0
|
||||
write_errors = {}
|
||||
requested_write_rate = self.load_params.write_rate
|
||||
requested_write_rate_str = f"{requested_write_rate}op/sec" if requested_write_rate else ""
|
||||
|
||||
read_operations = 0
|
||||
read_op_sec = 0
|
||||
read_throughput = 0
|
||||
read_errors = {}
|
||||
requested_read_rate = self.load_params.read_rate
|
||||
requested_read_rate_str = f"{requested_read_rate}op/sec" if requested_read_rate else ""
|
||||
|
||||
delete_operations = 0
|
||||
delete_op_sec = 0
|
||||
delete_errors = {}
|
||||
requested_delete_rate = self.load_params.delete_rate
|
||||
requested_delete_rate_str = (
|
||||
f"{requested_delete_rate}op/sec" if requested_delete_rate else ""
|
||||
)
|
||||
|
||||
if self.load_params.scenario in [LoadScenario.gRPC_CAR, LoadScenario.S3_CAR]:
|
||||
delete_vus = max(
|
||||
self.load_params.preallocated_deleters or 0, self.load_params.max_deleters or 0
|
||||
)
|
||||
write_vus = max(
|
||||
self.load_params.preallocated_writers or 0, self.load_params.max_writers or 0
|
||||
)
|
||||
read_vus = max(
|
||||
self.load_params.preallocated_readers or 0, self.load_params.max_readers or 0
|
||||
)
|
||||
else:
|
||||
write_vus = self.load_params.writers
|
||||
read_vus = self.load_params.readers
|
||||
delete_vus = self.load_params.deleters
|
||||
|
||||
write_vus_str = f"{write_vus}th"
|
||||
read_vus_str = f"{read_vus}th"
|
||||
delete_vus_str = f"{delete_vus}th"
|
||||
|
||||
write_section_required = False
|
||||
read_section_required = False
|
||||
delete_section_required = False
|
||||
|
||||
for node_key, load_summary in self.load_summaries.items():
|
||||
metrics = get_metrics_object(self.load_params.scenario, load_summary)
|
||||
write_operations += metrics.write_total_iterations
|
||||
if write_operations:
|
||||
write_section_required = True
|
||||
write_op_sec += metrics.write_rate
|
||||
write_throughput += metrics.write_throughput
|
||||
if metrics.write_failed_iterations:
|
||||
write_errors[node_key] = metrics.write_failed_iterations
|
||||
|
||||
read_operations += metrics.read_total_iterations
|
||||
if read_operations:
|
||||
read_section_required = True
|
||||
read_op_sec += metrics.read_rate
|
||||
read_throughput += metrics.read_throughput
|
||||
if metrics.read_failed_iterations:
|
||||
read_errors[node_key] = metrics.read_failed_iterations
|
||||
|
||||
delete_operations += metrics.delete_total_iterations
|
||||
if delete_operations:
|
||||
delete_section_required = True
|
||||
delete_op_sec += metrics.delete_rate
|
||||
if metrics.delete_failed_iterations:
|
||||
delete_errors[node_key] = metrics.delete_failed_iterations
|
||||
|
||||
if write_section_required:
|
||||
html += self._get_oprations_sub_section_html(
|
||||
"Write",
|
||||
write_operations,
|
||||
requested_write_rate_str,
|
||||
write_vus_str,
|
||||
write_op_sec,
|
||||
write_throughput,
|
||||
write_errors,
|
||||
)
|
||||
|
||||
if read_section_required:
|
||||
html += self._get_oprations_sub_section_html(
|
||||
"Read",
|
||||
read_operations,
|
||||
requested_read_rate_str,
|
||||
read_vus_str,
|
||||
read_op_sec,
|
||||
read_throughput,
|
||||
read_errors,
|
||||
)
|
||||
|
||||
if delete_section_required:
|
||||
html += self._get_oprations_sub_section_html(
|
||||
"Delete",
|
||||
delete_operations,
|
||||
requested_delete_rate_str,
|
||||
delete_vus_str,
|
||||
delete_op_sec,
|
||||
0,
|
||||
delete_errors,
|
||||
)
|
||||
|
||||
return html
|
Loading…
Add table
Add a link
Reference in a new issue