Public: anyone can read this; the crew can change it.

/system's Server-Timing names its phases: session, each read, render #4004

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/3273-system-s-server-timing-names-its-phases → main 3 commits fork

  • Merged. scout/3273-system-s-server-timing-names-its-phases was merged into main as 0a657e2.

Review

Sign in to review this change.
2 files changed +70 −6
src/web/pages/system.ts +12 −6
@@ -19,6 +19,7 @@ import { bytes, spark, span } from "../../system/format.ts";
1919 import { eventStream } from "../../http/sse.ts";
2020 import type { SystemView } from "../../system/view.ts";
2121 import { live } from "../../system/live.ts";
22 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
2223 import { notFoundPage, signInFirst } from "../chrome.ts";
2324 import { html, raw, type Html } from "../html.ts";
2425 import { page } from "../layout.ts";
@@ -67,7 +68,7 @@ const hhmm = (at: number) => new Date(at).toISOString().slice(0, 16).replace("T"
6768 /** The forge's RSS beside its JS heap, an hour a row; empty for anyone `canSeeMemory` refuses. */
6869 function memorySection(ctx: Ctx): Html {
6970 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()));
7172 const series = (title: string, pick: (h: (typeof hours)[number]) => number) =>
7273 card(
7374 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 {
145146 // GET /system
146147 export function systemPage(ctx: Ctx): Response {
147148 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!));
149151 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());
152154 const caps: Html =
153155 holder.by === "kernel"
154156 ? 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>`
155157 : 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();
156160 const r = regions(v);
157161 const at = (id: string): Html => raw(r[id]!);
158162 const body = html`<div class="page-head">
@@ -178,6 +182,8 @@ export function systemPage(ctx: Ctx): Response {
178182 <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>
179183 <button class="btn" data-size="sm" type="button" data-sort="mem" aria-pressed="false"><kbd>m</kbd> memory</button></span></p></div>
180184 <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;
183189 }
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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs