diff --git a/radiator/charts.py b/radiator/charts.py index 99f3fb6..a8dd1cb 100644 --- a/radiator/charts.py +++ b/radiator/charts.py @@ -227,3 +227,135 @@ def sy(y: float) -> float: return Markup( f'
No stage measurements.
') + + def seconds(ms: float) -> str: + return f"{ms / 1000:,.0f}s" if ms >= 10000 or ms == 0 else f"{ms / 1000:,.1f}s" + + values = [v for _, v in points] + ([slo_ms] if slo_ms else []) + ticks = nice_ticks(0, max(values)) + lo, hi = 0, ticks[-1] + left, right, top, bottom = 52, 16, 14, 30 + plot_w, plot_h = width - left - right, height - top - bottom + n = len(stages) + + def sx(i: int) -> float: + return left + plot_w * ((i + 0.5) / n) + + def sy(y: float) -> float: + return top + plot_h - (y - lo) / (hi - lo) * plot_h if hi != lo else top + + parts = [ + f'" + ) + + def fmt(value, f): + return None if value is None else f(value) + + hover = { + "width": width, + "xs": [round(sx(i), 1) for i in range(n)], + "titles": [f"{s.get('users', '?')} users" for s in stages], + "series": [ + { + "name": "p99", + "color": "var(--agent-1)", + "values": [fmt(s.get("p99_ms"), seconds) for s in stages], + }, + { + "name": "p50", + "color": None, + "values": [fmt(s.get("p50_ms"), seconds) for s in stages], + }, + { + "name": "errors", + "color": None, + "values": [ + fmt(s.get("error_rate"), lambda v: f"{v * 100:.1f}%") + for s in stages + ], + }, + { + "name": "requests/s", + "color": None, + "values": [fmt(s.get("rps"), lambda v: f"{v:.2f}") for s in stages], + }, + ], + } + data = escape(json.dumps(hover)) + return Markup( + f'HelmsDeep steps the load up and holds each level for several minutes, so one load test takes tens of minutes. Its numbers arrive all at once when it ends; this page updates by itself.
+What the previous run measured, for comparison while this one is going.
+| Service | Result | Max sustainable concurrency | Checkpoints |
|---|---|---|---|
| {{ perf_series_name(perf) }} | +{{ status_badge(perf.status) }} | +{{ conc(perf.max_sustainable_concurrency) }} | +{% if perf.checkpoints_passed is none %}none configured{% else %}{{ status_badge('PASSED' if perf.checkpoints_passed else 'FAILED') }}{% endif %} | +
{% if progress.running %}No load test has finished yet.{% else %}This run recorded no load test results.{% endif %}
+ {{ v.config.get('component') or perf.helmsdeep_target or 'HelmsDeep' }} + · run type {{ perf.helmsdeep_target or '–' }} + · {{ perf.profile or 'default' }} profile + {% if v.config.get('protocol') %}· {{ v.config.get('protocol') }}{% endif %} +
+p99 latency at each load level. The knee is the highest level that kept within the SLO and the error cap.
+| Stage | Users | Requests | Req/s | Errors | p50 | p95 | p99 | Concurrency | Within SLO |
|---|---|---|---|---|---|---|---|---|---|
| {{ s.get('stage', loop.index0) }}{% if v.knee and s.get('stage') == v.knee.get('stage') %} knee{% endif %} | +{{ s.get('users') if s.get('users') is not none else '–' }} | +{{ '{:,}'.format(s.get('requests')) if s.get('requests') is not none else '–' }} | +{{ '%.2f'|format(s.get('rps')) if s.get('rps') is not none else '–' }} | +{{ pct(s.get('error_rate'), 1) }}{% if s.get('errors') %} ({{ s.get('errors') }}){% endif %} | +{{ ms(s.get('p50_ms')) }} | +{{ ms(s.get('p95_ms')) }} | +{{ ms(s.get('p99_ms')) }} | +{{ '%.1f'|format(s.get('concurrency')) if s.get('concurrency') is not none else '–' }} | +{% if ok is none %}–{% else %}{{ status_badge('PASSED' if ok else 'FAILED') }}{% endif %} + {% for issue in issues %}{{ (issue.get('kind') or 'warning')|replace('_', ' ') }}{% endfor %} | +
HelmsDeep didn't report per-stage numbers for this run.
+ {% endif %} + + {% if v.checkpoints %} +Pass/fail questions about set load levels: does the service hold this many users?
+| Verdict | Users | Goal | p99 | p99 bar | Errors | Error cap | Detail |
|---|---|---|---|---|---|---|---|
| {% if c.get('verdict') == 'PASS' %}{{ status_badge('PASSED') }}{% elif c.get('verdict') == 'FAIL' %}{{ status_badge('FAILED') }}{% else %}{{ status_badge('NO_RESULTS') }}{% endif %} | +{{ c.get('users', '–') }} | +{{ c.get('goal') or '–' }} | +{{ ms(c.get('p99_ms')) }} | +{{ ms(c.get('p99_slo_ms')) if c.get('p99_slo_ms') is not none else 'not judged' }} | +{{ pct(c.get('error_rate'), 1) }} | +{{ pct(c.get('max_error_rate'), 1) }} | +{{ c.get('detail') or '' }} | +