Download src/tinyllm/web/status.html from CoolDip00/tiny-llm: direct link, hf CLI and curl.
- Browser
- Download file 22.5 kB
-
https://huggingface.co/spaces/CoolDip00/tiny-llm/resolve/main/src/tinyllm/web/status.html
- Command line
-
hf download hf://spaces/CoolDip00/tiny-llm/src/tinyllm/web/status.html
-
curl -L -o status.html https://huggingface.co/spaces/CoolDip00/tiny-llm/resolve/main/src/tinyllm/web/status.html
22.5 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>tiny-llm status</title> | |
| <style> | |
| :root { | |
| color-scheme: light; | |
| --page: #f9f9f7; --surface: #fcfcfb; --ink: #0b0b0b; --ink-2: #52514e; --muted: #898781; | |
| --grid: #e1e0d9; --axis: #c3c2b7; --ring: rgba(11,11,11,0.10); | |
| --s1: #2a78d6; --s2: #eb6834; --s3: #1baf7a; --track: #cde2fb; | |
| --good: #0ca30c; --warn: #fab219; --bad: #d03b3b; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root:not([data-theme="light"]) { | |
| color-scheme: dark; | |
| --page: #0d0d0d; --surface: #1a1a19; --ink: #f0efec; --ink-2: #c3c2b7; --muted: #898781; | |
| --grid: #2c2c2a; --axis: #383835; --ring: rgba(255,255,255,0.10); | |
| --s1: #3987e5; --s2: #d95926; --s3: #199e70; --track: #184f95; | |
| } | |
| } | |
| :root[data-theme="dark"] { | |
| color-scheme: dark; | |
| --page: #0d0d0d; --surface: #1a1a19; --ink: #f0efec; --ink-2: #c3c2b7; --muted: #898781; | |
| --grid: #2c2c2a; --axis: #383835; --ring: rgba(255,255,255,0.10); | |
| --s1: #3987e5; --s2: #d95926; --s3: #199e70; --track: #184f95; | |
| } | |
| * { box-sizing: border-box; } | |
| body { margin: 0; background: var(--page); color: var(--ink); | |
| font: 15px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; } | |
| main { max-width: 980px; margin: 0 auto; padding: 24px 16px 48px; } | |
| header { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 16px; } | |
| h1 { font-size: 22px; margin: 0; font-weight: 600; } | |
| h2 { font-size: 16px; margin: 0 0 2px; font-weight: 600; } | |
| a { color: var(--s1); } | |
| .meta, .sub { color: var(--ink-2); font-size: 13px; } | |
| .sub { margin: 0 0 10px; } | |
| .banner { display: none; margin: 12px 0 0; padding: 8px 12px; border-radius: 8px; | |
| border: 1px solid var(--ring); background: var(--surface); } | |
| .banner.show { display: block; } | |
| .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; margin: 20px 0 8px; } | |
| .card, .panel { background: var(--surface); border: 1px solid var(--ring); border-radius: 12px; padding: 14px 16px; } | |
| .card .value { font-size: 28px; font-weight: 600; margin: 4px 0 0; } | |
| .card .line { color: var(--ink-2); font-size: 13px; margin: 2px 0; } | |
| .state { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; } | |
| .state i { display: inline-grid; place-items: center; width: 16px; height: 16px; border-radius: 50%; | |
| font-style: normal; font-size: 11px; color: #fff; flex: none; } | |
| .state.none { font-weight: 400; align-items: flex-start; } .state.none i { margin-top: 2px; } | |
| .state.good i { background: var(--good); } .state.bad i { background: var(--bad); } | |
| .state.warn i { background: var(--warn); color: #0b0b0b; } .state.none i { background: var(--muted); } | |
| .meter { height: 8px; border-radius: 4px; background: var(--track); margin: 8px 0 4px; overflow: hidden; } | |
| .meter > div { height: 100%; border-radius: 4px; background: var(--s1); } | |
| .window { color: var(--ink-2); font-size: 13px; margin: 0 0 16px; } | |
| .panel { margin: 12px 0; position: relative; } | |
| .legend { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 13px; color: var(--ink-2); margin: 0 0 6px; } | |
| .legend span { display: inline-flex; align-items: center; gap: 6px; } | |
| .key { display: inline-block; width: 14px; height: 0; border-top: 2px solid; } | |
| .key.box { width: 10px; height: 10px; border: 0; border-radius: 2px; } | |
| .key.ref { border-top: 1px solid var(--muted); } | |
| .chart { width: 100%; height: 190px; display: block; outline: none; } | |
| .chart:focus-visible { box-shadow: 0 0 0 2px var(--s1); border-radius: 4px; } | |
| .chart text { fill: var(--muted); font-size: 11px; font-variant-numeric: tabular-nums; } | |
| .tip { position: absolute; pointer-events: none; display: none; min-width: 140px; z-index: 2; | |
| background: var(--surface); border: 1px solid var(--ring); border-radius: 8px; | |
| padding: 8px 10px; font-size: 13px; box-shadow: 0 4px 16px rgba(0,0,0,0.12); } | |
| .tip .t { color: var(--ink-2); margin-bottom: 4px; } | |
| .tip .r { display: flex; align-items: center; gap: 8px; } | |
| .tip .r b { font-weight: 600; min-width: 52px; } | |
| .tip .r span { color: var(--ink-2); } | |
| details { margin: 16px 0; } | |
| summary { cursor: pointer; color: var(--ink-2); } | |
| table { border-collapse: collapse; width: 100%; font-size: 13px; margin-top: 8px; font-variant-numeric: tabular-nums; } | |
| th, td { text-align: right; padding: 4px 8px; border-bottom: 1px solid var(--grid); } | |
| th:first-child, td:first-child { text-align: left; } | |
| .tablewrap { overflow-x: auto; } | |
| footer { color: var(--ink-2); font-size: 13px; margin-top: 28px; } | |
| .stale { opacity: 0.55; } | |
| </style> | |
| </head> | |
| <body> | |
| <main> | |
| <header> | |
| <h1>Status</h1> | |
| <span class="meta" id="meta">Loadingβ¦</span> | |
| </header> | |
| <p class="meta"><a href="./">β Demo</a> Β· numbers are measured on this server, from arrival to token sent</p> | |
| <div class="banner" id="banner" role="status"></div> | |
| <section class="cards" id="cards" aria-label="Service level objectives"></section> | |
| <p class="window" id="window"></p> | |
| <section class="panel" id="replicas-panel" hidden> | |
| <h2>Replicas</h2> | |
| <p class="sub">State and queue from the last poll. The router polls only while visitors are active, so with no traffic this says "Not polled".</p> | |
| <div class="tablewrap"><table id="replicas"></table></div> | |
| </section> | |
| <section class="panel"> | |
| <h2>Requests per minute</h2> | |
| <p class="sub">Last 60 minutes, by outcome</p> | |
| <div class="legend"> | |
| <span><i class="key box" style="background:var(--s1)"></i>Served</span> | |
| <span><i class="key box" style="background:var(--s2)"></i>Busy (429)</span> | |
| <span><i class="key box" style="background:var(--s3)"></i>Server error (5xx)</span> | |
| </div> | |
| <svg class="chart" id="c-req" tabindex="0" role="img" aria-label="Requests per minute, last hour"></svg> | |
| <div class="tip"></div> | |
| </section> | |
| <section class="panel"> | |
| <h2>Latency</h2> | |
| <p class="sub">Per minute, successful requests; time to first token includes the wait for a slot</p> | |
| <div class="legend"> | |
| <span><i class="key" style="border-color:var(--s1)"></i>First token p50</span> | |
| <span><i class="key" style="border-color:var(--s2)"></i>First token p95</span> | |
| <span><i class="key" style="border-color:var(--s3)"></i>Whole request p95</span> | |
| <span><i class="key ref"></i><span id="ref-label">SLO</span></span> | |
| </div> | |
| <svg class="chart" id="c-lat" tabindex="0" role="img" aria-label="Latency per minute, last hour"></svg> | |
| <div class="tip"></div> | |
| </section> | |
| <section class="panel"> | |
| <h2>Load</h2> | |
| <p class="sub">Engine snapshots every 5 s</p> | |
| <div class="legend"> | |
| <span><i class="key" style="border-color:var(--s1)"></i>Busy slots</span> | |
| <span><i class="key" style="border-color:var(--s2)"></i>Waiting in queue</span> | |
| <span><i class="key ref"></i><span id="slots-label">Slots</span></span> | |
| </div> | |
| <svg class="chart" id="c-load" tabindex="0" role="img" aria-label="Busy slots and queue length, last hour"></svg> | |
| <div class="tip"></div> | |
| </section> | |
| <details> | |
| <summary>Show the last hour as a table</summary> | |
| <div class="tablewrap"><table id="table"></table></div> | |
| </details> | |
| <footer> | |
| SLOs and the error budget are defined in | |
| <a href="https://github.com/cooldip00/tiny-llm/blob/main/docs/SERVING.md#slos-413-step-3-written-before-the-load-test">docs/SERVING.md</a>. | |
| History is kept in memory, so it starts again when the server restarts. | |
| The server runs on CPU here; the speed numbers in the project README come from a laptop GPU. | |
| </footer> | |
| </main> | |
| <script> | |
| ; | |
| const NS = "http://www.w3.org/2000/svg"; | |
| const SLO_NAMES = { ttft: "Time to first token", availability: "Availability", shed: "Requests not shed" }; | |
| let last = null; | |
| function el(tag, attrs, parent, text) { | |
| const e = document.createElementNS(NS, tag); | |
| for (const k in attrs) e.setAttribute(k, attrs[k]); | |
| if (text !== undefined) e.textContent = text; | |
| if (parent) parent.appendChild(e); | |
| return e; | |
| } | |
| function h(tag, cls, text) { | |
| const e = document.createElement(tag); | |
| if (cls) e.className = cls; | |
| if (text !== undefined) e.textContent = text; | |
| return e; | |
| } | |
| function niceStep(v) { | |
| if (!(v > 0)) return 1; | |
| const p = Math.pow(10, Math.floor(Math.log10(v))); | |
| for (const m of [1, 2, 5]) if (m * p >= v) return m * p; | |
| return 10 * p; | |
| } | |
| const pct = (x, d = 1) => x === null || x === undefined ? "β" : (100 * x).toFixed(d) + "%"; | |
| function secs(x) { | |
| if (x === null || x === undefined) return "β"; | |
| return x < 1 ? Math.round(x * 1000) + " ms" : x.toFixed(2) + " s"; | |
| } | |
| function dur(s) { | |
| s = Math.round(s); | |
| const d = Math.floor(s / 86400), hh = Math.floor(s % 86400 / 3600), m = Math.floor(s % 3600 / 60); | |
| if (d) return `${d} d ${hh} h`; | |
| if (hh) return `${hh} h ${m} min`; | |
| return m ? `${m} min` : `${s} s`; | |
| } | |
| const minLabel = (ago) => ago === 0 ? "this minute" : `${ago} min ago`; | |
| /* One chart: x in minutes from -60 to 0; series of {name, color, values: [{x, y}|null...]}. | |
| kind "stack": columns, one per minute; kind "line": 2px lines with an end dot, and a dot on lone points. */ | |
| function drawChart(svg, spec) { | |
| const W = svg.clientWidth || 600, H = svg.clientHeight || 190; | |
| const m = { l: 44, r: 14, t: 10, b: 24 }; | |
| const pw = W - m.l - m.r, ph = H - m.t - m.b; | |
| svg.setAttribute("viewBox", `0 0 ${W} ${H}`); | |
| svg.replaceChildren(); | |
| const X = (x) => m.l + (x + 60) / 60 * pw; | |
| let ymax = spec.ymin || 0; | |
| for (const p of spec.points) ymax = Math.max(ymax, spec.total(p)); | |
| if (spec.ref) ymax = Math.max(ymax, spec.ref.value * 1.15); | |
| let step = niceStep(ymax / 4); // ticks on 1 / 2 / 5 Γ 10^k, 3β5 gridlines | |
| if (spec.integer) step = Math.max(1, Math.round(step)); | |
| const nTicks = Math.max(1, Math.ceil(ymax / step - 1e-9)); | |
| ymax = nTicks * step; | |
| const Y = (y) => m.t + ph - y / ymax * ph; | |
| for (let i = 0; i <= nTicks; i++) { | |
| const v = +(step * i).toPrecision(12), y = Y(v); | |
| el("line", { x1: m.l, x2: W - m.r, y1: y, y2: y, stroke: i ? "var(--grid)" : "var(--axis)", "stroke-width": 1 }, svg); | |
| el("text", { x: m.l - 6, y: y + 4, "text-anchor": "end" }, svg, spec.fmt(v)); | |
| } | |
| for (const ago of [60, 45, 30, 15, 0]) | |
| el("text", { x: X(-ago), y: H - 6, "text-anchor": ago === 60 ? "start" : ago === 0 ? "end" : "middle" }, | |
| svg, ago ? `β${ago} min` : "now"); | |
| if (spec.ref) { | |
| const y = Y(spec.ref.value); | |
| el("line", { x1: m.l, x2: W - m.r, y1: y, y2: y, stroke: "var(--muted)", "stroke-width": 1 }, svg); | |
| el("text", { x: W - m.r, y: y - 4, "text-anchor": "end" }, svg, spec.ref.label); | |
| } | |
| if (spec.kind === "stack") { | |
| const slot = pw / 60, bw = Math.max(2, Math.min(24, slot - 2)); | |
| for (const p of spec.points) { | |
| if (!p) continue; | |
| let base = 0; | |
| const parts = spec.series.map((s) => ({ s, v: s.get(p) })).filter((q) => q.v > 0); | |
| parts.forEach((q, i) => { | |
| const x = X(p.x) - slot / 2 + (slot - bw) / 2, y0 = Y(base), y1 = Y(base + q.v); | |
| const top = i === parts.length - 1, gap = i ? 2 : 0, hgt = Math.max(0, y0 - y1 - gap); | |
| const r = top ? Math.min(2, bw / 2, hgt) : 0, yb = y0 - gap; | |
| el("path", { d: `M${x},${yb} V${yb - hgt + r} Q${x},${yb - hgt} ${x + r},${yb - hgt} H${x + bw - r} ` + | |
| `Q${x + bw},${yb - hgt} ${x + bw},${yb - hgt + r} V${yb} Z`, fill: q.s.color }, svg); | |
| base += q.v; | |
| }); | |
| } | |
| } else { | |
| for (const s of spec.series) { | |
| // A point with no neighbour on either side is a lone "M" (zero length, never drawn), | |
| // so each such point gets its own small dot; sparse traffic makes most minutes like that. | |
| let d = "", lastPt = null, prev = null, run = []; | |
| const lone = []; | |
| const endRun = () => { if (run.length === 1) lone.push(run[0]); run = []; }; | |
| for (const p of spec.points) { | |
| const v = p ? s.get(p) : null; | |
| if (v === null || v === undefined) { endRun(); prev = null; continue; } | |
| const gapBreak = prev && p.x - prev.x > (spec.maxGap || 1.01); | |
| if (!prev || gapBreak) endRun(); | |
| const pt = [X(p.x), Y(v)]; | |
| d += `${prev && !gapBreak ? "L" : "M"}${pt[0].toFixed(1)},${pt[1].toFixed(1)} `; | |
| run.push(pt); prev = p; lastPt = pt; | |
| } | |
| endRun(); | |
| if (d) el("path", { d, fill: "none", stroke: s.color, "stroke-width": 2, "stroke-linejoin": "round", "stroke-linecap": "round" }, svg); | |
| for (const q of lone) if (q !== lastPt) el("circle", { cx: q[0], cy: q[1], r: 3, fill: s.color }, svg); | |
| if (lastPt) el("circle", { cx: lastPt[0], cy: lastPt[1], r: 4, fill: s.color, stroke: "var(--surface)", "stroke-width": 2 }, svg); | |
| } | |
| } | |
| attachHover(svg, spec, X, m, H); | |
| } | |
| function attachHover(svg, spec, X, m, H) { | |
| const tip = svg.nextElementSibling; | |
| const pts = spec.points.filter(Boolean); | |
| const cross = el("line", { y1: m.t, y2: H - m.b, stroke: "var(--axis)", "stroke-width": 1, visibility: "hidden" }, svg); | |
| let idx = -1; | |
| function show(i) { | |
| if (!pts.length) return; | |
| idx = Math.max(0, Math.min(pts.length - 1, i)); | |
| const p = pts[idx], x = X(p.x); | |
| cross.setAttribute("x1", x); cross.setAttribute("x2", x); cross.setAttribute("visibility", "visible"); | |
| tip.replaceChildren(h("div", "t", spec.when(p))); | |
| for (const s of spec.series) { | |
| const r = h("div", "r"), k = h("i", spec.kind === "stack" ? "key box" : "key"); | |
| k.style[spec.kind === "stack" ? "background" : "borderColor"] = s.color; | |
| r.append(k, h("b", "", spec.fmtTip(s.get(p))), h("span", "", s.name)); | |
| tip.appendChild(r); | |
| } | |
| tip.style.display = "block"; | |
| const box = svg.getBoundingClientRect(), panel = svg.parentElement.getBoundingClientRect(); | |
| let left = box.left - panel.left + x + 12; | |
| if (left + tip.offsetWidth > panel.width - 8) left = box.left - panel.left + x - tip.offsetWidth - 12; | |
| tip.style.left = Math.max(8, left) + "px"; | |
| tip.style.top = (box.top - panel.top + 8) + "px"; | |
| } | |
| function hide() { cross.setAttribute("visibility", "hidden"); tip.style.display = "none"; } | |
| function nearest(clientX) { | |
| const box = svg.getBoundingClientRect(), x = clientX - box.left; | |
| let best = -1, bd = Infinity; | |
| pts.forEach((p, i) => { const d = Math.abs(X(p.x) - x); if (d < bd) { bd = d; best = i; } }); | |
| return best; | |
| } | |
| svg.onpointermove = (e) => show(nearest(e.clientX)); | |
| svg.onpointerleave = hide; | |
| svg.onblur = hide; | |
| svg.onfocus = () => show(pts.length - 1); | |
| svg.onkeydown = (e) => { | |
| if (e.key === "ArrowLeft") { show(idx - 1); e.preventDefault(); } | |
| if (e.key === "ArrowRight") { show(idx + 1); e.preventDefault(); } | |
| if (e.key === "Escape") hide(); | |
| }; | |
| } | |
| function state(cls, icon, label) { | |
| const s = h("span", "state " + cls); s.append(h("i", "", icon), document.createTextNode(label)); return s; | |
| } | |
| function renderCards(d) { | |
| const cards = document.getElementById("cards"); | |
| cards.replaceChildren(); | |
| for (const key of ["ttft", "availability", "shed"]) { | |
| const s = d.slo[key], c = h("div", "card"); | |
| const target = key === "ttft" ? `${pct(s.target, 0)} of requests get a first token within ${secs(s.threshold_s)}` | |
| : key === "availability" ? `${pct(s.target, 0)} of requests not failed by the server` | |
| : `at most ${pct(1 - s.target, 0)} of requests turned away (429) at rated load`; | |
| c.append(h("h2", "", SLO_NAMES[key]), h("div", "line", "Target: " + target)); | |
| c.appendChild(h("div", "value", s.total ? pct(s.compliance, 2) : "β")); | |
| c.appendChild(h("div", "line", s.total ? `${s.good.toLocaleString()} of ${s.total.toLocaleString()} requests` : "No requests yet")); | |
| if (key === "shed") c.appendChild(state("none", "i", "Shown, not judged: the rated load for this server is not measured yet")); | |
| else if (!s.total) c.appendChild(state("none", "β", "No data")); | |
| else c.appendChild(s.met ? state("good", "β", "Met") : state("bad", "β", "Missed")); | |
| if (s.total && key !== "shed") { | |
| const left = Math.max(0, s.budget_left), mtr = h("div", "meter"), fill = h("div"); | |
| fill.style.width = (100 * left).toFixed(1) + "%"; mtr.appendChild(fill); | |
| mtr.setAttribute("role", "meter"); mtr.setAttribute("aria-valuenow", (100 * left).toFixed(1)); | |
| mtr.setAttribute("aria-valuemin", "0"); mtr.setAttribute("aria-valuemax", "100"); | |
| mtr.setAttribute("aria-label", "Error budget left"); | |
| c.append(mtr, h("div", "line", s.budget_left > 0 ? `Error budget left: ${pct(s.budget_left, 0)}` : "Error budget used up")); | |
| } | |
| if (s.burn_rate_1h !== null) { | |
| const line = h("div", "line", `Last hour: burn rate ${s.burn_rate_1h.toFixed(1)}Γ over ${s.requests_1h} requests `); | |
| if (s.fast_burn && key !== "shed") line.appendChild(state("warn", "!", `fast burn (> ${d.fast_burn_threshold}Γ)`)); | |
| else if (s.requests_1h < d.burn_min_requests) line.appendChild(document.createTextNode(`(too few to flag, < ${d.burn_min_requests})`)); | |
| c.appendChild(line); | |
| } | |
| cards.appendChild(c); | |
| } | |
| document.getElementById("window").textContent = | |
| `SLO window: rolling 7 days, counted since the last restart (${dur(d.slo_window_s)} so far). ` + | |
| "Burn rate = the last hour's bad share Γ· the budgeted share; a fast burn above " + | |
| `${d.fast_burn_threshold}Γ would spend ~9% of the 7-day budget in an hour.`; | |
| } | |
| function render(d) { | |
| const v = Object.entries(d.variants)[0] || ["?", {}]; | |
| document.getElementById("meta").textContent = | |
| `${v[1].version || v[0]} Β· norm ${v[1].norm_backend || "?"} Β· ${v[1].n_slots || "?"} slots Β· up ${dur(d.uptime_s)}`; | |
| renderCards(d); | |
| renderReplicas(d); | |
| const mins = d.minutes.map((p) => p.covered ? { ...p, x: -p.minutes_ago } : null); | |
| const threshold = d.slo.ttft.threshold_s; | |
| document.getElementById("ref-label").textContent = `SLO: first token β€ ${secs(threshold)}`; | |
| drawChart(document.getElementById("c-req"), { | |
| kind: "stack", points: mins, total: (p) => p ? p.ok + p.shed + p.failed : 0, ymin: 4, integer: true, | |
| fmt: (v) => Number.isInteger(v) ? v.toLocaleString() : v.toFixed(1), | |
| fmtTip: (v) => (v || 0).toLocaleString(), when: (p) => minLabel(p.minutes_ago), | |
| series: [{ name: "Served", color: "var(--s1)", get: (p) => p.ok }, | |
| { name: "Busy (429)", color: "var(--s2)", get: (p) => p.shed }, | |
| { name: "Server error", color: "var(--s3)", get: (p) => p.failed }] }); | |
| drawChart(document.getElementById("c-lat"), { | |
| kind: "line", points: mins, ymin: 0.1, | |
| total: (p) => p ? Math.max(p.ttft_p95_s || 0, p.latency_p95_s || 0) : 0, | |
| ref: { value: threshold, label: "SLO " + secs(threshold) }, | |
| fmt: (v) => v < 1 ? Math.round(v * 1000) + " ms" : (Number.isInteger(v) ? v : v.toFixed(1)) + " s", | |
| fmtTip: secs, when: (p) => minLabel(p.minutes_ago), | |
| series: [{ name: "First token p50", color: "var(--s1)", get: (p) => p.ttft_p50_s }, | |
| { name: "First token p95", color: "var(--s2)", get: (p) => p.ttft_p95_s }, | |
| { name: "Whole request p95", color: "var(--s3)", get: (p) => p.latency_p95_s }] }); | |
| const slots = v[1].n_slots || 1; | |
| document.getElementById("slots-label").textContent = `${slots} slots`; | |
| const snaps = d.snapshots.map((s) => ({ ...s, x: -s.seconds_ago / 60 })); | |
| drawChart(document.getElementById("c-load"), { | |
| kind: "line", points: snaps, maxGap: 0.5, ymin: slots, integer: true, | |
| total: (p) => p ? Math.max(p.busy_slots, p.queue_len) : 0, | |
| ref: { value: slots, label: `${slots} slots` }, | |
| fmt: (v) => Number.isInteger(v) ? String(v) : v.toFixed(1), | |
| fmtTip: (v) => String(v), when: (p) => `${Math.round(p.seconds_ago)} s ago`, | |
| series: [{ name: "Busy slots", color: "var(--s1)", get: (p) => p.busy_slots }, | |
| { name: "Waiting in queue", color: "var(--s2)", get: (p) => p.queue_len }] }); | |
| renderTable(d); | |
| } | |
| const REPLICA_STATES = { | |
| ok: ["good", "β", "Ready"], not_ready: ["warn", "!", "Starting"], asleep: ["none", "β", "Asleep"], | |
| down: ["bad", "β", "Down"], misconfigured: ["bad", "β", "Misconfigured"], | |
| }; | |
| /* Only the router sends `replicas`; on a single server this panel stays hidden. */ | |
| function renderReplicas(d) { | |
| const panel = document.getElementById("replicas-panel"); | |
| panel.hidden = !d.replicas; | |
| if (!d.replicas) return; | |
| const t = document.getElementById("replicas"); | |
| t.replaceChildren(); | |
| const head = h("tr"); | |
| for (const c of ["Replica", "State", "Queue", "Slots", "Last poll"]) head.appendChild(h("th", "", c)); | |
| t.appendChild(head); | |
| for (const x of d.replicas) { | |
| const known = x.state && (d.polling || x.state === "misconfigured"); // a wrong token doesn't fix itself | |
| const [cls, icon, label] = known ? (REPLICA_STATES[x.state] || ["none", "β", x.state]) : ["none", "β", "Not polled"]; | |
| const r = h("tr"), cell = h("td"); | |
| cell.appendChild(state(cls, icon, label)); | |
| r.append(h("td", "", x.name), cell, h("td", "", known ? String(x.queue) : "β"), h("td", "", x.n_slots ? String(x.n_slots) : "β"), | |
| h("td", "", x.polled_s_ago === null ? "never" : dur(x.polled_s_ago) + " ago")); | |
| t.appendChild(r); | |
| } | |
| } | |
| function renderTable(d) { | |
| const t = document.getElementById("table"); | |
| t.replaceChildren(); | |
| const head = h("tr"); | |
| for (const c of ["Minute", "Requests", "Served", "429", "5xx", "First token p50", "p95", "Whole p95", "Tokens"]) | |
| head.appendChild(h("th", "", c)); | |
| t.appendChild(head); | |
| for (const p of [...d.minutes].reverse()) { | |
| if (!p.covered) continue; | |
| const r = h("tr"); | |
| for (const c of [minLabel(p.minutes_ago), p.requests, p.ok, p.shed, p.failed, secs(p.ttft_p50_s), | |
| secs(p.ttft_p95_s), secs(p.latency_p95_s), p.completion_tokens.toLocaleString()]) | |
| r.appendChild(h("td", "", String(c))); | |
| t.appendChild(r); | |
| } | |
| } | |
| async function refresh() { | |
| const banner = document.getElementById("banner"); | |
| try { | |
| const r = await fetch("stats/history", { cache: "no-store" }); | |
| if (!r.ok) throw new Error("HTTP " + r.status); | |
| last = await r.json(); | |
| banner.classList.remove("show"); | |
| document.querySelector("main").classList.remove("stale"); | |
| render(last); | |
| } catch (e) { | |
| banner.textContent = "Can't reach the server right now (it may be starting or asleep). Retrying every 5 s."; | |
| banner.classList.add("show"); | |
| document.querySelector("main").classList.add("stale"); | |
| } | |
| } | |
| refresh(); | |
| setInterval(refresh, 5000); | |
| let rt; | |
| window.addEventListener("resize", () => { clearTimeout(rt); rt = setTimeout(() => last && render(last), 150); }); | |
| </script> | |
| </body> | |
| </html> | |