Public: anyone can read this; the crew can change it.
/system lists each first-opened page's warm median beside its budget, one plain line per page #11567
scout/forge:scout/11544-system-lists-each-first-opened-page-s-wa → main 7 commits fork
7 files
changed +94
−2
docs/features/page-budget.md added +3
| @@ -0,0 +1,3 @@ | ||
| 1 | # Page budget on /system | |
| 2 | ||
| 3 | /system lists each first-opened page from `PAGE_BUDGETS` with its warm median of the last 15 minutes (from the in-memory perf record, no SQL) beside its budget, one plain line per page. A page over its budget reads "map answers in 82 ms, budget 50 ms"; a page nobody opened reads "not opened in the last 15 minutes". `src/perf/page_budget_read.ts` makes the rows. |
scripts/unreached-allow.json −1
| @@ -1,5 +1,4 @@ | ||
| 1 | 1 | [ |
| 2 | "src/perf/page_budget.ts", | |
| 3 | 2 | "src/spawn/pod.ts", |
| 4 | 3 | "src/tui/ui/qr.ts", |
| 5 | 4 | "src/web/studio/held_posts.ts" |
src/perf/page_budget_read.ts added +29
| @@ -0,0 +1,29 @@ | ||
| 1 | /** | |
| 2 | * The page budget read against what the forge measured (#11544): one row per | |
| 3 | * page in PAGE_BUDGETS order, with the warm median of the last 15 minutes from | |
| 4 | * the in-memory perf record. A page nobody opened reads `medianMs: null`, never 0. | |
| 5 | * Pure: the caller hands over the samples it already holds, no SQL. | |
| 6 | */ | |
| 7 | import { PAGE_BUDGETS } from "./page_budget.ts"; | |
| 8 | import { WINDOW_MS, summarize, type Sample } from "./record.ts"; | |
| 9 | ||
| 10 | export interface PageBudgetRow { | |
| 11 | readonly page: string; | |
| 12 | readonly pattern: string; | |
| 13 | readonly budgetMs: number; | |
| 14 | readonly statements: number; | |
| 15 | readonly medianMs: number | null; | |
| 16 | readonly n: number; | |
| 17 | /** The median is past the budget. */ | |
| 18 | readonly over: boolean; | |
| 19 | } | |
| 20 | ||
| 21 | /** Samples keyed by router pattern; only those within the window count. */ | |
| 22 | export function pageBudgetRows(samples: ReadonlyMap<string, readonly Sample[]>, now: number): PageBudgetRow[] { | |
| 23 | const since = now - WINDOW_MS; | |
| 24 | return PAGE_BUDGETS.map((b) => { | |
| 25 | const stat = summarize((samples.get(b.pattern) ?? []).filter((s) => s.at >= since)); | |
| 26 | const medianMs = stat === null ? null : stat.p50; | |
| 27 | return { page: b.page, pattern: b.pattern, budgetMs: b.medianMs, statements: b.statements, medianMs, n: stat?.n ?? 0, over: medianMs !== null && medianMs > b.medianMs }; | |
| 28 | }); | |
| 29 | } |
src/web/pages/system-budget.ts added +20
| @@ -0,0 +1,20 @@ | ||
| 1 | /** /system's page-budget section (#11544): each first-opened page's warm median beside its budget, one plain line per page. */ | |
| 2 | import type { PageBudgetRow } from "../../perf/page_budget_read.ts"; | |
| 3 | import { html, type Html } from "../html.ts"; | |
| 4 | ||
| 5 | /** One line in words; an over-budget page says so plainly, no banner. */ | |
| 6 | export function budgetLine(r: PageBudgetRow): string { | |
| 7 | if (r.medianMs === null) return `${r.page}: not opened in the last 15 minutes (budget ${String(r.budgetMs)} ms)`; | |
| 8 | const ms = String(Math.round(r.medianMs)); | |
| 9 | return r.over | |
| 10 | ? `${r.page} answers in ${ms} ms, budget ${String(r.budgetMs)} ms` | |
| 11 | : `${r.page} answers in ${ms} ms, within its ${String(r.budgetMs)} ms budget`; | |
| 12 | } | |
| 13 | ||
| 14 | export function budgetSection(rows: readonly PageBudgetRow[]): Html { | |
| 15 | const over = rows.filter((r) => r.over).length; | |
| 16 | const head = over === 0 ? "Page budgets: all met" : `Page budgets: ${String(over)} over`; | |
| 17 | return html`<section class="settings-block" id="page-budget"><h2>First-opened pages against their budget</h2> | |
| 18 | <details data-over="${String(over)}"><summary>${head}</summary> | |
| 19 | <ul class="list" aria-label="Warm median of each first-opened page beside its budget">${rows.map((r) => html`<li data-page="${r.page}" data-over="${r.over ? "1" : "0"}">${budgetLine(r)}</li>`)}</ul></details></section>`; | |
| 20 | } |
src/web/pages/system.ts +4 −1
| @@ -33,7 +33,9 @@ import { readerPaces, type ReaderPace } from "../../system/telemetry_reads.ts"; | ||
| 33 | 33 | import { slowPageLine, slowPages, type SlowPage } from "../../perf/watch.ts"; |
| 34 | 34 | import { stallHour } from "../../metrics/stalls.ts"; |
| 35 | 35 | import { HOUR_MS, type RouteStall } from "../../perf/loop.ts"; |
| 36 | import { lagMinutes, loopWatch, stallBlame } from "../../perf/live.ts"; | |
| 36 | import { lagMinutes, loopWatch, perfRecord, stallBlame } from "../../perf/live.ts"; | |
| 37 | import { pageBudgetRows } from "../../perf/page_budget_read.ts"; | |
| 38 | import { budgetSection } from "./system-budget.ts"; | |
| 37 | 39 | import { LAG_BAR_MS, LAG_READINGS, overBar, type LagReading } from "../../perf/lag_minutes.ts"; |
| 38 | 40 | import { WORST_STALLS, type BlamedStall } from "../../perf/stall_blame.ts"; |
| 39 | 41 | import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts"; |
| @@ -404,6 +406,7 @@ export async function systemPage(ctx: Ctx): Promise<Response> { | ||
| 404 | 406 | ${featuresSection(timePhase(ctx.request, "features", () => featureHealth(ctx.app, reader)))} |
| 405 | 407 | ${lagSection(lagMinutes.readings())} |
| 406 | 408 | <div id="sys-stalls">${stallSection(stalls, worstStallsTable(worst, nowMono, Date.now()))}</div> |
| 409 | ${budgetSection(pageBudgetRows(new Map(perfRecord.patterns().map((p) => [p, perfRecord.traffic(p, 0)])), Date.now()))} | |
| 407 | 410 | <div id="sys-slow-pages">${slowPagesSection(timePhase(ctx.request, "slowpages", () => slowPagesCached(ctx.app.db)))}</div> |
| 408 | 411 | <div id="sys-readers">${readersSection(readerPaces())}</div> |
| 409 | 412 | <div id="sys-caches">${cachesSection(pageCaches())}</div> |
test/page-budget-read.test.ts added +37
| @@ -0,0 +1,37 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { PAGE_BUDGETS } from "../src/perf/page_budget.ts"; | |
| 3 | import { pageBudgetRows } from "../src/perf/page_budget_read.ts"; | |
| 4 | import { WINDOW_MS, type Sample } from "../src/perf/record.ts"; | |
| 5 | import { budgetLine, budgetSection } from "../src/web/pages/system-budget.ts"; | |
| 6 | ||
| 7 | const NOW = 10_000_000; | |
| 8 | const s = (ms: number, at = NOW - 1000): Sample => ({ at, ms, signedIn: true, phases: [] }); | |
| 9 | ||
| 10 | test("rows follow PAGE_BUDGETS; unsampled reads null, never 0", () => { | |
| 11 | const rows = pageBudgetRows(new Map(), NOW); | |
| 12 | expect(rows.map((r) => r.pattern)).toEqual(PAGE_BUDGETS.map((b) => b.pattern)); | |
| 13 | for (const r of rows) { | |
| 14 | expect(r.medianMs).toBeNull(); | |
| 15 | expect(r.n).toBe(0); | |
| 16 | expect(r.over).toBe(false); | |
| 17 | } | |
| 18 | }); | |
| 19 | ||
| 20 | test("over is true only past the budget; old samples do not count", () => { | |
| 21 | const budget = PAGE_BUDGETS[0]!; | |
| 22 | const at = (ms: number) => pageBudgetRows(new Map([[budget.pattern, [s(ms), s(ms), s(ms)]]]), NOW)[0]!; | |
| 23 | expect(at(budget.medianMs).over).toBe(false); | |
| 24 | expect(at(budget.medianMs + 1).over).toBe(true); | |
| 25 | expect(at(82).medianMs).toBe(82); | |
| 26 | const old = pageBudgetRows(new Map([[budget.pattern, [s(500, NOW - WINDOW_MS - 1)]]]), NOW)[0]!; | |
| 27 | expect(old.medianMs).toBeNull(); | |
| 28 | }); | |
| 29 | ||
| 30 | test("the section shows each page name and says it in words", () => { | |
| 31 | const rows = pageBudgetRows(new Map([["/map", [s(82)]]]), NOW); | |
| 32 | const out = String(budgetSection(rows)); | |
| 33 | for (const b of PAGE_BUDGETS) expect(out).toContain(b.page); | |
| 34 | expect(out).toContain("map answers in 82 ms, budget 50 ms"); | |
| 35 | expect(out).toContain("not opened in the last 15 minutes"); | |
| 36 | expect(budgetLine(rows[0]!)).toContain("not opened"); | |
| 37 | }); |
test/system-parts.test.ts +1
| @@ -54,6 +54,7 @@ test("one <h2> per part, each in a settings-block, and no card", async () => { | ||
| 54 | 54 | "Processes", |
| 55 | 55 | "Loop lag, last 20 minutes", |
| 56 | 56 | "Main-thread stalls, last hour", |
| 57 | "First-opened pages against their budget", | |
| 57 | 58 | "Telemetry readers", |
| 58 | 59 | "Code page caches, since start", |
| 59 | 60 | "Forge memory", |