feat: live timing waterfall with TTFB, DNS, TLS, TCP, sending, receiving

Backend:
- Track all k6 HTTP phase metrics via PHASE_MAP in ingestPoint:
  http_req_blocked (DNS+wait), http_req_connecting, http_req_tls_handshaking,
  http_req_sending, http_req_waiting (TTFB), http_req_receiving
- Compute avg + p95 per phase in computeLiveMetrics, broadcast every second
- parseSummary now captures all trend metrics with generic trendRe()

Frontend:
- Live waterfall bar chart updates every second during the test
- Each phase has a distinct colour: indigo/sky/purple/emerald/amber/blue
- Bar width = phase avg as % of total request time
- TTFB p95 + p99 shown below the waterfall
- Bars animate smoothly with CSS transitions
- Waterfall resets cleanly on new test start

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 20:47:03 +02:00
parent d3991416e8
commit 812131df4b
4 changed files with 215 additions and 23 deletions

View File

@@ -93,6 +93,7 @@ function updateGauges(m) {
setStat('sc-p95', fmtMs(m.p95));
setStat('sc-p99', fmtMs(m.p99));
updateBandwidth(m);
updateWaterfall(m);
// colour p95 cell based on threshold (5 s)
const p95cell = document.getElementById('sc-p95');
@@ -102,6 +103,42 @@ function updateGauges(m) {
}
}
function updateWaterfall(m) {
if (!m.phases) return;
const p = m.phases;
// Total avg across all phases (use this to normalise bar widths)
const total = (p.blocked.avg + p.connecting.avg + p.tls.avg +
p.sending.avg + p.ttfb.avg + p.receiving.avg) || 1;
const rows = [
{ id: 'wf-blocked', val: p.blocked.avg, p95: p.blocked.p95 },
{ id: 'wf-connecting', val: p.connecting.avg, p95: p.connecting.p95 },
{ id: 'wf-tls', val: p.tls.avg, p95: p.tls.p95 },
{ id: 'wf-sending', val: p.sending.avg, p95: p.sending.p95 },
{ id: 'wf-ttfb', val: p.ttfb.avg, p95: p.ttfb.p95, p99: p.ttfb.p99 },
{ id: 'wf-receiving', val: p.receiving.avg, p95: p.receiving.p95 },
];
for (const row of rows) {
const el = document.getElementById(row.id);
if (!el) continue;
const bar = el.querySelector('.wf-bar');
const valEl = el.querySelector('.wf-val');
const pct = Math.max(2, (row.val / total) * 100);
bar.style.width = pct + '%';
valEl.textContent = fmtMs(row.val);
}
// TTFB p95/p99 detail line
const p95row = document.getElementById('wf-p95-row');
if (p95row && p.ttfb.avg > 0) {
p95row.style.display = '';
document.getElementById('wf-ttfb-p95').textContent = fmtMs(p.ttfb.p95);
document.getElementById('wf-ttfb-p99').textContent = fmtMs(p.ttfb.p99);
}
}
function updateBandwidth(m) {
const inEl = document.getElementById('bw-in');
const outEl = document.getElementById('bw-out');
@@ -140,6 +177,10 @@ function resetGauges() {
if (bwSize) bwSize.textContent = '-- KB';
const bwWarn = document.getElementById('bw-warning');
if (bwWarn) bwWarn.style.display = 'none';
document.querySelectorAll('.wf-bar').forEach(b => b.style.width = '0%');
document.querySelectorAll('.wf-val').forEach(v => v.textContent = '--');
const p95row = document.getElementById('wf-p95-row');
if (p95row) p95row.style.display = 'none';
}
function renderThresholds(summary) {