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

The direction's number says where its seconds went #129

Merged opened this

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

Merged

scout/forge:scout/122-the-direction-s-number-says-where-its-se → main 1 commit fork

  • Merged. scout/122-the-direction-s-number-says-where-its-se was merged into main as ca39d2b.

Review

Sign in to review this change.
3 files changed +172 −19
src/core/runs.ts +48 −14
@@ -616,19 +616,36 @@ export function latestAgentRunsForRepos(
616616 return out;
617617 }
618618
619 /** How fast this app's deploys have reached their preview: the median, and the count it is drawn from. */
619 /**
620 * How fast this app's deploys have reached their preview: the total median
621 * and the count it is drawn from, plus the same walk split at `started_at`
622 * — the wait to be picked up, and the boot once it was. The split is its
623 * own two medians, not half of the total's; they will not sum to it.
624 */
620625 export interface PreviewLatency {
621626 readonly medianSeconds: number;
622627 readonly n: number;
628 /** Undefined when no served deploy in the window also has a `started_at`. */
629 readonly queueMedianSeconds: number | undefined;
630 readonly bootMedianSeconds: number | undefined;
631 /** How many of `n` carried a `started_at` and so fed the split. */
632 readonly splitN: number;
623633 }
624634
635 const median = (xs: readonly number[]): number => {
636 const mid = Math.floor(xs.length / 2);
637 return xs.length % 2 === 0 ? (xs[mid - 1]! + xs[mid]!) / 2 : xs[mid]!;
638 };
639
625640 /**
626641 * The median seconds from push (`created_at`) to serving (`serving_at`)
627642 * over this repo's deploys since `since` — the same reference instant
628643 * `servedPhrase` (`web/ui.ts`) words a single deploy from. A deploy that
629 * never served says nothing about speed and is left out, never a zero.
630 * One query, bounded by `limit` and served by the `run_repo` index (it
631 * walks newest-first and stops at the cap), never a scan of the table.
644 * never served says nothing about speed and is left out, never a zero;
645 * one that served but never records a `started_at` feeds the total but
646 * not the split. One query, bounded by `limit` and served by the
647 * `run_repo` index (it walks newest-first and stops at the cap), never a
648 * scan of the table.
632649 */
633650 export function previewLatency(
634651 db: Database,
@@ -636,22 +653,39 @@ export function previewLatency(
636653 since: string,
637654 limit = 500,
638655 ): PreviewLatency | undefined {
639 const secs = db
640 .query<{ secs: number }, [string, string, number]>(
641 `SELECT (julianday(serving_at) - julianday(created_at)) * 86400 AS secs
656 const rows = db
657 .query<
658 { secs: number; queueSecs: number | null; bootSecs: number | null },
659 [string, string, number]
660 >(
661 `SELECT (julianday(serving_at) - julianday(created_at)) * 86400 AS secs,
662 CASE WHEN started_at IS NOT NULL
663 THEN (julianday(started_at) - julianday(created_at)) * 86400 END AS queueSecs,
664 CASE WHEN started_at IS NOT NULL
665 THEN (julianday(serving_at) - julianday(started_at)) * 86400 END AS bootSecs
642666 FROM run
643667 WHERE repo_id = ?1 AND kind = 'deploy' AND serving_at IS NOT NULL
644668 AND created_at >= ?2
645669 ORDER BY id DESC LIMIT ?3`,
646670 )
647 .all(repoId, since, limit)
648 .map((r) => r.secs)
671 .all(repoId, since, limit);
672 if (rows.length === 0) return undefined;
673 const secs = rows.map((r) => r.secs).sort((a, b) => a - b);
674 const queueSecs = rows
675 .map((r) => r.queueSecs)
676 .filter((s): s is number => s !== null)
649677 .sort((a, b) => a - b);
650 if (secs.length === 0) return undefined;
651 const mid = Math.floor(secs.length / 2);
652 const medianSeconds =
653 secs.length % 2 === 0 ? (secs[mid - 1]! + secs[mid]!) / 2 : secs[mid]!;
654 return { medianSeconds, n: secs.length };
678 const bootSecs = rows
679 .map((r) => r.bootSecs)
680 .filter((s): s is number => s !== null)
681 .sort((a, b) => a - b);
682 return {
683 medianSeconds: median(secs),
684 n: secs.length,
685 queueMedianSeconds: queueSecs.length === 0 ? undefined : median(queueSecs),
686 bootMedianSeconds: bootSecs.length === 0 ? undefined : median(bootSecs),
687 splitN: queueSecs.length,
688 };
655689 }
656690
657691 /** Agent runs queued or running across many repositories, newest first. */
src/web/studio/app.ts +13 −2
@@ -78,15 +78,26 @@ const PREVIEW_LATENCY_MIN = 3;
7878 * How fast a change has reached its preview this week, worded like the
7979 * reference instant in `servedPhrase` (`web/ui.ts`): "reaches its preview
8080 * in 6.1s". Too few served deploys to trust a median, and the line says so
81 * rather than printing one from a run or two.
81 * rather than printing one from a run or two. Below the total, the same
82 * walk split at `started_at` — waiting to be picked up, then coming up —
83 * each its own median, not a piece of the total that ought to add up.
8284 */
8385 function previewLatencyWords(latency: PreviewLatency | undefined): Html {
8486 if (latency === undefined || latency.n < PREVIEW_LATENCY_MIN)
8587 return html`<span class="muted small"
8688 >not enough deploys this week to say</span
8789 >`;
90 const split =
91 latency.queueMedianSeconds === undefined ||
92 latency.bootMedianSeconds === undefined
93 ? null
94 : html`${latency.queueMedianSeconds.toFixed(1)}s waiting,
95 ${latency.bootMedianSeconds.toFixed(1)}s coming up${latency.splitN <
96 latency.n
97 ? ` (${latency.splitN} of ${latency.n})`
98 : ""} · `;
8899 return html`reaches its preview in ${latency.medianSeconds.toFixed(1)}s
89 <span class="muted">· ${latency.n} deploys</span>`;
100 <span class="muted">· ${split}${latency.n} deploys</span>`;
90101 }
91102
92103 export async function studioApp(
test/studio.test.ts +111 −3
@@ -3184,12 +3184,21 @@ describe("previewLatency: the median seconds from push to serving", () => {
31843184 await rm(dataDir, { recursive: true, force: true });
31853185 });
31863186
3187 /** A deploy run stamped with a known push instant and, unless null, a known serving instant. */
3188 const seedDeploy = (createdAt: string, servingAt: string | null): void => {
3187 /** A deploy run stamped with a known push instant and, unless null, known started/serving instants. */
3188 const seedDeploy = (
3189 createdAt: string,
3190 startedAt: string | null,
3191 servingAt: string | null,
3192 ): void => {
31893193 const deploy = createRun(app.db, { repoId: repo.id, kind: "deploy" });
31903194 app.db
31913195 .query("UPDATE run SET created_at = ?2 WHERE id = ?1")
31923196 .run(deploy.id, createdAt);
3197 if (startedAt !== null) {
3198 app.db
3199 .query("UPDATE run SET started_at = ?2 WHERE id = ?1")
3200 .run(deploy.id, startedAt);
3201 }
31933202 if (servingAt !== null) {
31943203 app.db
31953204 .query("UPDATE run SET serving_at = ?2 WHERE id = ?1")
@@ -3207,12 +3216,13 @@ describe("previewLatency: the median seconds from push to serving", () => {
32073216 // Before the window: a very slow deploy that must not pull the median up.
32083217 seedDeploy(
32093218 new Date(now - 10 * 24 * 60 * 60_000).toISOString(),
3219 null,
32103220 new Date(now - 10 * 24 * 60 * 60_000 + 600_000).toISOString(),
32113221 );
32123222 expect(previewLatency(app.db, repo.id, since)).toBeUndefined();
32133223
32143224 // A deploy that never served is left out, not counted as a zero.
3215 seedDeploy(new Date(now).toISOString(), null);
3225 seedDeploy(new Date(now).toISOString(), null, null);
32163226 expect(previewLatency(app.db, repo.id, since)).toBeUndefined();
32173227
32183228 // Three served deploys, seeded out of order: an odd-count median.
@@ -3221,6 +3231,7 @@ describe("previewLatency: the median seconds from push to serving", () => {
32213231 const createdAt = new Date(now + i * 1_000).toISOString();
32223232 seedDeploy(
32233233 createdAt,
3234 null,
32243235 new Date(Date.parse(createdAt) + d * 1_000).toISOString(),
32253236 );
32263237 });
@@ -3232,11 +3243,58 @@ describe("previewLatency: the median seconds from push to serving", () => {
32323243 const createdAt = new Date(now + 3_000).toISOString();
32333244 seedDeploy(
32343245 createdAt,
3246 null,
32353247 new Date(Date.parse(createdAt) + 10_000).toISOString(),
32363248 );
32373249 const even = previewLatency(app.db, repo.id, since)!;
32383250 expect(even.n).toBe(4);
32393251 expect(even.medianSeconds).toBeCloseTo(5, 2);
3252
3253 // None of the above carried a `started_at`: the split has nothing to say.
3254 expect(even.queueMedianSeconds).toBeUndefined();
3255 expect(even.bootMedianSeconds).toBeUndefined();
3256 expect(even.splitN).toBe(0);
3257 });
3258
3259 test("the split between waiting to be picked up and coming up, seeded beside a total", () => {
3260 const now = Date.now() + 10_000_000; // clear of the previous test's window
3261 const since = new Date(now - 60_000).toISOString();
3262
3263 // Three deploys with known queue and boot segments: (queue, boot) in seconds.
3264 const segments: readonly (readonly [number, number])[] = [
3265 [1, 3],
3266 [2, 6],
3267 [3, 9],
3268 ];
3269 segments.forEach(([queue, boot], i) => {
3270 const createdAt = now + i * 1_000;
3271 const startedAt = createdAt + queue * 1_000;
3272 const servingAt = startedAt + boot * 1_000;
3273 seedDeploy(
3274 new Date(createdAt).toISOString(),
3275 new Date(startedAt).toISOString(),
3276 new Date(servingAt).toISOString(),
3277 );
3278 });
3279 const full = previewLatency(app.db, repo.id, since)!;
3280 expect(full.n).toBe(3);
3281 expect(full.splitN).toBe(3);
3282 expect(full.queueMedianSeconds).toBeCloseTo(2, 2);
3283 expect(full.bootMedianSeconds).toBeCloseTo(6, 2);
3284
3285 // A fourth, served deploy with no `started_at`: it feeds the total but
3286 // is left out of the split, never counted as a zero-length queue or boot.
3287 const createdAt = now + 3_000;
3288 seedDeploy(
3289 new Date(createdAt).toISOString(),
3290 null,
3291 new Date(createdAt + 100_000).toISOString(),
3292 );
3293 const withGap = previewLatency(app.db, repo.id, since)!;
3294 expect(withGap.n).toBe(4);
3295 expect(withGap.splitN).toBe(3);
3296 expect(withGap.queueMedianSeconds).toBeCloseTo(2, 2);
3297 expect(withGap.bootMedianSeconds).toBeCloseTo(6, 2);
32403298 });
32413299 });
32423300
@@ -3318,6 +3376,56 @@ describe("the studio's own words for how fast a change reaches its preview", ()
33183376 expect(one).toContain("not enough deploys this week to say");
33193377 expect(one).not.toContain("reaches its preview in");
33203378 });
3379
3380 test("the split reads as the total's explanation, and says when it is drawn from fewer deploys", async () => {
3381 const seedDeploy = (
3382 createdAt: string,
3383 startedAt: string | null,
3384 servingAt: string,
3385 ): void => {
3386 const deploy = createRun(app.db, { repoId: repo.id, kind: "deploy" });
3387 app.db
3388 .query("UPDATE run SET created_at = ?2 WHERE id = ?1")
3389 .run(deploy.id, createdAt);
3390 if (startedAt !== null) {
3391 app.db
3392 .query("UPDATE run SET started_at = ?2 WHERE id = ?1")
3393 .run(deploy.id, startedAt);
3394 }
3395 app.db
3396 .query("UPDATE run SET serving_at = ?2 WHERE id = ?1")
3397 .run(deploy.id, servingAt);
3398 };
3399 // Three deploys with a known queue and boot each.
3400 const now = Date.now();
3401 const segments: readonly (readonly [number, number])[] = [
3402 [1, 3],
3403 [2, 6],
3404 [3, 9],
3405 ];
3406 segments.forEach(([queue, boot], i) => {
3407 const createdAt = now + i * 1_000;
3408 const startedAt = createdAt + queue * 1_000;
3409 const servingAt = startedAt + boot * 1_000;
3410 seedDeploy(
3411 new Date(createdAt).toISOString(),
3412 new Date(startedAt).toISOString(),
3413 new Date(servingAt).toISOString(),
3414 );
3415 });
3416 // A fourth, served deploy with no `started_at`: it feeds the total but
3417 // is left out of the split.
3418 seedDeploy(
3419 new Date(now + 3_000).toISOString(),
3420 null,
3421 new Date(now + 100_000).toISOString(),
3422 );
3423 const page = await (await get("/alice/trickle")).text();
3424 expect(page).toContain("4 deploys");
3425 expect(page).toContain("2.0s waiting");
3426 // Only 3 of the 4 served deploys carried a `started_at`: the split says so.
3427 expect(page).toContain("6.0s coming up (3 of 4)");
3428 });
33213429 });
33223430
33233431 describe("this week's runs and spend are counted by the database, not read off a page of runs", () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs