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

The flow vitals carry gate minutes and the red-batch rate, and a rate over 30% is named as a miss #6775

Merged opened this

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

Merged

scout/forge:scout/6428-the-flow-vitals-carry-gate-minutes-and-t → main 1 commit fork

  • Merged. scout/6428-the-flow-vitals-carry-gate-minutes-and-t was merged into main as d62951f.

Review

Sign in to review this change.
4 files changed +45 −10
src/crew/governor.test.ts +1 −1
@@ -3,7 +3,7 @@ import type { Vitals } from "../flow/vitals.ts";
33 import { governedSlots } from "./governor.ts";
44
55 const v = (p: Partial<Vitals>): Vitals => ({
6 open: 0, opened1h: 0, landed1h: 0, opened6h: 0, landed6h: 0, conflicted: 0, red: 0, queued: 0, oldestApprovedHours: null, strandedUsd: 0, ...p,
6 open: 0, opened1h: 0, landed1h: 0, opened6h: 0, landed6h: 0, conflicted: 0, red: 0, queued: 0, oldestApprovedHours: null, strandedUsd: 0, gateMinutes1h: 0, pullCheckMinutes1h: 0, batches6h: 0, redBatchRate: null, ...p,
77 });
88
99 describe("governedSlots", () => {
src/flow/vitals.ts +27 −4
@@ -11,6 +11,8 @@
1111
1212 import type { Database } from "bun:sqlite";
1313 import { conflictedPulls } from "./conflicted.ts";
14 import { readGateMinutes } from "./gate_minutes.ts";
15 import { readRedBatches } from "./red_batches.ts";
1416 import { readChecksPile } from "./checks_pile.ts";
1517 import { readOldestApproved } from "./oldest_approved.ts";
1618 import { readStranded } from "./stranded.ts";
@@ -21,6 +23,9 @@ export const OPEN_TARGET = 40;
2123 export const CONFLICTED_TARGET = 5;
2224 /** A clog is declared when the oldest approved pull has waited past this (#6422). */
2325 export const APPROVED_WAIT_HOURS = 24;
26 /** A merge-batch miss is named when more than this share went red, over at least `RED_BATCH_MIN` batches (#6428). */
27 export const RED_BATCH_TARGET = 0.3;
28 export const RED_BATCH_MIN = 3;
2429
2530 export interface Vitals {
2631 readonly open: number;
@@ -37,6 +42,14 @@ export interface Vitals {
3742 readonly oldestApprovedHours: number | null;
3843 /** Dollars of crew runs on open, unlanded pulls. */
3944 readonly strandedUsd: number;
45 /** Check-slot minutes the merge queue's gates spent in the last hour. */
46 readonly gateMinutes1h: number;
47 /** Check-slot minutes every other check spent in the last hour. */
48 readonly pullCheckMinutes1h: number;
49 /** Merge batches that finished in the last 6 h. */
50 readonly batches6h: number;
51 /** Share of those that went red; null when none finished. */
52 readonly redBatchRate: number | null;
4053 }
4154
4255 /** What a reader is shown: `strandedUsd` is left out for a reader without telemetry:read. */
@@ -55,7 +68,7 @@ export function readVitals(db: Database, now = new Date(), publicOnly = false):
5568 const h1 = new Date(now.getTime() - HOUR_MS).toISOString();
5669 const h6 = new Date(now.getTime() - 6 * HOUR_MS).toISOString();
5770 const row = db
58 .query<Omit<Vitals, "conflicted" | "red" | "queued" | "oldestApprovedHours" | "strandedUsd">, [string, string, string, number]>(
71 .query<Omit<Vitals, "conflicted" | "red" | "queued" | "oldestApprovedHours" | "strandedUsd" | "gateMinutes1h" | "pullCheckMinutes1h" | "batches6h" | "redBatchRate">, [string, string, string, number]>(
5972 `SELECT
6073 (SELECT COUNT(*) FROM pull p JOIN issue i ON i.id = p.issue_id JOIN repo r ON r.id = i.repo_id
6174 WHERE (?4 = 0 OR r.visibility = 'public') AND i.state = 'open' AND p.merged_at IS NULL) AS open,
@@ -70,6 +83,8 @@ export function readVitals(db: Database, now = new Date(), publicOnly = false):
7083 )
7184 .get(h1, h6, to, publicOnly ? 1 : 0);
7285 const pile = readChecksPile(db, publicOnly);
86 const gate = readGateMinutes(db, now, publicOnly);
87 const batches = readRedBatches(db, now, publicOnly);
7388 return {
7489 open: row?.open ?? 0,
7590 opened1h: row?.opened1h ?? 0,
@@ -81,6 +96,10 @@ export function readVitals(db: Database, now = new Date(), publicOnly = false):
8196 queued: pile.queued,
8297 oldestApprovedHours: readOldestApproved(db, now, publicOnly)?.hours ?? null,
8398 strandedUsd: readStranded(db, publicOnly).usd,
99 gateMinutes1h: gate.gate1h,
100 pullCheckMinutes1h: gate.pull1h,
101 batches6h: batches.finished,
102 redBatchRate: batches.rate,
84103 };
85104 }
86105
@@ -92,14 +111,18 @@ export function vitalsMisses(v: ShownVitals): string[] {
92111 if (v.landed6h < v.opened6h) out.push(`landed ${v.landed6h} < opened ${v.opened6h} over 6 h`);
93112 if (v.oldestApprovedHours !== null && v.oldestApprovedHours > APPROVED_WAIT_HOURS)
94113 out.push(`oldest approved pull has waited ${Math.floor(v.oldestApprovedHours)} h`);
114 if (v.redBatchRate !== null && v.redBatchRate > RED_BATCH_TARGET && v.batches6h >= RED_BATCH_MIN)
115 out.push(`${Math.round(v.redBatchRate * 100)}% of merge batches went red in 6 h`);
95116 return out;
96117 }
97118
98119 const usd = (n: number) => `$${n.toFixed(2)}`;
99120
100121 /** The new numbers, in the order both lines say them; each part is left out when it has nothing to say. */
101 function extras(v: ShownVitals): { red: string; oldest: string | null; stranded: string | null } {
122 function extras(v: ShownVitals): { red: string; oldest: string | null; stranded: string | null; gate: string; batches: string | null } {
102123 return {
124 gate: `gate ${Math.round(v.gateMinutes1h)} min, other checks ${Math.round(v.pullCheckMinutes1h)} min in 1 h`,
125 batches: v.redBatchRate === null ? null : `${Math.round(v.redBatchRate * 100)}% of ${v.batches6h} batches red in 6 h`,
103126 red: `${v.red} red, ${v.queued} queued`,
104127 oldest: v.oldestApprovedHours === null ? null : `oldest approved ${Math.floor(v.oldestApprovedHours)} h`,
105128 stranded: v.strandedUsd === undefined ? null : `${usd(v.strandedUsd)} stranded`,
@@ -109,11 +132,11 @@ function extras(v: ShownVitals): { red: string; oldest: string | null; stranded:
109132 /** The tick line's one value: `open 12, opened/landed 3/2 in 1 h, 9/11 in 6 h, conflicted 1, red 0, queued 0, oldest approved 30 h, stranded $1.20`. */
110133 export function vitalsWords(v: ShownVitals): string {
111134 const x = extras(v);
112 return `open ${v.open}, opened/landed ${v.opened1h}/${v.landed1h} in 1 h, ${v.opened6h}/${v.landed6h} in 6 h, conflicted ${v.conflicted}, ${[x.red, x.oldest, x.stranded].filter((w) => w !== null).join(", ")}`;
135 return `open ${v.open}, opened/landed ${v.opened1h}/${v.landed1h} in 1 h, ${v.opened6h}/${v.landed6h} in 6 h, conflicted ${v.conflicted}, ${[x.red, x.oldest, x.stranded, x.gate, x.batches].filter((w) => w !== null).join(", ")}`;
113136 }
114137
115138 /** Home's line for an admin; the counts are the whole of it. */
116139 export function vitalsLine(v: ShownVitals): string {
117140 const x = extras(v);
118 return `${v.open} open pulls · ${v.opened1h} opened, ${v.landed1h} landed in 1 h · ${v.opened6h} opened, ${v.landed6h} landed in 6 h · ${v.conflicted} conflicted · ${[x.red, x.oldest, x.stranded].filter((w) => w !== null).join(" · ")}`;
141 return `${v.open} open pulls · ${v.opened1h} opened, ${v.landed1h} landed in 1 h · ${v.opened6h} opened, ${v.landed6h} landed in 6 h · ${v.conflicted} conflicted · ${[x.red, x.oldest, x.stranded, x.gate, x.batches].filter((w) => w !== null).join(" · ")}`;
119142 }
test/flow-clog-prevent.test.ts +1 −1
@@ -52,7 +52,7 @@ const open = (title: string): Issue => {
5252 };
5353
5454 test("a seeded clog lowers the slots and holds a fresh build on the hot file", () => {
55 const clog: Vitals = { open: 8, opened1h: 51, landed1h: 15, opened6h: 120, landed6h: 40, conflicted: 0, red: 0, queued: 0, oldestApprovedHours: null, strandedUsd: 0 };
55 const clog: Vitals = { open: 8, opened1h: 51, landed1h: 15, opened6h: 120, landed6h: 40, conflicted: 0, red: 0, queued: 0, oldestApprovedHours: null, strandedUsd: 0, gateMinutes1h: 0, pullCheckMinutes1h: 0, batches6h: 0, redBatchRate: null };
5656 const ceiling = 7;
5757 const governed = governedSlots(clog, ceiling);
5858 expect(governed.slots).toBeLessThan(ceiling);
test/flow-vitals.test.ts +16 −4
@@ -52,11 +52,11 @@ describe("flow vitals", () => {
5252 .run(id(), repo.value.id, clash, ago(1));
5353
5454 const v = readVitals(app.db, NOW);
55 expect(v).toEqual({ open: 3, opened1h: 1, landed1h: 1, opened6h: 3, landed6h: 2, conflicted: 1, red: 0, queued: 0, oldestApprovedHours: null, strandedUsd: 0 });
55 expect(v).toEqual({ open: 3, opened1h: 1, landed1h: 1, opened6h: 3, landed6h: 2, conflicted: 1, red: 0, queued: 0, oldestApprovedHours: null, strandedUsd: 0, gateMinutes1h: 0, pullCheckMinutes1h: 0, batches6h: 0, redBatchRate: null });
5656 expect(vitalsMisses(v)).toEqual(["landed 2 < opened 3 over 6 h"]);
5757
5858 const tick = flowVitals(app.db, NOW);
59 expect(tick.pulls).toBe("open 3, opened/landed 1/1 in 1 h, 3/2 in 6 h, conflicted 1, 0 red, 0 queued, $0.00 stranded");
59 expect(tick.pulls).toBe("open 3, opened/landed 1/1 in 1 h, 3/2 in 6 h, conflicted 1, 0 red, 0 queued, $0.00 stranded, gate 0 min, other checks 0 min in 1 h");
6060 expect(tick.pulls).toBe(vitalsWords(v));
6161 expect(tick.offTarget).toBe("landed 2 < opened 3 over 6 h");
6262
@@ -121,7 +121,7 @@ describe("flow vitals", () => {
121121 const app = createApp({ dataDir: root, port: 0 });
122122 try {
123123 const v = readVitals(app.db, NOW);
124 expect(Object.values(v)).toEqual([0, 0, 0, 0, 0, 0, 0, 0, null, 0]);
124 expect(Object.values(v)).toEqual([0, 0, 0, 0, 0, 0, 0, 0, null, 0, 0, 0, 0, null]);
125125 expect(vitalsMisses(v)).toEqual([]);
126126 } finally {
127127 app.close();
@@ -130,7 +130,7 @@ describe("flow vitals", () => {
130130 });
131131
132132 test("an approved pull waiting over 24 h is a miss; an agent without telemetry:read gets no dollars", () => {
133 const base: Vitals = { open: 1, opened1h: 0, landed1h: 0, opened6h: 0, landed6h: 0, conflicted: 0, red: 2, queued: 3, oldestApprovedHours: 30.4, strandedUsd: 12.5 };
133 const base: Vitals = { open: 1, opened1h: 0, landed1h: 0, opened6h: 0, landed6h: 0, conflicted: 0, red: 2, queued: 3, oldestApprovedHours: 30.4, strandedUsd: 12.5, gateMinutes1h: 0, pullCheckMinutes1h: 0, batches6h: 0, redBatchRate: null };
134134 expect(vitalsMisses(base)).toEqual(["oldest approved pull has waited 30 h"]);
135135 expect(vitalsMisses({ ...base, oldestApprovedHours: 24 })).toEqual([]);
136136 expect(vitalsMisses({ ...base, oldestApprovedHours: null })).toEqual([]);
@@ -142,6 +142,18 @@ describe("flow vitals", () => {
142142 expect(vitalsFor(base, true).strandedUsd).toBe(12.5);
143143 });
144144
145 test("a red-batch rate over 30% of at least 3 batches is a miss; the gate minutes are said", () => {
146 const base: Vitals = { open: 1, opened1h: 0, landed1h: 0, opened6h: 0, landed6h: 0, conflicted: 0, red: 0, queued: 0, oldestApprovedHours: null, strandedUsd: 0, gateMinutes1h: 12.4, pullCheckMinutes1h: 30, batches6h: 4, redBatchRate: 0.5 };
147 expect(vitalsMisses(base)).toEqual(["50% of merge batches went red in 6 h"]);
148 expect(vitalsMisses({ ...base, batches6h: 10, redBatchRate: 0.3 })).toEqual([]);
149 expect(vitalsMisses({ ...base, batches6h: 10, redBatchRate: 0.4 })).toEqual(["40% of merge batches went red in 6 h"]);
150 expect(vitalsMisses({ ...base, batches6h: 2, redBatchRate: 1 })).toEqual([]);
151 expect(vitalsMisses({ ...base, batches6h: 3, redBatchRate: 1 })).toHaveLength(1);
152 expect(vitalsMisses({ ...base, batches6h: 0, redBatchRate: null })).toEqual([]);
153 expect(vitalsWords(base)).toContain("gate 12 min, other checks 30 min in 1 h, 50% of 4 batches red in 6 h");
154 expect(vitalsLine(base)).toContain("gate 12 min, other checks 30 min in 1 h · 50% of 4 batches red in 6 h");
155 });
156
145157 test("the dollars and the old approval pass through every door only to a reader who may read them", async () => {
146158 const root = mkdtempSync(join(tmpdir(), "forge-vitals-"));
147159 const app = createApp({ dataDir: root, port: 0 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs