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

Server-Timing on / names each part of readKpis: landed, live, checks, builders, spend, reach, backup #7689

Merged opened this

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

Merged

scout/forge:scout/7626-server-timing-on-names-each-part-of-read → main 2 commits fork 1 thread · 1 unresolved

  • Merged. scout/7626-server-timing-on-names-each-part-of-read was merged into main as 9d2c846.

Review

Sign in to review this change.
6 files changed +49 −21
src/core/route_timing.ts +2 −2
@@ -217,8 +217,8 @@ export interface Phase {
217217 readonly hit?: boolean;
218218 }
219219
220 /** A phase name is a header token: lowercase letters, nothing a viewer chose. */
221 const PHASE_NAME = /^[a-z][a-z0-9-]{0,15}$/;
220 /** A phase name is a header token: lowercase letters, a dot for a part (`kpis.live`), nothing a viewer chose. */
221 const PHASE_NAME = /^[a-z][a-z0-9.-]{0,15}$/;
222222
223223 const phaseLists = new WeakMap<Request, Phase[]>();
224224
src/flow/kpis.ts +28 −12
@@ -7,6 +7,9 @@
77 * last hour, bucket 23 the one that ended 23 hours ago. Nothing here reads
88 * the clock. One query per figure.
99 *
10 * `time`, when given, wraps each part so a caller can name what it cost
11 * (`kpis.landed` … `kpis.backup`, #7626); left out, each part just runs.
12 *
1013 * A landing counts when it put something on main. A pull that closed as
1114 * carried (its change was already there, #2036) lands no commit: its
1215 * `merged_oid` is the base it found, or a commit an earlier pull landed.
@@ -41,17 +44,25 @@ export interface Kpis {
4144 readonly reach: Reach;
4245 }
4346
47 /** The parts `readKpis` names to its `time` callback, in the order it runs them. */
48 export const KPI_PARTS = ["kpis.landed", "kpis.live", "kpis.checks", "kpis.builders", "kpis.spend", "kpis.reach", "kpis.backup"] as const;
49 export type KpiPart = (typeof KPI_PARTS)[number];
50 export type KpiTimer = <T>(name: KpiPart, fn: () => T) => T;
51
52 const untimed: KpiTimer = (_name, fn) => fn();
53
4454 export function readKpis(
4555 db: Database,
4656 now: Date,
4757 forge: { readonly ceilingUsd: number | null; readonly dataDir?: string } = { ceilingUsd: null },
58 time: KpiTimer = untimed,
4859 ): Kpis {
4960 const end = now.getTime();
5061 const to = now.toISOString();
5162 const since = new Date(end - HOURS * HOUR_MS).toISOString();
5263
5364 const landedHours = new Array<number>(HOURS).fill(0);
54 const landings = db
65 const landings = time("kpis.landed", () => db
5566 .query<{ at: string }, [string, string]>(
5667 `SELECT p.merged_at AS at
5768 FROM pull p JOIN issue i ON i.id = p.issue_id JOIN repo r ON r.id = i.repo_id
@@ -63,45 +74,50 @@ export function readKpis(
6374 WHERE j.repo_id = i.repo_id AND q.merged_oid = p.merged_oid
6475 AND (q.merged_at < p.merged_at OR (q.merged_at = p.merged_at AND q.issue_id < p.issue_id)))`,
6576 )
66 .all(since, to);
77 .all(since, to));
6778 for (const { at } of landings) {
6879 const h = Math.floor((end - Date.parse(at)) / HOUR_MS);
6980 if (h >= 0 && h < HOURS) landedHours[h]! += 1;
7081 }
7182
72 const live = db
83 const live = time("kpis.live", () => db
7384 .query<{ sha: string; at: string }, [string]>(
7485 `SELECT commit_oid AS sha, serving_at AS at FROM run
7586 WHERE kind = 'deploy' AND env = 'production' AND serving_at IS NOT NULL AND serving_at <= ?1
7687 ORDER BY serving_at DESC LIMIT 1`,
7788 )
78 .get(to);
89 .get(to));
7990
80 const spans = db
91 const spans = time("kpis.checks", () => db
8192 .query<{ ms: number }, [string, string]>(
8293 `SELECT (julianday(finished_at) - julianday(started_at)) * 86400000.0 AS ms FROM run
8394 WHERE kind = 'check' AND state IN ('ok','failed') AND started_at IS NOT NULL
8495 AND finished_at >= ?1 AND finished_at <= ?2`,
8596 )
8697 .all(since, to)
87 .map((r) => Math.round(r.ms));
98 .map((r) => Math.round(r.ms)));
8899
89 const builders = db
100 const builders = time("kpis.builders", () => db
90101 .query<{ running: number; slots: number }, []>(
91102 `SELECT (SELECT COUNT(*) FROM run WHERE kind = 'agent' AND state = 'running') AS running,
92103 (SELECT COALESCE(SUM(autonomy_slots), 0) FROM repo WHERE autonomy != 'off') AS slots`,
93104 )
94 .get() ?? { running: 0, slots: 0 };
105 .get() ?? { running: 0, slots: 0 });
95106
96 const todayUsd = spentTodayEverywhere(db, now);
107 const spend = time("kpis.spend", () => {
108 const todayUsd = spentTodayEverywhere(db, now);
109 return { todayUsd, ceilingUsd: forge.ceilingUsd, hitAt: ceilingReached(todayUsd, forge.ceilingUsd) ? ceilingHitAt(db, now, forge.ceilingUsd) : null };
110 });
97111
98 const backupAt = forge.dataDir === undefined ? null : lastOffBox(forge.dataDir);
112 const reach = time("kpis.reach", () => readReach(db, now));
113
114 const backupAt = time("kpis.backup", () => (forge.dataDir === undefined ? null : lastOffBox(forge.dataDir)));
99115
100116 return {
101117 backupAt,
102118 builders,
103 reach: readReach(db, now),
104 spend: { todayUsd, ceilingUsd: forge.ceilingUsd, hitAt: ceilingReached(todayUsd, forge.ceilingUsd) ? ceilingHitAt(db, now, forge.ceilingUsd) : null },
119 reach,
120 spend,
105121 landedHours,
106122 landed: landedHours.reduce((a, b) => a + b, 0),
107123 live: live ?? null,
src/web/studio/home.ts +4 −1
@@ -59,6 +59,7 @@ import { feedCandidateCache } from "../../feed/feed.ts";
5959 import { notePhase, notePhaseRest, takeSessionTime, timePhase, timePhaseAsync } from "../../core/route_timing.ts";
6060 import { idleReason } from "../../crew/idle_reason.ts";
6161 import { kpiCache, panelCache, waitingCache } from "./home_cache.ts";
62 import type { KpiTimer } from "../../flow/kpis.ts";
6263 import { archivedAppCount } from "../pages/apps.ts";
6364 import { pinnedPanels } from "./pinned.ts";
6465 import { isNavigation, lastVisit, noteVisit } from "../../core/home_visit.ts";
@@ -286,7 +287,9 @@ export async function studioHome(
286287 const ask = firstSteps ?? everydayCard(ctx, user);
287288 const figures = user.isAdmin && steps === undefined;
288289 const ceilingUsd = forgeCeiling(ctx.app.config);
289 const kpiOpts = { idle: figures ? idleReason(ctx.app, new Date()) : null, dataDir: ctx.app.config.dataDir, ...(ceilingUsd === null ? {} : { ceilingUsd }) };
290 // Each part of a cold read is its own Server-Timing entry (#7626); a cache hit names none.
291 const kpiTime: KpiTimer = (name, fn) => timePhase(ctx.request, name, fn);
292 const kpiOpts = { idle: figures ? idleReason(ctx.app, new Date()) : null, dataDir: ctx.app.config.dataDir, time: kpiTime, ...(ceilingUsd === null ? {} : { ceilingUsd }) };
290293 const body = html`<h1 class="sr-only">Home</h1>
291294 ${ask} ${address} ${figures
292295 ? timePhase(ctx.request, "kpis", () => kpiRow(ctx.app.db, kpiOpts), () => kpiCache(ctx.app.db))
src/web/studio/home_cache.ts +4 −3
@@ -10,7 +10,7 @@
1010 import type { Database } from "bun:sqlite";
1111 import type { Answer } from "../../boards/query.ts";
1212 import { writeStamp } from "../../core/write_clock.ts";
13 import { readKpis, type Kpis } from "../../flow/kpis.ts";
13 import { readKpis, type KpiTimer, type Kpis } from "../../flow/kpis.ts";
1414 import { readVitals, type Vitals } from "../../flow/vitals.ts";
1515 import { Stamped } from "../../kernel/stamped.ts";
1616
@@ -63,11 +63,12 @@ export function cachedKpis(
6363 db: Database,
6464 now: Date,
6565 forge: { readonly ceilingUsd: number | null; readonly dataDir?: string },
66 time?: KpiTimer,
6667 ): Kpis {
6768 const stamp = writeStamp(db);
68 if (stamp === undefined) return readKpis(db, now, forge);
69 if (stamp === undefined) return readKpis(db, now, forge, time);
6970 const key = `kpi:${now.toISOString().slice(0, 13)}:${forge.ceilingUsd ?? "none"}:${forge.dataDir ?? ""}`;
70 return kpiCache(db).get(key, stamp, () => readKpis(db, now, forge));
71 return kpiCache(db).get(key, stamp, () => readKpis(db, now, forge, time));
7172 }
7273
7374 /**
src/web/studio/kpi_row.ts +3 −3
@@ -10,7 +10,7 @@
1010 import type { Database } from "bun:sqlite";
1111 import { backupAge, backupFull, backupOverdue, buildersValue, checkTime, reactNote, speedWords, spendCeilingWords, spendFull, spendValue, usersValue, visitorsNote, visitorsValue, waitingWords } from "../../flow/kpi_words.ts";
1212 import type { IdleReason } from "../../crew/idle_reason.ts";
13 import type { Kpis } from "../../flow/kpis.ts";
13 import type { KpiTimer, Kpis } from "../../flow/kpis.ts";
1414 import { clogLines, readClog } from "../../flow/clog_read.ts";
1515 import { vitalsLine } from "../../flow/vitals.ts";
1616 import { cachedKpis, cachedVitals, cachedWaiting } from "./home_cache.ts";
@@ -165,10 +165,10 @@ export function kpiRowFor(k: Kpis, live: { sha: string; at: string } | null, now
165165 }
166166
167167 /** The figures count every repository, so the caller shows this to an admin only. */
168 export function kpiRow(db: Database, opts: { ceilingUsd?: number; dataDir?: string; idle?: IdleReason | null; now?: Date } = {}): Html {
168 export function kpiRow(db: Database, opts: { ceilingUsd?: number; dataDir?: string; idle?: IdleReason | null; now?: Date; time?: KpiTimer } = {}): Html {
169169 const now = opts.now ?? new Date();
170170 const forge = { ceilingUsd: opts.ceilingUsd ?? null, ...(opts.dataDir === undefined ? {} : { dataDir: opts.dataDir }) };
171 return kpiRowFor(cachedKpis(db, now, forge), liveNow(), now, opts.idle ?? null);
171 return kpiRowFor(cachedKpis(db, now, forge, opts.time), liveNow(), now, opts.idle ?? null);
172172 }
173173
174174 /**
test/home-timing.test.ts +8
@@ -1,6 +1,7 @@
11 /** `/`'s Server-Timing names its regions, and only the ones the viewer can see. */
22 import { afterAll, beforeAll, expect, test } from "bun:test";
33 import { serverTiming } from "../src/core/route_timing.ts";
4 import { KPI_PARTS } from "../src/flow/kpis.ts";
45 import { snappyFixture, type SnappyFixture } from "../bench/snappy.ts";
56
67 let f: SnappyFixture;
@@ -19,6 +20,12 @@ async function timing(cookie?: string): Promise<string[]> {
1920 }
2021 const names = (parts: string[]): string[] => parts.map((p) => p.split(";")[0]!);
2122
23 test("a cold look at the owner's home names each part of the kpis read", async () => {

This test passes only because it runs before any other owner request in the file. Reset the KPI cache at the start so reordering cannot flake it.

24 const cold = names(await timing(f.ownerCookie));
25 expect(KPI_PARTS).toEqual(["kpis.landed", "kpis.live", "kpis.checks", "kpis.builders", "kpis.spend", "kpis.reach", "kpis.backup"]);
26 expect(cold).toEqual(expect.arrayContaining(["kpis", ...KPI_PARTS]));
27 });
28
2229 test("the owner's home names feed, kpis and panels; a warm look is a hit", async () => {
2330 await timing(f.ownerCookie);
2431 const warm = await timing(f.ownerCookie);
@@ -32,6 +39,7 @@ test("a signed-in non-admin gets the feed and none of the admin-only phases", as
3239 const got = names(await timing(f.peerCookie));
3340 expect(got).toContain("feed");
3441 expect(got).not.toContain("kpis");
42 for (const part of KPI_PARTS) expect(got).not.toContain(part);
3543 expect(got).not.toContain("panels");
3644 });
3745

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs