@mark1russell7/rigor-bench 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,6 +54,9 @@ v.comparison; // the cases that gain and dro
54
54
  | `childLine(stdout)` | From `/fresh`: the last JSON line of a child, with its time `ms`, its `hash` and the reason of a skip. |
55
55
  | `pairOrder(pairs, round, caseIndex)` | From `/fresh`: the order of the pairs of one case in one round, and the first arm of each pair. |
56
56
  | `freshRun(options)` | From `/fresh`: the records of each pair, with each arm in its own fresh child process. |
57
+ | `superviseCases(options)` | From `/supervise`: each case in a child process, held to a time budget, with a hard stop and a resume. |
58
+ | `caseProgress(file)` | From `/supervise`: the writer of the progress file of a child. |
59
+ | `startSupervisedProcess(argv, options)` | From `/supervise`: a child process that `superviseCases` can stop with its tree. |
57
60
 
58
61
  ## A claim with a limit
59
62
 
@@ -183,6 +186,35 @@ A run of many jobs sets `roundOffset` to `job * rounds`. Then the merged records
183
186
 
184
187
  A rotation of N arms turns the order of two arms only when they are N/2 apart in the rotation. For 4 arms, two arms 1 apart come in the order 3:1 over 4 rounds. Thus each pair of `freshRun` has its own two children, and the order of each pair turns each round.
185
188
 
189
+ ## Run cases with a time budget
190
+
191
+ The entry `@mark1russell7/rigor-bench/supervise` starts child processes, so it is for Node only.
192
+
193
+ The function `superviseCases` holds each case to a time budget. Code that does not return cannot stop itself, so the cases run in a child process and a hard stop ends the child.
194
+
195
+ ```ts
196
+ import { startSupervisedProcess, superviseCases } from "@mark1russell7/rigor-bench/supervise";
197
+
198
+ const result = await superviseCases({
199
+ cases: [{ id: "a", budgetMs: 300_000, groups: { "": "router-1" } }],
200
+ progressFile: "out/progress.ndjson",
201
+ start: (left) => startSupervisedProcess([process.execPath, "run-cases.js", ...left]),
202
+ onEnd: (end) => writeRecord(end),
203
+ });
204
+ ```
205
+
206
+ The child calls `caseProgress(file).start(id, stage)` before each stage, and `end(id)` after the result is on the disk.
207
+
208
+ - A stage over its budget gives `over-budget`. This time is a lower bound, so a statistic treats it as censored, not as a value. The next child runs the cases that are left.
209
+ - After 2 overruns in one group (the option `overruns`), each other case of the group gives `not-run`, with the ids of the 2 overruns. An overrun is measured, and a skip is not. Keep these tags different.
210
+ - An overrun counts for the group of its stage only. A slow router thus does not count against the packer of its cases.
211
+ - A case in `done` does not run, so a resume runs no case two times.
212
+ - The option `earlier` takes the ends of a stopped run. Its overruns count for their groups, so a stop and a resume give the same ends as a run with no stop. Keep each case in `cases`, and put the ids of the finished cases in `done`.
213
+ - Only an overrun starts a new child, so the other cases keep the process and the caches of a normal run.
214
+ - A case can end between a read of the file and the stop. That case has its result, so it does not get a tag.
215
+
216
+ The callback `onEnd` writes the record of each tag. The callback `start` can also repair a file that a stop cut.
217
+
186
218
  ## The pooled verdict
187
219
 
188
220
  `pooledVerdict` gives the verdict of the records of all jobs together:
@@ -1 +1 @@
1
- {"version":3,"file":"fresh.d.ts","names":[],"sources":["../src/fresh.ts"],"mappings":";;iBASiB;WACN;WACA;WACA;WACA;WACA;;WAEA;;;wBAIK,YAAY;;YAQhB,YAAY,YAAY;;iBAGnB;WACN;;WAEA;WACA;;;iBAIM;;WAEN;WACA;WACA;;;;;;wBAOK,UAAU,iBAAiB;;iBAwB1B;WACN;WACA,OAAO;;;;;;;;wBASF,UAAU,eAAe,eAAe,oBAAoB;;iBAY3D;WACN;WACA;;;iBAIM;;WAEN;;WAEA;;;iBAIM;WACN;WACA;;WAEA;;WAEA;;WAEA;;;YAIC,cAAc,yBAAyB,SAAS,iBAAiB,QAAQ;;iBAGpE;;WAEN,MAAM;;WAEN,qBAAqB;;WAErB;;WAEA;;;;;;WAMA;;WAEA;;WAEA,QAAQ;;WAER;;WAEA;;WAEA;;WAEA,MAAM;;WAEN,QAAQ;;;iBAIF,oBAAoB;WAC1B;WACA,KAAK;;;iBAIC;WACN;WACA;;WAEA;WACA;;;iBAIM;WACN,KAAK;WACL;;WAEA,OAAO,eAAe;WACtB,SAAS;;;;;;;;;;;wBAkBE,SAAS,SAAS,eAAe,QAAQ"}
1
+ {"version":3,"file":"fresh.d.ts","names":[],"sources":["../src/fresh.ts"],"mappings":";;iBAUiB;WACN;WACA;WACA;WACA;WACA;;WAEA;;;wBAIK,YAAY;;YAQhB,YAAY,YAAY;;iBAGnB;WACN;;WAEA;WACA;;;iBAIM;;WAEN;WACA;WACA;;;;;;wBAOK,UAAU,iBAAiB;;iBAwB1B;WACN;WACA,OAAO;;;;;;;;wBASF,UAAU,eAAe,eAAe,oBAAoB;;iBAY3D;WACN;WACA;;;iBAIM;;WAEN;;WAEA;;;iBAIM;WACN;WACA;;WAEA;;WAEA;;WAEA;;;YAIC,cAAc,yBAAyB,SAAS,iBAAiB,QAAQ;;iBAGpE;;WAEN,MAAM;;WAEN,qBAAqB;;WAErB;;WAEA;;;;;;WAMA;;WAEA;;WAEA,QAAQ;;WAER;;WAEA;;WAEA;;WAEA,MAAM;;WAEN,QAAQ;;;iBAIF,oBAAoB;WAC1B;WACA,KAAK;;;iBAIC;WACN;WACA;;WAEA;WACA;;;iBAIM;WACN,KAAK;WACL;;WAEA,OAAO,eAAe;WACtB,SAAS;;;;;;;;;;;wBAkBE,SAAS,SAAS,eAAe,QAAQ"}
package/build/fresh.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { r as firstArm } from "./paired.js";
2
+ import { t as stopTree } from "./tree.js";
2
3
  import { spawn } from "node:child_process";
3
4
  import { arch, cpus, platform } from "node:os";
4
5
  // src/fresh.ts
@@ -234,27 +235,7 @@ function startProcess(argv, options) {
234
235
  let stopping = false;
235
236
  let error;
236
237
  let grace;
237
- const signal = (name) => {
238
- const pid = proc.pid;
239
- if (pid === void 0) return;
240
- if (windows) {
241
- spawn("taskkill", [
242
- "/pid",
243
- String(pid),
244
- "/T",
245
- "/F"
246
- ], {
247
- stdio: "ignore",
248
- windowsHide: true
249
- }).on("error", () => proc.kill());
250
- return;
251
- }
252
- try {
253
- process.kill(-pid, name);
254
- } catch {
255
- proc.kill(name);
256
- }
257
- };
238
+ const signal = (name) => stopTree(proc, name);
258
239
  const finish = (code) => {
259
240
  if (settled) return;
260
241
  settled = true;
@@ -1 +1 @@
1
- {"version":3,"file":"fresh.js","names":[],"sources":["../src/fresh.ts"],"sourcesContent":["/**\n * The fresh-process runner of a bench, for Node only. Each arm of each pair, case and round is one fresh child process.\n * The child prints one JSON line with its time. The runner keeps the order of `pairOrder`, and gives `PairedTime` records.\n */\nimport { spawn } from \"node:child_process\";\nimport { arch, cpus, platform } from \"node:os\";\nimport { type Arm, type PairedTime, firstArm } from \"./paired.ts\";\n\n/** The computer of a run. The `cpu` is the model of the first CPU. */\nexport interface BenchEnv {\n readonly cpu: string;\n readonly cores: number;\n readonly os: string;\n readonly arch: string;\n readonly node: string;\n /** The name of the runner, from `BENCH_RUNNER` or else `RUNNER_NAME`. An empty value is absent. */\n readonly runner?: string;\n}\n\n/** This function gives the computer of this process. The runner comes from `BENCH_RUNNER`, or else from `RUNNER_NAME`. */\nexport function benchEnv(): BenchEnv {\n const all = cpus();\n const env = { cpu: all[0]?.model.trim() || \"unknown\", cores: all.length, os: platform(), arch: arch(), node: process.version };\n const runner = process.env[\"BENCH_RUNNER\"] || process.env[\"RUNNER_NAME\"];\n return runner === undefined || runner === \"\" ? env : { ...env, runner };\n}\n\n/** The JSON line of a child: its time in milliseconds, the hash of its output, and the reason of a skip. */\nexport type ChildLine = ChildTime | ChildSkip;\n\n/** The result line of a child that timed the arm. */\nexport interface ChildTime {\n readonly ms: number;\n /** The hash of the output of the arm. Two different hashes of one arm on one case are an error. */\n readonly hash?: string;\n readonly skipped?: undefined;\n}\n\n/** The result line of a child that skipped the case. A skip line does not need a time. */\nexport interface ChildSkip {\n /** The reason of the skip. The runner removes the case from each pair. */\n readonly skipped: string;\n readonly ms?: number;\n readonly hash?: string;\n}\n\n/**\n * This function reads the last line of `stdout` that starts with `{`. The time `ms` is a finite number above 0.\n * A skip line has the reason `skipped` and does not need a time. No such line, bad JSON or a bad field is an error.\n */\nexport function childLine(stdout: string): ChildLine {\n const line = stdout\n .split(\"\\n\")\n .map((l) => l.trim())\n .findLast((l) => l.startsWith(\"{\"));\n if (line === undefined) throw new Error(\"childLine: the output has no line that starts with {.\");\n let value: unknown;\n try {\n value = JSON.parse(line);\n } catch {\n throw new Error(`childLine: the last line that starts with { is not JSON: ${line.slice(0, 200)}`);\n }\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) throw new Error(`childLine: the last JSON line is not an object: ${line.slice(0, 200)}`);\n const { ms, hash, skipped } = value as Record<string, unknown>;\n if (hash !== undefined && typeof hash !== \"string\") throw new Error(`childLine: the hash is a string, not ${JSON.stringify(hash)}.`);\n if (skipped !== undefined) {\n if (!(typeof skipped === \"string\" && skipped !== \"\")) throw new Error(`childLine: the reason of a skip is a string that is not empty, not ${JSON.stringify(skipped)}.`);\n return { skipped, ...(typeof ms === \"number\" ? { ms } : {}), ...(hash === undefined ? {} : { hash }) };\n }\n if (!(typeof ms === \"number\" && Number.isFinite(ms) && ms > 0)) throw new Error(`childLine: the time ms is a finite number above 0, not ${JSON.stringify(ms)}.`);\n return { ms, ...(hash === undefined ? {} : { hash }) };\n}\n\n/** One slot of the order of a round: the index of the pair, and the arm of that pair that starts first. */\nexport interface PairSlot {\n readonly pair: number;\n readonly first: Arm;\n}\n\n/**\n * This function gives the order of the pairs of one case in one round. `round` is the global round.\n * - Pair `p` starts with `firstArm(round, caseIndex + p)`.\n * - The pairs rotate by `(Math.floor(round / 2) + caseIndex) % pairs`. Thus the pair that starts first turns each second round.\n * - For 2 pairs, the order is the order of `timeArmOrder` of the joint bench.\n */\nexport function pairOrder(pairs: number, round: number, caseIndex: number): PairSlot[] {\n if (!Number.isSafeInteger(pairs) || pairs < 1) throw new RangeError(`pairOrder: the count of the pairs is a whole number of 1 or more, not ${pairs}.`);\n if (!Number.isSafeInteger(round) || round < 0) throw new RangeError(`pairOrder: the round is a whole number of 0 or more, not ${round}.`);\n if (!Number.isSafeInteger(caseIndex) || caseIndex < 0) throw new RangeError(`pairOrder: the index of the case is a whole number of 0 or more, not ${caseIndex}.`);\n const shift = (Math.floor(round / 2) + caseIndex) % pairs;\n return Array.from({ length: pairs }, (_, i) => {\n const pair = (shift + i) % pairs;\n return { pair, first: firstArm(round, caseIndex + pair) };\n });\n}\n\n/** One arm of a run: its id and the argv of its child. The first item of `command` is the program. */\nexport interface FreshArm {\n readonly id: string;\n readonly command: readonly string[];\n}\n\n/** The limits of one child. */\nexport interface StartOptions {\n /** The longest time of the child in milliseconds. */\n readonly timeoutMs: number;\n /** The most bytes of `stdout` or `stderr`. */\n readonly maxBuffer: number;\n}\n\n/** The end of one child. */\nexport interface ChildExit {\n readonly stdout: string;\n readonly stderr: string;\n /** The exit code, or `null` when a signal stopped the child. */\n readonly code: number | null;\n /** The value is `true` when the child took more time than the limit. */\n readonly timedOut?: boolean;\n /** The reason of a fault outside the child, for example a program that does not exist or too much output. */\n readonly error?: string;\n}\n\n/** The start of one child: it gives the end of the child. The default starts a process with `spawn`, without a shell. */\nexport type StartChild = (argv: readonly string[], options: StartOptions) => Promise<ChildExit>;\n\n/** The options of a fresh run. */\nexport interface FreshOptions {\n /** The base arm. It is arm `a` of each pair. */\n readonly base: FreshArm;\n /** The candidate arms. Each candidate is arm `b` of its own pair, and the id of the pair is its id. */\n readonly candidates: readonly FreshArm[];\n /** The ids of the cases. */\n readonly cases: readonly string[];\n /** The count of the rounds of this job. */\n readonly rounds: number;\n /**\n * The global round of the first round of this job. The default is 0.\n * A run of many jobs sets it to `job * rounds`, so each round is global over the jobs.\n * Without it, each job repeats the first orders, and the merge removes more records.\n */\n readonly roundOffset?: number;\n /** The value `true` adds the A/A pair (base, base) with the id `${base.id}~aa`. The default is `true`. */\n readonly aa?: boolean;\n /** The arguments of a case after the command of an arm. The default is `[\"--case\", id]`. */\n readonly args?: (caseId: string) => readonly string[];\n /** The longest time of one child in milliseconds. The default is 600000 (10 minutes). */\n readonly timeoutMs?: number;\n /** The most bytes of the output of one child. The default is 16 MiB. */\n readonly maxBuffer?: number;\n /** The label of the job. The default is `\"0\"`. */\n readonly job?: string;\n /** The computer of the run. The default is `benchEnv()`. */\n readonly env?: BenchEnv;\n /** The start of a child. The default starts a process. A test gives its own. */\n readonly start?: StartChild;\n}\n\n/** One record of a fresh run: a `PairedTime` record with the job and the computer of its job. */\nexport interface FreshRecord extends PairedTime {\n readonly job: string;\n readonly env: BenchEnv;\n}\n\n/** A case that an arm skipped. The case is in no pair. */\nexport interface FreshSkip {\n readonly arm: string;\n readonly case: string;\n /** The global round of the skip. A skip is in the first round of its job. */\n readonly round: number;\n readonly reason: string;\n}\n\n/** The result of a fresh run. */\nexport interface FreshRun {\n readonly env: BenchEnv;\n readonly job: string;\n /** The records of each pair, by the id of the pair. */\n readonly pairs: Record<string, FreshRecord[]>;\n readonly skipped: FreshSkip[];\n}\n\n/** The suffix of the id of the A/A pair. */\nconst AA_SUFFIX = \"~aa\";\n\n/** The longest tail of `stderr` in an error. */\nconst TAIL = 2000;\n\n/**\n * This function starts each arm of each pair, case and round in a fresh child, and gives the records of each pair.\n * - Each candidate is a pair (base, candidate) with its own base child. The A/A pair is (base, base).\n * - In each round and case, the pairs come in the order of `pairOrder`. The first arm of a pair starts first.\n * - A child that exits with an error, takes too long or prints no JSON line is an error. Only a skip line skips a case.\n * - A case that an arm skips in the first round of the job leaves each pair, and the runner starts no more children for it.\n * - A skip after the first round is an error. It shows a child that is not deterministic, and it drops paid data.\n * - Two different hashes of one arm on one case are an error.\n */\nexport async function freshRun(options: FreshOptions): Promise<FreshRun> {\n const { base, candidates, cases, rounds } = options;\n const roundOffset = options.roundOffset ?? 0;\n const job = options.job ?? \"0\";\n const timeoutMs = options.timeoutMs ?? 600_000;\n const maxBuffer = options.maxBuffer ?? 1 << 24;\n const args = options.args ?? ((id: string) => [\"--case\", id]);\n const start = options.start ?? startProcess;\n if (!Number.isSafeInteger(rounds) || rounds < 1) throw new RangeError(`freshRun: the rounds are a whole number of 1 or more, not ${rounds}.`);\n if (!Number.isSafeInteger(roundOffset) || roundOffset < 0) throw new RangeError(`freshRun: the round offset is a whole number of 0 or more, not ${roundOffset}.`);\n if (!(timeoutMs > 0) || !(maxBuffer > 0)) throw new RangeError(\"freshRun: the time limit and the output limit are above 0.\");\n if (cases.length === 0 || new Set(cases).size !== cases.length || cases.some((c) => typeof c !== \"string\" || c === \"\")) {\n throw new RangeError(\"freshRun: the cases are 1 or more different strings that are not empty.\");\n }\n const pairs: { id: string; a: FreshArm; b: FreshArm }[] = candidates.map((c) => ({ id: c.id, a: base, b: c }));\n if (options.aa ?? true) pairs.push({ id: `${base.id}${AA_SUFFIX}`, a: base, b: base });\n if (pairs.length === 0) throw new RangeError(\"freshRun: the run has no pairs. Give a candidate, or keep the A/A pair.\");\n for (const arm of [base, ...candidates]) {\n if (typeof arm.id !== \"string\" || arm.id === \"\") throw new RangeError(\"freshRun: the id of an arm is a string that is not empty.\");\n if (arm.command.length === 0 || arm.command.some((s) => typeof s !== \"string\")) throw new RangeError(`freshRun: the command of the arm ${JSON.stringify(arm.id)} is 1 or more strings.`);\n }\n const ids = new Set([base.id]);\n for (const { id } of pairs) {\n if (ids.has(id)) throw new RangeError(`freshRun: two arms or pairs have the id ${JSON.stringify(id)}.`);\n ids.add(id);\n }\n\n const env = options.env ?? benchEnv();\n const out: Record<string, FreshRecord[]> = Object.fromEntries(pairs.map((p) => [p.id, []]));\n const skipped: FreshSkip[] = [];\n const gone = new Set<string>();\n const hashes = new Map<string, string>();\n\n for (let r = 0; r < rounds; r++) {\n const round = roundOffset + r;\n for (let i = 0; i < cases.length; i++) {\n const id = cases[i]!;\n if (gone.has(id)) continue;\n const records: { pair: string; record: FreshRecord }[] = [];\n let skip: FreshSkip | undefined;\n for (const slot of pairOrder(pairs.length, round, i)) {\n const pair = pairs[slot.pair]!;\n const times: Partial<Record<Arm, number>> = {};\n for (const arm of [slot.first, slot.first === \"a\" ? \"b\" : \"a\"] as const) {\n const who = pair[arm];\n const where = `the arm ${JSON.stringify(who.id)} (pair ${JSON.stringify(pair.id)}, arm ${arm}) on the case ${JSON.stringify(id)}, round ${round}`;\n const line = await child(start, [...who.command, ...args(id)], { timeoutMs, maxBuffer }, where);\n if (line.skipped !== undefined) {\n if (r > 0) throw new Error(`freshRun: ${where} skipped the case after the first round of the job (${JSON.stringify(line.skipped)}). A child that skips only in a later round is not deterministic.`);\n skip = { arm: who.id, case: id, round, reason: line.skipped };\n break;\n }\n if (line.hash !== undefined) {\n const key = `${who.id}\\u0000${id}`;\n const known = hashes.get(key);\n if (known !== undefined && known !== line.hash) throw new Error(`freshRun: ${where} gave the hash ${line.hash}, and an earlier child of the arm gave ${known}.`);\n hashes.set(key, line.hash);\n }\n times[arm] = line.ms;\n }\n if (skip !== undefined) break;\n records.push({ pair: pair.id, record: { case: id, round, first: slot.first, a: times.a!, b: times.b!, job, env } });\n }\n if (skip !== undefined) {\n skipped.push(skip);\n gone.add(id);\n continue;\n }\n for (const { pair, record } of records) out[pair]!.push(record);\n }\n }\n return { env, job, pairs: out, skipped };\n}\n\n/** This function starts one child and reads its JSON line. A fault gives an error that names the arm, the case, the round and the tail of `stderr`. */\nasync function child(start: StartChild, argv: readonly string[], limits: StartOptions, where: string): Promise<ChildLine> {\n let exit: ChildExit;\n try {\n exit = await start(argv, limits);\n } catch (error) {\n throw new Error(`freshRun: ${where} did not start: ${error instanceof Error ? error.message : String(error)}`, { cause: error });\n }\n const fail = (why: string): Error => {\n const tail = exit.stderr.length > TAIL ? `...${exit.stderr.slice(-TAIL)}` : exit.stderr;\n return new Error(`freshRun: ${where} ${why}. The tail of stderr: ${tail.trim() === \"\" ? \"(empty)\" : tail.trimEnd()}`);\n };\n if (exit.error !== undefined) throw fail(`failed: ${exit.error}`);\n if (exit.timedOut === true) throw fail(`took more than ${limits.timeoutMs} ms`);\n if (exit.code !== 0) throw fail(exit.code === null ? \"stopped on a signal\" : `exited with the code ${exit.code}`);\n try {\n return childLine(exit.stdout);\n } catch (error) {\n throw fail(`printed no good JSON line (${error instanceof Error ? error.message : String(error)})`);\n }\n}\n\n/** The wait, in milliseconds, for the pipes of a child after its exit, and for the exit of a child after a stop. */\nconst GRACE_MS = 1000;\n\n/**\n * This function starts a process with `spawn` and no shell. It stops the process and its tree at the time limit or at the output limit.\n *\n * - The promise settles at the close of the pipes, or a grace time after the exit of the child. A grandchild can keep a pipe open.\n * - A stop sends SIGTERM to the process group, and SIGKILL after the grace time. On Windows, `taskkill /T /F` stops the tree.\n */\nfunction startProcess(argv: readonly string[], options: StartOptions): Promise<ChildExit> {\n return new Promise((resolve) => {\n const [program, ...rest] = argv;\n const windows = process.platform === \"win32\";\n // On POSIX, the child leads its own process group, so a signal to the group reaches the grandchildren.\n const proc = spawn(program!, rest, { stdio: [\"ignore\", \"pipe\", \"pipe\"], windowsHide: true, detached: !windows });\n const out: Buffer[] = [];\n const err: Buffer[] = [];\n let timedOut = false;\n let settled = false;\n let stopping = false;\n let error: string | undefined;\n let grace: ReturnType<typeof setTimeout> | undefined;\n const signal = (name: NodeJS.Signals): void => {\n const pid = proc.pid;\n if (pid === undefined) return;\n if (windows) {\n // TerminateProcess stops only the direct child. The flag /T of taskkill also stops its tree.\n spawn(\"taskkill\", [\"/pid\", String(pid), \"/T\", \"/F\"], { stdio: \"ignore\", windowsHide: true }).on(\"error\", () => proc.kill());\n return;\n }\n try {\n process.kill(-pid, name);\n } catch {\n proc.kill(name);\n }\n };\n const finish = (code: number | null): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n clearTimeout(grace);\n proc.stdout.destroy();\n proc.stderr.destroy();\n const exit: ChildExit = { stdout: Buffer.concat(out).toString(\"utf8\"), stderr: Buffer.concat(err).toString(\"utf8\"), code, timedOut };\n resolve(error === undefined ? exit : { ...exit, error });\n };\n const stop = (): void => {\n if (stopping) return;\n stopping = true;\n signal(\"SIGTERM\");\n clearTimeout(grace);\n grace = setTimeout(() => {\n if (!windows) signal(\"SIGKILL\");\n finish(null);\n }, GRACE_MS);\n };\n const timer = setTimeout(() => {\n timedOut = true;\n stop();\n }, options.timeoutMs);\n const take = (list: Buffer[]) => {\n let bytes = 0;\n return (chunk: Buffer): void => {\n bytes += chunk.length;\n if (bytes > options.maxBuffer) {\n error ??= `an output is more than ${options.maxBuffer} bytes`;\n stop();\n return;\n }\n list.push(chunk);\n };\n };\n proc.stdout.on(\"data\", take(out));\n proc.stderr.on(\"data\", take(err));\n proc.on(\"error\", (e) => {\n error ??= e.message;\n // A program that does not start gives no event close.\n if (proc.pid === undefined) finish(null);\n });\n proc.on(\"exit\", (code) => {\n clearTimeout(timer);\n if (stopping) return;\n // A grandchild that holds a pipe keeps the event close away. After the grace time, the runner stops the group.\n grace = setTimeout(() => {\n if (!windows) signal(\"SIGKILL\");\n finish(code);\n }, GRACE_MS);\n });\n proc.on(\"close\", (code) => finish(code));\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fresh.js","names":[],"sources":["../src/fresh.ts"],"sourcesContent":["/**\n * The fresh-process runner of a bench, for Node only. Each arm of each pair, case and round is one fresh child process.\n * The child prints one JSON line with its time. The runner keeps the order of `pairOrder`, and gives `PairedTime` records.\n */\nimport { spawn } from \"node:child_process\";\nimport { arch, cpus, platform } from \"node:os\";\nimport { type Arm, type PairedTime, firstArm } from \"./paired.ts\";\nimport { stopTree } from \"./tree.ts\";\n\n/** The computer of a run. The `cpu` is the model of the first CPU. */\nexport interface BenchEnv {\n readonly cpu: string;\n readonly cores: number;\n readonly os: string;\n readonly arch: string;\n readonly node: string;\n /** The name of the runner, from `BENCH_RUNNER` or else `RUNNER_NAME`. An empty value is absent. */\n readonly runner?: string;\n}\n\n/** This function gives the computer of this process. The runner comes from `BENCH_RUNNER`, or else from `RUNNER_NAME`. */\nexport function benchEnv(): BenchEnv {\n const all = cpus();\n const env = { cpu: all[0]?.model.trim() || \"unknown\", cores: all.length, os: platform(), arch: arch(), node: process.version };\n const runner = process.env[\"BENCH_RUNNER\"] || process.env[\"RUNNER_NAME\"];\n return runner === undefined || runner === \"\" ? env : { ...env, runner };\n}\n\n/** The JSON line of a child: its time in milliseconds, the hash of its output, and the reason of a skip. */\nexport type ChildLine = ChildTime | ChildSkip;\n\n/** The result line of a child that timed the arm. */\nexport interface ChildTime {\n readonly ms: number;\n /** The hash of the output of the arm. Two different hashes of one arm on one case are an error. */\n readonly hash?: string;\n readonly skipped?: undefined;\n}\n\n/** The result line of a child that skipped the case. A skip line does not need a time. */\nexport interface ChildSkip {\n /** The reason of the skip. The runner removes the case from each pair. */\n readonly skipped: string;\n readonly ms?: number;\n readonly hash?: string;\n}\n\n/**\n * This function reads the last line of `stdout` that starts with `{`. The time `ms` is a finite number above 0.\n * A skip line has the reason `skipped` and does not need a time. No such line, bad JSON or a bad field is an error.\n */\nexport function childLine(stdout: string): ChildLine {\n const line = stdout\n .split(\"\\n\")\n .map((l) => l.trim())\n .findLast((l) => l.startsWith(\"{\"));\n if (line === undefined) throw new Error(\"childLine: the output has no line that starts with {.\");\n let value: unknown;\n try {\n value = JSON.parse(line);\n } catch {\n throw new Error(`childLine: the last line that starts with { is not JSON: ${line.slice(0, 200)}`);\n }\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) throw new Error(`childLine: the last JSON line is not an object: ${line.slice(0, 200)}`);\n const { ms, hash, skipped } = value as Record<string, unknown>;\n if (hash !== undefined && typeof hash !== \"string\") throw new Error(`childLine: the hash is a string, not ${JSON.stringify(hash)}.`);\n if (skipped !== undefined) {\n if (!(typeof skipped === \"string\" && skipped !== \"\")) throw new Error(`childLine: the reason of a skip is a string that is not empty, not ${JSON.stringify(skipped)}.`);\n return { skipped, ...(typeof ms === \"number\" ? { ms } : {}), ...(hash === undefined ? {} : { hash }) };\n }\n if (!(typeof ms === \"number\" && Number.isFinite(ms) && ms > 0)) throw new Error(`childLine: the time ms is a finite number above 0, not ${JSON.stringify(ms)}.`);\n return { ms, ...(hash === undefined ? {} : { hash }) };\n}\n\n/** One slot of the order of a round: the index of the pair, and the arm of that pair that starts first. */\nexport interface PairSlot {\n readonly pair: number;\n readonly first: Arm;\n}\n\n/**\n * This function gives the order of the pairs of one case in one round. `round` is the global round.\n * - Pair `p` starts with `firstArm(round, caseIndex + p)`.\n * - The pairs rotate by `(Math.floor(round / 2) + caseIndex) % pairs`. Thus the pair that starts first turns each second round.\n * - For 2 pairs, the order is the order of `timeArmOrder` of the joint bench.\n */\nexport function pairOrder(pairs: number, round: number, caseIndex: number): PairSlot[] {\n if (!Number.isSafeInteger(pairs) || pairs < 1) throw new RangeError(`pairOrder: the count of the pairs is a whole number of 1 or more, not ${pairs}.`);\n if (!Number.isSafeInteger(round) || round < 0) throw new RangeError(`pairOrder: the round is a whole number of 0 or more, not ${round}.`);\n if (!Number.isSafeInteger(caseIndex) || caseIndex < 0) throw new RangeError(`pairOrder: the index of the case is a whole number of 0 or more, not ${caseIndex}.`);\n const shift = (Math.floor(round / 2) + caseIndex) % pairs;\n return Array.from({ length: pairs }, (_, i) => {\n const pair = (shift + i) % pairs;\n return { pair, first: firstArm(round, caseIndex + pair) };\n });\n}\n\n/** One arm of a run: its id and the argv of its child. The first item of `command` is the program. */\nexport interface FreshArm {\n readonly id: string;\n readonly command: readonly string[];\n}\n\n/** The limits of one child. */\nexport interface StartOptions {\n /** The longest time of the child in milliseconds. */\n readonly timeoutMs: number;\n /** The most bytes of `stdout` or `stderr`. */\n readonly maxBuffer: number;\n}\n\n/** The end of one child. */\nexport interface ChildExit {\n readonly stdout: string;\n readonly stderr: string;\n /** The exit code, or `null` when a signal stopped the child. */\n readonly code: number | null;\n /** The value is `true` when the child took more time than the limit. */\n readonly timedOut?: boolean;\n /** The reason of a fault outside the child, for example a program that does not exist or too much output. */\n readonly error?: string;\n}\n\n/** The start of one child: it gives the end of the child. The default starts a process with `spawn`, without a shell. */\nexport type StartChild = (argv: readonly string[], options: StartOptions) => Promise<ChildExit>;\n\n/** The options of a fresh run. */\nexport interface FreshOptions {\n /** The base arm. It is arm `a` of each pair. */\n readonly base: FreshArm;\n /** The candidate arms. Each candidate is arm `b` of its own pair, and the id of the pair is its id. */\n readonly candidates: readonly FreshArm[];\n /** The ids of the cases. */\n readonly cases: readonly string[];\n /** The count of the rounds of this job. */\n readonly rounds: number;\n /**\n * The global round of the first round of this job. The default is 0.\n * A run of many jobs sets it to `job * rounds`, so each round is global over the jobs.\n * Without it, each job repeats the first orders, and the merge removes more records.\n */\n readonly roundOffset?: number;\n /** The value `true` adds the A/A pair (base, base) with the id `${base.id}~aa`. The default is `true`. */\n readonly aa?: boolean;\n /** The arguments of a case after the command of an arm. The default is `[\"--case\", id]`. */\n readonly args?: (caseId: string) => readonly string[];\n /** The longest time of one child in milliseconds. The default is 600000 (10 minutes). */\n readonly timeoutMs?: number;\n /** The most bytes of the output of one child. The default is 16 MiB. */\n readonly maxBuffer?: number;\n /** The label of the job. The default is `\"0\"`. */\n readonly job?: string;\n /** The computer of the run. The default is `benchEnv()`. */\n readonly env?: BenchEnv;\n /** The start of a child. The default starts a process. A test gives its own. */\n readonly start?: StartChild;\n}\n\n/** One record of a fresh run: a `PairedTime` record with the job and the computer of its job. */\nexport interface FreshRecord extends PairedTime {\n readonly job: string;\n readonly env: BenchEnv;\n}\n\n/** A case that an arm skipped. The case is in no pair. */\nexport interface FreshSkip {\n readonly arm: string;\n readonly case: string;\n /** The global round of the skip. A skip is in the first round of its job. */\n readonly round: number;\n readonly reason: string;\n}\n\n/** The result of a fresh run. */\nexport interface FreshRun {\n readonly env: BenchEnv;\n readonly job: string;\n /** The records of each pair, by the id of the pair. */\n readonly pairs: Record<string, FreshRecord[]>;\n readonly skipped: FreshSkip[];\n}\n\n/** The suffix of the id of the A/A pair. */\nconst AA_SUFFIX = \"~aa\";\n\n/** The longest tail of `stderr` in an error. */\nconst TAIL = 2000;\n\n/**\n * This function starts each arm of each pair, case and round in a fresh child, and gives the records of each pair.\n * - Each candidate is a pair (base, candidate) with its own base child. The A/A pair is (base, base).\n * - In each round and case, the pairs come in the order of `pairOrder`. The first arm of a pair starts first.\n * - A child that exits with an error, takes too long or prints no JSON line is an error. Only a skip line skips a case.\n * - A case that an arm skips in the first round of the job leaves each pair, and the runner starts no more children for it.\n * - A skip after the first round is an error. It shows a child that is not deterministic, and it drops paid data.\n * - Two different hashes of one arm on one case are an error.\n */\nexport async function freshRun(options: FreshOptions): Promise<FreshRun> {\n const { base, candidates, cases, rounds } = options;\n const roundOffset = options.roundOffset ?? 0;\n const job = options.job ?? \"0\";\n const timeoutMs = options.timeoutMs ?? 600_000;\n const maxBuffer = options.maxBuffer ?? 1 << 24;\n const args = options.args ?? ((id: string) => [\"--case\", id]);\n const start = options.start ?? startProcess;\n if (!Number.isSafeInteger(rounds) || rounds < 1) throw new RangeError(`freshRun: the rounds are a whole number of 1 or more, not ${rounds}.`);\n if (!Number.isSafeInteger(roundOffset) || roundOffset < 0) throw new RangeError(`freshRun: the round offset is a whole number of 0 or more, not ${roundOffset}.`);\n if (!(timeoutMs > 0) || !(maxBuffer > 0)) throw new RangeError(\"freshRun: the time limit and the output limit are above 0.\");\n if (cases.length === 0 || new Set(cases).size !== cases.length || cases.some((c) => typeof c !== \"string\" || c === \"\")) {\n throw new RangeError(\"freshRun: the cases are 1 or more different strings that are not empty.\");\n }\n const pairs: { id: string; a: FreshArm; b: FreshArm }[] = candidates.map((c) => ({ id: c.id, a: base, b: c }));\n if (options.aa ?? true) pairs.push({ id: `${base.id}${AA_SUFFIX}`, a: base, b: base });\n if (pairs.length === 0) throw new RangeError(\"freshRun: the run has no pairs. Give a candidate, or keep the A/A pair.\");\n for (const arm of [base, ...candidates]) {\n if (typeof arm.id !== \"string\" || arm.id === \"\") throw new RangeError(\"freshRun: the id of an arm is a string that is not empty.\");\n if (arm.command.length === 0 || arm.command.some((s) => typeof s !== \"string\")) throw new RangeError(`freshRun: the command of the arm ${JSON.stringify(arm.id)} is 1 or more strings.`);\n }\n const ids = new Set([base.id]);\n for (const { id } of pairs) {\n if (ids.has(id)) throw new RangeError(`freshRun: two arms or pairs have the id ${JSON.stringify(id)}.`);\n ids.add(id);\n }\n\n const env = options.env ?? benchEnv();\n const out: Record<string, FreshRecord[]> = Object.fromEntries(pairs.map((p) => [p.id, []]));\n const skipped: FreshSkip[] = [];\n const gone = new Set<string>();\n const hashes = new Map<string, string>();\n\n for (let r = 0; r < rounds; r++) {\n const round = roundOffset + r;\n for (let i = 0; i < cases.length; i++) {\n const id = cases[i]!;\n if (gone.has(id)) continue;\n const records: { pair: string; record: FreshRecord }[] = [];\n let skip: FreshSkip | undefined;\n for (const slot of pairOrder(pairs.length, round, i)) {\n const pair = pairs[slot.pair]!;\n const times: Partial<Record<Arm, number>> = {};\n for (const arm of [slot.first, slot.first === \"a\" ? \"b\" : \"a\"] as const) {\n const who = pair[arm];\n const where = `the arm ${JSON.stringify(who.id)} (pair ${JSON.stringify(pair.id)}, arm ${arm}) on the case ${JSON.stringify(id)}, round ${round}`;\n const line = await child(start, [...who.command, ...args(id)], { timeoutMs, maxBuffer }, where);\n if (line.skipped !== undefined) {\n if (r > 0) throw new Error(`freshRun: ${where} skipped the case after the first round of the job (${JSON.stringify(line.skipped)}). A child that skips only in a later round is not deterministic.`);\n skip = { arm: who.id, case: id, round, reason: line.skipped };\n break;\n }\n if (line.hash !== undefined) {\n const key = `${who.id}\\u0000${id}`;\n const known = hashes.get(key);\n if (known !== undefined && known !== line.hash) throw new Error(`freshRun: ${where} gave the hash ${line.hash}, and an earlier child of the arm gave ${known}.`);\n hashes.set(key, line.hash);\n }\n times[arm] = line.ms;\n }\n if (skip !== undefined) break;\n records.push({ pair: pair.id, record: { case: id, round, first: slot.first, a: times.a!, b: times.b!, job, env } });\n }\n if (skip !== undefined) {\n skipped.push(skip);\n gone.add(id);\n continue;\n }\n for (const { pair, record } of records) out[pair]!.push(record);\n }\n }\n return { env, job, pairs: out, skipped };\n}\n\n/** This function starts one child and reads its JSON line. A fault gives an error that names the arm, the case, the round and the tail of `stderr`. */\nasync function child(start: StartChild, argv: readonly string[], limits: StartOptions, where: string): Promise<ChildLine> {\n let exit: ChildExit;\n try {\n exit = await start(argv, limits);\n } catch (error) {\n throw new Error(`freshRun: ${where} did not start: ${error instanceof Error ? error.message : String(error)}`, { cause: error });\n }\n const fail = (why: string): Error => {\n const tail = exit.stderr.length > TAIL ? `...${exit.stderr.slice(-TAIL)}` : exit.stderr;\n return new Error(`freshRun: ${where} ${why}. The tail of stderr: ${tail.trim() === \"\" ? \"(empty)\" : tail.trimEnd()}`);\n };\n if (exit.error !== undefined) throw fail(`failed: ${exit.error}`);\n if (exit.timedOut === true) throw fail(`took more than ${limits.timeoutMs} ms`);\n if (exit.code !== 0) throw fail(exit.code === null ? \"stopped on a signal\" : `exited with the code ${exit.code}`);\n try {\n return childLine(exit.stdout);\n } catch (error) {\n throw fail(`printed no good JSON line (${error instanceof Error ? error.message : String(error)})`);\n }\n}\n\n/** The wait, in milliseconds, for the pipes of a child after its exit, and for the exit of a child after a stop. */\nconst GRACE_MS = 1000;\n\n/**\n * This function starts a process with `spawn` and no shell. It stops the process and its tree at the time limit or at the output limit.\n *\n * - The promise settles at the close of the pipes, or a grace time after the exit of the child. A grandchild can keep a pipe open.\n * - A stop sends SIGTERM to the process group, and SIGKILL after the grace time. On Windows, `taskkill /T /F` stops the tree.\n */\nfunction startProcess(argv: readonly string[], options: StartOptions): Promise<ChildExit> {\n return new Promise((resolve) => {\n const [program, ...rest] = argv;\n const windows = process.platform === \"win32\";\n // On POSIX, the child leads its own process group, so a signal to the group reaches the grandchildren.\n const proc = spawn(program!, rest, { stdio: [\"ignore\", \"pipe\", \"pipe\"], windowsHide: true, detached: !windows });\n const out: Buffer[] = [];\n const err: Buffer[] = [];\n let timedOut = false;\n let settled = false;\n let stopping = false;\n let error: string | undefined;\n let grace: ReturnType<typeof setTimeout> | undefined;\n const signal = (name: NodeJS.Signals): void => stopTree(proc, name);\n const finish = (code: number | null): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n clearTimeout(grace);\n proc.stdout.destroy();\n proc.stderr.destroy();\n const exit: ChildExit = { stdout: Buffer.concat(out).toString(\"utf8\"), stderr: Buffer.concat(err).toString(\"utf8\"), code, timedOut };\n resolve(error === undefined ? exit : { ...exit, error });\n };\n const stop = (): void => {\n if (stopping) return;\n stopping = true;\n signal(\"SIGTERM\");\n clearTimeout(grace);\n grace = setTimeout(() => {\n if (!windows) signal(\"SIGKILL\");\n finish(null);\n }, GRACE_MS);\n };\n const timer = setTimeout(() => {\n timedOut = true;\n stop();\n }, options.timeoutMs);\n const take = (list: Buffer[]) => {\n let bytes = 0;\n return (chunk: Buffer): void => {\n bytes += chunk.length;\n if (bytes > options.maxBuffer) {\n error ??= `an output is more than ${options.maxBuffer} bytes`;\n stop();\n return;\n }\n list.push(chunk);\n };\n };\n proc.stdout.on(\"data\", take(out));\n proc.stderr.on(\"data\", take(err));\n proc.on(\"error\", (e) => {\n error ??= e.message;\n // A program that does not start gives no event close.\n if (proc.pid === undefined) finish(null);\n });\n proc.on(\"exit\", (code) => {\n clearTimeout(timer);\n if (stopping) return;\n // A grandchild that holds a pipe keeps the event close away. After the grace time, the runner stops the group.\n grace = setTimeout(() => {\n if (!windows) signal(\"SIGKILL\");\n finish(code);\n }, GRACE_MS);\n });\n proc.on(\"close\", (code) => finish(code));\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ /** The count of overruns in one group, after which the other cases of the group do not run. */
2
+ export declare const DEFAULT_OVERRUNS = 2;
3
+ /** The time between two reads of the progress file, in milliseconds. */
4
+ export declare const DEFAULT_POLL_MS = 1e3;
5
+ /** One case of a supervised run. */
6
+ export interface SupervisedCase {
7
+ readonly id: string;
8
+ /** The time budget of each stage of the case, in milliseconds. */
9
+ readonly budgetMs: number;
10
+ /**
11
+ * The group of each stage of the case. An overrun in a stage counts for the group of that stage only.
12
+ * A case with stages `pack` and `route` can thus count against a router, or against a packer, and not against both.
13
+ */
14
+ readonly groups?: Readonly<Record<string, string>>;
15
+ }
16
+ /** The end of a case that did not finish: an overrun, which is measured, or a skip, which is not. */
17
+ export type SupervisedEnd = {
18
+ readonly kind: "over-budget";
19
+ readonly id: string;
20
+ readonly budgetMs: number;
21
+ readonly stage: string;
22
+ } | {
23
+ readonly kind: "not-run";
24
+ readonly id: string;
25
+ readonly budgetMs: number;
26
+ readonly group: string;
27
+ readonly because: readonly string[];
28
+ };
29
+ /** One running child. */
30
+ export interface SupervisedChild {
31
+ /** This function stops the child and its tree at once. */
32
+ stop(): void;
33
+ /** The exit code of the child, or `null` when a signal stopped it. */
34
+ readonly exited: Promise<number | null>;
35
+ }
36
+ /** The options of `superviseCases`. */
37
+ export interface SuperviseOptions {
38
+ /** The cases, in the order of the run. */
39
+ readonly cases: readonly SupervisedCase[];
40
+ /** The ids of the cases of an earlier run. These cases do not run. */
41
+ readonly done?: ReadonlySet<string>;
42
+ /**
43
+ * The ends of an earlier run of these cases, in their order. These cases do not run, and an `over-budget` end needs its case in `cases`.
44
+ * Each `over-budget` end counts for its group, so a stop and a resume give the same ends as a run with no stop.
45
+ */
46
+ readonly earlier?: readonly SupervisedEnd[];
47
+ /** The file that the child writes with `caseProgress`. The supervisor makes it empty at the start. */
48
+ readonly progressFile: string;
49
+ /** This function starts a child on the ids that are left. */
50
+ readonly start: (left: readonly string[]) => SupervisedChild;
51
+ /** This function is the callback for the record of each case that did not finish. */
52
+ readonly onEnd: (end: SupervisedEnd) => void;
53
+ /** The default is `DEFAULT_OVERRUNS`. */
54
+ readonly overruns?: number;
55
+ /** The default is `DEFAULT_POLL_MS`. */
56
+ readonly pollMs?: number;
57
+ /** The clock of the supervisor. It is the clock of `caseProgress`. */
58
+ readonly now?: () => number;
59
+ }
60
+ /** The result of `superviseCases`. */
61
+ export interface SuperviseResult {
62
+ /** The exit code of the last child, or `null` when no child ran or a signal stopped the last child. */
63
+ readonly code: number | null;
64
+ /** The count of the children. */
65
+ readonly starts: number;
66
+ readonly ends: readonly SupervisedEnd[];
67
+ /** The ids that have no result, because the last child ended before them. */
68
+ readonly left: readonly string[];
69
+ }
70
+ /** The writer of a progress file, for the child. */
71
+ export interface CaseProgress {
72
+ /** This function tells that a stage of a case starts. Each stage has its own budget. */
73
+ start(id: string, stage?: string): void;
74
+ /** This function tells that a case ended. Call it after the result of the case is on the disk. */
75
+ end(id: string): void;
76
+ }
77
+ /** This function gives the writer of a progress file. Each write is synchronous, so it works in a loop that does not yield. */
78
+ export declare function caseProgress(file: string, now?: () => number): CaseProgress;
79
+ /**
80
+ * This function runs the cases and holds each case to its budget.
81
+ *
82
+ * - A stage over its budget stops the child, and `onEnd` gets `over-budget`. The next child runs the cases that are left.
83
+ * - After `overruns` overruns in one group, each other case of the group ends as `not-run`, with the ids of those overruns.
84
+ * - A case in `done` or `earlier` does not run. A case that ended, or that has an end from `onEnd`, does not run again.
85
+ * - An `over-budget` end of `earlier` counts as an overrun of this run. `onEnd` does not get the end again.
86
+ * - A child that ends without a stop ends the run. Its exit code is the code of the result.
87
+ * - A line of the progress file that is not JSON stops the child, and this function then throws the error.
88
+ */
89
+ export declare function superviseCases(options: SuperviseOptions): Promise<SuperviseResult>;
90
+ /** This function starts a process for `superviseCases`, with no shell. The process shares the output of the supervisor, and `stop` stops its tree at once. */
91
+ export declare function startSupervisedProcess(argv: readonly string[], options?: {
92
+ readonly cwd?: string;
93
+ readonly env?: NodeJS.ProcessEnv;
94
+ }): SupervisedChild;
95
+ //# sourceMappingURL=supervise.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supervise.d.ts","names":[],"sources":["../src/supervise.ts"],"mappings":";qBAYa;;qBAGA;;iBAGI;WACN;;WAEA;;;;;WAKA,SAAS,SAAS;;;YAIjB;WACG;WAA8B;WAAqB;WAA2B;;WAC9E;WAA0B;WAAqB;WAA2B;WAAwB;;;iBAGhG;;EAEf;;WAES,QAAQ;;;iBAIF;;WAEN,gBAAgB;;WAEhB,OAAO;;;;;WAKP,mBAAmB;;WAEnB;;WAEA,QAAQ,4BAA4B;;WAEpC,QAAQ,KAAK;;WAEb;;WAEA;;WAEA;;;iBAIM;;WAEN;;WAEA;WACA,eAAe;;WAEf;;;iBAIM;;EAEf,MAAM,YAAY;;EAElB,IAAI;;;wBAWU,aAAa,cAAc,qBAA+B;;;;;;;;;;;wBA8BpD,eAAe,SAAS,mBAAmB,QAAQ;;wBAiGzD,uBAAuB,yBAAyB;WAAoB;WAAuB,MAAM,OAAO;IAAoB"}
@@ -0,0 +1,178 @@
1
+ import { t as stopTree } from "./tree.js";
2
+ import { spawn } from "node:child_process";
3
+ import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
4
+ // src/supervise.ts
5
+ /**
6
+ * The runner of cases with a time budget. A case runs in a child process, so a hard stop works on code that does not return.
7
+ * One child runs each case that is left. The child writes the start and the end of each case to a progress file.
8
+ * When a case passes its budget, the supervisor stops the child, reports the case, and starts a new child on the cases that are left.
9
+ * Only an overrun costs a new child, so the times of the other cases do not change.
10
+ */
11
+ /** The count of overruns in one group, after which the other cases of the group do not run. */
12
+ const DEFAULT_OVERRUNS = 2;
13
+ /** The time between two reads of the progress file, in milliseconds. */
14
+ const DEFAULT_POLL_MS = 1e3;
15
+ /** This function gives the writer of a progress file. Each write is synchronous, so it works in a loop that does not yield. */
16
+ function caseProgress(file, now = Date.now) {
17
+ const write = (event) => appendFileSync(file, `${JSON.stringify(event)}\n`);
18
+ return {
19
+ start: (id, stage = "") => write({
20
+ event: "start",
21
+ id,
22
+ stage,
23
+ at: now()
24
+ }),
25
+ end: (id) => write({
26
+ event: "end",
27
+ id
28
+ })
29
+ };
30
+ }
31
+ /** The reader of a progress file: each call gives the events of the lines that are whole and were not read. */
32
+ function progressReader(file) {
33
+ let read = 0;
34
+ return () => {
35
+ const text = readFileSync(file, "utf8");
36
+ const whole = text.slice(0, text.lastIndexOf("\n") + 1);
37
+ const lines = whole.slice(read).split("\n").filter((line) => line !== "");
38
+ read = whole.length;
39
+ return lines.map((line) => JSON.parse(line));
40
+ };
41
+ }
42
+ /**
43
+ * This function runs the cases and holds each case to its budget.
44
+ *
45
+ * - A stage over its budget stops the child, and `onEnd` gets `over-budget`. The next child runs the cases that are left.
46
+ * - After `overruns` overruns in one group, each other case of the group ends as `not-run`, with the ids of those overruns.
47
+ * - A case in `done` or `earlier` does not run. A case that ended, or that has an end from `onEnd`, does not run again.
48
+ * - An `over-budget` end of `earlier` counts as an overrun of this run. `onEnd` does not get the end again.
49
+ * - A child that ends without a stop ends the run. Its exit code is the code of the result.
50
+ * - A line of the progress file that is not JSON stops the child, and this function then throws the error.
51
+ */
52
+ async function superviseCases(options) {
53
+ const { cases, progressFile, start, onEnd, now = Date.now } = options;
54
+ const limit = options.overruns ?? 2;
55
+ const pollMs = options.pollMs ?? 1e3;
56
+ const byId = new Map(cases.map((c) => [c.id, c]));
57
+ const finished = new Set(options.done ?? []);
58
+ const groupOverruns = /* @__PURE__ */ new Map();
59
+ const ends = [];
60
+ const left = () => cases.filter((c) => !finished.has(c.id)).map((c) => c.id);
61
+ const report = (end) => {
62
+ finished.add(end.id);
63
+ ends.push(end);
64
+ onEnd(end);
65
+ };
66
+ const countOverrun = (id, stage) => {
67
+ const group = byId.get(id)?.groups?.[stage];
68
+ if (group === void 0) return;
69
+ const ids = [...groupOverruns.get(group) ?? [], id];
70
+ groupOverruns.set(group, ids);
71
+ if (ids.length < limit) return;
72
+ for (const c of cases) if (!finished.has(c.id) && Object.values(c.groups ?? {}).includes(group)) report({
73
+ kind: "not-run",
74
+ id: c.id,
75
+ budgetMs: c.budgetMs,
76
+ group,
77
+ because: ids.slice(0, limit)
78
+ });
79
+ };
80
+ const earlier = options.earlier ?? [];
81
+ for (const end of earlier) {
82
+ if (end.kind === "over-budget" && !byId.has(end.id)) throw new RangeError(`The case "${end.id}" of the option earlier is not in the option cases.`);
83
+ finished.add(end.id);
84
+ }
85
+ for (const end of earlier) if (end.kind === "over-budget") countOverrun(end.id, end.stage);
86
+ writeFileSync(progressFile, "");
87
+ const readEvents = progressReader(progressFile);
88
+ const state = {};
89
+ const update = () => {
90
+ for (const event of readEvents()) if (event.event === "start") state.running = {
91
+ id: event.id,
92
+ stage: event.stage ?? "",
93
+ at: event.at ?? now()
94
+ };
95
+ else {
96
+ finished.add(event.id);
97
+ if (state.running?.id === event.id) state.running = void 0;
98
+ }
99
+ };
100
+ const forgetChild = () => {
101
+ state.running = void 0;
102
+ state.exit = void 0;
103
+ };
104
+ let starts = 0;
105
+ let code = null;
106
+ while (left().length > 0) {
107
+ forgetChild();
108
+ starts += 1;
109
+ const child = start(left());
110
+ const exited = child.exited.then((value) => {
111
+ state.exit = { code: value };
112
+ });
113
+ let over;
114
+ while (state.exit === void 0 && over === void 0) {
115
+ let timer;
116
+ await Promise.race([exited, new Promise((resolve) => timer = setTimeout(resolve, pollMs))]);
117
+ clearTimeout(timer);
118
+ try {
119
+ update();
120
+ } catch (error) {
121
+ child.stop();
122
+ await exited;
123
+ throw error;
124
+ }
125
+ const current = state.running;
126
+ const budgetMs = current === void 0 ? void 0 : byId.get(current.id)?.budgetMs;
127
+ if (state.exit === void 0 && current !== void 0 && budgetMs !== void 0 && now() - current.at > budgetMs) over = {
128
+ id: current.id,
129
+ stage: current.stage,
130
+ budgetMs
131
+ };
132
+ }
133
+ if (over === void 0) {
134
+ code = state.exit?.code ?? null;
135
+ break;
136
+ }
137
+ child.stop();
138
+ await exited;
139
+ code = null;
140
+ update();
141
+ if (finished.has(over.id)) continue;
142
+ report({
143
+ kind: "over-budget",
144
+ ...over
145
+ });
146
+ countOverrun(over.id, over.stage);
147
+ }
148
+ return {
149
+ code,
150
+ starts,
151
+ ends,
152
+ left: left()
153
+ };
154
+ }
155
+ /** This function starts a process for `superviseCases`, with no shell. The process shares the output of the supervisor, and `stop` stops its tree at once. */
156
+ function startSupervisedProcess(argv, options = {}) {
157
+ const [program, ...rest] = argv;
158
+ const proc = spawn(program, rest, {
159
+ stdio: [
160
+ "ignore",
161
+ "inherit",
162
+ "inherit"
163
+ ],
164
+ windowsHide: true,
165
+ detached: process.platform !== "win32",
166
+ ...options
167
+ });
168
+ return {
169
+ stop: () => stopTree(proc, "SIGKILL"),
170
+ exited: new Promise((resolve) => {
171
+ proc.on("error", () => resolve(null));
172
+ proc.on("close", (code) => resolve(code));
173
+ })
174
+ };
175
+ }
176
+ export { DEFAULT_OVERRUNS, DEFAULT_POLL_MS, caseProgress, startSupervisedProcess, superviseCases };
177
+
178
+ //# sourceMappingURL=supervise.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supervise.js","names":[],"sources":["../src/supervise.ts"],"sourcesContent":["/**\n * The runner of cases with a time budget. A case runs in a child process, so a hard stop works on code that does not return.\n * One child runs each case that is left. The child writes the start and the end of each case to a progress file.\n * When a case passes its budget, the supervisor stops the child, reports the case, and starts a new child on the cases that are left.\n * Only an overrun costs a new child, so the times of the other cases do not change.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { appendFileSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { stopTree } from \"./tree.ts\";\n\n/** The count of overruns in one group, after which the other cases of the group do not run. */\nexport const DEFAULT_OVERRUNS = 2;\n\n/** The time between two reads of the progress file, in milliseconds. */\nexport const DEFAULT_POLL_MS = 1000;\n\n/** One case of a supervised run. */\nexport interface SupervisedCase {\n readonly id: string;\n /** The time budget of each stage of the case, in milliseconds. */\n readonly budgetMs: number;\n /**\n * The group of each stage of the case. An overrun in a stage counts for the group of that stage only.\n * A case with stages `pack` and `route` can thus count against a router, or against a packer, and not against both.\n */\n readonly groups?: Readonly<Record<string, string>>;\n}\n\n/** The end of a case that did not finish: an overrun, which is measured, or a skip, which is not. */\nexport type SupervisedEnd =\n | { readonly kind: \"over-budget\"; readonly id: string; readonly budgetMs: number; readonly stage: string }\n | { readonly kind: \"not-run\"; readonly id: string; readonly budgetMs: number; readonly group: string; readonly because: readonly string[] };\n\n/** One running child. */\nexport interface SupervisedChild {\n /** This function stops the child and its tree at once. */\n stop(): void;\n /** The exit code of the child, or `null` when a signal stopped it. */\n readonly exited: Promise<number | null>;\n}\n\n/** The options of `superviseCases`. */\nexport interface SuperviseOptions {\n /** The cases, in the order of the run. */\n readonly cases: readonly SupervisedCase[];\n /** The ids of the cases of an earlier run. These cases do not run. */\n readonly done?: ReadonlySet<string>;\n /**\n * The ends of an earlier run of these cases, in their order. These cases do not run, and an `over-budget` end needs its case in `cases`.\n * Each `over-budget` end counts for its group, so a stop and a resume give the same ends as a run with no stop.\n */\n readonly earlier?: readonly SupervisedEnd[];\n /** The file that the child writes with `caseProgress`. The supervisor makes it empty at the start. */\n readonly progressFile: string;\n /** This function starts a child on the ids that are left. */\n readonly start: (left: readonly string[]) => SupervisedChild;\n /** This function is the callback for the record of each case that did not finish. */\n readonly onEnd: (end: SupervisedEnd) => void;\n /** The default is `DEFAULT_OVERRUNS`. */\n readonly overruns?: number;\n /** The default is `DEFAULT_POLL_MS`. */\n readonly pollMs?: number;\n /** The clock of the supervisor. It is the clock of `caseProgress`. */\n readonly now?: () => number;\n}\n\n/** The result of `superviseCases`. */\nexport interface SuperviseResult {\n /** The exit code of the last child, or `null` when no child ran or a signal stopped the last child. */\n readonly code: number | null;\n /** The count of the children. */\n readonly starts: number;\n readonly ends: readonly SupervisedEnd[];\n /** The ids that have no result, because the last child ended before them. */\n readonly left: readonly string[];\n}\n\n/** The writer of a progress file, for the child. */\nexport interface CaseProgress {\n /** This function tells that a stage of a case starts. Each stage has its own budget. */\n start(id: string, stage?: string): void;\n /** This function tells that a case ended. Call it after the result of the case is on the disk. */\n end(id: string): void;\n}\n\ninterface ProgressEvent {\n readonly event: \"start\" | \"end\";\n readonly id: string;\n readonly stage?: string;\n readonly at?: number;\n}\n\n/** This function gives the writer of a progress file. Each write is synchronous, so it works in a loop that does not yield. */\nexport function caseProgress(file: string, now: () => number = Date.now): CaseProgress {\n const write = (event: ProgressEvent): void => appendFileSync(file, `${JSON.stringify(event)}\\n`);\n return {\n start: (id, stage = \"\") => write({ event: \"start\", id, stage, at: now() }),\n end: (id) => write({ event: \"end\", id }),\n };\n}\n\n/** The reader of a progress file: each call gives the events of the lines that are whole and were not read. */\nfunction progressReader(file: string): () => ProgressEvent[] {\n let read = 0;\n return () => {\n const text = readFileSync(file, \"utf8\");\n const whole = text.slice(0, text.lastIndexOf(\"\\n\") + 1);\n const lines = whole.slice(read).split(\"\\n\").filter((line) => line !== \"\");\n read = whole.length;\n return lines.map((line) => JSON.parse(line) as ProgressEvent);\n };\n}\n\n/**\n * This function runs the cases and holds each case to its budget.\n *\n * - A stage over its budget stops the child, and `onEnd` gets `over-budget`. The next child runs the cases that are left.\n * - After `overruns` overruns in one group, each other case of the group ends as `not-run`, with the ids of those overruns.\n * - A case in `done` or `earlier` does not run. A case that ended, or that has an end from `onEnd`, does not run again.\n * - An `over-budget` end of `earlier` counts as an overrun of this run. `onEnd` does not get the end again.\n * - A child that ends without a stop ends the run. Its exit code is the code of the result.\n * - A line of the progress file that is not JSON stops the child, and this function then throws the error.\n */\nexport async function superviseCases(options: SuperviseOptions): Promise<SuperviseResult> {\n const { cases, progressFile, start, onEnd, now = Date.now } = options;\n const limit = options.overruns ?? DEFAULT_OVERRUNS;\n const pollMs = options.pollMs ?? DEFAULT_POLL_MS;\n const byId = new Map(cases.map((c) => [c.id, c]));\n const finished = new Set(options.done ?? []);\n const groupOverruns = new Map<string, string[]>();\n const ends: SupervisedEnd[] = [];\n const left = (): string[] => cases.filter((c) => !finished.has(c.id)).map((c) => c.id);\n const report = (end: SupervisedEnd): void => {\n finished.add(end.id);\n ends.push(end);\n onEnd(end);\n };\n const countOverrun = (id: string, stage: string): void => {\n const group = byId.get(id)?.groups?.[stage];\n if (group === undefined) return;\n const ids = [...(groupOverruns.get(group) ?? []), id];\n groupOverruns.set(group, ids);\n if (ids.length < limit) return;\n for (const c of cases) {\n if (!finished.has(c.id) && Object.values(c.groups ?? {}).includes(group)) report({ kind: \"not-run\", id: c.id, budgetMs: c.budgetMs, group, because: ids.slice(0, limit) });\n }\n };\n\n const earlier = options.earlier ?? [];\n for (const end of earlier) {\n if (end.kind === \"over-budget\" && !byId.has(end.id)) throw new RangeError(`The case \"${end.id}\" of the option earlier is not in the option cases.`);\n finished.add(end.id);\n }\n for (const end of earlier) {\n if (end.kind === \"over-budget\") countOverrun(end.id, end.stage);\n }\n\n writeFileSync(progressFile, \"\");\n const readEvents = progressReader(progressFile);\n const state: { running?: { readonly id: string; readonly stage: string; readonly at: number } | undefined; exit?: { readonly code: number | null } | undefined } = {};\n const update = (): void => {\n for (const event of readEvents()) {\n if (event.event === \"start\") {\n state.running = { id: event.id, stage: event.stage ?? \"\", at: event.at ?? now() };\n } else {\n finished.add(event.id);\n if (state.running?.id === event.id) state.running = undefined;\n }\n }\n };\n\n // A function makes this reset, so that the checks of the loop see the declared types of the state.\n const forgetChild = (): void => {\n state.running = undefined;\n state.exit = undefined;\n };\n\n let starts = 0;\n let code: number | null = null;\n while (left().length > 0) {\n forgetChild();\n starts += 1;\n const child = start(left());\n const exited = child.exited.then((value) => {\n state.exit = { code: value };\n });\n let over: { readonly id: string; readonly stage: string; readonly budgetMs: number } | undefined;\n while (state.exit === undefined && over === undefined) {\n let timer: ReturnType<typeof setTimeout> | undefined;\n await Promise.race([exited, new Promise((resolve) => (timer = setTimeout(resolve, pollMs)))]);\n clearTimeout(timer);\n try {\n update();\n } catch (error) {\n // A progress file that cannot be read cannot hold the child to its budget, so the child must not outlive the supervisor.\n child.stop();\n await exited;\n throw error;\n }\n const current = state.running;\n const budgetMs = current === undefined ? undefined : byId.get(current.id)?.budgetMs;\n if (state.exit === undefined && current !== undefined && budgetMs !== undefined && now() - current.at > budgetMs) over = { id: current.id, stage: current.stage, budgetMs };\n }\n if (over === undefined) {\n code = state.exit?.code ?? null;\n break;\n }\n child.stop();\n await exited;\n code = null;\n update();\n // The case can end between the read and the stop. Then its result is on the disk, and it has no overrun.\n if (finished.has(over.id)) continue;\n report({ kind: \"over-budget\", ...over });\n countOverrun(over.id, over.stage);\n }\n return { code, starts, ends, left: left() };\n}\n\n/** This function starts a process for `superviseCases`, with no shell. The process shares the output of the supervisor, and `stop` stops its tree at once. */\nexport function startSupervisedProcess(argv: readonly string[], options: { readonly cwd?: string; readonly env?: NodeJS.ProcessEnv } = {}): SupervisedChild {\n const [program, ...rest] = argv;\n // On POSIX, the child leads its own process group, so a signal to the group reaches the grandchildren.\n const proc = spawn(program!, rest, { stdio: [\"ignore\", \"inherit\", \"inherit\"], windowsHide: true, detached: process.platform !== \"win32\", ...options });\n const exited = new Promise<number | null>((resolve) => {\n proc.on(\"error\", () => resolve(null));\n proc.on(\"close\", (code) => resolve(code));\n });\n return { stop: () => stopTree(proc, \"SIGKILL\"), exited };\n}\n"],"mappings":"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"}
package/build/tree.js ADDED
@@ -0,0 +1,28 @@
1
+ import { spawn } from "node:child_process";
2
+ // src/tree.ts
3
+ /** The hard stop of a child process, which the fresh runner and the supervisor share. */
4
+ /** This function sends a signal to a child and its tree. The child leads its own process group on POSIX. On Windows, each signal is a hard stop. */
5
+ function stopTree(proc, name) {
6
+ const pid = proc.pid;
7
+ if (pid === void 0) return;
8
+ if (process.platform === "win32") {
9
+ spawn("taskkill", [
10
+ "/pid",
11
+ String(pid),
12
+ "/T",
13
+ "/F"
14
+ ], {
15
+ stdio: "ignore",
16
+ windowsHide: true
17
+ }).on("error", () => proc.kill());
18
+ return;
19
+ }
20
+ try {
21
+ process.kill(-pid, name);
22
+ } catch {
23
+ proc.kill(name);
24
+ }
25
+ }
26
+ export { stopTree as t };
27
+
28
+ //# sourceMappingURL=tree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.js","names":[],"sources":["../src/tree.ts"],"sourcesContent":["/** The hard stop of a child process, which the fresh runner and the supervisor share. */\n\nimport { type ChildProcess, spawn } from \"node:child_process\";\n\n/** This function sends a signal to a child and its tree. The child leads its own process group on POSIX. On Windows, each signal is a hard stop. */\nexport function stopTree(proc: ChildProcess, name: NodeJS.Signals): void {\n const pid = proc.pid;\n if (pid === undefined) return;\n if (process.platform === \"win32\") {\n // TerminateProcess stops only the direct child. The flag /T of taskkill also stops its tree.\n spawn(\"taskkill\", [\"/pid\", String(pid), \"/T\", \"/F\"], { stdio: \"ignore\", windowsHide: true }).on(\"error\", () => proc.kill());\n return;\n }\n try {\n process.kill(-pid, name);\n } catch {\n proc.kill(name);\n }\n}\n"],"mappings":";;;;AAKA,SAAgB,SAAS,MAAoB,MAA4B;CACvE,MAAM,MAAM,KAAK;CACjB,IAAI,QAAQ,KAAA,GAAW;CACvB,IAAI,QAAQ,aAAa,SAAS;EAEhC,MAAM,YAAY;GAAC;GAAQ,OAAO,GAAG;GAAG;GAAM;EAAI,GAAG;GAAE,OAAO;GAAU,aAAa;EAAK,CAAC,CAAC,CAAC,GAAG,eAAe,KAAK,KAAK,CAAC;EAC1H;CACF;CACA,IAAI;EACF,QAAQ,KAAK,CAAC,KAAK,IAAI;CACzB,QAAQ;EACN,KAAK,KAAK,IAAI;CAChB;AACF"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/rigor-bench",
4
- "version": "0.2.7",
4
+ "version": "0.2.8",
5
5
  "description": "The paired-time records of a bench: the order that turns each round, the ratio of each case, and the time verdict against the band of an A/A arm.",
6
6
  "keywords": [
7
7
  "benchmark",
@@ -35,6 +35,10 @@
35
35
  "types": "./build/fresh.d.ts",
36
36
  "import": "./build/fresh.js"
37
37
  },
38
+ "./supervise": {
39
+ "types": "./build/supervise.d.ts",
40
+ "import": "./build/supervise.js"
41
+ },
38
42
  "./package.json": "./package.json"
39
43
  },
40
44
  "files": [