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:
@@ -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) {
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Live stat strip -->
|
||||
<!-- Live stat strip — response times -->
|
||||
<div class="stats-strip" id="stats-strip">
|
||||
<div class="stat-cell" id="sc-reqs">
|
||||
<span class="sc-val">--</span><span class="sc-lbl">Requests</span>
|
||||
@@ -163,7 +163,7 @@
|
||||
<span class="sc-val">--</span><span class="sc-lbl">Req / s</span>
|
||||
</div>
|
||||
<div class="stat-cell" id="sc-avg">
|
||||
<span class="sc-val">--</span><span class="sc-lbl">Avg</span>
|
||||
<span class="sc-val">--</span><span class="sc-lbl">Avg total</span>
|
||||
</div>
|
||||
<div class="stat-cell" id="sc-p90">
|
||||
<span class="sc-val">--</span><span class="sc-lbl">p(90)</span>
|
||||
@@ -176,6 +176,49 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live timing phases waterfall -->
|
||||
<div class="waterfall-wrap" id="waterfall-wrap">
|
||||
<div class="waterfall-title">Request timing breakdown <span class="wf-hint">(avg per phase)</span></div>
|
||||
<div class="waterfall" id="waterfall">
|
||||
<div class="wf-row" id="wf-blocked">
|
||||
<span class="wf-label">DNS / Blocked</span>
|
||||
<div class="wf-bar-wrap"><div class="wf-bar wf-blocked"></div></div>
|
||||
<span class="wf-val">--</span>
|
||||
</div>
|
||||
<div class="wf-row" id="wf-connecting">
|
||||
<span class="wf-label">TCP connect</span>
|
||||
<div class="wf-bar-wrap"><div class="wf-bar wf-connecting"></div></div>
|
||||
<span class="wf-val">--</span>
|
||||
</div>
|
||||
<div class="wf-row" id="wf-tls">
|
||||
<span class="wf-label">TLS handshake</span>
|
||||
<div class="wf-bar-wrap"><div class="wf-bar wf-tls"></div></div>
|
||||
<span class="wf-val">--</span>
|
||||
</div>
|
||||
<div class="wf-row" id="wf-sending">
|
||||
<span class="wf-label">Sending</span>
|
||||
<div class="wf-bar-wrap"><div class="wf-bar wf-sending"></div></div>
|
||||
<span class="wf-val">--</span>
|
||||
</div>
|
||||
<div class="wf-row" id="wf-ttfb">
|
||||
<span class="wf-label">TTFB (waiting)</span>
|
||||
<div class="wf-bar-wrap"><div class="wf-bar wf-ttfb"></div></div>
|
||||
<span class="wf-val">--</span>
|
||||
</div>
|
||||
<div class="wf-row" id="wf-receiving">
|
||||
<span class="wf-label">Receiving</span>
|
||||
<div class="wf-bar-wrap"><div class="wf-bar wf-receiving"></div></div>
|
||||
<span class="wf-val">--</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wf-p95-row" id="wf-p95-row" style="display:none">
|
||||
<span class="wf-p95-label">TTFB p(95)</span>
|
||||
<span class="wf-p95-val" id="wf-ttfb-p95">--</span>
|
||||
<span class="wf-p95-label" style="margin-left:1rem">TTFB p(99)</span>
|
||||
<span class="wf-p95-val" id="wf-ttfb-p99">--</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bandwidth row -->
|
||||
<div class="bw-row" id="bw-row">
|
||||
<div class="bw-item">
|
||||
|
||||
@@ -307,6 +307,64 @@ textarea { font-family: var(--mono); resize: vertical; }
|
||||
letter-spacing: .05em;
|
||||
}
|
||||
|
||||
/* ── TIMING WATERFALL ───────────────────────────────────────────────────────── */
|
||||
.waterfall-wrap {
|
||||
margin-top: .75rem;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: .9rem;
|
||||
}
|
||||
.waterfall-title {
|
||||
font-size: .78rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
margin-bottom: .6rem;
|
||||
}
|
||||
.wf-hint { font-weight: 400; text-transform: none; letter-spacing: 0; }
|
||||
|
||||
.wf-row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr 70px;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
margin-bottom: .35rem;
|
||||
}
|
||||
.wf-label { font-size: .78rem; color: var(--muted); white-space: nowrap; }
|
||||
.wf-bar-wrap {
|
||||
background: var(--border);
|
||||
border-radius: 999px;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wf-bar {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
width: 0%;
|
||||
transition: width .6s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
.wf-val { font-size: .78rem; font-family: var(--mono); color: var(--text); text-align: right; }
|
||||
|
||||
/* Phase colours */
|
||||
.wf-blocked { background: #818cf8; } /* indigo — DNS/blocked */
|
||||
.wf-connecting { background: #38bdf8; } /* sky — TCP connect */
|
||||
.wf-tls { background: #a78bfa; } /* purple — TLS */
|
||||
.wf-sending { background: #34d399; } /* emerald — sending */
|
||||
.wf-ttfb { background: #f59e0b; } /* amber — TTFB (most important) */
|
||||
.wf-receiving { background: #60a5fa; } /* blue — receiving */
|
||||
|
||||
.wf-p95-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
margin-top: .4rem;
|
||||
padding-top: .4rem;
|
||||
border-top: 1px dashed var(--border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.wf-p95-label { font-size: .72rem; color: var(--muted); }
|
||||
.wf-p95-val { font-size: .85rem; font-weight: 700; font-family: var(--mono); color: var(--yellow); }
|
||||
|
||||
/* ── BANDWIDTH ROW ──────────────────────────────────────────────────────────── */
|
||||
.bw-row {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user