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

Every background timer on the forge runs inside `during`, so no loop stall is blamed on `unknown` #11127

Merged opened this

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

Merged

mason/forge:mason/11117-every-background-timer-on-the-forge-runs → main 6 commits fork 3 threads · 3 unresolved

  • Merged. mason/11117-every-background-timer-on-the-forge-runs was merged into main as 15411a7.

Review

Sign in to review this change.
14 files changed +247 −19
src/apps/contain.ts +2 −1
@@ -48,6 +48,7 @@ import {
4848 type CgroupHost,
4949 type IoMax,
5050 } from "./cgroup.ts";
51 import { during } from "../perf/jobs.ts";
5152
5253 /** `ceiling`: a declaration was cut to the machine's maximum; `declared`: forge.json's own; `default`: neither. */
5354 export type CeilingFrom = "env" | "pod" | "default";
@@ -1047,7 +1048,7 @@ export function watchTree(
10471048 cap,
10481049 };
10491050 watched.add(entry);
1050 timer ??= setInterval(tick, WATCH.tickMs);
1051 timer ??= setInterval(() => during("cell-watch", tick), WATCH.tickMs);
10511052 return () => {
10521053 watched.delete(entry);
10531054 if (watched.size === 0 && timer !== undefined) {
src/apps/egress.ts +3 −2
@@ -21,6 +21,7 @@ import { lookup } from "node:dns/promises";
2121 import { isIP } from "node:net";
2222 import type { Socket } from "bun";
2323 import { neverGrantable } from "../core/egress_grants.ts";
24 import { during } from "../perf/jobs.ts";
2425
2526 export interface EgressPolicy {
2627 /** Host names from `forge.json` `egress`: exact, or `*.example.com`. */
@@ -791,7 +792,7 @@ export function startEgress(
791792 // that stopped sending) is cut, so it cannot hold an upstream open.
792793 const idleMs = policy.idleMs ?? IDLE_MS;
793794 const sweep = setInterval(
794 () => {
795 () => during("egress-idle", () => {
795796 const now = Date.now();
796797 for (const client of live)
797798 if (now - client.data.last > idleMs) {
@@ -800,7 +801,7 @@ export function startEgress(
800801 client.terminate();
801802 live.delete(client);
802803 }
803 },
804 }),
804805 Math.max(50, Math.min(5_000, idleMs / 2)),
805806 );
806807 sweep.unref();
src/apps/presence.ts +2 −1
@@ -20,6 +20,7 @@ import { hostname } from "node:os";
2020 import { join } from "node:path";
2121 import { startWarming } from "../http/health.ts";
2222 import type { Log } from "../kernel/log.ts";
23 import { during } from "../perf/jobs.ts";
2324
2425 export const BEAT_MS = 5_000;
2526 /** Three beats missed: the forge that wrote it is gone. */
@@ -115,7 +116,7 @@ export function takePresence(dataDir: string, beatMs = BEAT_MS): Presence {
115116 }
116117 };
117118 write();
118 let timer: ReturnType<typeof setInterval> | undefined = setInterval(write, beatMs);
119 let timer: ReturnType<typeof setInterval> | undefined = setInterval(() => during("presence", write), beatMs);
119120 timer.unref?.();
120121 return {
121122 others: (at = Date.now()) => beatsOf(dataDir, self.id).some((b) => alive(b, at, self.host)),
src/apps/runtime.ts +2 −1
@@ -121,6 +121,7 @@ import {
121121 type StartError,
122122 type StartInput,
123123 } from "./process.ts";
124 import { during } from "../perf/jobs.ts";
124125
125126 const log = createLog("apps");
126127
@@ -780,7 +781,7 @@ export class AppsRuntime {
780781 this.pruneDirs(known);
781782 await this.sampleStorage();
782783 this.sweeper = setInterval(
783 () => void this.sweep(),
784 () => void during("apps-sweep", () => this.sweep()),
784785 this.options.sweepEveryMs ?? SWEEP_EVERY_MS,
785786 );
786787 }
src/git/read.ts +2 −1
@@ -9,6 +9,7 @@ import { defect } from "../kernel/error.ts";
99 import { err, ok, type Result } from "../kernel/result.ts";
1010 import { gitEnv, mediaGate, runGit, runGitRaw, type GitFailure } from "./env.ts";
1111 import { isSafeOid, isSafeRef } from "./guard.ts";
12 import { during } from "../perf/jobs.ts";
1213
1314 export type ObjectType = "blob" | "tree" | "commit" | "tag";
1415
@@ -127,7 +128,7 @@ export function readerFor(dir: string): RepoReader {
127128 const reader = new RepoReader(dir);
128129 pool.set(dir, { reader, usedAt: Date.now() });
129130 if (sweeper === undefined) {
130 sweeper = setInterval(() => sweepReaders(), READER_IDLE_MS);
131 sweeper = setInterval(() => during("git-readers", () => sweepReaders()), READER_IDLE_MS);
131132 // The pool must never be the reason a process refuses to exit.
132133 sweeper.unref?.();
133134 }
src/perf/github_traffic.ts +3 −2
@@ -11,6 +11,7 @@
1111
1212 import type { Database } from "bun:sqlite";
1313 import { quietly } from "../core/write_clock.ts";
14 import { during } from "./jobs.ts";
1415
1516 const API = "https://api.github.com";
1617 const REPO = /^(?!\.+\/)[A-Za-z0-9_.-]{1,100}\/(?!\.+$)[A-Za-z0-9_.-]{1,100}$/;
@@ -149,8 +150,8 @@ export function startGithubPull(
149150 if (!pullDue(db, config, Date.now())) return;
150151 pullGithub(db, config, fetcher).catch((error) => log.warn("could not pull GitHub traffic", { error: String(error) }));
151152 };
152 const first = setTimeout(run, 30_000);
153 const tick = setInterval(run, CHECK_MS);
153 const first = setTimeout(() => during("github-traffic", run), 30_000);
154 const tick = setInterval(() => during("github-traffic", run), CHECK_MS);
154155 first.unref();
155156 tick.unref();
156157 return {
src/perf/reads.ts +2 −1
@@ -16,6 +16,7 @@ import type { Database } from "bun:sqlite";
1616 import { runGit } from "../git/env.ts";
1717 import { withoutWaiting } from "../kernel/db.ts";
1818 import { quietly } from "../core/write_clock.ts";
19 import { during } from "./jobs.ts";
1920
2021 const HOUR_MS = 3_600_000;
2122 const MAX_KEYS = 2_000;
@@ -189,7 +190,7 @@ export function createReadWriter(
189190 log.warn("could not write read history", { error: String(error) });
190191 }
191192 };
192 const tick = setInterval(flush, flushMs);
193 const tick = setInterval(() => during("reads", flush), flushMs);
193194 tick.unref();
194195 return {
195196 flush,
src/perf/traffic_store.ts +3 −2
@@ -31,6 +31,7 @@ import {
3131 type TrafficTops,
3232 type TrafficTotals,
3333 } from "./traffic.ts";
34 import { during } from "./jobs.ts";
3435
3536 const MINUTE_MS = 60_000;
3637 const HOUR_MINUTES = 60;
@@ -141,14 +142,14 @@ export function createTrafficWriter(
141142 trimmedHour = hour;
142143 ledger.markWritten(snaps.map((s) => s.minute));
143144 };
144 const tick = setInterval(() => {
145 const tick = setInterval(() => during("traffic", () => {
145146 try {
146147 flush();
147148 } catch (error) {
148149 // The minutes stay in the ledger; the next tick writes them.
149150 log.warn("could not write traffic history", { error: String(error) });
150151 }
151 }, flushMs);
152 }), flushMs);
152153 tick.unref();
153154 return {
154155 flush,
src/perf/views_store.ts +2 −1
@@ -16,6 +16,7 @@ import { withoutWaiting } from "../kernel/db.ts";
1616 import { dayOf, saltFor } from "../web/measure.ts";
1717 import { forgetVisits } from "./views_report.ts";
1818 import { isScraperAgent, type Origin } from "./sources.ts";
19 import { during } from "./jobs.ts";
1920
2021 export const BUFFER_MAX = 5_000;
2122 export const DAY_MAX = 50_000;
@@ -130,7 +131,7 @@ export function createViewWriter(
130131 log.warn("could not write page views", { error: String(error) });
131132 }
132133 };
133 const tick = setInterval(run, flushMs);
134 const tick = setInterval(() => during("views", run), flushMs);
134135 tick.unref();
135136 return {
136137 stop() {
src/runs/lease.ts +2 −1
@@ -12,6 +12,7 @@
1212
1313 import type { Database } from "bun:sqlite";
1414 import { heartbeatRuns } from "../core/runs.ts";
15 import { during } from "../perf/jobs.ts";
1516
1617 /** How often a held lease is renewed. */
1718 export const HEARTBEAT_MS = 15_000;
@@ -46,7 +47,7 @@ export function holdLease(db: Database, runId: string): () => void {
4647 ids.add(runId);
4748 heartbeatRuns(db, [runId]);
4849 if (timer === undefined) {
49 timer = setInterval(renewAll, HEARTBEAT_MS);
50 timer = setInterval(() => during("lease-renew", renewAll), HEARTBEAT_MS);
5051 timer.unref();
5152 }
5253 return () => {
src/snapshot/snapshot.ts +4 −3
@@ -39,6 +39,7 @@ import { countRows, type DrillCounts } from "./counts.ts";
3939 import { pack, partPath, PART_BYTES, type PackedFile } from "./parts.ts";
4040 import type { Store } from "./store.ts";
4141 import { unwrapKey, wrapKey, WRAPPED_KEY } from "./wrap.ts";
42 import { during } from "../perf/jobs.ts";
4243
4344 const log = createLog("snapshot");
4445
@@ -291,7 +292,7 @@ export async function takeSnapshot(
291292 elapsedMs: Date.now() - started,
292293 });
293294 };
294 const ticker = setInterval(say, everyMs);
295 const ticker = setInterval(() => during("snapshot-report", say), everyMs);
295296 const pending = new Set<Promise<void>>();
296297 const before = forge.resume ? await readJournal(store, prefix) : new Map<string, Landed>();
297298 const journal = new Map<string, Landed>();
@@ -308,9 +309,9 @@ export async function takeSnapshot(
308309 };
309310 // A release kills the process without running a `finally`: what landed must
310311 // already be written, so a changed journal goes out on a clock too.
311 const journalTicker = setInterval(() => {
312 const journalTicker = setInterval(() => during("snapshot-journal", () => {
312313 if (unwritten) writeJournal();
313 }, forge.journalEveryMs ?? JOURNAL_MS);
314 }), forge.journalEveryMs ?? JOURNAL_MS);
314315 const remember = (path: string, l: Landed): void => {
315316 journal.set(path, l);
316317 unwritten = true;
src/spawn/host.ts +3 −2
@@ -33,6 +33,7 @@ import { awakeRefusal, idleAfterMs, WakeRate } from "./sleep.ts";
3333 import { wakingResponse } from "../web/pages/waking.ts";
3434 import { takeDoor } from "../http/strangers.ts";
3535 import { vouched } from "./vouch.ts";
36 import { during } from "../perf/jobs.ts";
3637 import { readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3738 import { join } from "node:path";
3839
@@ -233,7 +234,7 @@ const floorWatchers = new WeakMap<App, ReturnType<typeof setInterval>>();
233234
234235 function keepWatchingFloor(parent: App): void {
235236 if (floorWatchers.has(parent)) return;
236 const timer = setInterval(() => void sleepBelowFloor(parent).catch(() => {}), FLOOR_EVERY_MS);
237 const timer = setInterval(() => void during("spawn-floor", () => sleepBelowFloor(parent)).catch(() => {}), FLOOR_EVERY_MS);
237238 timer.unref?.();
238239 floorWatchers.set(parent, timer);
239240 }
@@ -282,7 +283,7 @@ async function closeOne(parent: App, cache: Map<string, App>, bodyId: string, ch
282283 function keepSweeping(parent: App): void {
283284 if (sweepers.has(parent)) return;
284285 const every = Math.max(1000, Math.min(60_000, Math.floor(idleAfterMs() / 4)));
285 const timer = setInterval(() => void sleepIdleChildren(parent).catch(() => {}), every);
286 const timer = setInterval(() => void during("spawn-idle", () => sleepIdleChildren(parent)).catch(() => {}), every);
286287 timer.unref?.();
287288 sweepers.set(parent, timer);
288289 }
src/system/live.ts +2 −1
@@ -9,6 +9,7 @@ import type { App } from "../app.ts";
99 import { cellRoots } from "../apps/contain.ts";
1010 import { HOST, readRaw, type Paths, type Raw } from "./proc.ts";
1111 import { buildSystem, forAgent, type Roots, type SystemView } from "./view.ts";
12 import { during } from "../perf/jobs.ts";
1213
1314 const FRESH_MS = 1_000;
1415
@@ -32,7 +33,7 @@ export type Live = {
3233 export type Schedule = (tick: () => void, ms: number) => () => void;
3334
3435 const realSchedule: Schedule = (tick, ms) => {
35 const t = setInterval(tick, ms);
36 const t = setInterval(() => during("system-live", tick), ms);
3637 t.unref?.();
3738 return () => clearInterval(t);
3839 };
test/timers-named.test.ts added +215
@@ -0,0 +1,215 @@
1 /**
2 * Every server-side background timer names its job (#11117): a loop stall is
3 * blamed only on work run inside `during` (`src/perf/jobs.ts`), so a timer
4 * whose callback is not wrapped reads `unknown` in the stall blame.
5 *
6 * The scan reads src/ — not the browser files under src/web, src/web/vendor
7 * or the terminal UI in src/tui — as syntax, so a timer written inside a
8 * template string (the toolbar's browser script, a scaffolded app) is text,
9 * not a timer. Every `setInterval(` callback, and every `setTimeout(` callback
10 * in the files #11117 names, must do its work inside `during`: be a `during`
11 * call (behind `void`, `&&`, `.catch`, `.finally` or `.then(() => during(…))`),
12 * or a body of such calls with only guards (`if (stopped) return;`), plain
13 * flag writes and `try` around them; else it must be one occurrence on the
14 * allow-list below, with its reason.
15 *
16 * `setTimeout` is read only in the named files: elsewhere in src/ it is a
17 * one-shot deadline or a delay (over a hundred of them), not a loop, and
18 * wrapping each would put a ring slot per request into the job watch.
19 */
20
21 import { describe, expect, test } from "bun:test";
22 import { readdirSync, readFileSync, statSync } from "node:fs";
23 import { join, relative } from "node:path";
outdated

A whole-file exemption hides any later server-side setInterval in these files. Exempt only the template-string occurrence.

24 import * as ts from "typescript-api";
25
26 const ROOT = join(import.meta.dir, "..");
27 const SRC = join(ROOT, "src");
28
29 /** Browser and terminal code: no server loop runs there. */
30 const SKIPPED_DIRS = new Set(["src/tui", "src/web/vendor"]);
31 const isBrowserFile = (path: string): boolean =>
32 path.startsWith("src/web/") && /(^|[-_/])client\.ts$/.test(path);
33
34 /** The files #11117 names: their `setTimeout`s are read as well. */
35 const NAMED = new Set([
36 "src/perf/views_store.ts",
37 "src/perf/traffic_store.ts",
38 "src/perf/reads.ts",
39 "src/perf/duty.ts",
40 "src/perf/github_traffic.ts",
41 "src/apps/contain.ts",
42 "src/apps/runtime.ts",
43 "src/apps/toolbar.ts",
44 "src/apps/presence.ts",
45 "src/apps/egress.ts",
46 "src/apps/scaffold.ts",
47 "src/apps/tty_registry.ts",
48 "src/system/live.ts",
49 "src/runs/lease.ts",
50 "src/snapshot/snapshot.ts",
51 ]);
52
53 type Call = "setInterval" | "setTimeout";
54
55 interface Allowed {
56 readonly file: string;
57 readonly call: Call;
58 /** How the callback begins, whitespace collapsed: it names one occurrence. */
59 readonly starts: string;
60 readonly why: string;
61 }
62
63 /**
64 * The job ring keeps 256 spans (`jobs.ts`). A timer that exists once per open
65 * stream, session or fetch scales with load; after a stall every overdue one
66 * fires at once and would evict the span of the job that stalled.
67 */
68 const PER_CONNECTION = "one per open connection: wrapped, a burst of them evicts the real jobs from the 256-span ring";
69
70 const ALLOWED: readonly Allowed[] = [
71 { file: "src/http/sse.ts", call: "setInterval", starts: "() => write(", why: `SSE heartbeat, ${PER_CONNECTION}` },
72 { file: "src/web/pages/talk.ts", call: "setInterval", starts: "tick", why: `a talk stream's poll, ${PER_CONNECTION}` },
73 { file: "src/machines/shell.ts", call: "setInterval", starts: "() => { if (idle(session", why: `a shell session's idle check, ${PER_CONNECTION}` },
74 { file: "src/apps/tty_registry.ts", call: "setInterval", starts: "() => { if (idleFor(entry", why: `an app terminal's idle check, ${PER_CONNECTION}` },
outdated

/\bduring\(/ matches anywhere in the callback, including a comment or an unrelated call. Require the callback to start with during( or () => during(/() => void during(, or to be a wrapped body.

75 { file: "src/git/progress.ts", call: "setInterval", starts: "() => { slices.push(current)", why: `a git fetch's stall check, ${PER_CONNECTION}` },
76 { file: "src/git/capped-fetch.ts", call: "setInterval", starts: "() => { void newBytes()", why: `a capped fetch's size check, ${PER_CONNECTION}` },
77 { file: "src/apps/toolbar.ts", call: "setTimeout", starts: "resolve", why: "a one-shot deadline on reading one page, not a loop" },
78 { file: "src/apps/contain.ts", call: "setTimeout", starts: "() => { timedOut = true;", why: "a one-shot deadline on the sandbox probe, not a loop" },
79 { file: "src/apps/contain.ts", call: "setTimeout", starts: "() => { try { process.kill(root", why: "a one-shot kill a second after the tree's, not a loop" },
80 { file: "src/apps/runtime.ts", call: "setTimeout", starts: "() => { if (this.stopped) return;", why: "a one-shot restart after backoff; the deploy it asks for is its own job" },
81 ];
82
83 function sources(dir: string, out: string[] = []): string[] {
84 for (const name of readdirSync(dir)) {
85 const path = join(dir, name);
86 const rel = relative(ROOT, path);
87 if (statSync(path).isDirectory()) {
88 if (!SKIPPED_DIRS.has(rel)) sources(path, out);
89 } else if (name.endsWith(".ts") && !isBrowserFile(rel)) out.push(rel);
90 }
91 return out;
92 }
93
94 /** The first argument of each `call(` in `text`: its text, whitespace collapsed, and whether its work runs inside `during`. */
95 export function callbacks(text: string, call: Call): { callback: string; named: boolean }[] {
96 const source = ts.createSourceFile("x.ts", text, ts.ScriptTarget.Latest, true);
97 const found: { callback: string; named: boolean }[] = [];
98 const visit = (node: ts.Node): void => {
99 if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === call) {
100 const first = node.arguments[0];
101 if (first !== undefined)
102 found.push({ callback: first.getText(source).replace(/\s+/g, " ").trim(), named: isNamed(first) });
103 }
104 ts.forEachChild(node, visit);
105 };
106 visit(source);
107 return found;
108 }
109
110 /** An expression whose work is a `during` call: the call itself, or it behind `void`, `&&`, `.catch`, `.finally` or a `.then(() => during(…))`. */
111 function isDuring(expr: ts.Expression): boolean {
112 if (ts.isParenthesizedExpression(expr) || ts.isVoidExpression(expr)) return isDuring(expr.expression);
113 if (ts.isBinaryExpression(expr)) {
114 const op = expr.operatorToken.kind;
115 if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.EqualsToken)
116 return isDuring(expr.right);
117 return false;
118 }
119 if (!ts.isCallExpression(expr)) return false;
120 const callee = expr.expression;
121 if (ts.isIdentifier(callee)) return callee.text === "during";
122 if (!ts.isPropertyAccessExpression(callee)) return false;
123 if (isDuring(callee.expression)) return true;
124 return callee.name.text === "then" && expr.arguments.length > 0 && expr.arguments.every(isNamed);
125 }
126
127 /** A statement that does no work of its own: a guard that returns, or a flag set to a plain value. */
128 function isInert(st: ts.Statement): boolean {
129 if (ts.isIfStatement(st))
130 return st.elseStatement === undefined &&
131 (ts.isReturnStatement(st.thenStatement) && st.thenStatement.expression === undefined ||
132 ts.isBlock(st.thenStatement) && st.thenStatement.statements.every((s) => ts.isReturnStatement(s) && s.expression === undefined));
133 if (!ts.isExpressionStatement(st) || !ts.isBinaryExpression(st.expression)) return false;
134 const { operatorToken, right } = st.expression;
135 return operatorToken.kind === ts.SyntaxKind.EqualsToken && (ts.isLiteralExpression(right) || right.kind === ts.SyntaxKind.TrueKeyword || right.kind === ts.SyntaxKind.FalseKeyword || ts.isIdentifier(right));
136 }
137
138 /** Every statement is inert or a `during` (a `try` counts by its block), and at least one is a `during`. */
139 function blockNamed(statements: readonly ts.Statement[]): boolean {
140 let any = false;
141 for (const st of statements) {
142 if (ts.isExpressionStatement(st) && isDuring(st.expression)) any = true;
143 else if (ts.isTryStatement(st) && blockNamed(st.tryBlock.statements)) any = true;
144 else if (ts.isIfStatement(st) && st.elseStatement === undefined && blockNamed(ts.isBlock(st.thenStatement) ? st.thenStatement.statements : [st.thenStatement])) any = true;
145 else if (!isInert(st)) return false;
146 }
147 return any;
148 }
149
150 /** True when a timer callback's work all runs inside `during`, not merely mentions it. */
151 function isNamed(node: ts.Node): boolean {
152 if (ts.isArrowFunction(node) || ts.isFunctionExpression(node))
153 return ts.isBlock(node.body) ? blockNamed(node.body.statements) : isDuring(node.body);
154 return ts.isExpression(node) && isDuring(node);
155 }
156
157 /** Whether the callback of the one `setInterval` in `text` counts as named. */
158 export const isWrapped = (text: string): boolean => callbacks(text, "setInterval")[0]?.named ?? false;
159
160 /** Each timer callback the scan reads, with the file and call it came from. */
161 function timers(): { file: string; call: Call; callback: string; named: boolean }[] {
162 const out: { file: string; call: Call; callback: string; named: boolean }[] = [];
163 for (const file of sources(SRC)) {
164 const text = readFileSync(join(ROOT, file), "utf8");
165 if (!/\bset(Interval|Timeout)\(/.test(text)) continue;
166 const calls: Call[] = NAMED.has(file) ? ["setInterval", "setTimeout"] : ["setInterval"];
167 for (const call of calls) for (const { callback, named } of callbacks(text, call)) out.push({ file, call, callback, named });
168 }
169 return out;
170 }
171
172 const matches = (a: Allowed, t: { file: string; call: Call; callback: string }): boolean =>
173 a.file === t.file && a.call === t.call && t.callback.startsWith(a.starts);
174
175 describe("background timers run inside during", () => {
176 test("the scan reads each timer's first argument", () => {
177 const read = callbacks(`setInterval(tick, 10); setInterval(() => {\n g(1, 2);\n}, 5)`, "setInterval");
178 expect(read.map((c) => c.callback)).toEqual(["tick", "() => { g(1, 2); }"]);
179 });
180
181 test("only a callback whose work runs inside during counts as named", () => {
182 const named = (cb: string) => isWrapped(`setInterval(${cb}, 10)`);
183 expect(named(`() => during("a", f)`)).toBe(true);
184 expect(named(`() => void during("a", () => f()).catch(() => {})`)).toBe(true);
185 expect(named(`() => !stopped && during("a", f)`)).toBe(true);
186 expect(named(`() => { if (stopped) return; running = true; void during("a", f).finally(() => (running = false)); }`)).toBe(true);
187 expect(named(`() => { try { during("a", f); } catch (e) { log(e); } }`)).toBe(true);
188 expect(named(`() => { queue = queue.then(() => during("a", f)); }`)).toBe(true);
189 expect(named(`() => { f(); during("a", g); }`)).toBe(false);
190 expect(named(`() => { /* during(x) */ f(); }`)).toBe(false);
191 expect(named(`() => f(during)`)).toBe(false);
192 expect(named(`() => { if (x) f(); else return; during("a", g); }`)).toBe(false);
193 expect(named(`() => { if (!stopped) during("a", f); }`)).toBe(true);
194 expect(named(`tick`)).toBe(false);
195 });
196
197 test("a setInterval inside a template string is page text, not a timer", () => {
198 expect(callbacks("const js = `setInterval(refresh, 1000)`; setInterval(tick, 5);", "setInterval").map((c) => c.callback)).toEqual(["tick"]);
199 });
200
201 test("every server-side timer callback is wrapped in during or allowed by name", () => {
202 const unnamed = timers()
203 .filter((t) => !t.named && !ALLOWED.some((a) => matches(a, t)))
204 .map((t) => `${t.file}: ${t.call}(${t.callback.slice(0, 60)}`);
205 expect(unnamed).toEqual([]);
206 });
207
208 test("each allowed entry names exactly one timer, and says why", () => {
209 const all = timers();
210 for (const a of ALLOWED) {
211 expect(a.why.length).toBeGreaterThan(10);
212 expect({ ...a, found: all.filter((t) => matches(a, t)).length }).toEqual({ ...a, found: 1 });
213 }
214 });
215 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs