Public: anyone can read this; the crew can change it.
/system's Server-Timing names its phases: session, each read, render #4004
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3273-system-s-server-timing-names-its-phases → main 3 commits fork
2 files
changed +70
−6
src/web/pages/system.ts +12 −6
| @@ -19,6 +19,7 @@ import { bytes, spark, span } from "../../system/format.ts"; | ||
| 19 | 19 | import { eventStream } from "../../http/sse.ts"; |
| 20 | 20 | import type { SystemView } from "../../system/view.ts"; |
| 21 | 21 | import { live } from "../../system/live.ts"; |
| 22 | import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts"; | |
| 22 | 23 | import { notFoundPage, signInFirst } from "../chrome.ts"; |
| 23 | 24 | import { html, raw, type Html } from "../html.ts"; |
| 24 | 25 | import { page } from "../layout.ts"; |
| @@ -67,7 +68,7 @@ const hhmm = (at: number) => new Date(at).toISOString().slice(0, 16).replace("T" | ||
| 67 | 68 | /** The forge's RSS beside its JS heap, an hour a row; empty for anyone `canSeeMemory` refuses. */ |
| 68 | 69 | function memorySection(ctx: Ctx): Html { |
| 69 | 70 | if (!canSeeMemory(ctx.auth.user)) return html``; |
| 70 | const hours = memoryHours(ctx.app.db, Date.now()); | |
| 71 | const hours = timePhase(ctx.request, "memory", () => memoryHours(ctx.app.db, Date.now())); | |
| 71 | 72 | const series = (title: string, pick: (h: (typeof hours)[number]) => number) => |
| 72 | 73 | card( |
| 73 | 74 | html`<div class="chart-box">${raw(drawChart("line", hours.map((h) => ({ label: hhmm(h.at).slice(11), value: pick(h) / 1_048_576 })), title))}</div>`, |
| @@ -145,14 +146,17 @@ export function systemStream(ctx: Ctx): Response { | ||
| 145 | 146 | // GET /system |
| 146 | 147 | export function systemPage(ctx: Ctx): Response { |
| 147 | 148 | if (!ctx.auth.user) return signInFirst(ctx); |
| 148 | const reader = telemetryReader(ctx.app, ctx.auth.user); | |
| 149 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 150 | const reader = timePhase(ctx.request, "reader", () => telemetryReader(ctx.app, ctx.auth.user!)); | |
| 149 | 151 | if (reader?.kind !== "owner") return notFoundPage(ctx); |
| 150 | const v = live().read(); | |
| 151 | const holder = memoryCapHolder(); | |
| 152 | const v = timePhase(ctx.request, "sample", () => live().read()); | |
| 153 | const holder = timePhase(ctx.request, "caps", () => memoryCapHolder()); | |
| 152 | 154 | const caps: Html = |
| 153 | 155 | holder.by === "kernel" |
| 154 | 156 | ? html`<p class="small" id="cell-caps" data-by="kernel">Each cell's memory is capped by the kernel (its own cgroup's <code>memory.max</code>): a cell over it is killed alone.</p>` |
| 155 | 157 | : html`<p class="small" id="cell-caps" data-by="watch">Each cell's memory is watched once a second and the cell stopped over its cap; the kernel does not cap it (${holder.reason}).</p>`; |
| 158 | const memory = memorySection(ctx); | |
| 159 | const rendering = performance.now(); | |
| 156 | 160 | const r = regions(v); |
| 157 | 161 | const at = (id: string): Html => raw(r[id]!); |
| 158 | 162 | const body = html`<div class="page-head"> |
| @@ -178,6 +182,8 @@ export function systemPage(ctx: Ctx): Response { | ||
| 178 | 182 | <p class="small muted"><span class="js-only">Sort by <button class="btn" data-size="sm" type="button" data-sort="cpu" aria-pressed="true"><kbd>c</kbd> CPU</button> |
| 179 | 183 | <button class="btn" data-size="sm" type="button" data-sort="mem" aria-pressed="false"><kbd>m</kbd> memory</button></span></p></div> |
| 180 | 184 | <div id="sys-procs">${at("sys-procs")}</div> |
| 181 | ${memorySection(ctx)}`; | |
| 182 | return page({ ctx, title: "System", body, wide: true, scriptJs: systemLiveJs }); | |
| 185 | ${memory}`; | |
| 186 | const response = page({ ctx, title: "System", body, wide: true, scriptJs: systemLiveJs }); | |
| 187 | notePhase(ctx.request, "render", performance.now() - rendering); | |
| 188 | return response; | |
| 183 | 189 | } |
test/system-timing.test.ts added +58
| @@ -0,0 +1,58 @@ | ||
| 1 | /** `/system` names session, reader, sample, caps, memory and render in Server-Timing (#3273). */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp } from "../src/app.ts"; | |
| 7 | import { createToken } from "../src/core/tokens.ts"; | |
| 8 | import { createUser } from "../src/core/users.ts"; | |
| 9 | import { serve } from "../src/http/server.ts"; | |
| 10 | ||
| 11 | let dir: string; | |
| 12 | let app: ReturnType<typeof createApp>; | |
| 13 | let servers: Awaited<ReturnType<typeof serve>>; | |
| 14 | let token = ""; | |
| 15 | ||
| 16 | beforeAll(async () => { | |
| 17 | dir = await mkdtemp(join(tmpdir(), "forge-system-timing-")); | |
| 18 | app = createApp({ dataDir: dir, port: 0 }); | |
| 19 | servers = await serve(app); | |
| 20 | const r = await createUser(app.db, { | |
| 21 | origin: "seed", | |
| 22 | trusted: true, | |
| 23 | username: "alice", | |
| 24 | email: "[email protected]", | |
| 25 | password: "a-good-passphrase", | |
| 26 | adminIfFirst: true, | |
| 27 | }); | |
| 28 | if (!r.ok) throw new Error(r.error); | |
| 29 | token = createToken(app.db, r.value.id, "t", "admin").token; | |
| 30 | }); | |
| 31 | afterAll(async () => { | |
| 32 | await servers.stop(); | |
| 33 | await rm(dir, { recursive: true, force: true }); | |
| 34 | }); | |
| 35 | ||
| 36 | test("GET /system names its phases", async () => { | |
| 37 | const res = await fetch(`${servers.url}/system`, { headers: { authorization: `Bearer ${token}` } }); | |
| 38 | expect(res.status).toBe(200); | |
| 39 | const parts = (res.headers.get("server-timing") ?? "").split(", "); | |
| 40 | for (const name of ["app", "session", "reader", "sample", "caps", "memory", "render"]) | |
| 41 | expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/); | |
| 42 | }); | |
| 43 | ||
| 44 | test("a non-owner gets the 404 page and no sample or memory phase", async () => { | |
| 45 | const r = await createUser(app.db, { | |
| 46 | origin: "everyday", | |
| 47 | username: "bob", | |
| 48 | email: "[email protected]", | |
| 49 | password: "a-good-passphrase", | |
| 50 | }); | |
| 51 | if (!r.ok) throw new Error(r.error); | |
| 52 | const bob = createToken(app.db, r.value.id, "t", "admin").token; | |
| 53 | const res = await fetch(`${servers.url}/system`, { headers: { authorization: `Bearer ${bob}` } }); | |
| 54 | expect(res.status).toBe(404); | |
| 55 | const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]); | |
| 56 | expect(names).toContain("reader"); | |
| 57 | for (const name of ["sample", "caps", "memory", "render"]) expect(names).not.toContain(name); | |
| 58 | }); |
new above
offline