slotWaits
uses node:path
Says a run's waits for slots, quietly: once when it starts waiting (with what sets how many there are), again only
when who holds them changes or how many are ahead of it changes much (a minute after the last at the soonest), or
after five minutes of the same, and how long it waited once it has them. Its takes waiting at once (a worker each)
are one wait. Other work is named by its kind and branch (tcm tests by branch alone), with its codebase if it's
another and its pid if two have the same name; this one's own slots are "this run".
import { waitTime } from "#codebase/tcmts/cli/budget"; import type { SlotHolder, SlotWait, SlotWaits } from "#codebase/tcmts/cli/budget"; import path from "node:path"; export function slotWaits(say: (line: string) => void, clock: () => number = Date.now): SlotWaits { const RUN = /^([a-z][a-z' ]*?) of (.+?)(?: on (\S+))?$/; const AGAIN = 60_000; const QUIET = 5 * 60_000; const slots = (n: number) => (n === 1 ? "a slot" : `${n} slots`); // takes waiting now, since when, and how long before let waiting = 0; let since = 0; let total = 0; // whether this wait was said, and what was said last let said = false; let last: { at: number; who: string; ahead: number } | undefined; const words = (w: SlotWait, what: string) => { const root = RUN.exec(what)?.[2]; const nameOf = (h: SlotHolder) => { if (h.pid === process.pid) return "this run"; const m = RUN.exec(h.what); if (!m) return h.what; const [, kind, at, branch] = m as unknown as [string, string, string, string | undefined]; const who = kind === "tcm test" ? (branch ?? "tcm test") : `${branch ? `${branch}'s ` : ""}${kind}`; return at === root ? who : `${who} of …/${path.basename(path.dirname(at))}/${path.basename(at)}`; }; // by run (a process), its pid said where two have the same name const runs = new Map<number, { name: string; n: number }>(); for (const h of w.holders) { const r = runs.get(h.pid) ?? { name: nameOf(h), n: 0 }; r.n++; runs.set(h.pid, r); } const same = (name: string) => [...runs.values()].filter((r) => r.name === name).length > 1; const busy = [...runs].map( ([pid, r]) => `${r.n} by ${r.name}${pid && same(r.name) ? ` (pid ${pid})` : ""}`, ); const text = [ w.holders.length ? `${w.holders.length} of ${w.size} in use (${busy.join(", ")})` : "", w.ahead ? `${w.ahead} ahead in line` : "", ] .filter(Boolean) .join(", ") || `all ${w.size} in use`; // (who holds them, as far as it changes: not this run, nor a process starting) const who = [...runs] .filter(([pid]) => pid && pid !== process.pid) .map(([pid, r]) => `${pid} ${r.name}`) .sort() .join("\n"); return { text, who }; }; const big = (a: number, b: number) => Math.abs(a - b) >= Math.max(3, Math.max(a, b) / 2); return { async take(pool, what, count = 1) { let mine = false; let want = count; const onWait = (w: SlotWait) => { const now = clock(); want = w.want; if (!mine) { mine = true; if (!waiting++) { since = now; said = false; } } const { text, who } = words(w, what); const due = !last || now - last.at >= QUIET || (now - last.at >= AGAIN && (who !== last.who || big(w.ahead, last.ahead))); if (!due) return; say( !last ? `waiting for ${slots(w.want)}: ${text} (TCM_SLOTS sets how many)` : said || now - since >= 1000 ? `still waiting for ${slots(w.want)} after ${waitTime(now - since)}: ${text}` : `waiting for ${slots(w.want)}: ${text}`, ); last = { at: now, who, ahead: w.ahead }; said = true; }; try { return await pool.take(what, onWait, count); } finally { if (mine && !--waiting) { const took = clock() - since; total += took; if (said) say(`got ${slots(want)} after ${waitTime(took)}`); said = false; } } }, waited: () => total + (waiting ? clock() - since : 0), }; }
uses
- tcmts.cli.budget.SlotHolder
- tcmts.cli.budget.SlotWait
- tcmts.cli.budget.SlotWaits
- tcmts.cli.budget.waitTime