@mark1russell7/rigor-bench 0.2.7 → 0.2.9
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 +35 -1
- package/build/fresh.d.ts.map +1 -1
- package/build/fresh.js +2 -21
- package/build/fresh.js.map +1 -1
- package/build/index.d.ts +23 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +19 -1
- package/build/index.js.map +1 -1
- package/build/supervise.d.ts +95 -0
- package/build/supervise.d.ts.map +1 -0
- package/build/supervise.js +178 -0
- package/build/supervise.js.map +1 -0
- package/build/tree.js +28 -0
- package/build/tree.js.map +1 -0
- package/package.json +6 -2
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
|
|
|
@@ -119,14 +122,16 @@ v.next; // the first rung that is not
|
|
|
119
122
|
v.failed; // true when a locked rung is not within
|
|
120
123
|
```
|
|
121
124
|
|
|
122
|
-
There are
|
|
125
|
+
There are four kinds of budget:
|
|
123
126
|
|
|
124
127
|
| Kind | Evidence | The verdict |
|
|
125
128
|
| --- | --- | --- |
|
|
126
129
|
| `median` | Rounds and an A/A arm | `budgetVerdict`, so the rules of a budget apply. |
|
|
130
|
+
| `quantile` | The values | The distribution-free interval of the quantile at 95 % against the limit. |
|
|
127
131
|
| `rate` | Failures and trials | The Wilson interval at 95 % against the limit. |
|
|
128
132
|
| `worst` | The value of each drill | The largest value against the limit. One bad drill is `beyond`. |
|
|
129
133
|
|
|
134
|
+
- A p99 needs 368 independent values for an upper bound at 95 %. A p95 needs 72 and a p99.9 needs 3,688. `quantileSamples` gives the count. With fewer values, the verdict is `cannot decide` or `beyond`. It is not `within`. Latencies of one run can follow each other: space the samples apart, or give one value for each run.
|
|
130
135
|
- A failed rate of 0 of 38,500 holds a limit of 0.01 %. A failed rate of 0 of 30,000 is `cannot decide`, because the Wilson bound of that run is 0.0128 %.
|
|
131
136
|
- A locked rung fails when it is `beyond` and when it is `cannot decide`. A skipped test is not a pass.
|
|
132
137
|
- The verdict reads the newest record of each rung, so an old pass does not hide a new failure. With `maxAgeDays`, an older record gives `cannot decide`.
|
|
@@ -183,6 +188,35 @@ A run of many jobs sets `roundOffset` to `job * rounds`. Then the merged records
|
|
|
183
188
|
|
|
184
189
|
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
190
|
|
|
191
|
+
## Run cases with a time budget
|
|
192
|
+
|
|
193
|
+
The entry `@mark1russell7/rigor-bench/supervise` starts child processes, so it is for Node only.
|
|
194
|
+
|
|
195
|
+
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.
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { startSupervisedProcess, superviseCases } from "@mark1russell7/rigor-bench/supervise";
|
|
199
|
+
|
|
200
|
+
const result = await superviseCases({
|
|
201
|
+
cases: [{ id: "a", budgetMs: 300_000, groups: { "": "router-1" } }],
|
|
202
|
+
progressFile: "out/progress.ndjson",
|
|
203
|
+
start: (left) => startSupervisedProcess([process.execPath, "run-cases.js", ...left]),
|
|
204
|
+
onEnd: (end) => writeRecord(end),
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The child calls `caseProgress(file).start(id, stage)` before each stage, and `end(id)` after the result is on the disk.
|
|
209
|
+
|
|
210
|
+
- 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.
|
|
211
|
+
- 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.
|
|
212
|
+
- An overrun counts for the group of its stage only. A slow router thus does not count against the packer of its cases.
|
|
213
|
+
- A case in `done` does not run, so a resume runs no case two times.
|
|
214
|
+
- 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`.
|
|
215
|
+
- Only an overrun starts a new child, so the other cases keep the process and the caches of a normal run.
|
|
216
|
+
- 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.
|
|
217
|
+
|
|
218
|
+
The callback `onEnd` writes the record of each tag. The callback `start` can also repair a file that a stop cut.
|
|
219
|
+
|
|
186
220
|
## The pooled verdict
|
|
187
221
|
|
|
188
222
|
`pooledVerdict` gives the verdict of the records of all jobs together:
|
package/build/fresh.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fresh.d.ts","names":[],"sources":["../src/fresh.ts"],"mappings":";;
|
|
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;
|
package/build/fresh.js.map
CHANGED
|
@@ -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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"}
|
package/build/index.d.ts
CHANGED
|
@@ -99,8 +99,25 @@ interface WorstBudget {
|
|
|
99
99
|
/** The number of drills that a verdict needs, 1 or more. With fewer, the budget is `cannot decide`. */
|
|
100
100
|
readonly minDrills: number;
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* A budget on a high quantile, for example the p99 of a read. A record gives the values. The verdict is the distribution-free
|
|
104
|
+
* interval of the quantile against the limit. A p99 needs 368 independent values at the level 0.95: refer to `quantileSamples`.
|
|
105
|
+
* Successive times of one run can follow each other. Space them apart, or give one value for each run.
|
|
106
|
+
*/
|
|
107
|
+
interface QuantileBudget {
|
|
108
|
+
readonly kind: "quantile";
|
|
109
|
+
readonly measure: string;
|
|
110
|
+
/** The unit of the limit and of the values. */
|
|
111
|
+
readonly unit: string;
|
|
112
|
+
/** The quantile, more than 0 and less than 1, for example 0.99. */
|
|
113
|
+
readonly quantile: number;
|
|
114
|
+
/** The largest quantile that the budget permits. */
|
|
115
|
+
readonly limit: number;
|
|
116
|
+
/** The level of the interval. The default is 0.95. */
|
|
117
|
+
readonly level?: number;
|
|
118
|
+
}
|
|
102
119
|
/** One budget of a rung. */
|
|
103
|
-
type RungBudget = MedianBudget | RateBudget | WorstBudget;
|
|
120
|
+
type RungBudget = MedianBudget | QuantileBudget | RateBudget | WorstBudget;
|
|
104
121
|
/** One rung of a ladder. */
|
|
105
122
|
interface Rung {
|
|
106
123
|
/** The name of the rung, unique in the ladder. */
|
|
@@ -127,6 +144,10 @@ type RungEvidence = {
|
|
|
127
144
|
readonly measure: string;
|
|
128
145
|
readonly rounds: readonly BudgetRound[];
|
|
129
146
|
readonly aa: readonly BudgetRound[];
|
|
147
|
+
} | {
|
|
148
|
+
readonly kind: "quantile";
|
|
149
|
+
readonly measure: string;
|
|
150
|
+
readonly values: readonly number[];
|
|
130
151
|
} | {
|
|
131
152
|
readonly kind: "rate";
|
|
132
153
|
readonly measure: string;
|
|
@@ -609,5 +630,5 @@ interface Rounds {
|
|
|
609
630
|
* keeps the work.
|
|
610
631
|
*/
|
|
611
632
|
export declare function timeRounds(arms: Readonly<Record<Arm, () => number>>, options: RoundsOptions): Rounds;
|
|
612
|
-
export { type Arm, type Balanced, type Band, type BandKind, type BandOptions, type BudgetOptions, type BudgetOutcome, type BudgetResult, type BudgetRound, type BudgetVerdict, type CaseBudget, type CaseInterval, type CaseLimit, type CaseRatio, type JobLimitVerdict, type JobRecords, type JobVerdict, type Ladder, type LadderDimension, type LadderOptions, type LadderVerdict, type LimitOptions, type LimitOutcome, type LimitVerdict, type MedianBudget, type NoiseCheck, type PairedTime, type PoolFacts, type PoolOptions, type PooledJob, type PooledLimitVerdict, type PooledRecords, type PooledVerdict, type Power, type PowerOptions, type RateBudget, type Rounds, type RoundsOptions, type RunPairs, type Rung, type RungBudget, type RungEvidence, type RungRecord, type RungVerdict, type TimeOutcome, type TimeVerdict, type VerdictOptions, type VerdictRule, type WorstBudget, caseRatios, checkPairedTime, firstArm };
|
|
633
|
+
export { type Arm, type Balanced, type Band, type BandKind, type BandOptions, type BudgetOptions, type BudgetOutcome, type BudgetResult, type BudgetRound, type BudgetVerdict, type CaseBudget, type CaseInterval, type CaseLimit, type CaseRatio, type JobLimitVerdict, type JobRecords, type JobVerdict, type Ladder, type LadderDimension, type LadderOptions, type LadderVerdict, type LimitOptions, type LimitOutcome, type LimitVerdict, type MedianBudget, type NoiseCheck, type PairedTime, type PoolFacts, type PoolOptions, type PooledJob, type PooledLimitVerdict, type PooledRecords, type PooledVerdict, type Power, type PowerOptions, type QuantileBudget, type RateBudget, type Rounds, type RoundsOptions, type RunPairs, type Rung, type RungBudget, type RungEvidence, type RungRecord, type RungVerdict, type TimeOutcome, type TimeVerdict, type VerdictOptions, type VerdictRule, type WorstBudget, caseRatios, checkPairedTime, firstArm };
|
|
613
634
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/budget.ts","../src/ladder.ts","../src/verdict.ts","../src/limit.ts","../src/merge.ts","../src/power.ts","../src/rounds.ts"],"mappings":";;;UAQiB;;WAEN;;WAEA;WACA;;;KAIC;;UAGK;;WAEN;;WAEA;;WAEA;;;UAIM;WACN;;WAEA;;WAEA;WACA;;WAEA;;WAEA;;WAEA;WACA;;WAEA;WACA,SAAS;;WAET;;;UAIM;WACN,SAAS;;WAET;WACA;WACA;WACA;WACA,gBAAgB;;;;;;;;;;;;wBAyDX,cAAc,iBAAiB,eAAe,aAAa,eAAe,SAAS,gBAAgB;;qBC3GtG;;UAGI;WACN;;WAEA;;;UAIM;WACN;WACA;;WAEA;;WAEA;;WAEA;;WAEA;;;UAIM;WACN;WACA;;WAEA;;;UAIM;WACN;WACA;WACA;;WAEA;;WAEA;;;KAIC,aAAa,eAAe,aAAa;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/budget.ts","../src/ladder.ts","../src/verdict.ts","../src/limit.ts","../src/merge.ts","../src/power.ts","../src/rounds.ts"],"mappings":"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"}
|
package/build/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as checkPairedTime, r as firstArm, t as caseRatios } from "./paired.js";
|
|
2
|
-
import { bootstrapMedian, bootstrapTwo, compareRatios, median, medianInterval, quantile, wilson } from "@mark1russell7/rigor-statistics";
|
|
2
|
+
import { bootstrapMedian, bootstrapTwo, compareRatios, median, medianInterval, quantile, quantileInterval, quantileSamples, wilson } from "@mark1russell7/rigor-statistics";
|
|
3
3
|
import { random } from "@mark1russell7/rigor-generators";
|
|
4
4
|
// src/budget.ts
|
|
5
5
|
/**
|
|
@@ -115,6 +115,8 @@ function budgetProblems(where, budget) {
|
|
|
115
115
|
if (budget.kind === "rate" && budget.limit > 1) out.push(`${name}: a rate limit is at most 1, not ${budget.limit}.`);
|
|
116
116
|
if (budget.kind === "median" && !(Number.isFinite(budget.maxDifference) && budget.maxDifference >= 0)) out.push(`${name}: the largest difference is finite and 0 or more, not ${budget.maxDifference}.`);
|
|
117
117
|
if (budget.kind === "worst" && !(Number.isInteger(budget.minDrills) && budget.minDrills >= 1)) out.push(`${name}: the number of drills is a whole number of 1 or more, not ${budget.minDrills}.`);
|
|
118
|
+
if (budget.kind === "quantile" && !(budget.quantile > 0 && budget.quantile < 1)) out.push(`${name}: a quantile is more than 0 and less than 1, not ${budget.quantile}.`);
|
|
119
|
+
if (budget.kind === "quantile" && budget.level !== void 0 && !(budget.level > 0 && budget.level < 1)) out.push(`${name}: a level is more than 0 and less than 1, not ${budget.level}.`);
|
|
118
120
|
return out;
|
|
119
121
|
}
|
|
120
122
|
/** This function gives the problems of one rung, in the order of its fields. */
|
|
@@ -185,6 +187,21 @@ function worstResult(budget, values) {
|
|
|
185
187
|
reason: `The measure ${JSON.stringify(budget.measure)} is "${outcome}" (${facts}, ${budget.minDrills} drills needed).`
|
|
186
188
|
};
|
|
187
189
|
}
|
|
190
|
+
/** This function gives the verdict of a quantile: its distribution-free interval against the limit. */
|
|
191
|
+
function quantileResult(budget, values) {
|
|
192
|
+
const level = budget.level ?? .95;
|
|
193
|
+
const needed = quantileSamples(budget.quantile, level);
|
|
194
|
+
const say = (outcome, facts) => ({
|
|
195
|
+
measure: budget.measure,
|
|
196
|
+
outcome,
|
|
197
|
+
reason: `The measure ${JSON.stringify(budget.measure)} is "${outcome}" (${facts}, ${needed} values needed).`
|
|
198
|
+
});
|
|
199
|
+
if (values.length === 0) return say("cannot decide", `0 values, limit ${budget.limit} ${budget.unit}`);
|
|
200
|
+
if (values.some((value) => Number.isNaN(value))) return say("cannot decide", "the values have a NaN");
|
|
201
|
+
const interval = quantileInterval(values, budget.quantile, level);
|
|
202
|
+
const facts = `${values.length} values, quantile ${budget.quantile} at ${quantile(values, budget.quantile, "nearest-rank")} ${budget.unit}, interval ${interval.low} to ${interval.high}, limit ${budget.limit}`;
|
|
203
|
+
return say(interval.high <= budget.limit ? "within" : interval.low > budget.limit ? "beyond" : "cannot decide", facts);
|
|
204
|
+
}
|
|
188
205
|
/** This function gives the verdict of one budget from the evidence of a record. */
|
|
189
206
|
function budgetResult(budget, evidence) {
|
|
190
207
|
const found = evidence.find((e) => e.measure === budget.measure);
|
|
@@ -206,6 +223,7 @@ function budgetResult(budget, evidence) {
|
|
|
206
223
|
reason: v.reason
|
|
207
224
|
};
|
|
208
225
|
}
|
|
226
|
+
if (budget.kind === "quantile" && found.kind === "quantile") return quantileResult(budget, found.values);
|
|
209
227
|
if (budget.kind === "rate" && found.kind === "rate") return rateResult(budget, found.failures, found.trials);
|
|
210
228
|
if (budget.kind === "worst" && found.kind === "worst") return worstResult(budget, found.values);
|
|
211
229
|
return none(`the budget is ${budget.kind}, and the evidence is ${found.kind}`);
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["round","round"],"sources":["../src/budget.ts","../src/ladder.ts","../src/verdict.ts","../src/limit.ts","../src/merge.ts","../src/power.ts","../src/rounds.ts"],"sourcesContent":["/**\n * The budget verdict of one build: is the median of a measure of each case at most an absolute limit? An example is the\n * claim \"the median blocking time of this page is at most 450 ms\". The time verdict and the limit verdict judge a ratio\n * of two arms, so they cannot use a value of 0. Their A/A band is also a ratio.\n */\nimport { median, medianInterval } from \"@mark1russell7/rigor-statistics\";\n\n/** One value of one build on one case. The value is finite and 0 or more. */\nexport interface BudgetRound {\n /** The name of the case: one input of the bench. */\n readonly case: string;\n /** The number of the round, a whole number of 0 or more. */\n readonly round: number;\n readonly value: number;\n}\n\n/** The three outcomes of a budget verdict. */\nexport type BudgetOutcome = \"within\" | \"beyond\" | \"cannot decide\";\n\n/** The options of a budget verdict. */\nexport interface BudgetOptions {\n /** The largest median that the claim permits, in the unit of the values. */\n readonly limit: number;\n /** The largest difference between the medians of the two arms of the same build. A larger difference makes the run too noisy. */\n readonly maxDifference: number;\n /** The level of the interval of each median. The default is 0.95, which needs 6 rounds or more in each arm. */\n readonly caseLevel?: number;\n}\n\n/** The verdict of one case against the budget. */\nexport interface CaseBudget {\n readonly case: string;\n /** The number of rounds of the case. */\n readonly rounds: number;\n /** The number of rounds of the case in the A/A arm. */\n readonly aaRounds: number;\n readonly median: number;\n /** The median of the A/A arm, or undefined when the A/A arm does not have the case. */\n readonly aaMedian: number | undefined;\n /** The size of the difference between the two medians, or undefined when the A/A arm does not have the case. */\n readonly difference: number | undefined;\n /** The ends of the distribution-free interval of the median. */\n readonly low: number;\n readonly high: number;\n /** The coverage of the interval. Below the level, the case has too few rounds. */\n readonly coverage: number;\n readonly outcome: BudgetOutcome;\n /** The reason of the outcome, in one sentence, with the limit, the medians, the difference and the rounds. */\n readonly reason: string;\n}\n\n/** A budget verdict, with the facts that it uses. */\nexport interface BudgetVerdict {\n readonly outcome: BudgetOutcome;\n /** The reason of the outcome, in one sentence. */\n readonly reason: string;\n readonly limit: number;\n readonly maxDifference: number;\n readonly level: number;\n readonly cases: readonly CaseBudget[];\n}\n\n/** This function gives the values of each case, and it refuses a value out of range or a round that comes twice. */\nfunction valuesOfCases(name: string, rounds: readonly BudgetRound[]): Map<string, number[]> {\n const seen = new Set<string>();\n const out = new Map<string, number[]>();\n for (const r of rounds) {\n if (!(Number.isFinite(r.value) && r.value >= 0)) throw new RangeError(`budgetVerdict: the value of ${JSON.stringify(r.case)} in ${name} is finite and 0 or more, not ${r.value}.`);\n const key = JSON.stringify([r.case, r.round]);\n if (seen.has(key)) throw new RangeError(`budgetVerdict: the round ${r.round} of ${JSON.stringify(r.case)} comes twice in ${name}.`);\n seen.add(key);\n out.set(r.case, [...(out.get(r.case) ?? []), r.value]);\n }\n return out;\n}\n\n/** This function gives the verdict of one case. */\nfunction caseBudget(name: string, values: readonly number[], aaValues: readonly number[], options: BudgetOptions, level: number): CaseBudget {\n const { limit, maxDifference } = options;\n const interval = medianInterval(values, level);\n const middle = median(values);\n const aaMedian = aaValues.length > 0 ? median(aaValues) : undefined;\n const difference = aaMedian === undefined ? undefined : Math.abs(middle - aaMedian);\n const aaCoverage = aaValues.length > 0 ? medianInterval(aaValues, level).coverage : 0;\n const facts = `median ${round(middle)} of ${values.length} rounds, interval ${round(interval.low)} to ${round(interval.high)}, limit ${limit}, A/A median ${aaMedian === undefined ? \"none\" : round(aaMedian)} of ${aaValues.length} rounds, difference ${difference === undefined ? \"none\" : round(difference)}`;\n const decide = (outcome: BudgetOutcome, why: string): CaseBudget => ({\n case: name,\n rounds: values.length,\n aaRounds: aaValues.length,\n median: middle,\n aaMedian,\n difference,\n low: interval.low,\n high: interval.high,\n coverage: interval.coverage,\n outcome,\n reason: `The case ${JSON.stringify(name)} has the outcome \"${outcome}\": ${why} (${facts}).`,\n });\n if (difference === undefined) return decide(\"cannot decide\", \"the A/A arm does not have the case\");\n if (interval.coverage < level || aaCoverage < level) return decide(\"cannot decide\", `an arm has too few rounds for the level ${level}`);\n if (difference > maxDifference) return decide(\"cannot decide\", `the A/A difference is above ${maxDifference}`);\n if (interval.high <= limit) return decide(\"within\", \"the interval is at or below the limit\");\n if (interval.low > limit) return decide(\"beyond\", \"the interval is above the limit\");\n return decide(\"cannot decide\", \"the interval holds the limit\");\n}\n\n/**\n * This function gives the verdict of the claim \"the median of each case is at most `limit`\". The arm `aa` is a second\n * arm of the same build, and it shows the noise of the run as a difference. Values of 0 are valid.\n * - A case is `cannot decide` when the A/A arm does not have it, or when an arm has too few rounds for the level.\n * - A case is also `cannot decide` when the medians of the two arms differ by more than `maxDifference`.\n * - Other cases are `within` when the high end of the interval is at most the limit.\n * - They are `beyond` when the low end of the interval is above the limit.\n * - The verdict is `beyond` when a case is `beyond`, and `within` when each case is `within`.\n * - Each other verdict is `cannot decide`, and so is a verdict with no cases.\n */\nexport function budgetVerdict(rounds: readonly BudgetRound[], aa: readonly BudgetRound[], options: BudgetOptions): BudgetVerdict {\n const { limit, maxDifference } = options;\n const level = options.caseLevel ?? 0.95;\n if (!(limit >= 0 && Number.isFinite(limit))) throw new RangeError(`budgetVerdict: the limit is finite and 0 or more, not ${limit}.`);\n if (!(maxDifference >= 0 && Number.isFinite(maxDifference))) throw new RangeError(`budgetVerdict: the largest difference is finite and 0 or more, not ${maxDifference}.`);\n const values = valuesOfCases(\"the rounds\", rounds);\n const aaValues = valuesOfCases(\"the A/A arm\", aa);\n const cases = [...values.keys()].sort().map((name) => caseBudget(name, values.get(name) ?? [], aaValues.get(name) ?? [], options, level));\n const facts = { limit, maxDifference, level, cases };\n if (cases.length === 0) return { outcome: \"cannot decide\", reason: \"The verdict has no cases.\", ...facts };\n const first = cases.find((c) => c.outcome === \"beyond\") ?? cases.find((c) => c.outcome === \"cannot decide\");\n if (first === undefined) {\n const top = cases.reduce((a, b) => (b.high > a.high ? b : a));\n return { outcome: \"within\", reason: `Each of the ${cases.length} cases is within. The largest is this one: ${top.reason}`, ...facts };\n }\n const count = cases.filter((c) => c.outcome === first.outcome).length;\n return { outcome: first.outcome, reason: `${count} of ${cases.length} cases have this outcome. The first is this one: ${first.reason}`, ...facts };\n}\n\nfunction round(x: number): number {\n return Math.round(x * 10_000) / 10_000;\n}\n","/**\n * The scale ladder of a product, as data. A ladder has rungs at sizes that grow, for example 1, 10 and 100 tenants. Each\n * rung has budgets, and it names the test that holds it. A rung is held only when a record of that test shows each budget\n * `within`. A number in a document is not a rung.\n */\nimport { wilson } from \"@mark1russell7/rigor-statistics\";\nimport { type BudgetOutcome, type BudgetRound, budgetVerdict } from \"./budget.ts\";\n\n/** The format name of a ladder. It changes when a reader of an older form cannot read the data. */\nexport const LADDER_FORMAT = \"rigor-ladder/1\";\n\n/** One dimension of a ladder, for example tenants or nodes. */\nexport interface LadderDimension {\n readonly name: string;\n /** What one unit of the dimension is, in plain words. */\n readonly describe: string;\n}\n\n/** A budget on the median of each case, for example the time of a read. A record gives rounds and an A/A arm. */\nexport interface MedianBudget {\n readonly kind: \"median\";\n readonly measure: string;\n /** The unit of the limit and of the values. */\n readonly unit: string;\n /** The largest median that the budget permits. */\n readonly limit: number;\n /** The largest difference between the medians of the two arms. A larger difference makes the run too noisy. */\n readonly maxDifference: number;\n /** The level of the interval of each median. The default is 0.95. */\n readonly caseLevel?: number;\n}\n\n/** A budget on the share of failed trials, for example failed requests. A record gives failures and trials. */\nexport interface RateBudget {\n readonly kind: \"rate\";\n readonly measure: string;\n /** The largest share of failures that the budget permits, from 0 to 1. */\n readonly limit: number;\n}\n\n/** A budget on the largest value of a set of drills, for example the writes that a restore lost. One bad drill is `beyond`. */\nexport interface WorstBudget {\n readonly kind: \"worst\";\n readonly measure: string;\n readonly unit: string;\n /** The largest value that the budget permits in any drill. */\n readonly limit: number;\n /** The number of drills that a verdict needs, 1 or more. With fewer, the budget is `cannot decide`. */\n readonly minDrills: number;\n}\n\n/** One budget of a rung. */\nexport type RungBudget = MedianBudget | RateBudget | WorstBudget;\n\n/** One rung of a ladder. */\nexport interface Rung {\n /** The name of the rung, unique in the ladder. */\n readonly name: string;\n /** The size of the rung in each dimension that it names. */\n readonly at: Readonly<Record<string, number>>;\n readonly budgets: readonly RungBudget[];\n /** The name of the automatic test that holds the rung. */\n readonly test: string;\n /** True when a gate holds the rung: the verdict fails when the rung is not `within`. */\n readonly locked?: boolean;\n}\n\n/** The ladder of one subject. The rungs go from the smallest to the largest. */\nexport interface Ladder {\n readonly format: typeof LADDER_FORMAT;\n /** The product or the service that the ladder is for. */\n readonly subject: string;\n readonly dimensions: readonly LadderDimension[];\n readonly rungs: readonly Rung[];\n}\n\n/** The measured evidence for one budget. Its `kind` is the kind of the budget. */\nexport type RungEvidence =\n | { readonly kind: \"median\"; readonly measure: string; readonly rounds: readonly BudgetRound[]; readonly aa: readonly BudgetRound[] }\n | { readonly kind: \"rate\"; readonly measure: string; readonly failures: number; readonly trials: number }\n | { readonly kind: \"worst\"; readonly measure: string; readonly values: readonly number[] };\n\n/** One run of the test of a rung. */\nexport interface RungRecord {\n /** The name of the rung. */\n readonly rung: string;\n /** The commit that the run measured. */\n readonly commit: string;\n /** The time of the run, as an ISO 8601 string. */\n readonly at: string;\n /** What the run did for real, for example \"2 processes\" or \"a restore from a real backup\". */\n readonly real: readonly string[];\n /** What the run made up, for example \"90 % of the tenants as data at rest\". An empty list states that nothing was synthetic. */\n readonly synthetic: readonly string[];\n readonly evidence: readonly RungEvidence[];\n}\n\n/** The verdict of one budget of a rung. */\nexport interface BudgetResult {\n readonly measure: string;\n readonly outcome: BudgetOutcome;\n /** The reason of the outcome, in one sentence, with the numbers. */\n readonly reason: string;\n}\n\n/** The verdict of one rung. */\nexport interface RungVerdict {\n readonly rung: string;\n readonly test: string;\n readonly locked: boolean;\n readonly outcome: BudgetOutcome;\n /** The reason of the outcome, in one sentence. */\n readonly reason: string;\n readonly budgets: readonly BudgetResult[];\n /** The commit of the record, or undefined when the rung has no record. */\n readonly commit: string | undefined;\n readonly real: readonly string[];\n readonly synthetic: readonly string[];\n}\n\n/** The verdict of a ladder. */\nexport interface LadderVerdict {\n readonly subject: string;\n /** The highest rung with each rung up to it `within`, or undefined when the first rung is not `within`. */\n readonly held: string | undefined;\n /** The first rung that is not `within`, or undefined when each rung is `within`. */\n readonly next: string | undefined;\n /** True when a locked rung is not `within`. A rung with no result is not held, so `cannot decide` fails too. */\n readonly failed: boolean;\n /** The reason of `failed`, in one sentence. */\n readonly reason: string;\n readonly rungs: readonly RungVerdict[];\n}\n\n/** The options of a ladder verdict. */\nexport interface LadderOptions {\n /** The time of the verdict, as an ISO 8601 string. It is an input, so that a verdict is the same at each read. */\n readonly now: string;\n /** The largest age of a record, in days. An older record gives `cannot decide`. The default accepts any age. */\n readonly maxAgeDays?: number;\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/** This function gives the problems of one budget. */\nfunction budgetProblems(where: string, budget: RungBudget): string[] {\n const out: string[] = [];\n const name = `${where}, budget ${JSON.stringify(budget.measure)}`;\n if (budget.measure.trim() === \"\") out.push(`${where}: a budget has no measure.`);\n if (!(Number.isFinite(budget.limit) && budget.limit >= 0)) out.push(`${name}: the limit is finite and 0 or more, not ${budget.limit}.`);\n if (budget.kind === \"rate\" && budget.limit > 1) out.push(`${name}: a rate limit is at most 1, not ${budget.limit}.`);\n if (budget.kind === \"median\" && !(Number.isFinite(budget.maxDifference) && budget.maxDifference >= 0)) {\n out.push(`${name}: the largest difference is finite and 0 or more, not ${budget.maxDifference}.`);\n }\n if (budget.kind === \"worst\" && !(Number.isInteger(budget.minDrills) && budget.minDrills >= 1)) {\n out.push(`${name}: the number of drills is a whole number of 1 or more, not ${budget.minDrills}.`);\n }\n return out;\n}\n\n/** This function gives the problems of one rung, in the order of its fields. */\nfunction rungProblems(rung: Rung, dimensions: ReadonlySet<string>): string[] {\n const where = `The rung ${JSON.stringify(rung.name)}`;\n const out: string[] = [];\n const names = Object.keys(rung.at);\n if (names.length === 0) out.push(`${where} names no dimension.`);\n for (const name of names) {\n if (!dimensions.has(name)) out.push(`${where} names the unknown dimension ${JSON.stringify(name)}.`);\n const size = rung.at[name];\n if (!(typeof size === \"number\" && Number.isFinite(size) && size > 0)) out.push(`${where}: the size of ${JSON.stringify(name)} is finite and above 0, not ${size}.`);\n }\n if (rung.test.trim() === \"\") out.push(`${where} names no test.`);\n if (rung.budgets.length === 0) out.push(`${where} has no budgets.`);\n const measures = new Set<string>();\n for (const budget of rung.budgets) {\n if (measures.has(budget.measure)) out.push(`${where} has two budgets of ${JSON.stringify(budget.measure)}.`);\n measures.add(budget.measure);\n out.push(...budgetProblems(where, budget));\n }\n return out;\n}\n\n/**\n * This function gives each problem of a ladder as data, or an empty list. A ladder needs a subject, one dimension or more,\n * and one rung or more. Names are unique. Each rung names a test and has budgets. From one rung to the next, each dimension\n * that the two rungs name grows.\n */\nexport function checkLadder(ladder: Ladder): readonly string[] {\n const out: string[] = [];\n if (ladder.format !== LADDER_FORMAT) out.push(`The format is ${JSON.stringify(LADDER_FORMAT)}, not ${JSON.stringify(ladder.format)}.`);\n if (ladder.subject.trim() === \"\") out.push(\"The ladder has no subject.\");\n if (ladder.dimensions.length === 0) out.push(\"The ladder has no dimensions.\");\n if (ladder.rungs.length === 0) out.push(\"The ladder has no rungs.\");\n const dimensions = new Set(ladder.dimensions.map((d) => d.name));\n if (dimensions.size !== ladder.dimensions.length) out.push(\"Two dimensions have the same name.\");\n if (new Set(ladder.rungs.map((r) => r.name)).size !== ladder.rungs.length) out.push(\"Two rungs have the same name.\");\n ladder.rungs.forEach((rung, index) => {\n out.push(...rungProblems(rung, dimensions));\n const before = ladder.rungs[index - 1];\n if (before === undefined) return;\n for (const [name, size] of Object.entries(rung.at)) {\n const low = before.at[name];\n if (low !== undefined && !(size > low)) out.push(`The rung ${JSON.stringify(rung.name)}: ${JSON.stringify(name)} does not grow from ${low} to ${size}.`);\n }\n });\n return out;\n}\n\n/** This function gives the verdict of a rate: the Wilson interval at 95 % against the limit. */\nfunction rateResult(budget: RateBudget, failures: number, trials: number): BudgetResult {\n const interval = wilson(failures, trials);\n const facts = `${failures} failures of ${trials} trials, interval ${interval.low} to ${interval.high}, limit ${budget.limit}`;\n const outcome: BudgetOutcome = trials === 0 ? \"cannot decide\" : interval.high <= budget.limit ? \"within\" : interval.low > budget.limit ? \"beyond\" : \"cannot decide\";\n return { measure: budget.measure, outcome, reason: `The measure ${JSON.stringify(budget.measure)} is \"${outcome}\" (${facts}).` };\n}\n\n/** This function gives the verdict of a set of drills: the largest value against the limit. */\nfunction worstResult(budget: WorstBudget, values: readonly number[]): BudgetResult {\n const largest = values.length === 0 ? undefined : Math.max(...values);\n const facts = `${values.length} drills, the largest ${largest ?? \"none\"} ${budget.unit}, limit ${budget.limit}`;\n const outcome: BudgetOutcome = largest !== undefined && largest > budget.limit ? \"beyond\" : values.length < budget.minDrills ? \"cannot decide\" : \"within\";\n return { measure: budget.measure, outcome, reason: `The measure ${JSON.stringify(budget.measure)} is \"${outcome}\" (${facts}, ${budget.minDrills} drills needed).` };\n}\n\n/** This function gives the verdict of one budget from the evidence of a record. */\nfunction budgetResult(budget: RungBudget, evidence: readonly RungEvidence[]): BudgetResult {\n const found = evidence.find((e) => e.measure === budget.measure);\n const none = (why: string): BudgetResult => ({ measure: budget.measure, outcome: \"cannot decide\", reason: `The measure ${JSON.stringify(budget.measure)} is \"cannot decide\": ${why}.` });\n if (found === undefined) return none(\"the record has no evidence for it\");\n if (budget.kind === \"median\" && found.kind === \"median\") {\n const v = budgetVerdict(found.rounds, found.aa, { limit: budget.limit, maxDifference: budget.maxDifference, ...(budget.caseLevel === undefined ? {} : { caseLevel: budget.caseLevel }) });\n return { measure: budget.measure, outcome: v.outcome, reason: v.reason };\n }\n if (budget.kind === \"rate\" && found.kind === \"rate\") return rateResult(budget, found.failures, found.trials);\n if (budget.kind === \"worst\" && found.kind === \"worst\") return worstResult(budget, found.values);\n return none(`the budget is ${budget.kind}, and the evidence is ${found.kind}`);\n}\n\n/** This function gives the reason that a record cannot decide a rung, or undefined when the record can. */\nfunction recordProblem(record: RungRecord, options: LadderOptions): string | undefined {\n if (record.real.length === 0) return \"the record does not state what was real\";\n const { maxAgeDays } = options;\n if (maxAgeDays === undefined) return undefined;\n const age = (Date.parse(options.now) - Date.parse(record.at)) / DAY_MS;\n if (!Number.isFinite(age)) return `a time does not parse: now ${options.now}, record ${record.at}`;\n return age > maxAgeDays ? `the record of ${record.at} is older than ${maxAgeDays} days` : undefined;\n}\n\n/** This function gives the outcome of a set of budgets: `beyond` first, then `cannot decide`, and `within` only when each budget is. */\nfunction worstOutcome(results: readonly BudgetResult[]): BudgetOutcome {\n if (results.some((r) => r.outcome === \"beyond\")) return \"beyond\";\n return results.length > 0 && results.every((r) => r.outcome === \"within\") ? \"within\" : \"cannot decide\";\n}\n\n/** This function gives the verdict of one rung from its record. With no record, the rung is `cannot decide`. */\nexport function rungVerdict(rung: Rung, record: RungRecord | undefined, options: LadderOptions): RungVerdict {\n const base = { rung: rung.name, test: rung.test, locked: rung.locked === true };\n if (record === undefined) {\n return { ...base, outcome: \"cannot decide\", reason: `The rung ${JSON.stringify(rung.name)} has no record of the test ${JSON.stringify(rung.test)}.`, budgets: [], commit: undefined, real: [], synthetic: [] };\n }\n const facts = { commit: record.commit, real: record.real, synthetic: record.synthetic };\n const problem = recordProblem(record, options);\n if (problem !== undefined) return { ...base, ...facts, outcome: \"cannot decide\", reason: `The rung ${JSON.stringify(rung.name)} is \"cannot decide\": ${problem}.`, budgets: [] };\n const budgets = rung.budgets.map((b) => budgetResult(b, record.evidence));\n const outcome = worstOutcome(budgets);\n const first = budgets.find((b) => b.outcome === outcome);\n return { ...base, ...facts, outcome, reason: outcome === \"within\" ? `The ${budgets.length} budgets of the rung ${JSON.stringify(rung.name)} are within.` : `The rung ${JSON.stringify(rung.name)} is \"${outcome}\". ${first?.reason ?? \"\"}`, budgets };\n}\n\n/** This function gives the newest record of each rung. It keeps the first of two records with the same time. */\nfunction newestRecords(records: readonly RungRecord[]): Map<string, RungRecord> {\n const out = new Map<string, RungRecord>();\n for (const record of records) {\n const old = out.get(record.rung);\n if (old === undefined || Date.parse(record.at) > Date.parse(old.at)) out.set(record.rung, record);\n }\n return out;\n}\n\n/**\n * This function gives the verdict of a ladder from the records of its rungs. It uses the newest record of each rung, so an\n * older pass does not hide a newer failure. A rung high up does not count when a rung below it is not `within`. It throws\n * a `RangeError` for a ladder with problems, because a verdict on a bad ladder has no meaning. See `checkLadder`.\n */\nexport function ladderVerdict(ladder: Ladder, records: readonly RungRecord[], options: LadderOptions): LadderVerdict {\n const problems = checkLadder(ladder);\n if (problems.length > 0) throw new RangeError(`ladderVerdict: ${problems.join(\" \")}`);\n const newest = newestRecords(records);\n const rungs = ladder.rungs.map((r) => rungVerdict(r, newest.get(r.name), options));\n const firstOpen = rungs.findIndex((r) => r.outcome !== \"within\");\n const heldCount = firstOpen === -1 ? rungs.length : firstOpen;\n const lost = rungs.filter((r) => r.locked && r.outcome !== \"within\");\n const reason = lost.length === 0 ? `Each of the ${rungs.filter((r) => r.locked).length} locked rungs is within.` : `${lost.length} locked rungs are not within. ${lost[0]?.reason ?? \"\"}`;\n return { subject: ladder.subject, held: rungs[heldCount - 1]?.rung, next: rungs[heldCount]?.rung, failed: lost.length > 0, reason, rungs };\n}\n","/**\n * The time verdict of a candidate against a baseline, with the noise of an A/A arm. An A/A pair runs the same code in\n * its two arms. Its ratios show how far apart two equal arms come on this machine. A candidate is faster or slower only\n * when its interval is out of that band.\n */\nimport { type BootstrapOptions, type Comparison, type Interval, bootstrapMedian, bootstrapTwo, compareRatios, median, quantile } from \"@mark1russell7/rigor-statistics\";\nimport { type CaseRatio, type PairedTime, caseRatios } from \"./paired.ts\";\n\n/**\n * The two kinds of an A/A band.\n * - The band `case` holds the ratio of one A/A case. It is wide, so it finds only a large change.\n * - The band `median` holds the median ratio of the A/A cases. It has the scale of the median of the candidate.\n */\nexport type BandKind = \"case\" | \"median\";\n\n/** The options of the A/A band. */\nexport interface BandOptions {\n /** The kind of the band. The default is `median`, from the power test of `verdictPower`. */\n readonly kind?: BandKind;\n /** The quantile of the band `case`, or the level of the bootstrap of the band `median`. The default is 0.95. */\n readonly level?: number;\n /** The fewest A/A cases that give a band. The default is 5, the 5-seed rule. */\n readonly minimum?: number;\n /** The bootstrap of the band `median`. The level of the band replaces its level. */\n readonly bootstrap?: BootstrapOptions;\n}\n\n/** The band of the A/A arm: two equal arms come out in this band of ratios. */\nexport interface Band {\n /** The count of the A/A cases. */\n readonly cases: number;\n readonly kind: BandKind;\n /**\n * The half width of the band on a log scale.\n * For the band `case`, it is the `level` quantile of `abs(log(ratio))` over the A/A cases.\n * For the band `median`, it is the larger distance of an end of the bootstrap interval of the A/A median from 0.\n */\n readonly noise: number;\n /** The ends of the band: `exp(-noise)` and `exp(noise)`. */\n readonly low: number;\n readonly high: number;\n /** The median ratio of the A/A cases. A value far from 1 shows a bias of the order or of the arm slots. */\n readonly median: number;\n}\n\n/** This function gives the band of the A/A arm. The band is symmetric on a log scale. It refuses fewer cases than `minimum`. */\nexport function aaBand(aa: readonly PairedTime[], options: BandOptions = {}): Band {\n const ratios = caseRatios(aa);\n const minimum = options.minimum ?? 5;\n if (ratios.length < minimum) throw new RangeError(`aaBand: the A/A arm has ${ratios.length} cases, and a band needs ${minimum} or more.`);\n const logs = ratios.map((r) => Math.log(r.ratio));\n const kind = options.kind ?? \"median\";\n const level = options.level ?? 0.95;\n let noise: number;\n if (kind === \"case\") noise = quantile(logs.map(Math.abs), level);\n else {\n const interval = bootstrapMedian(logs, { ...options.bootstrap, level });\n noise = Math.max(Math.abs(interval.low), Math.abs(interval.high));\n }\n return { cases: ratios.length, kind, noise, low: Math.exp(-noise), high: Math.exp(noise), median: Math.exp(quantile(logs, 0.5)) };\n}\n\n/**\n * This function gives the A/A spread: the `level` quantile of `abs(log(ratio))` over the A/A cases. The default level is 0.9.\n * It refuses an A/A arm with no cases.\n */\nexport function aaSpread(aa: readonly PairedTime[], level = 0.9): number {\n const ratios = caseRatios(aa);\n if (ratios.length === 0) throw new RangeError(\"aaSpread: the A/A arm has no cases.\");\n return spreadOf(ratios, level);\n}\n\nfunction spreadOf(ratios: readonly CaseRatio[], level: number): number {\n return quantile(ratios.map((r) => Math.abs(Math.log(r.ratio))), level);\n}\n\n/** The options of a pooled band: the A/A records of earlier quiet runs, and a floor. The caller keeps the pool. */\nexport interface PoolOptions {\n /** The A/A records of the earlier runs. Give each run its own case names, for example with the run id as a prefix. */\n readonly records: readonly PairedTime[];\n /** The smallest noise of the band, on a log scale. The default is 0. A floor of `Math.log(1.05)` gives at least the band `[1 / 1.05, 1.05]`. */\n readonly floor?: number;\n /** The fewest pooled cases that give a spread. With fewer cases, the noise of the band is the floor. The default is 5. */\n readonly minimum?: number;\n /**\n * The run of a record, for example the id of its CI run. The verdict counts the runs of the pool with it.\n * The cases of one run share one runner, so they are not independent. Many cases from few runs show the noise of few runners.\n */\n readonly run?: (record: PairedTime) => string;\n}\n\n/** The facts of the pooled band of a verdict. */\nexport interface PoolFacts {\n /** The count of the pooled A/A cases. */\n readonly cases: number;\n /** The count of the runs of the pool, from the option `run`. It is undefined without that option. */\n readonly runs: number | undefined;\n /** The A/A spread of the pooled cases, or undefined when the pool has fewer cases than `minimum`. */\n readonly spread: number | undefined;\n readonly floor: number;\n /** This value is `true` when the spread of the pool is larger than the floor, so the spread gives the noise of the band. */\n readonly pooled: boolean;\n}\n\n/**\n * This function gives the pooled band of the kind `case`. Its noise is the larger of the floor and the A/A spread of the pooled records.\n * The spread is the `level` quantile of `abs(log(ratio))`, with the default level 0.9.\n *\n * A band with fewer pooled cases than `minimum` uses the floor alone. A band with no pooled cases has the median 1.\n */\nexport function pooledBand(pool: PoolOptions, level = 0.9): Band {\n return poolOf(pool, level).band;\n}\n\nfunction poolOf(pool: PoolOptions, level: number): { band: Band; facts: PoolFacts } {\n const floor = pool.floor ?? 0;\n const minimum = pool.minimum ?? 5;\n if (!(floor >= 0 && Number.isFinite(floor))) throw new RangeError(`pooledBand: the floor is a finite number of 0 or more, not ${floor}.`);\n if (!Number.isSafeInteger(minimum) || minimum < 1) throw new RangeError(`pooledBand: the minimum is a whole number of 1 or more, not ${minimum}.`);\n const ratios = caseRatios(pool.records);\n const spread = ratios.length >= minimum ? spreadOf(ratios, level) : undefined;\n const noise = Math.max(floor, spread ?? 0);\n const median = ratios.length === 0 ? 1 : Math.exp(quantile(ratios.map((r) => Math.log(r.ratio)), 0.5));\n const band: Band = { cases: ratios.length, kind: \"case\", noise, low: Math.exp(-noise), high: Math.exp(noise), median };\n const runs = pool.run === undefined ? undefined : new Set(pool.records.map(pool.run)).size;\n return { band, facts: { cases: ratios.length, runs, spread, floor, pooled: spread !== undefined && spread > floor } };\n}\n\n/** The four outcomes of a time verdict. The candidate is the arm `b`, so `faster` is a ratio below 1. */\nexport type TimeOutcome = \"faster\" | \"slower\" | \"same\" | \"cannot decide\";\n\n/** The options of a time verdict. The bootstrap gives the interval of the median ratio of the candidate, and the band `median`. */\n/**\n * The two rules of a time verdict.\n * - The rule `band` compares the interval of the median ratio of the candidate with the A/A band.\n * - The rule `difference` compares the interval of the candidate median over the A/A median with 1 and with `margin`.\n */\nexport type VerdictRule = \"band\" | \"difference\";\n\n/** The options of a time verdict. The bootstrap gives each interval of the verdict. */\nexport interface VerdictOptions extends BandOptions {\n /** The rule of the verdict. The default is `difference`, from the power test of `verdictPower`. */\n readonly rule?: VerdictRule;\n /**\n * The half width on a log scale of the ratios that the rule `difference` calls `same`. The default is 0.05.\n * An interval in the margin is `same`, also when it is wholly below 1.\n */\n readonly margin?: number;\n /**\n * The noise cap on the A/A spread of the run, on a log scale. Above the cap, the outcome is `cannot decide`.\n * The cap also limits the bias of the A/A arm: an A/A median ratio out of `[exp(-noiseCap), exp(noiseCap)]` gives `cannot decide`.\n * The default is no cap.\n */\n readonly noiseCap?: number;\n /** The quantile of the A/A spread of the noise cap and of the pooled band. The default is 0.9. */\n readonly spreadLevel?: number;\n /**\n * The pooled band. It replaces the band of the A/A arm of the run in the rule `band` and in `comparison`.\n * The rule `difference` uses the larger of `margin` and the noise of the pooled band as its margin. The default is no pool.\n */\n readonly pool?: PoolOptions;\n}\n\n/** The noise cap of a verdict: the A/A spread of the run, the A/A median ratio of the run and the cap. */\nexport interface NoiseCheck {\n /** The A/A spread of the run, or undefined when the A/A arm has no cases. */\n readonly spread: number | undefined;\n readonly cap: number;\n /** This value is `true` when the spread is above the cap, or when the A/A arm has no cases. */\n readonly noisy: boolean;\n /** The median ratio of the A/A cases of the run, or undefined when the A/A arm has no cases. A value far from 1 shows a bias of the order or of the arm slots. */\n readonly aaMedian: number | undefined;\n /**\n * This value is `true` when the A/A median ratio of the run is out of `[exp(-cap), exp(cap)]`.\n * With a spread level of 0.5 or more, a biased run is also noisy. The check decides alone only below that level.\n */\n readonly biased: boolean;\n}\n\n/** A time verdict, with the facts that it uses. */\nexport interface TimeVerdict {\n readonly outcome: TimeOutcome;\n /** The reason of the outcome, in one sentence. */\n readonly reason: string;\n readonly cases: readonly CaseRatio[];\n readonly rule: VerdictRule;\n /**\n * The interval that the rule compares, or undefined when there are no candidate cases.\n * With too few A/A cases, it is the interval of the median ratio of the candidate.\n * For the rule `band`, it is the bootstrap interval of the median ratio of the candidate.\n * For the rule `difference`, it is the interval of the candidate median ratio over the A/A median ratio.\n */\n readonly interval: Interval | undefined;\n /** The band of the A/A arm, or undefined when the A/A arm has too few cases. */\n readonly band: Band | undefined;\n /**\n * The counts of the cases out of the band, or undefined without a band.\n * The comparison uses the value `1 + log(ratio)` of each case and the noise of the band, so its two sides are the ends of the band.\n */\n readonly comparison: Comparison | undefined;\n /** The facts of the pooled band. The verdict has this field only with the option `pool`. */\n readonly pool?: PoolFacts;\n /** The noise cap. The verdict has this field only with the option `noiseCap`. */\n readonly noise?: NoiseCheck;\n}\n\n/**\n * This function gives the time verdict of the candidate arm `b` against the baseline arm `a`.\n * - The rule `difference` gives `same` when the interval is in `[exp(-margin), exp(margin)]`. This test comes first, so\n * an interval in the margin and below 1 is `same`, not `faster`.\n * - Out of the margin, the rule `difference` gives `faster` when the whole interval is below 1, and `slower` when it is above 1.\n * With the margin 0, each outcome stays the same as in 0.1.2.\n * - The rule `band` gives `faster` below the band, `slower` above the band, and `same` in the band.\n * - Each other case is `cannot decide`, and so is an A/A arm with too few cases.\n * - With `noiseCap`, an A/A spread of the run above the cap gives `cannot decide`.\n * - With `noiseCap`, an A/A median ratio of the run out of `[exp(-noiseCap), exp(noiseCap)]` also gives `cannot decide`.\n * - With `pool`, the pooled band replaces the band of the A/A arm of the run. The run still needs `minimum` A/A cases.\n */\nexport function timeVerdict(candidate: readonly PairedTime[], aa: readonly PairedTime[], options: VerdictOptions = {}): TimeVerdict {\n const level = options.spreadLevel ?? 0.9;\n if (options.noiseCap !== undefined && !(options.noiseCap >= 0 && Number.isFinite(options.noiseCap))) {\n throw new RangeError(`timeVerdict: the noise cap is a finite number of 0 or more, not ${options.noiseCap}.`);\n }\n const pool = poolFor(options);\n let verdict = ruleVerdict(candidate, aa, options, pool?.band);\n if (pool !== undefined) verdict = { ...verdict, pool: pool.facts };\n if (options.noiseCap !== undefined) {\n const cap = options.noiseCap;\n const aaRatios = caseRatios(aa);\n const spread = aaRatios.length === 0 ? undefined : spreadOf(aaRatios, level);\n const aaMedian = aaRatios.length === 0 ? undefined : Math.exp(quantile(aaRatios.map((r) => Math.log(r.ratio)), 0.5));\n const biased = aaMedian !== undefined && Math.abs(Math.log(aaMedian)) > cap;\n const noise: NoiseCheck = { spread, cap, noisy: spread === undefined || spread > cap, aaMedian, biased };\n verdict = { ...verdict, noise };\n // The early outcomes (no candidate cases, too few A/A cases) keep their reason. Each other outcome gives way to the cap.\n if (noise.noisy && verdict.band !== undefined) {\n const why = spread === undefined ? \"The A/A arm of the run has no cases, so the noise cap has no spread.\" : `The A/A spread of the run is ${round(spread)}, above the noise cap ${cap}.`;\n const reason = `${why} Without the cap, the outcome is \"${verdict.outcome}\".`;\n verdict = { ...verdict, outcome: \"cannot decide\", reason };\n } else if (noise.biased && verdict.band !== undefined) {\n const reason = `The A/A median ratio of the run is ${round(aaMedian!)}, out of [${round(Math.exp(-cap))}, ${round(Math.exp(cap))}] of the noise cap ${cap}. Without the cap, the outcome is \"${verdict.outcome}\".`;\n verdict = { ...verdict, outcome: \"cannot decide\", reason };\n }\n }\n return verdict;\n}\n\n/** This function gives the pooled band and its facts of the options, or undefined without the option `pool`. */\nfunction poolFor(options: VerdictOptions): { band: Band; facts: PoolFacts } | undefined {\n return options.pool === undefined ? undefined : poolOf(options.pool, options.spreadLevel ?? 0.9);\n}\n\n/** This function gives the margin of the rule `difference`: the larger of `margin` and the noise of the pooled band. It refuses a margin below 0. */\nfunction marginOf(options: VerdictOptions, pooled: Band | undefined): number {\n const given = options.margin ?? 0.05;\n if (!(given >= 0)) throw new RangeError(`timeVerdict: the margin is 0 or more, not ${given}.`);\n return pooled === undefined ? given : Math.max(given, pooled.noise);\n}\n\n/**\n * This function gives the margin that the rule `difference` of `timeVerdict` uses with these options, on a log scale.\n * The margin is the larger of the option `margin` (default 0.05) and the noise of the pooled band. It uses the same code as the verdict.\n * The pooled band comes from the records of the option `pool`, not from the records of the run.\n * An interval in `[exp(-margin), exp(margin)]` has the outcome `same`. A page can draw that zone with this function.\n */\nexport function verdictMargin(options: VerdictOptions = {}): number {\n return marginOf(options, poolFor(options)?.band);\n}\n\n/** This function gives the outcome of the rule. A pooled band replaces the band of the A/A arm, and it widens the margin of the rule `difference`. */\nfunction ruleVerdict(candidate: readonly PairedTime[], aa: readonly PairedTime[], options: VerdictOptions, pooled: Band | undefined): TimeVerdict {\n const rule = options.rule ?? \"difference\";\n const cases = caseRatios(candidate);\n const aaRatios = caseRatios(aa);\n const minimum = options.minimum ?? 5;\n const none = { cases, rule, interval: undefined, band: undefined, comparison: undefined };\n if (cases.length === 0) return { ...none, outcome: \"cannot decide\", reason: \"The candidate has no cases.\" };\n if (aaRatios.length < minimum) {\n const interval = bootstrapMedian(cases.map((c) => c.ratio), options.bootstrap);\n return { ...none, interval, outcome: \"cannot decide\", reason: `The A/A arm has ${aaRatios.length} cases, and a band needs ${minimum} or more.` };\n }\n const band = pooled ?? aaBand(aa, options);\n const comparison = compareRatios(cases.map((c) => 1 + Math.log(c.ratio)), { better: \"lower\", noise: band.noise });\n const facts = { cases, rule, band, comparison };\n if (rule === \"difference\") {\n const margin = marginOf(options, pooled);\n const shown = pooled === undefined ? margin : round(margin);\n const logs = cases.map((c) => Math.log(c.ratio));\n const aaLogs = aaRatios.map((c) => Math.log(c.ratio));\n const d = bootstrapTwo(logs, aaLogs, (x, y) => median(x) - median(y), options.bootstrap);\n const interval: Interval = { ...d, estimate: Math.exp(d.estimate), low: Math.exp(d.low), high: Math.exp(d.high) };\n const span = `The interval of the candidate median over the A/A median is [${round(interval.low)}, ${round(interval.high)}].`;\n // The margin comes first: an interval in the margin is `same`, also when it is wholly below or above 1.\n if (d.low >= -margin && d.high <= margin) return { ...facts, interval, outcome: \"same\", reason: `${span} The interval is in the margin of ${shown}.` };\n if (interval.high < 1) return { ...facts, interval, outcome: \"faster\", reason: `${span} The interval is below 1.` };\n if (interval.low > 1) return { ...facts, interval, outcome: \"slower\", reason: `${span} The interval is above 1.` };\n return { ...facts, interval, outcome: \"cannot decide\", reason: `${span} The interval holds 1 and crosses the margin of ${shown}.` };\n }\n const interval = bootstrapMedian(cases.map((c) => c.ratio), options.bootstrap);\n const span = `The interval of the median ratio is [${round(interval.low)}, ${round(interval.high)}], and the A/A band is [${round(band.low)}, ${round(band.high)}].`;\n if (interval.high < band.low) return { ...facts, interval, outcome: \"faster\", reason: `${span} The interval is below the band.` };\n if (interval.low > band.high) return { ...facts, interval, outcome: \"slower\", reason: `${span} The interval is above the band.` };\n if (interval.low >= band.low && interval.high <= band.high) return { ...facts, interval, outcome: \"same\", reason: `${span} The interval is in the band.` };\n return { ...facts, interval, outcome: \"cannot decide\", reason: `${span} The interval crosses an end of the band.` };\n}\n\nfunction round(x: number): number {\n return Math.round(x * 10_000) / 10_000;\n}\n","/**\n * The limit verdict of a bench: is the ratio of each case at most a limit? For example, the claim \"the new code takes at most\n * 2 times the time of the old code\" needs it. The time verdict compares only with 1.\n */\nimport { medianInterval } from \"@mark1russell7/rigor-statistics\";\nimport { type CaseRatio, type PairedTime, caseRatios } from \"./paired.ts\";\nimport { type Band, type BandOptions, aaBand } from \"./verdict.ts\";\n\n/** The ratio of one case with the interval of its median over the rounds. */\nexport interface CaseInterval extends CaseRatio {\n /** The ends of the interval: `exp` of `medianInterval` over the values `log(b / a)` of the rounds. */\n readonly low: number;\n readonly high: number;\n /** The coverage of the interval. It is at least the level. */\n readonly coverage: number;\n}\n\n/**\n * This function gives the ratio of each case with a distribution-free interval of its median, in the order of\n * `caseRatios`. It needs enough rounds for the level: 6 rounds or more for the level 0.95.\n */\nexport function caseIntervals(records: readonly PairedTime[], level = 0.95): CaseInterval[] {\n const logs = new Map<string, number[]>();\n for (const r of records) {\n const list = logs.get(r.case) ?? [];\n list.push(Math.log(r.b / r.a));\n logs.set(r.case, list);\n }\n return caseRatios(records).map((c) => {\n const interval = medianInterval(logs.get(c.case)!, level);\n return { ...c, low: Math.exp(interval.low), high: Math.exp(interval.high), coverage: interval.coverage };\n });\n}\n\n/** The three outcomes of a limit verdict. */\nexport type LimitOutcome = \"within\" | \"beyond\" | \"cannot decide\";\n\n/** The options of a limit verdict. */\nexport interface LimitOptions extends BandOptions {\n /** The largest ratio `b / a` that the claim permits, for example 2. */\n readonly limit: number;\n /** The level of the interval of each case. The default is 0.95. */\n readonly caseLevel?: number;\n /**\n * The share of the cases that the claim needs `within`, above 0 and at most 1. The default 1 needs each case. With 0.95\n * and 20 cases, 1 case can be `beyond` or `cannot decide`. The option `timeTailQ` of a promotion record is this share.\n */\n readonly share?: number;\n}\n\n/** The verdict of one case against the limit. */\nexport interface CaseLimit extends CaseInterval {\n /** The high end of the interval of the case, times the high end of the A/A band. */\n readonly bound: number;\n /** The low end of the interval of the case, times the low end of the A/A band. */\n readonly floor: number;\n readonly outcome: LimitOutcome;\n}\n\n/** A limit verdict, with the facts that it uses. */\nexport interface LimitVerdict {\n readonly outcome: LimitOutcome;\n /** The reason of the outcome, in one sentence. */\n readonly reason: string;\n readonly limit: number;\n readonly cases: readonly CaseLimit[];\n /** The band of the A/A arm, or undefined when the A/A arm has too few cases. */\n readonly band: Band | undefined;\n /** The share of the option `share`. */\n readonly share: number;\n /** The number of cases that can be other than `within`: the floor of the number of cases times `1 - share`. */\n readonly allowed: number;\n}\n\n/**\n * This function gives the verdict of the claim \"the ratio `b / a` of each case is at most `limit`\". The A/A band widens\n * the interval of each case: the bound is the high end times the high end of the band.\n * - A case is `within` when its bound is at most the limit, and `beyond` when its floor is above the limit.\n * - The verdict is `within` when `allowed` cases or fewer are other than `within`. With the default share 1, `allowed` is 0.\n * - The verdict is `beyond` when more than `allowed` cases are `beyond`.\n * - Each other verdict is `cannot decide`, and so is an A/A arm with too few cases or a candidate with no cases.\n */\nexport function limitVerdict(candidate: readonly PairedTime[], aa: readonly PairedTime[], options: LimitOptions): LimitVerdict {\n const { limit } = options;\n if (!(limit > 0 && Number.isFinite(limit))) throw new RangeError(`limitVerdict: the limit is a finite number above 0, not ${limit}.`);\n const share = options.share ?? 1;\n if (!(share > 0 && share <= 1)) throw new RangeError(`limitVerdict: the share is above 0 and at most 1, not ${share}.`);\n const intervals = caseIntervals(candidate, options.caseLevel);\n const minimum = options.minimum ?? 5;\n // The small term keeps a product such as 10 * (1 - 0.9) = 0.9999999999999998 from a floor of 0.\n const allowed = Math.floor(intervals.length * (1 - share) + 1e-9);\n if (intervals.length === 0) return { outcome: \"cannot decide\", reason: \"The candidate has no cases.\", limit, cases: [], band: undefined, share, allowed };\n const aaCount = caseRatios(aa).length;\n if (aaCount < minimum) {\n const cases = intervals.map((c) => ({ ...c, bound: c.high, floor: c.low, outcome: \"cannot decide\" as const }));\n return { outcome: \"cannot decide\", reason: `The A/A arm has ${aaCount} cases, and a band needs ${minimum} or more.`, limit, cases, band: undefined, share, allowed };\n }\n const band = aaBand(aa, options);\n const cases = intervals.map((c): CaseLimit => {\n const bound = c.high * band.high;\n const floor = c.low * band.low;\n const outcome: LimitOutcome = bound <= limit ? \"within\" : floor > limit ? \"beyond\" : \"cannot decide\";\n return { ...c, bound, floor, outcome };\n });\n const facts = { limit, cases, band, share, allowed };\n const beyond = cases.filter((c) => c.outcome === \"beyond\");\n const others = cases.filter((c) => c.outcome !== \"within\");\n const permit = allowed === 0 ? \"\" : `, and the share ${share} permits ${allowed}`;\n if (beyond.length > allowed) {\n return { outcome: \"beyond\", reason: `${beyond.length} of ${cases.length} cases are above the limit ${limit}${permit}. The first is ${JSON.stringify(beyond[0]!.case)}.`, ...facts };\n }\n if (others.length <= allowed) {\n const inside = cases.filter((c) => c.outcome === \"within\");\n const top = (inside.length > 0 ? inside : cases).reduce((a, b) => (b.bound > a.bound ? b : a));\n const rest = others.length === 0 ? \"\" : ` ${others.length} of ${cases.length} cases are not within${permit}.`;\n return { outcome: \"within\", reason: `The largest bound of a case within is ${round(top.bound)} in the case ${JSON.stringify(top.case)}, and it is at most the limit ${limit}.${rest}`, ...facts };\n }\n const top = cases.reduce((a, b) => (b.bound > a.bound ? b : a));\n return { outcome: \"cannot decide\", reason: `${others.length} of ${cases.length} cases are not within${permit}. The bound of the case ${JSON.stringify(top.case)} is ${round(top.bound)}, above the limit ${limit}, and too few floors are above it.`, ...facts };\n}\n\nfunction round(x: number): number {\n return Math.round(x * 10_000) / 10_000;\n}\n","/**\n * The merge of the records of many jobs, and the pooled verdicts over them. Each job of a bench gives its own records.\n * A pooled verdict uses all jobs together, and the verdict of each job alone guards it.\n */\nimport { type LimitOptions, type LimitOutcome, type LimitVerdict, limitVerdict } from \"./limit.ts\";\nimport { type Arm, type PairedTime } from \"./paired.ts\";\nimport { type TimeOutcome, type TimeVerdict, type VerdictOptions, timeVerdict } from \"./verdict.ts\";\n\n/** The records of each case with a turned order, and the count of the records that the balance removed. */\nexport interface Balanced<T extends PairedTime> {\n readonly records: T[];\n /** The count of the removed records. It is 0 when each case has a turned order. */\n readonly trimmed: number;\n}\n\n/**\n * This function removes records until the two orders of each case differ in count by 1 or less.\n * It removes the records of the larger order with the highest rounds first. The other records keep their order.\n * A job that stopped early, or a missing job, can leave a case with 2 more rounds of one order. `caseRatios` refuses such a case.\n */\nexport function balanceOrders<T extends PairedTime>(records: readonly T[]): Balanced<T> {\n const byCase = new Map<string, Record<Arm, number[]>>();\n records.forEach((record, index) => {\n const orders = byCase.get(record.case) ?? { a: [], b: [] };\n orders[record.first === \"b\" ? \"b\" : \"a\"].push(index);\n byCase.set(record.case, orders);\n });\n const drop = new Set<number>();\n const highFirst = (x: number, y: number): number => records[y]!.round - records[x]!.round;\n for (const { a, b } of byCase.values()) {\n const [more, fewer] = a.length > b.length ? [a, b] : [b, a];\n const extra = more.length - fewer.length - 1;\n if (extra > 0) for (const index of more.toSorted(highFirst).slice(0, extra)) drop.add(index);\n }\n return { records: records.filter((_, index) => !drop.has(index)), trimmed: drop.size };\n}\n\n/** The records of one job: the candidate pair and the A/A pair. */\nexport interface JobRecords {\n /** The label of the job, for example the index of its CI job. A record with a field `job` has this label, or `poolRecords` refuses it. */\n readonly job: string;\n readonly candidate: readonly PairedTime[];\n readonly aa: readonly PairedTime[];\n /** The cases that the job skipped, as `freshRun` gives them. `poolRecords` removes each of these cases from all jobs. */\n readonly skipped?: readonly { readonly case: string }[];\n}\n\n/** The records of one job in a pool: the cases that no job skipped, balanced with `balanceOrders`. */\nexport interface PooledJob {\n readonly job: string;\n readonly candidate: PairedTime[];\n readonly aa: PairedTime[];\n}\n\n/** The pool of the jobs: the merged records of all jobs, and the records of each job alone. */\nexport interface PooledRecords {\n /** The merged candidate records of all jobs, balanced with `balanceOrders`. */\n readonly candidate: PairedTime[];\n /** The merged A/A records of all jobs, balanced with `balanceOrders`. */\n readonly aa: PairedTime[];\n /** The records of each job alone, in the order of the jobs. */\n readonly jobs: readonly PooledJob[];\n /** The count of the records that `balanceOrders` removed from the merged candidate and A/A records. */\n readonly trimmed: number;\n /** The cases that a job skipped, in sorted order. The pool has no record of these cases. */\n readonly dropped: string[];\n}\n\n/**\n * This function gives the pool of the jobs. `pooledVerdict` and `pooledLimitVerdict` use it.\n * - It refuses two jobs with one label, and a record with a different job label. It also refuses two jobs with a record of one case and one round.\n * - It removes each case that a job skipped from all jobs, so all jobs have the same cases.\n * - It merges the records of the jobs, and it balances the merged records and the records of each job with `balanceOrders`.\n */\nexport function poolRecords(jobs: readonly JobRecords[]): PooledRecords {\n return pool(jobs, \"poolRecords\");\n}\n\n/** The pool of `poolRecords`. Each error starts with the name `caller`. */\nfunction pool(jobs: readonly JobRecords[], caller: string): PooledRecords {\n const labels = new Set<string>();\n for (const { job } of jobs) {\n if (labels.has(job)) throw new RangeError(`${caller}: two jobs have the label ${JSON.stringify(job)}.`);\n labels.add(job);\n }\n for (const { job, candidate: c, aa: a } of jobs) {\n for (const r of [...c, ...a] as readonly (PairedTime & { readonly job?: unknown })[]) {\n if (r.job !== undefined && r.job !== job) throw new RangeError(`${caller}: a record of the job ${JSON.stringify(job)} has the job ${JSON.stringify(r.job)}.`);\n }\n }\n const dropped = [...new Set(jobs.flatMap((j) => (j.skipped ?? []).map((s) => s.case)))].sort();\n const gone = new Set(dropped);\n const keep = (records: readonly PairedTime[]): PairedTime[] => records.filter((r) => !gone.has(r.case));\n const kept = jobs.map((j) => ({ job: j.job, candidate: keep(j.candidate), aa: keep(j.aa) }));\n uniqueRounds(kept, \"candidate\", caller);\n uniqueRounds(kept, \"aa\", caller);\n const candidate = balanceOrders(kept.flatMap((j) => j.candidate));\n const aa = balanceOrders(kept.flatMap((j) => j.aa));\n const each = kept.map(({ job, candidate: c, aa: a }): PooledJob => ({ job, candidate: balanceOrders(c).records, aa: balanceOrders(a).records }));\n return { candidate: candidate.records, aa: aa.records, jobs: each, trimmed: candidate.trimmed + aa.trimmed, dropped };\n}\n\n/** This function refuses two jobs with a record of one case and one round in one pair. */\nfunction uniqueRounds(jobs: readonly PooledJob[], pair: \"candidate\" | \"aa\", caller: string): void {\n const seen = new Map<string, string>();\n for (const { job, [pair]: records } of jobs) {\n for (const r of records) {\n const key = `${r.case}\\u0000${r.round}`;\n const other = seen.get(key);\n if (other !== undefined && other !== job) {\n throw new RangeError(\n `${caller}: the jobs ${JSON.stringify(other)} and ${JSON.stringify(job)} both have the round ${r.round} of the case ${JSON.stringify(r.case)} in the ${pair} pair. Set roundOffset of each job to job * rounds.`,\n );\n }\n seen.set(key, job);\n }\n }\n}\n\n/** The guard of a pooled verdict: the outcome, the opposed jobs and the reason. */\ninterface Guarded<O extends string> {\n readonly outcome: O | \"cannot decide\";\n readonly opposed: string[];\n readonly reason: string;\n}\n\n/**\n * This function applies the guard of the jobs to a pooled verdict. A job whose outcome is `opposite` of the pooled outcome\n * makes the outcome `cannot decide`. Each other job does not change the outcome.\n */\nfunction guard<O extends string>(\n pooled: { readonly outcome: O; readonly reason: string },\n jobs: readonly { readonly job: string; readonly verdict: { readonly outcome: O } }[],\n opposite: Partial<Record<O, O>>,\n): Guarded<O> {\n const against = opposite[pooled.outcome];\n const opposed = against === undefined ? [] : jobs.filter((j) => j.verdict.outcome === against).map((j) => j.job);\n if (opposed.length === 0) return { outcome: pooled.outcome, opposed, reason: pooled.reason };\n const names = opposed.map((j) => JSON.stringify(j)).join(\", \");\n const reason = `${pooled.reason} The pooled outcome is \"${pooled.outcome}\", but the jobs ${names} give \"${against}\". Thus the outcome is \"cannot decide\".`;\n return { outcome: \"cannot decide\", opposed, reason };\n}\n\n/** The verdict of one job alone. */\nexport interface JobVerdict {\n readonly job: string;\n readonly verdict: TimeVerdict;\n}\n\n/** The pooled verdict over the jobs, with the verdict of each job alone. */\nexport interface PooledVerdict {\n /** The outcome of the pooled verdict, or `cannot decide` when a job decides the opposite direction. */\n readonly outcome: TimeOutcome;\n /** The verdict of the merged and balanced records of all jobs. */\n readonly pooled: TimeVerdict;\n /** The verdict of each job alone, with the options of the pooled verdict. */\n readonly jobs: readonly JobVerdict[];\n /** The jobs whose own outcome is the opposite direction of the pooled outcome. */\n readonly opposed: string[];\n /** The count of the records that `balanceOrders` removed from the merged candidate and A/A records. */\n readonly trimmed: number;\n /** The cases that a job skipped, in sorted order. The verdicts use no record of these cases. */\n readonly dropped: string[];\n /** The reason of the pooled verdict, and the opposed jobs. */\n readonly reason: string;\n}\n\nconst opposite: Partial<Record<TimeOutcome, TimeOutcome>> = { faster: \"slower\", slower: \"faster\" };\n\n/**\n * This function gives the time verdict over the records of all jobs.\n * - It makes the pool of the jobs with `poolRecords`, and it gives the time verdict of the merged records.\n * - The verdict of each job alone uses the same options. A job with `cannot decide` or `same` does not change the outcome.\n * - A job that decides the opposite direction of the pooled outcome makes the outcome `cannot decide`.\n */\nexport function pooledVerdict(jobs: readonly JobRecords[], options: VerdictOptions = {}): PooledVerdict {\n const { candidate, aa, jobs: each, trimmed, dropped } = pool(jobs, \"pooledVerdict\");\n const pooled = timeVerdict(candidate, aa, options);\n const verdicts = each.map(({ job, candidate: c, aa: a }): JobVerdict => ({ job, verdict: timeVerdict(c, a, options) }));\n const { outcome, opposed, reason } = guard(pooled, verdicts, opposite);\n return { outcome, pooled, jobs: verdicts, opposed, trimmed, dropped, reason };\n}\n\n/** The limit verdict of one job alone. */\nexport interface JobLimitVerdict {\n readonly job: string;\n readonly verdict: LimitVerdict;\n}\n\n/** The pooled limit verdict over the jobs, with the limit verdict of each job alone. */\nexport interface PooledLimitVerdict {\n /** The outcome of the pooled limit verdict, or `cannot decide` when a job gives the opposite outcome. */\n readonly outcome: LimitOutcome;\n /** The limit verdict of the merged and balanced records of all jobs. */\n readonly pooled: LimitVerdict;\n /** The limit verdict of each job alone, with the options of the pooled verdict. */\n readonly jobs: readonly JobLimitVerdict[];\n /** The jobs whose own outcome is the opposite of the pooled outcome: `beyond` against `within`, or the reverse. */\n readonly opposed: string[];\n /** The count of the records that `balanceOrders` removed from the merged candidate and A/A records. */\n readonly trimmed: number;\n /** The cases that a job skipped, in sorted order. The verdicts use no record of these cases. */\n readonly dropped: string[];\n /** The reason of the pooled limit verdict, and the opposed jobs. */\n readonly reason: string;\n}\n\nconst limitOpposite: Partial<Record<LimitOutcome, LimitOutcome>> = { within: \"beyond\", beyond: \"within\" };\n\n/**\n * This function gives the limit verdict over the records of all jobs, for example for a speed gate on limits over a CI matrix.\n * - It makes the pool of the jobs with `poolRecords`, and it gives the limit verdict of the merged records.\n * - The verdict of each job alone uses the same options. A job with `cannot decide` does not change the outcome.\n * - A job with `beyond` against a pooled `within`, or the reverse, makes the outcome `cannot decide`.\n */\nexport function pooledLimitVerdict(jobs: readonly JobRecords[], options: LimitOptions): PooledLimitVerdict {\n const { candidate, aa, jobs: each, trimmed, dropped } = pool(jobs, \"pooledLimitVerdict\");\n const pooled = limitVerdict(candidate, aa, options);\n const verdicts = each.map(({ job, candidate: c, aa: a }): JobLimitVerdict => ({ job, verdict: limitVerdict(c, a, options) }));\n const { outcome, opposed, reason } = guard(pooled, verdicts, limitOpposite);\n return { outcome, pooled, jobs: verdicts, opposed, trimmed, dropped, reason };\n}\n\n/** The pairs of a run: the records of each pair, by the id of the pair. */\nexport interface RunPairs {\n readonly pairs: Readonly<Record<string, readonly PairedTime[]>>;\n}\n\n/**\n * This function gives the verdict of each candidate pair of a run against its A/A pair `aaPair`.\n * It balances the records of each pair with `balanceOrders` first. It refuses an `aaPair` that the run does not have.\n */\nexport function armVerdicts(run: RunPairs, aaPair: string, options: VerdictOptions = {}): Record<string, TimeVerdict> {\n const aaRecords = run.pairs[aaPair];\n if (aaRecords === undefined) throw new RangeError(`armVerdicts: the run has no pair ${JSON.stringify(aaPair)}.`);\n const aa = balanceOrders(aaRecords).records;\n const out: Record<string, TimeVerdict> = {};\n for (const [pair, records] of Object.entries(run.pairs)) {\n if (pair !== aaPair) out[pair] = timeVerdict(balanceOrders(records).records, aa, options);\n }\n return out;\n}\n","/**\n * The power test of a time verdict. It draws seeded benches with a known speedup and counts the verdicts that find it.\n * It also draws benches with equal arms and counts the false verdicts.\n */\nimport { random } from \"@mark1russell7/rigor-generators\";\nimport { firstArm, type PairedTime } from \"./paired.ts\";\nimport { type TimeOutcome, type VerdictOptions, timeVerdict, verdictMargin } from \"./verdict.ts\";\n\n/** The options of a power test. */\nexport interface PowerOptions {\n /** The count of the cases of the candidate and of the A/A arm. */\n readonly cases: number;\n /** The count of the rounds of each case. The default is 5. */\n readonly rounds?: number;\n /** The true ratio of the candidate time to the baseline time, for example 0.93 for a speedup of 7%. */\n readonly ratio: number;\n /** The standard deviation of the log time of one arm in one round. The default is 0.1. */\n readonly sigma?: number;\n /** The count of the seeded benches. The default is 200. */\n readonly trials?: number;\n /** The seed of the draws. The default is 1. */\n readonly seed?: number | string;\n /** The options of the verdict of each bench. */\n readonly verdict?: VerdictOptions;\n}\n\n/** The result of a power test: the share of each outcome over the trials. */\nexport interface Power {\n readonly trials: number;\n readonly outcomes: Readonly<Record<TimeOutcome, number>>;\n /**\n * The share of the trials whose outcome is the true one. For the rule `difference`, a ratio in the margin of the verdict has the true outcome `same`.\n * That margin is the larger of `margin` and the noise of the pooled band. The pool is the same in each trial, so each trial has the same margin.\n * Out of that margin, and for the rule `band`, the true outcome is `faster` below 1, `slower` above 1 and `same` at 1.\n */\n readonly power: number;\n}\n\n/**\n * This function draws `trials` benches and gives the share of each verdict. Each case has its own log scale.\n * Each arm of each round adds a normal noise of `sigma` to the log time.\n */\nexport function verdictPower(options: PowerOptions): Power {\n const rounds = options.rounds ?? 5;\n const sigma = options.sigma ?? 0.1;\n const trials = options.trials ?? 200;\n if (!Number.isSafeInteger(options.cases) || options.cases < 1) throw new RangeError(`verdictPower: cases is a whole number of 1 or more, not ${options.cases}.`);\n if (!(options.ratio > 0 && Number.isFinite(options.ratio))) throw new RangeError(`verdictPower: the ratio is a finite number above 0, not ${options.ratio}.`);\n const r = random(options.seed ?? 1);\n const bench = (ratio: number, prefix: string): PairedTime[] => {\n const out: PairedTime[] = [];\n for (let c = 0; c < options.cases; c++) {\n const scale = 10 * Math.exp(r.normal());\n for (let round = 0; round < rounds; round++) {\n const a = scale * Math.exp(r.normal(0, sigma));\n const b = scale * ratio * Math.exp(r.normal(0, sigma));\n out.push({ case: `${prefix}${c}`, round, first: firstArm(round, c), a, b });\n }\n }\n return out;\n };\n const truth = trueOutcome(options.ratio, options.verdict);\n const counts: Record<TimeOutcome, number> = { faster: 0, slower: 0, same: 0, \"cannot decide\": 0 };\n for (let t = 0; t < trials; t++) {\n const aa = bench(1, \"aa\");\n const candidate = bench(options.ratio, \"c\");\n counts[timeVerdict(candidate, aa, options.verdict).outcome]++;\n }\n const outcomes = Object.fromEntries(Object.entries(counts).map(([k, n]) => [k, n / trials])) as Record<TimeOutcome, number>;\n return { trials, outcomes, power: outcomes[truth] };\n}\n\n/** This function gives the true outcome of a ratio. The margin of the rule `difference` comes first, as in `timeVerdict`. */\nfunction trueOutcome(ratio: number, verdict: VerdictOptions = {}): TimeOutcome {\n if ((verdict.rule ?? \"difference\") === \"difference\" && Math.abs(Math.log(ratio)) <= verdictMargin(verdict)) return \"same\";\n return ratio < 1 ? \"faster\" : ratio > 1 ? \"slower\" : \"same\";\n}\n","/**\n * The timed rounds of one case in one process. The two arms first run for a warm-up time. Then the count of calls of\n * one batch grows until a batch of arm `a` takes the target time. Each round times one batch of each arm, in the order\n * of `firstArm`. Start this function in a fresh process for each case.\n */\nimport { type Arm, type PairedTime, firstArm } from \"./paired.ts\";\n\n/** The options of the timed rounds of one case. */\nexport interface RoundsOptions {\n /** The name of the case. */\n readonly case: string;\n /** The count of the rounds. */\n readonly rounds: number;\n /** The offset of `firstArm`: give the index of the case, so half of the cases start with each arm. The default is 0. */\n readonly offset?: number;\n /** The least time in milliseconds of one batch of arm `a`. The default is 10. */\n readonly targetMs?: number;\n /** The time in milliseconds of the warm-up of the two arms. The default is 1000. */\n readonly warmMs?: number;\n /** The clock in milliseconds. The default is `performance.now` of the global `performance` of Node and of each browser. */\n readonly now?: () => number;\n}\n\n/** The result of the timed rounds of one case. */\nexport interface Rounds {\n readonly records: PairedTime[];\n /** The count of calls of each arm in one batch. */\n readonly count: number;\n /** The sum of the numbers that the arms gave. The engine cannot remove the work of a call, because this sum uses it. */\n readonly sink: number;\n}\n\n/**\n * This function times the arms `a` and `b` of one case, and gives one `PairedTime` record for each round. The time of\n * a record is the time of one batch, in milliseconds. Each call of an arm gives a number, and the sum of these numbers\n * keeps the work.\n */\nexport function timeRounds(arms: Readonly<Record<Arm, () => number>>, options: RoundsOptions): Rounds {\n const { rounds } = options;\n const offset = options.offset ?? 0;\n const targetMs = options.targetMs ?? 10;\n const warmMs = options.warmMs ?? 1000;\n const now = options.now ?? (() => performance.now());\n if (!Number.isSafeInteger(rounds) || rounds < 1) throw new RangeError(`timeRounds: the rounds are a whole number of 1 or more, not ${rounds}.`);\n if (!Number.isSafeInteger(offset)) throw new RangeError(`timeRounds: the offset is a whole number, not ${offset}.`);\n if (!(targetMs > 0) || !(warmMs >= 0)) throw new RangeError(\"timeRounds: the target time is above 0, and the warm-up time is 0 or more.\");\n let sink = 0;\n const batch = (arm: Arm, count: number): number => {\n const run = arms[arm];\n const start = now();\n for (let i = 0; i < count; i++) sink += run();\n return now() - start;\n };\n let count = 1;\n const warmUntil = now() + warmMs;\n while (now() < warmUntil) {\n if (batch(\"a\", count) < targetMs / 4) count *= 2;\n batch(\"b\", count);\n }\n while (batch(\"a\", count) < targetMs) count *= 2;\n const records: PairedTime[] = [];\n for (let round = 0; round < rounds; round++) {\n const first = firstArm(round, offset);\n const second: Arm = first === \"a\" ? \"b\" : \"a\";\n const times = { [first]: batch(first, count), [second]: batch(second, count) } as Record<Arm, number>;\n records.push({ case: options.case, round, first, a: times.a, b: times.b });\n }\n return { records, count, sink };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["round","round"],"sources":["../src/budget.ts","../src/ladder.ts","../src/verdict.ts","../src/limit.ts","../src/merge.ts","../src/power.ts","../src/rounds.ts"],"sourcesContent":["/**\n * The budget verdict of one build: is the median of a measure of each case at most an absolute limit? An example is the\n * claim \"the median blocking time of this page is at most 450 ms\". The time verdict and the limit verdict judge a ratio\n * of two arms, so they cannot use a value of 0. Their A/A band is also a ratio.\n */\nimport { median, medianInterval } from \"@mark1russell7/rigor-statistics\";\n\n/** One value of one build on one case. The value is finite and 0 or more. */\nexport interface BudgetRound {\n /** The name of the case: one input of the bench. */\n readonly case: string;\n /** The number of the round, a whole number of 0 or more. */\n readonly round: number;\n readonly value: number;\n}\n\n/** The three outcomes of a budget verdict. */\nexport type BudgetOutcome = \"within\" | \"beyond\" | \"cannot decide\";\n\n/** The options of a budget verdict. */\nexport interface BudgetOptions {\n /** The largest median that the claim permits, in the unit of the values. */\n readonly limit: number;\n /** The largest difference between the medians of the two arms of the same build. A larger difference makes the run too noisy. */\n readonly maxDifference: number;\n /** The level of the interval of each median. The default is 0.95, which needs 6 rounds or more in each arm. */\n readonly caseLevel?: number;\n}\n\n/** The verdict of one case against the budget. */\nexport interface CaseBudget {\n readonly case: string;\n /** The number of rounds of the case. */\n readonly rounds: number;\n /** The number of rounds of the case in the A/A arm. */\n readonly aaRounds: number;\n readonly median: number;\n /** The median of the A/A arm, or undefined when the A/A arm does not have the case. */\n readonly aaMedian: number | undefined;\n /** The size of the difference between the two medians, or undefined when the A/A arm does not have the case. */\n readonly difference: number | undefined;\n /** The ends of the distribution-free interval of the median. */\n readonly low: number;\n readonly high: number;\n /** The coverage of the interval. Below the level, the case has too few rounds. */\n readonly coverage: number;\n readonly outcome: BudgetOutcome;\n /** The reason of the outcome, in one sentence, with the limit, the medians, the difference and the rounds. */\n readonly reason: string;\n}\n\n/** A budget verdict, with the facts that it uses. */\nexport interface BudgetVerdict {\n readonly outcome: BudgetOutcome;\n /** The reason of the outcome, in one sentence. */\n readonly reason: string;\n readonly limit: number;\n readonly maxDifference: number;\n readonly level: number;\n readonly cases: readonly CaseBudget[];\n}\n\n/** This function gives the values of each case, and it refuses a value out of range or a round that comes twice. */\nfunction valuesOfCases(name: string, rounds: readonly BudgetRound[]): Map<string, number[]> {\n const seen = new Set<string>();\n const out = new Map<string, number[]>();\n for (const r of rounds) {\n if (!(Number.isFinite(r.value) && r.value >= 0)) throw new RangeError(`budgetVerdict: the value of ${JSON.stringify(r.case)} in ${name} is finite and 0 or more, not ${r.value}.`);\n const key = JSON.stringify([r.case, r.round]);\n if (seen.has(key)) throw new RangeError(`budgetVerdict: the round ${r.round} of ${JSON.stringify(r.case)} comes twice in ${name}.`);\n seen.add(key);\n out.set(r.case, [...(out.get(r.case) ?? []), r.value]);\n }\n return out;\n}\n\n/** This function gives the verdict of one case. */\nfunction caseBudget(name: string, values: readonly number[], aaValues: readonly number[], options: BudgetOptions, level: number): CaseBudget {\n const { limit, maxDifference } = options;\n const interval = medianInterval(values, level);\n const middle = median(values);\n const aaMedian = aaValues.length > 0 ? median(aaValues) : undefined;\n const difference = aaMedian === undefined ? undefined : Math.abs(middle - aaMedian);\n const aaCoverage = aaValues.length > 0 ? medianInterval(aaValues, level).coverage : 0;\n const facts = `median ${round(middle)} of ${values.length} rounds, interval ${round(interval.low)} to ${round(interval.high)}, limit ${limit}, A/A median ${aaMedian === undefined ? \"none\" : round(aaMedian)} of ${aaValues.length} rounds, difference ${difference === undefined ? \"none\" : round(difference)}`;\n const decide = (outcome: BudgetOutcome, why: string): CaseBudget => ({\n case: name,\n rounds: values.length,\n aaRounds: aaValues.length,\n median: middle,\n aaMedian,\n difference,\n low: interval.low,\n high: interval.high,\n coverage: interval.coverage,\n outcome,\n reason: `The case ${JSON.stringify(name)} has the outcome \"${outcome}\": ${why} (${facts}).`,\n });\n if (difference === undefined) return decide(\"cannot decide\", \"the A/A arm does not have the case\");\n if (interval.coverage < level || aaCoverage < level) return decide(\"cannot decide\", `an arm has too few rounds for the level ${level}`);\n if (difference > maxDifference) return decide(\"cannot decide\", `the A/A difference is above ${maxDifference}`);\n if (interval.high <= limit) return decide(\"within\", \"the interval is at or below the limit\");\n if (interval.low > limit) return decide(\"beyond\", \"the interval is above the limit\");\n return decide(\"cannot decide\", \"the interval holds the limit\");\n}\n\n/**\n * This function gives the verdict of the claim \"the median of each case is at most `limit`\". The arm `aa` is a second\n * arm of the same build, and it shows the noise of the run as a difference. Values of 0 are valid.\n * - A case is `cannot decide` when the A/A arm does not have it, or when an arm has too few rounds for the level.\n * - A case is also `cannot decide` when the medians of the two arms differ by more than `maxDifference`.\n * - Other cases are `within` when the high end of the interval is at most the limit.\n * - They are `beyond` when the low end of the interval is above the limit.\n * - The verdict is `beyond` when a case is `beyond`, and `within` when each case is `within`.\n * - Each other verdict is `cannot decide`, and so is a verdict with no cases.\n */\nexport function budgetVerdict(rounds: readonly BudgetRound[], aa: readonly BudgetRound[], options: BudgetOptions): BudgetVerdict {\n const { limit, maxDifference } = options;\n const level = options.caseLevel ?? 0.95;\n if (!(limit >= 0 && Number.isFinite(limit))) throw new RangeError(`budgetVerdict: the limit is finite and 0 or more, not ${limit}.`);\n if (!(maxDifference >= 0 && Number.isFinite(maxDifference))) throw new RangeError(`budgetVerdict: the largest difference is finite and 0 or more, not ${maxDifference}.`);\n const values = valuesOfCases(\"the rounds\", rounds);\n const aaValues = valuesOfCases(\"the A/A arm\", aa);\n const cases = [...values.keys()].sort().map((name) => caseBudget(name, values.get(name) ?? [], aaValues.get(name) ?? [], options, level));\n const facts = { limit, maxDifference, level, cases };\n if (cases.length === 0) return { outcome: \"cannot decide\", reason: \"The verdict has no cases.\", ...facts };\n const first = cases.find((c) => c.outcome === \"beyond\") ?? cases.find((c) => c.outcome === \"cannot decide\");\n if (first === undefined) {\n const top = cases.reduce((a, b) => (b.high > a.high ? b : a));\n return { outcome: \"within\", reason: `Each of the ${cases.length} cases is within. The largest is this one: ${top.reason}`, ...facts };\n }\n const count = cases.filter((c) => c.outcome === first.outcome).length;\n return { outcome: first.outcome, reason: `${count} of ${cases.length} cases have this outcome. The first is this one: ${first.reason}`, ...facts };\n}\n\nfunction round(x: number): number {\n return Math.round(x * 10_000) / 10_000;\n}\n","/**\n * The scale ladder of a product, as data. A ladder has rungs at sizes that grow, for example 1, 10 and 100 tenants. Each\n * rung has budgets, and it names the test that holds it. A rung is held only when a record of that test shows each budget\n * `within`. A number in a document is not a rung.\n */\nimport { quantile, quantileInterval, quantileSamples, wilson } from \"@mark1russell7/rigor-statistics\";\nimport { type BudgetOutcome, type BudgetRound, budgetVerdict } from \"./budget.ts\";\n\n/** The format name of a ladder. It changes when a reader of an older form cannot read the data. */\nexport const LADDER_FORMAT = \"rigor-ladder/1\";\n\n/** One dimension of a ladder, for example tenants or nodes. */\nexport interface LadderDimension {\n readonly name: string;\n /** What one unit of the dimension is, in plain words. */\n readonly describe: string;\n}\n\n/** A budget on the median of each case, for example the time of a read. A record gives rounds and an A/A arm. */\nexport interface MedianBudget {\n readonly kind: \"median\";\n readonly measure: string;\n /** The unit of the limit and of the values. */\n readonly unit: string;\n /** The largest median that the budget permits. */\n readonly limit: number;\n /** The largest difference between the medians of the two arms. A larger difference makes the run too noisy. */\n readonly maxDifference: number;\n /** The level of the interval of each median. The default is 0.95. */\n readonly caseLevel?: number;\n}\n\n/** A budget on the share of failed trials, for example failed requests. A record gives failures and trials. */\nexport interface RateBudget {\n readonly kind: \"rate\";\n readonly measure: string;\n /** The largest share of failures that the budget permits, from 0 to 1. */\n readonly limit: number;\n}\n\n/** A budget on the largest value of a set of drills, for example the writes that a restore lost. One bad drill is `beyond`. */\nexport interface WorstBudget {\n readonly kind: \"worst\";\n readonly measure: string;\n readonly unit: string;\n /** The largest value that the budget permits in any drill. */\n readonly limit: number;\n /** The number of drills that a verdict needs, 1 or more. With fewer, the budget is `cannot decide`. */\n readonly minDrills: number;\n}\n\n/**\n * A budget on a high quantile, for example the p99 of a read. A record gives the values. The verdict is the distribution-free\n * interval of the quantile against the limit. A p99 needs 368 independent values at the level 0.95: refer to `quantileSamples`.\n * Successive times of one run can follow each other. Space them apart, or give one value for each run.\n */\nexport interface QuantileBudget {\n readonly kind: \"quantile\";\n readonly measure: string;\n /** The unit of the limit and of the values. */\n readonly unit: string;\n /** The quantile, more than 0 and less than 1, for example 0.99. */\n readonly quantile: number;\n /** The largest quantile that the budget permits. */\n readonly limit: number;\n /** The level of the interval. The default is 0.95. */\n readonly level?: number;\n}\n\n/** One budget of a rung. */\nexport type RungBudget = MedianBudget | QuantileBudget | RateBudget | WorstBudget;\n\n/** One rung of a ladder. */\nexport interface Rung {\n /** The name of the rung, unique in the ladder. */\n readonly name: string;\n /** The size of the rung in each dimension that it names. */\n readonly at: Readonly<Record<string, number>>;\n readonly budgets: readonly RungBudget[];\n /** The name of the automatic test that holds the rung. */\n readonly test: string;\n /** True when a gate holds the rung: the verdict fails when the rung is not `within`. */\n readonly locked?: boolean;\n}\n\n/** The ladder of one subject. The rungs go from the smallest to the largest. */\nexport interface Ladder {\n readonly format: typeof LADDER_FORMAT;\n /** The product or the service that the ladder is for. */\n readonly subject: string;\n readonly dimensions: readonly LadderDimension[];\n readonly rungs: readonly Rung[];\n}\n\n/** The measured evidence for one budget. Its `kind` is the kind of the budget. */\nexport type RungEvidence =\n | { readonly kind: \"median\"; readonly measure: string; readonly rounds: readonly BudgetRound[]; readonly aa: readonly BudgetRound[] }\n | { readonly kind: \"quantile\"; readonly measure: string; readonly values: readonly number[] }\n | { readonly kind: \"rate\"; readonly measure: string; readonly failures: number; readonly trials: number }\n | { readonly kind: \"worst\"; readonly measure: string; readonly values: readonly number[] };\n\n/** One run of the test of a rung. */\nexport interface RungRecord {\n /** The name of the rung. */\n readonly rung: string;\n /** The commit that the run measured. */\n readonly commit: string;\n /** The time of the run, as an ISO 8601 string. */\n readonly at: string;\n /** What the run did for real, for example \"2 processes\" or \"a restore from a real backup\". */\n readonly real: readonly string[];\n /** What the run made up, for example \"90 % of the tenants as data at rest\". An empty list states that nothing was synthetic. */\n readonly synthetic: readonly string[];\n readonly evidence: readonly RungEvidence[];\n}\n\n/** The verdict of one budget of a rung. */\nexport interface BudgetResult {\n readonly measure: string;\n readonly outcome: BudgetOutcome;\n /** The reason of the outcome, in one sentence, with the numbers. */\n readonly reason: string;\n}\n\n/** The verdict of one rung. */\nexport interface RungVerdict {\n readonly rung: string;\n readonly test: string;\n readonly locked: boolean;\n readonly outcome: BudgetOutcome;\n /** The reason of the outcome, in one sentence. */\n readonly reason: string;\n readonly budgets: readonly BudgetResult[];\n /** The commit of the record, or undefined when the rung has no record. */\n readonly commit: string | undefined;\n readonly real: readonly string[];\n readonly synthetic: readonly string[];\n}\n\n/** The verdict of a ladder. */\nexport interface LadderVerdict {\n readonly subject: string;\n /** The highest rung with each rung up to it `within`, or undefined when the first rung is not `within`. */\n readonly held: string | undefined;\n /** The first rung that is not `within`, or undefined when each rung is `within`. */\n readonly next: string | undefined;\n /** True when a locked rung is not `within`. A rung with no result is not held, so `cannot decide` fails too. */\n readonly failed: boolean;\n /** The reason of `failed`, in one sentence. */\n readonly reason: string;\n readonly rungs: readonly RungVerdict[];\n}\n\n/** The options of a ladder verdict. */\nexport interface LadderOptions {\n /** The time of the verdict, as an ISO 8601 string. It is an input, so that a verdict is the same at each read. */\n readonly now: string;\n /** The largest age of a record, in days. An older record gives `cannot decide`. The default accepts any age. */\n readonly maxAgeDays?: number;\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/** This function gives the problems of one budget. */\nfunction budgetProblems(where: string, budget: RungBudget): string[] {\n const out: string[] = [];\n const name = `${where}, budget ${JSON.stringify(budget.measure)}`;\n if (budget.measure.trim() === \"\") out.push(`${where}: a budget has no measure.`);\n if (!(Number.isFinite(budget.limit) && budget.limit >= 0)) out.push(`${name}: the limit is finite and 0 or more, not ${budget.limit}.`);\n if (budget.kind === \"rate\" && budget.limit > 1) out.push(`${name}: a rate limit is at most 1, not ${budget.limit}.`);\n if (budget.kind === \"median\" && !(Number.isFinite(budget.maxDifference) && budget.maxDifference >= 0)) {\n out.push(`${name}: the largest difference is finite and 0 or more, not ${budget.maxDifference}.`);\n }\n if (budget.kind === \"worst\" && !(Number.isInteger(budget.minDrills) && budget.minDrills >= 1)) {\n out.push(`${name}: the number of drills is a whole number of 1 or more, not ${budget.minDrills}.`);\n }\n if (budget.kind === \"quantile\" && !(budget.quantile > 0 && budget.quantile < 1)) out.push(`${name}: a quantile is more than 0 and less than 1, not ${budget.quantile}.`);\n if (budget.kind === \"quantile\" && budget.level !== undefined && !(budget.level > 0 && budget.level < 1)) out.push(`${name}: a level is more than 0 and less than 1, not ${budget.level}.`);\n return out;\n}\n\n/** This function gives the problems of one rung, in the order of its fields. */\nfunction rungProblems(rung: Rung, dimensions: ReadonlySet<string>): string[] {\n const where = `The rung ${JSON.stringify(rung.name)}`;\n const out: string[] = [];\n const names = Object.keys(rung.at);\n if (names.length === 0) out.push(`${where} names no dimension.`);\n for (const name of names) {\n if (!dimensions.has(name)) out.push(`${where} names the unknown dimension ${JSON.stringify(name)}.`);\n const size = rung.at[name];\n if (!(typeof size === \"number\" && Number.isFinite(size) && size > 0)) out.push(`${where}: the size of ${JSON.stringify(name)} is finite and above 0, not ${size}.`);\n }\n if (rung.test.trim() === \"\") out.push(`${where} names no test.`);\n if (rung.budgets.length === 0) out.push(`${where} has no budgets.`);\n const measures = new Set<string>();\n for (const budget of rung.budgets) {\n if (measures.has(budget.measure)) out.push(`${where} has two budgets of ${JSON.stringify(budget.measure)}.`);\n measures.add(budget.measure);\n out.push(...budgetProblems(where, budget));\n }\n return out;\n}\n\n/**\n * This function gives each problem of a ladder as data, or an empty list. A ladder needs a subject, one dimension or more,\n * and one rung or more. Names are unique. Each rung names a test and has budgets. From one rung to the next, each dimension\n * that the two rungs name grows.\n */\nexport function checkLadder(ladder: Ladder): readonly string[] {\n const out: string[] = [];\n if (ladder.format !== LADDER_FORMAT) out.push(`The format is ${JSON.stringify(LADDER_FORMAT)}, not ${JSON.stringify(ladder.format)}.`);\n if (ladder.subject.trim() === \"\") out.push(\"The ladder has no subject.\");\n if (ladder.dimensions.length === 0) out.push(\"The ladder has no dimensions.\");\n if (ladder.rungs.length === 0) out.push(\"The ladder has no rungs.\");\n const dimensions = new Set(ladder.dimensions.map((d) => d.name));\n if (dimensions.size !== ladder.dimensions.length) out.push(\"Two dimensions have the same name.\");\n if (new Set(ladder.rungs.map((r) => r.name)).size !== ladder.rungs.length) out.push(\"Two rungs have the same name.\");\n ladder.rungs.forEach((rung, index) => {\n out.push(...rungProblems(rung, dimensions));\n const before = ladder.rungs[index - 1];\n if (before === undefined) return;\n for (const [name, size] of Object.entries(rung.at)) {\n const low = before.at[name];\n if (low !== undefined && !(size > low)) out.push(`The rung ${JSON.stringify(rung.name)}: ${JSON.stringify(name)} does not grow from ${low} to ${size}.`);\n }\n });\n return out;\n}\n\n/** This function gives the verdict of a rate: the Wilson interval at 95 % against the limit. */\nfunction rateResult(budget: RateBudget, failures: number, trials: number): BudgetResult {\n const interval = wilson(failures, trials);\n const facts = `${failures} failures of ${trials} trials, interval ${interval.low} to ${interval.high}, limit ${budget.limit}`;\n const outcome: BudgetOutcome = trials === 0 ? \"cannot decide\" : interval.high <= budget.limit ? \"within\" : interval.low > budget.limit ? \"beyond\" : \"cannot decide\";\n return { measure: budget.measure, outcome, reason: `The measure ${JSON.stringify(budget.measure)} is \"${outcome}\" (${facts}).` };\n}\n\n/** This function gives the verdict of a set of drills: the largest value against the limit. */\nfunction worstResult(budget: WorstBudget, values: readonly number[]): BudgetResult {\n const largest = values.length === 0 ? undefined : Math.max(...values);\n const facts = `${values.length} drills, the largest ${largest ?? \"none\"} ${budget.unit}, limit ${budget.limit}`;\n const outcome: BudgetOutcome = largest !== undefined && largest > budget.limit ? \"beyond\" : values.length < budget.minDrills ? \"cannot decide\" : \"within\";\n return { measure: budget.measure, outcome, reason: `The measure ${JSON.stringify(budget.measure)} is \"${outcome}\" (${facts}, ${budget.minDrills} drills needed).` };\n}\n\n/** This function gives the verdict of a quantile: its distribution-free interval against the limit. */\nfunction quantileResult(budget: QuantileBudget, values: readonly number[]): BudgetResult {\n const level = budget.level ?? 0.95;\n const needed = quantileSamples(budget.quantile, level);\n const say = (outcome: BudgetOutcome, facts: string): BudgetResult => ({ measure: budget.measure, outcome, reason: `The measure ${JSON.stringify(budget.measure)} is \"${outcome}\" (${facts}, ${needed} values needed).` });\n if (values.length === 0) return say(\"cannot decide\", `0 values, limit ${budget.limit} ${budget.unit}`);\n if (values.some((value) => Number.isNaN(value))) return say(\"cannot decide\", \"the values have a NaN\");\n const interval = quantileInterval(values, budget.quantile, level);\n const facts = `${values.length} values, quantile ${budget.quantile} at ${quantile(values, budget.quantile, \"nearest-rank\")} ${budget.unit}, interval ${interval.low} to ${interval.high}, limit ${budget.limit}`;\n return say(interval.high <= budget.limit ? \"within\" : interval.low > budget.limit ? \"beyond\" : \"cannot decide\", facts);\n}\n\n/** This function gives the verdict of one budget from the evidence of a record. */\nfunction budgetResult(budget: RungBudget, evidence: readonly RungEvidence[]): BudgetResult {\n const found = evidence.find((e) => e.measure === budget.measure);\n const none = (why: string): BudgetResult => ({ measure: budget.measure, outcome: \"cannot decide\", reason: `The measure ${JSON.stringify(budget.measure)} is \"cannot decide\": ${why}.` });\n if (found === undefined) return none(\"the record has no evidence for it\");\n if (budget.kind === \"median\" && found.kind === \"median\") {\n const v = budgetVerdict(found.rounds, found.aa, { limit: budget.limit, maxDifference: budget.maxDifference, ...(budget.caseLevel === undefined ? {} : { caseLevel: budget.caseLevel }) });\n return { measure: budget.measure, outcome: v.outcome, reason: v.reason };\n }\n if (budget.kind === \"quantile\" && found.kind === \"quantile\") return quantileResult(budget, found.values);\n if (budget.kind === \"rate\" && found.kind === \"rate\") return rateResult(budget, found.failures, found.trials);\n if (budget.kind === \"worst\" && found.kind === \"worst\") return worstResult(budget, found.values);\n return none(`the budget is ${budget.kind}, and the evidence is ${found.kind}`);\n}\n\n/** This function gives the reason that a record cannot decide a rung, or undefined when the record can. */\nfunction recordProblem(record: RungRecord, options: LadderOptions): string | undefined {\n if (record.real.length === 0) return \"the record does not state what was real\";\n const { maxAgeDays } = options;\n if (maxAgeDays === undefined) return undefined;\n const age = (Date.parse(options.now) - Date.parse(record.at)) / DAY_MS;\n if (!Number.isFinite(age)) return `a time does not parse: now ${options.now}, record ${record.at}`;\n return age > maxAgeDays ? `the record of ${record.at} is older than ${maxAgeDays} days` : undefined;\n}\n\n/** This function gives the outcome of a set of budgets: `beyond` first, then `cannot decide`, and `within` only when each budget is. */\nfunction worstOutcome(results: readonly BudgetResult[]): BudgetOutcome {\n if (results.some((r) => r.outcome === \"beyond\")) return \"beyond\";\n return results.length > 0 && results.every((r) => r.outcome === \"within\") ? \"within\" : \"cannot decide\";\n}\n\n/** This function gives the verdict of one rung from its record. With no record, the rung is `cannot decide`. */\nexport function rungVerdict(rung: Rung, record: RungRecord | undefined, options: LadderOptions): RungVerdict {\n const base = { rung: rung.name, test: rung.test, locked: rung.locked === true };\n if (record === undefined) {\n return { ...base, outcome: \"cannot decide\", reason: `The rung ${JSON.stringify(rung.name)} has no record of the test ${JSON.stringify(rung.test)}.`, budgets: [], commit: undefined, real: [], synthetic: [] };\n }\n const facts = { commit: record.commit, real: record.real, synthetic: record.synthetic };\n const problem = recordProblem(record, options);\n if (problem !== undefined) return { ...base, ...facts, outcome: \"cannot decide\", reason: `The rung ${JSON.stringify(rung.name)} is \"cannot decide\": ${problem}.`, budgets: [] };\n const budgets = rung.budgets.map((b) => budgetResult(b, record.evidence));\n const outcome = worstOutcome(budgets);\n const first = budgets.find((b) => b.outcome === outcome);\n return { ...base, ...facts, outcome, reason: outcome === \"within\" ? `The ${budgets.length} budgets of the rung ${JSON.stringify(rung.name)} are within.` : `The rung ${JSON.stringify(rung.name)} is \"${outcome}\". ${first?.reason ?? \"\"}`, budgets };\n}\n\n/** This function gives the newest record of each rung. It keeps the first of two records with the same time. */\nfunction newestRecords(records: readonly RungRecord[]): Map<string, RungRecord> {\n const out = new Map<string, RungRecord>();\n for (const record of records) {\n const old = out.get(record.rung);\n if (old === undefined || Date.parse(record.at) > Date.parse(old.at)) out.set(record.rung, record);\n }\n return out;\n}\n\n/**\n * This function gives the verdict of a ladder from the records of its rungs. It uses the newest record of each rung, so an\n * older pass does not hide a newer failure. A rung high up does not count when a rung below it is not `within`. It throws\n * a `RangeError` for a ladder with problems, because a verdict on a bad ladder has no meaning. See `checkLadder`.\n */\nexport function ladderVerdict(ladder: Ladder, records: readonly RungRecord[], options: LadderOptions): LadderVerdict {\n const problems = checkLadder(ladder);\n if (problems.length > 0) throw new RangeError(`ladderVerdict: ${problems.join(\" \")}`);\n const newest = newestRecords(records);\n const rungs = ladder.rungs.map((r) => rungVerdict(r, newest.get(r.name), options));\n const firstOpen = rungs.findIndex((r) => r.outcome !== \"within\");\n const heldCount = firstOpen === -1 ? rungs.length : firstOpen;\n const lost = rungs.filter((r) => r.locked && r.outcome !== \"within\");\n const reason = lost.length === 0 ? `Each of the ${rungs.filter((r) => r.locked).length} locked rungs is within.` : `${lost.length} locked rungs are not within. ${lost[0]?.reason ?? \"\"}`;\n return { subject: ladder.subject, held: rungs[heldCount - 1]?.rung, next: rungs[heldCount]?.rung, failed: lost.length > 0, reason, rungs };\n}\n","/**\n * The time verdict of a candidate against a baseline, with the noise of an A/A arm. An A/A pair runs the same code in\n * its two arms. Its ratios show how far apart two equal arms come on this machine. A candidate is faster or slower only\n * when its interval is out of that band.\n */\nimport { type BootstrapOptions, type Comparison, type Interval, bootstrapMedian, bootstrapTwo, compareRatios, median, quantile } from \"@mark1russell7/rigor-statistics\";\nimport { type CaseRatio, type PairedTime, caseRatios } from \"./paired.ts\";\n\n/**\n * The two kinds of an A/A band.\n * - The band `case` holds the ratio of one A/A case. It is wide, so it finds only a large change.\n * - The band `median` holds the median ratio of the A/A cases. It has the scale of the median of the candidate.\n */\nexport type BandKind = \"case\" | \"median\";\n\n/** The options of the A/A band. */\nexport interface BandOptions {\n /** The kind of the band. The default is `median`, from the power test of `verdictPower`. */\n readonly kind?: BandKind;\n /** The quantile of the band `case`, or the level of the bootstrap of the band `median`. The default is 0.95. */\n readonly level?: number;\n /** The fewest A/A cases that give a band. The default is 5, the 5-seed rule. */\n readonly minimum?: number;\n /** The bootstrap of the band `median`. The level of the band replaces its level. */\n readonly bootstrap?: BootstrapOptions;\n}\n\n/** The band of the A/A arm: two equal arms come out in this band of ratios. */\nexport interface Band {\n /** The count of the A/A cases. */\n readonly cases: number;\n readonly kind: BandKind;\n /**\n * The half width of the band on a log scale.\n * For the band `case`, it is the `level` quantile of `abs(log(ratio))` over the A/A cases.\n * For the band `median`, it is the larger distance of an end of the bootstrap interval of the A/A median from 0.\n */\n readonly noise: number;\n /** The ends of the band: `exp(-noise)` and `exp(noise)`. */\n readonly low: number;\n readonly high: number;\n /** The median ratio of the A/A cases. A value far from 1 shows a bias of the order or of the arm slots. */\n readonly median: number;\n}\n\n/** This function gives the band of the A/A arm. The band is symmetric on a log scale. It refuses fewer cases than `minimum`. */\nexport function aaBand(aa: readonly PairedTime[], options: BandOptions = {}): Band {\n const ratios = caseRatios(aa);\n const minimum = options.minimum ?? 5;\n if (ratios.length < minimum) throw new RangeError(`aaBand: the A/A arm has ${ratios.length} cases, and a band needs ${minimum} or more.`);\n const logs = ratios.map((r) => Math.log(r.ratio));\n const kind = options.kind ?? \"median\";\n const level = options.level ?? 0.95;\n let noise: number;\n if (kind === \"case\") noise = quantile(logs.map(Math.abs), level);\n else {\n const interval = bootstrapMedian(logs, { ...options.bootstrap, level });\n noise = Math.max(Math.abs(interval.low), Math.abs(interval.high));\n }\n return { cases: ratios.length, kind, noise, low: Math.exp(-noise), high: Math.exp(noise), median: Math.exp(quantile(logs, 0.5)) };\n}\n\n/**\n * This function gives the A/A spread: the `level` quantile of `abs(log(ratio))` over the A/A cases. The default level is 0.9.\n * It refuses an A/A arm with no cases.\n */\nexport function aaSpread(aa: readonly PairedTime[], level = 0.9): number {\n const ratios = caseRatios(aa);\n if (ratios.length === 0) throw new RangeError(\"aaSpread: the A/A arm has no cases.\");\n return spreadOf(ratios, level);\n}\n\nfunction spreadOf(ratios: readonly CaseRatio[], level: number): number {\n return quantile(ratios.map((r) => Math.abs(Math.log(r.ratio))), level);\n}\n\n/** The options of a pooled band: the A/A records of earlier quiet runs, and a floor. The caller keeps the pool. */\nexport interface PoolOptions {\n /** The A/A records of the earlier runs. Give each run its own case names, for example with the run id as a prefix. */\n readonly records: readonly PairedTime[];\n /** The smallest noise of the band, on a log scale. The default is 0. A floor of `Math.log(1.05)` gives at least the band `[1 / 1.05, 1.05]`. */\n readonly floor?: number;\n /** The fewest pooled cases that give a spread. With fewer cases, the noise of the band is the floor. The default is 5. */\n readonly minimum?: number;\n /**\n * The run of a record, for example the id of its CI run. The verdict counts the runs of the pool with it.\n * The cases of one run share one runner, so they are not independent. Many cases from few runs show the noise of few runners.\n */\n readonly run?: (record: PairedTime) => string;\n}\n\n/** The facts of the pooled band of a verdict. */\nexport interface PoolFacts {\n /** The count of the pooled A/A cases. */\n readonly cases: number;\n /** The count of the runs of the pool, from the option `run`. It is undefined without that option. */\n readonly runs: number | undefined;\n /** The A/A spread of the pooled cases, or undefined when the pool has fewer cases than `minimum`. */\n readonly spread: number | undefined;\n readonly floor: number;\n /** This value is `true` when the spread of the pool is larger than the floor, so the spread gives the noise of the band. */\n readonly pooled: boolean;\n}\n\n/**\n * This function gives the pooled band of the kind `case`. Its noise is the larger of the floor and the A/A spread of the pooled records.\n * The spread is the `level` quantile of `abs(log(ratio))`, with the default level 0.9.\n *\n * A band with fewer pooled cases than `minimum` uses the floor alone. A band with no pooled cases has the median 1.\n */\nexport function pooledBand(pool: PoolOptions, level = 0.9): Band {\n return poolOf(pool, level).band;\n}\n\nfunction poolOf(pool: PoolOptions, level: number): { band: Band; facts: PoolFacts } {\n const floor = pool.floor ?? 0;\n const minimum = pool.minimum ?? 5;\n if (!(floor >= 0 && Number.isFinite(floor))) throw new RangeError(`pooledBand: the floor is a finite number of 0 or more, not ${floor}.`);\n if (!Number.isSafeInteger(minimum) || minimum < 1) throw new RangeError(`pooledBand: the minimum is a whole number of 1 or more, not ${minimum}.`);\n const ratios = caseRatios(pool.records);\n const spread = ratios.length >= minimum ? spreadOf(ratios, level) : undefined;\n const noise = Math.max(floor, spread ?? 0);\n const median = ratios.length === 0 ? 1 : Math.exp(quantile(ratios.map((r) => Math.log(r.ratio)), 0.5));\n const band: Band = { cases: ratios.length, kind: \"case\", noise, low: Math.exp(-noise), high: Math.exp(noise), median };\n const runs = pool.run === undefined ? undefined : new Set(pool.records.map(pool.run)).size;\n return { band, facts: { cases: ratios.length, runs, spread, floor, pooled: spread !== undefined && spread > floor } };\n}\n\n/** The four outcomes of a time verdict. The candidate is the arm `b`, so `faster` is a ratio below 1. */\nexport type TimeOutcome = \"faster\" | \"slower\" | \"same\" | \"cannot decide\";\n\n/** The options of a time verdict. The bootstrap gives the interval of the median ratio of the candidate, and the band `median`. */\n/**\n * The two rules of a time verdict.\n * - The rule `band` compares the interval of the median ratio of the candidate with the A/A band.\n * - The rule `difference` compares the interval of the candidate median over the A/A median with 1 and with `margin`.\n */\nexport type VerdictRule = \"band\" | \"difference\";\n\n/** The options of a time verdict. The bootstrap gives each interval of the verdict. */\nexport interface VerdictOptions extends BandOptions {\n /** The rule of the verdict. The default is `difference`, from the power test of `verdictPower`. */\n readonly rule?: VerdictRule;\n /**\n * The half width on a log scale of the ratios that the rule `difference` calls `same`. The default is 0.05.\n * An interval in the margin is `same`, also when it is wholly below 1.\n */\n readonly margin?: number;\n /**\n * The noise cap on the A/A spread of the run, on a log scale. Above the cap, the outcome is `cannot decide`.\n * The cap also limits the bias of the A/A arm: an A/A median ratio out of `[exp(-noiseCap), exp(noiseCap)]` gives `cannot decide`.\n * The default is no cap.\n */\n readonly noiseCap?: number;\n /** The quantile of the A/A spread of the noise cap and of the pooled band. The default is 0.9. */\n readonly spreadLevel?: number;\n /**\n * The pooled band. It replaces the band of the A/A arm of the run in the rule `band` and in `comparison`.\n * The rule `difference` uses the larger of `margin` and the noise of the pooled band as its margin. The default is no pool.\n */\n readonly pool?: PoolOptions;\n}\n\n/** The noise cap of a verdict: the A/A spread of the run, the A/A median ratio of the run and the cap. */\nexport interface NoiseCheck {\n /** The A/A spread of the run, or undefined when the A/A arm has no cases. */\n readonly spread: number | undefined;\n readonly cap: number;\n /** This value is `true` when the spread is above the cap, or when the A/A arm has no cases. */\n readonly noisy: boolean;\n /** The median ratio of the A/A cases of the run, or undefined when the A/A arm has no cases. A value far from 1 shows a bias of the order or of the arm slots. */\n readonly aaMedian: number | undefined;\n /**\n * This value is `true` when the A/A median ratio of the run is out of `[exp(-cap), exp(cap)]`.\n * With a spread level of 0.5 or more, a biased run is also noisy. The check decides alone only below that level.\n */\n readonly biased: boolean;\n}\n\n/** A time verdict, with the facts that it uses. */\nexport interface TimeVerdict {\n readonly outcome: TimeOutcome;\n /** The reason of the outcome, in one sentence. */\n readonly reason: string;\n readonly cases: readonly CaseRatio[];\n readonly rule: VerdictRule;\n /**\n * The interval that the rule compares, or undefined when there are no candidate cases.\n * With too few A/A cases, it is the interval of the median ratio of the candidate.\n * For the rule `band`, it is the bootstrap interval of the median ratio of the candidate.\n * For the rule `difference`, it is the interval of the candidate median ratio over the A/A median ratio.\n */\n readonly interval: Interval | undefined;\n /** The band of the A/A arm, or undefined when the A/A arm has too few cases. */\n readonly band: Band | undefined;\n /**\n * The counts of the cases out of the band, or undefined without a band.\n * The comparison uses the value `1 + log(ratio)` of each case and the noise of the band, so its two sides are the ends of the band.\n */\n readonly comparison: Comparison | undefined;\n /** The facts of the pooled band. The verdict has this field only with the option `pool`. */\n readonly pool?: PoolFacts;\n /** The noise cap. The verdict has this field only with the option `noiseCap`. */\n readonly noise?: NoiseCheck;\n}\n\n/**\n * This function gives the time verdict of the candidate arm `b` against the baseline arm `a`.\n * - The rule `difference` gives `same` when the interval is in `[exp(-margin), exp(margin)]`. This test comes first, so\n * an interval in the margin and below 1 is `same`, not `faster`.\n * - Out of the margin, the rule `difference` gives `faster` when the whole interval is below 1, and `slower` when it is above 1.\n * With the margin 0, each outcome stays the same as in 0.1.2.\n * - The rule `band` gives `faster` below the band, `slower` above the band, and `same` in the band.\n * - Each other case is `cannot decide`, and so is an A/A arm with too few cases.\n * - With `noiseCap`, an A/A spread of the run above the cap gives `cannot decide`.\n * - With `noiseCap`, an A/A median ratio of the run out of `[exp(-noiseCap), exp(noiseCap)]` also gives `cannot decide`.\n * - With `pool`, the pooled band replaces the band of the A/A arm of the run. The run still needs `minimum` A/A cases.\n */\nexport function timeVerdict(candidate: readonly PairedTime[], aa: readonly PairedTime[], options: VerdictOptions = {}): TimeVerdict {\n const level = options.spreadLevel ?? 0.9;\n if (options.noiseCap !== undefined && !(options.noiseCap >= 0 && Number.isFinite(options.noiseCap))) {\n throw new RangeError(`timeVerdict: the noise cap is a finite number of 0 or more, not ${options.noiseCap}.`);\n }\n const pool = poolFor(options);\n let verdict = ruleVerdict(candidate, aa, options, pool?.band);\n if (pool !== undefined) verdict = { ...verdict, pool: pool.facts };\n if (options.noiseCap !== undefined) {\n const cap = options.noiseCap;\n const aaRatios = caseRatios(aa);\n const spread = aaRatios.length === 0 ? undefined : spreadOf(aaRatios, level);\n const aaMedian = aaRatios.length === 0 ? undefined : Math.exp(quantile(aaRatios.map((r) => Math.log(r.ratio)), 0.5));\n const biased = aaMedian !== undefined && Math.abs(Math.log(aaMedian)) > cap;\n const noise: NoiseCheck = { spread, cap, noisy: spread === undefined || spread > cap, aaMedian, biased };\n verdict = { ...verdict, noise };\n // The early outcomes (no candidate cases, too few A/A cases) keep their reason. Each other outcome gives way to the cap.\n if (noise.noisy && verdict.band !== undefined) {\n const why = spread === undefined ? \"The A/A arm of the run has no cases, so the noise cap has no spread.\" : `The A/A spread of the run is ${round(spread)}, above the noise cap ${cap}.`;\n const reason = `${why} Without the cap, the outcome is \"${verdict.outcome}\".`;\n verdict = { ...verdict, outcome: \"cannot decide\", reason };\n } else if (noise.biased && verdict.band !== undefined) {\n const reason = `The A/A median ratio of the run is ${round(aaMedian!)}, out of [${round(Math.exp(-cap))}, ${round(Math.exp(cap))}] of the noise cap ${cap}. Without the cap, the outcome is \"${verdict.outcome}\".`;\n verdict = { ...verdict, outcome: \"cannot decide\", reason };\n }\n }\n return verdict;\n}\n\n/** This function gives the pooled band and its facts of the options, or undefined without the option `pool`. */\nfunction poolFor(options: VerdictOptions): { band: Band; facts: PoolFacts } | undefined {\n return options.pool === undefined ? undefined : poolOf(options.pool, options.spreadLevel ?? 0.9);\n}\n\n/** This function gives the margin of the rule `difference`: the larger of `margin` and the noise of the pooled band. It refuses a margin below 0. */\nfunction marginOf(options: VerdictOptions, pooled: Band | undefined): number {\n const given = options.margin ?? 0.05;\n if (!(given >= 0)) throw new RangeError(`timeVerdict: the margin is 0 or more, not ${given}.`);\n return pooled === undefined ? given : Math.max(given, pooled.noise);\n}\n\n/**\n * This function gives the margin that the rule `difference` of `timeVerdict` uses with these options, on a log scale.\n * The margin is the larger of the option `margin` (default 0.05) and the noise of the pooled band. It uses the same code as the verdict.\n * The pooled band comes from the records of the option `pool`, not from the records of the run.\n * An interval in `[exp(-margin), exp(margin)]` has the outcome `same`. A page can draw that zone with this function.\n */\nexport function verdictMargin(options: VerdictOptions = {}): number {\n return marginOf(options, poolFor(options)?.band);\n}\n\n/** This function gives the outcome of the rule. A pooled band replaces the band of the A/A arm, and it widens the margin of the rule `difference`. */\nfunction ruleVerdict(candidate: readonly PairedTime[], aa: readonly PairedTime[], options: VerdictOptions, pooled: Band | undefined): TimeVerdict {\n const rule = options.rule ?? \"difference\";\n const cases = caseRatios(candidate);\n const aaRatios = caseRatios(aa);\n const minimum = options.minimum ?? 5;\n const none = { cases, rule, interval: undefined, band: undefined, comparison: undefined };\n if (cases.length === 0) return { ...none, outcome: \"cannot decide\", reason: \"The candidate has no cases.\" };\n if (aaRatios.length < minimum) {\n const interval = bootstrapMedian(cases.map((c) => c.ratio), options.bootstrap);\n return { ...none, interval, outcome: \"cannot decide\", reason: `The A/A arm has ${aaRatios.length} cases, and a band needs ${minimum} or more.` };\n }\n const band = pooled ?? aaBand(aa, options);\n const comparison = compareRatios(cases.map((c) => 1 + Math.log(c.ratio)), { better: \"lower\", noise: band.noise });\n const facts = { cases, rule, band, comparison };\n if (rule === \"difference\") {\n const margin = marginOf(options, pooled);\n const shown = pooled === undefined ? margin : round(margin);\n const logs = cases.map((c) => Math.log(c.ratio));\n const aaLogs = aaRatios.map((c) => Math.log(c.ratio));\n const d = bootstrapTwo(logs, aaLogs, (x, y) => median(x) - median(y), options.bootstrap);\n const interval: Interval = { ...d, estimate: Math.exp(d.estimate), low: Math.exp(d.low), high: Math.exp(d.high) };\n const span = `The interval of the candidate median over the A/A median is [${round(interval.low)}, ${round(interval.high)}].`;\n // The margin comes first: an interval in the margin is `same`, also when it is wholly below or above 1.\n if (d.low >= -margin && d.high <= margin) return { ...facts, interval, outcome: \"same\", reason: `${span} The interval is in the margin of ${shown}.` };\n if (interval.high < 1) return { ...facts, interval, outcome: \"faster\", reason: `${span} The interval is below 1.` };\n if (interval.low > 1) return { ...facts, interval, outcome: \"slower\", reason: `${span} The interval is above 1.` };\n return { ...facts, interval, outcome: \"cannot decide\", reason: `${span} The interval holds 1 and crosses the margin of ${shown}.` };\n }\n const interval = bootstrapMedian(cases.map((c) => c.ratio), options.bootstrap);\n const span = `The interval of the median ratio is [${round(interval.low)}, ${round(interval.high)}], and the A/A band is [${round(band.low)}, ${round(band.high)}].`;\n if (interval.high < band.low) return { ...facts, interval, outcome: \"faster\", reason: `${span} The interval is below the band.` };\n if (interval.low > band.high) return { ...facts, interval, outcome: \"slower\", reason: `${span} The interval is above the band.` };\n if (interval.low >= band.low && interval.high <= band.high) return { ...facts, interval, outcome: \"same\", reason: `${span} The interval is in the band.` };\n return { ...facts, interval, outcome: \"cannot decide\", reason: `${span} The interval crosses an end of the band.` };\n}\n\nfunction round(x: number): number {\n return Math.round(x * 10_000) / 10_000;\n}\n","/**\n * The limit verdict of a bench: is the ratio of each case at most a limit? For example, the claim \"the new code takes at most\n * 2 times the time of the old code\" needs it. The time verdict compares only with 1.\n */\nimport { medianInterval } from \"@mark1russell7/rigor-statistics\";\nimport { type CaseRatio, type PairedTime, caseRatios } from \"./paired.ts\";\nimport { type Band, type BandOptions, aaBand } from \"./verdict.ts\";\n\n/** The ratio of one case with the interval of its median over the rounds. */\nexport interface CaseInterval extends CaseRatio {\n /** The ends of the interval: `exp` of `medianInterval` over the values `log(b / a)` of the rounds. */\n readonly low: number;\n readonly high: number;\n /** The coverage of the interval. It is at least the level. */\n readonly coverage: number;\n}\n\n/**\n * This function gives the ratio of each case with a distribution-free interval of its median, in the order of\n * `caseRatios`. It needs enough rounds for the level: 6 rounds or more for the level 0.95.\n */\nexport function caseIntervals(records: readonly PairedTime[], level = 0.95): CaseInterval[] {\n const logs = new Map<string, number[]>();\n for (const r of records) {\n const list = logs.get(r.case) ?? [];\n list.push(Math.log(r.b / r.a));\n logs.set(r.case, list);\n }\n return caseRatios(records).map((c) => {\n const interval = medianInterval(logs.get(c.case)!, level);\n return { ...c, low: Math.exp(interval.low), high: Math.exp(interval.high), coverage: interval.coverage };\n });\n}\n\n/** The three outcomes of a limit verdict. */\nexport type LimitOutcome = \"within\" | \"beyond\" | \"cannot decide\";\n\n/** The options of a limit verdict. */\nexport interface LimitOptions extends BandOptions {\n /** The largest ratio `b / a` that the claim permits, for example 2. */\n readonly limit: number;\n /** The level of the interval of each case. The default is 0.95. */\n readonly caseLevel?: number;\n /**\n * The share of the cases that the claim needs `within`, above 0 and at most 1. The default 1 needs each case. With 0.95\n * and 20 cases, 1 case can be `beyond` or `cannot decide`. The option `timeTailQ` of a promotion record is this share.\n */\n readonly share?: number;\n}\n\n/** The verdict of one case against the limit. */\nexport interface CaseLimit extends CaseInterval {\n /** The high end of the interval of the case, times the high end of the A/A band. */\n readonly bound: number;\n /** The low end of the interval of the case, times the low end of the A/A band. */\n readonly floor: number;\n readonly outcome: LimitOutcome;\n}\n\n/** A limit verdict, with the facts that it uses. */\nexport interface LimitVerdict {\n readonly outcome: LimitOutcome;\n /** The reason of the outcome, in one sentence. */\n readonly reason: string;\n readonly limit: number;\n readonly cases: readonly CaseLimit[];\n /** The band of the A/A arm, or undefined when the A/A arm has too few cases. */\n readonly band: Band | undefined;\n /** The share of the option `share`. */\n readonly share: number;\n /** The number of cases that can be other than `within`: the floor of the number of cases times `1 - share`. */\n readonly allowed: number;\n}\n\n/**\n * This function gives the verdict of the claim \"the ratio `b / a` of each case is at most `limit`\". The A/A band widens\n * the interval of each case: the bound is the high end times the high end of the band.\n * - A case is `within` when its bound is at most the limit, and `beyond` when its floor is above the limit.\n * - The verdict is `within` when `allowed` cases or fewer are other than `within`. With the default share 1, `allowed` is 0.\n * - The verdict is `beyond` when more than `allowed` cases are `beyond`.\n * - Each other verdict is `cannot decide`, and so is an A/A arm with too few cases or a candidate with no cases.\n */\nexport function limitVerdict(candidate: readonly PairedTime[], aa: readonly PairedTime[], options: LimitOptions): LimitVerdict {\n const { limit } = options;\n if (!(limit > 0 && Number.isFinite(limit))) throw new RangeError(`limitVerdict: the limit is a finite number above 0, not ${limit}.`);\n const share = options.share ?? 1;\n if (!(share > 0 && share <= 1)) throw new RangeError(`limitVerdict: the share is above 0 and at most 1, not ${share}.`);\n const intervals = caseIntervals(candidate, options.caseLevel);\n const minimum = options.minimum ?? 5;\n // The small term keeps a product such as 10 * (1 - 0.9) = 0.9999999999999998 from a floor of 0.\n const allowed = Math.floor(intervals.length * (1 - share) + 1e-9);\n if (intervals.length === 0) return { outcome: \"cannot decide\", reason: \"The candidate has no cases.\", limit, cases: [], band: undefined, share, allowed };\n const aaCount = caseRatios(aa).length;\n if (aaCount < minimum) {\n const cases = intervals.map((c) => ({ ...c, bound: c.high, floor: c.low, outcome: \"cannot decide\" as const }));\n return { outcome: \"cannot decide\", reason: `The A/A arm has ${aaCount} cases, and a band needs ${minimum} or more.`, limit, cases, band: undefined, share, allowed };\n }\n const band = aaBand(aa, options);\n const cases = intervals.map((c): CaseLimit => {\n const bound = c.high * band.high;\n const floor = c.low * band.low;\n const outcome: LimitOutcome = bound <= limit ? \"within\" : floor > limit ? \"beyond\" : \"cannot decide\";\n return { ...c, bound, floor, outcome };\n });\n const facts = { limit, cases, band, share, allowed };\n const beyond = cases.filter((c) => c.outcome === \"beyond\");\n const others = cases.filter((c) => c.outcome !== \"within\");\n const permit = allowed === 0 ? \"\" : `, and the share ${share} permits ${allowed}`;\n if (beyond.length > allowed) {\n return { outcome: \"beyond\", reason: `${beyond.length} of ${cases.length} cases are above the limit ${limit}${permit}. The first is ${JSON.stringify(beyond[0]!.case)}.`, ...facts };\n }\n if (others.length <= allowed) {\n const inside = cases.filter((c) => c.outcome === \"within\");\n const top = (inside.length > 0 ? inside : cases).reduce((a, b) => (b.bound > a.bound ? b : a));\n const rest = others.length === 0 ? \"\" : ` ${others.length} of ${cases.length} cases are not within${permit}.`;\n return { outcome: \"within\", reason: `The largest bound of a case within is ${round(top.bound)} in the case ${JSON.stringify(top.case)}, and it is at most the limit ${limit}.${rest}`, ...facts };\n }\n const top = cases.reduce((a, b) => (b.bound > a.bound ? b : a));\n return { outcome: \"cannot decide\", reason: `${others.length} of ${cases.length} cases are not within${permit}. The bound of the case ${JSON.stringify(top.case)} is ${round(top.bound)}, above the limit ${limit}, and too few floors are above it.`, ...facts };\n}\n\nfunction round(x: number): number {\n return Math.round(x * 10_000) / 10_000;\n}\n","/**\n * The merge of the records of many jobs, and the pooled verdicts over them. Each job of a bench gives its own records.\n * A pooled verdict uses all jobs together, and the verdict of each job alone guards it.\n */\nimport { type LimitOptions, type LimitOutcome, type LimitVerdict, limitVerdict } from \"./limit.ts\";\nimport { type Arm, type PairedTime } from \"./paired.ts\";\nimport { type TimeOutcome, type TimeVerdict, type VerdictOptions, timeVerdict } from \"./verdict.ts\";\n\n/** The records of each case with a turned order, and the count of the records that the balance removed. */\nexport interface Balanced<T extends PairedTime> {\n readonly records: T[];\n /** The count of the removed records. It is 0 when each case has a turned order. */\n readonly trimmed: number;\n}\n\n/**\n * This function removes records until the two orders of each case differ in count by 1 or less.\n * It removes the records of the larger order with the highest rounds first. The other records keep their order.\n * A job that stopped early, or a missing job, can leave a case with 2 more rounds of one order. `caseRatios` refuses such a case.\n */\nexport function balanceOrders<T extends PairedTime>(records: readonly T[]): Balanced<T> {\n const byCase = new Map<string, Record<Arm, number[]>>();\n records.forEach((record, index) => {\n const orders = byCase.get(record.case) ?? { a: [], b: [] };\n orders[record.first === \"b\" ? \"b\" : \"a\"].push(index);\n byCase.set(record.case, orders);\n });\n const drop = new Set<number>();\n const highFirst = (x: number, y: number): number => records[y]!.round - records[x]!.round;\n for (const { a, b } of byCase.values()) {\n const [more, fewer] = a.length > b.length ? [a, b] : [b, a];\n const extra = more.length - fewer.length - 1;\n if (extra > 0) for (const index of more.toSorted(highFirst).slice(0, extra)) drop.add(index);\n }\n return { records: records.filter((_, index) => !drop.has(index)), trimmed: drop.size };\n}\n\n/** The records of one job: the candidate pair and the A/A pair. */\nexport interface JobRecords {\n /** The label of the job, for example the index of its CI job. A record with a field `job` has this label, or `poolRecords` refuses it. */\n readonly job: string;\n readonly candidate: readonly PairedTime[];\n readonly aa: readonly PairedTime[];\n /** The cases that the job skipped, as `freshRun` gives them. `poolRecords` removes each of these cases from all jobs. */\n readonly skipped?: readonly { readonly case: string }[];\n}\n\n/** The records of one job in a pool: the cases that no job skipped, balanced with `balanceOrders`. */\nexport interface PooledJob {\n readonly job: string;\n readonly candidate: PairedTime[];\n readonly aa: PairedTime[];\n}\n\n/** The pool of the jobs: the merged records of all jobs, and the records of each job alone. */\nexport interface PooledRecords {\n /** The merged candidate records of all jobs, balanced with `balanceOrders`. */\n readonly candidate: PairedTime[];\n /** The merged A/A records of all jobs, balanced with `balanceOrders`. */\n readonly aa: PairedTime[];\n /** The records of each job alone, in the order of the jobs. */\n readonly jobs: readonly PooledJob[];\n /** The count of the records that `balanceOrders` removed from the merged candidate and A/A records. */\n readonly trimmed: number;\n /** The cases that a job skipped, in sorted order. The pool has no record of these cases. */\n readonly dropped: string[];\n}\n\n/**\n * This function gives the pool of the jobs. `pooledVerdict` and `pooledLimitVerdict` use it.\n * - It refuses two jobs with one label, and a record with a different job label. It also refuses two jobs with a record of one case and one round.\n * - It removes each case that a job skipped from all jobs, so all jobs have the same cases.\n * - It merges the records of the jobs, and it balances the merged records and the records of each job with `balanceOrders`.\n */\nexport function poolRecords(jobs: readonly JobRecords[]): PooledRecords {\n return pool(jobs, \"poolRecords\");\n}\n\n/** The pool of `poolRecords`. Each error starts with the name `caller`. */\nfunction pool(jobs: readonly JobRecords[], caller: string): PooledRecords {\n const labels = new Set<string>();\n for (const { job } of jobs) {\n if (labels.has(job)) throw new RangeError(`${caller}: two jobs have the label ${JSON.stringify(job)}.`);\n labels.add(job);\n }\n for (const { job, candidate: c, aa: a } of jobs) {\n for (const r of [...c, ...a] as readonly (PairedTime & { readonly job?: unknown })[]) {\n if (r.job !== undefined && r.job !== job) throw new RangeError(`${caller}: a record of the job ${JSON.stringify(job)} has the job ${JSON.stringify(r.job)}.`);\n }\n }\n const dropped = [...new Set(jobs.flatMap((j) => (j.skipped ?? []).map((s) => s.case)))].sort();\n const gone = new Set(dropped);\n const keep = (records: readonly PairedTime[]): PairedTime[] => records.filter((r) => !gone.has(r.case));\n const kept = jobs.map((j) => ({ job: j.job, candidate: keep(j.candidate), aa: keep(j.aa) }));\n uniqueRounds(kept, \"candidate\", caller);\n uniqueRounds(kept, \"aa\", caller);\n const candidate = balanceOrders(kept.flatMap((j) => j.candidate));\n const aa = balanceOrders(kept.flatMap((j) => j.aa));\n const each = kept.map(({ job, candidate: c, aa: a }): PooledJob => ({ job, candidate: balanceOrders(c).records, aa: balanceOrders(a).records }));\n return { candidate: candidate.records, aa: aa.records, jobs: each, trimmed: candidate.trimmed + aa.trimmed, dropped };\n}\n\n/** This function refuses two jobs with a record of one case and one round in one pair. */\nfunction uniqueRounds(jobs: readonly PooledJob[], pair: \"candidate\" | \"aa\", caller: string): void {\n const seen = new Map<string, string>();\n for (const { job, [pair]: records } of jobs) {\n for (const r of records) {\n const key = `${r.case}\\u0000${r.round}`;\n const other = seen.get(key);\n if (other !== undefined && other !== job) {\n throw new RangeError(\n `${caller}: the jobs ${JSON.stringify(other)} and ${JSON.stringify(job)} both have the round ${r.round} of the case ${JSON.stringify(r.case)} in the ${pair} pair. Set roundOffset of each job to job * rounds.`,\n );\n }\n seen.set(key, job);\n }\n }\n}\n\n/** The guard of a pooled verdict: the outcome, the opposed jobs and the reason. */\ninterface Guarded<O extends string> {\n readonly outcome: O | \"cannot decide\";\n readonly opposed: string[];\n readonly reason: string;\n}\n\n/**\n * This function applies the guard of the jobs to a pooled verdict. A job whose outcome is `opposite` of the pooled outcome\n * makes the outcome `cannot decide`. Each other job does not change the outcome.\n */\nfunction guard<O extends string>(\n pooled: { readonly outcome: O; readonly reason: string },\n jobs: readonly { readonly job: string; readonly verdict: { readonly outcome: O } }[],\n opposite: Partial<Record<O, O>>,\n): Guarded<O> {\n const against = opposite[pooled.outcome];\n const opposed = against === undefined ? [] : jobs.filter((j) => j.verdict.outcome === against).map((j) => j.job);\n if (opposed.length === 0) return { outcome: pooled.outcome, opposed, reason: pooled.reason };\n const names = opposed.map((j) => JSON.stringify(j)).join(\", \");\n const reason = `${pooled.reason} The pooled outcome is \"${pooled.outcome}\", but the jobs ${names} give \"${against}\". Thus the outcome is \"cannot decide\".`;\n return { outcome: \"cannot decide\", opposed, reason };\n}\n\n/** The verdict of one job alone. */\nexport interface JobVerdict {\n readonly job: string;\n readonly verdict: TimeVerdict;\n}\n\n/** The pooled verdict over the jobs, with the verdict of each job alone. */\nexport interface PooledVerdict {\n /** The outcome of the pooled verdict, or `cannot decide` when a job decides the opposite direction. */\n readonly outcome: TimeOutcome;\n /** The verdict of the merged and balanced records of all jobs. */\n readonly pooled: TimeVerdict;\n /** The verdict of each job alone, with the options of the pooled verdict. */\n readonly jobs: readonly JobVerdict[];\n /** The jobs whose own outcome is the opposite direction of the pooled outcome. */\n readonly opposed: string[];\n /** The count of the records that `balanceOrders` removed from the merged candidate and A/A records. */\n readonly trimmed: number;\n /** The cases that a job skipped, in sorted order. The verdicts use no record of these cases. */\n readonly dropped: string[];\n /** The reason of the pooled verdict, and the opposed jobs. */\n readonly reason: string;\n}\n\nconst opposite: Partial<Record<TimeOutcome, TimeOutcome>> = { faster: \"slower\", slower: \"faster\" };\n\n/**\n * This function gives the time verdict over the records of all jobs.\n * - It makes the pool of the jobs with `poolRecords`, and it gives the time verdict of the merged records.\n * - The verdict of each job alone uses the same options. A job with `cannot decide` or `same` does not change the outcome.\n * - A job that decides the opposite direction of the pooled outcome makes the outcome `cannot decide`.\n */\nexport function pooledVerdict(jobs: readonly JobRecords[], options: VerdictOptions = {}): PooledVerdict {\n const { candidate, aa, jobs: each, trimmed, dropped } = pool(jobs, \"pooledVerdict\");\n const pooled = timeVerdict(candidate, aa, options);\n const verdicts = each.map(({ job, candidate: c, aa: a }): JobVerdict => ({ job, verdict: timeVerdict(c, a, options) }));\n const { outcome, opposed, reason } = guard(pooled, verdicts, opposite);\n return { outcome, pooled, jobs: verdicts, opposed, trimmed, dropped, reason };\n}\n\n/** The limit verdict of one job alone. */\nexport interface JobLimitVerdict {\n readonly job: string;\n readonly verdict: LimitVerdict;\n}\n\n/** The pooled limit verdict over the jobs, with the limit verdict of each job alone. */\nexport interface PooledLimitVerdict {\n /** The outcome of the pooled limit verdict, or `cannot decide` when a job gives the opposite outcome. */\n readonly outcome: LimitOutcome;\n /** The limit verdict of the merged and balanced records of all jobs. */\n readonly pooled: LimitVerdict;\n /** The limit verdict of each job alone, with the options of the pooled verdict. */\n readonly jobs: readonly JobLimitVerdict[];\n /** The jobs whose own outcome is the opposite of the pooled outcome: `beyond` against `within`, or the reverse. */\n readonly opposed: string[];\n /** The count of the records that `balanceOrders` removed from the merged candidate and A/A records. */\n readonly trimmed: number;\n /** The cases that a job skipped, in sorted order. The verdicts use no record of these cases. */\n readonly dropped: string[];\n /** The reason of the pooled limit verdict, and the opposed jobs. */\n readonly reason: string;\n}\n\nconst limitOpposite: Partial<Record<LimitOutcome, LimitOutcome>> = { within: \"beyond\", beyond: \"within\" };\n\n/**\n * This function gives the limit verdict over the records of all jobs, for example for a speed gate on limits over a CI matrix.\n * - It makes the pool of the jobs with `poolRecords`, and it gives the limit verdict of the merged records.\n * - The verdict of each job alone uses the same options. A job with `cannot decide` does not change the outcome.\n * - A job with `beyond` against a pooled `within`, or the reverse, makes the outcome `cannot decide`.\n */\nexport function pooledLimitVerdict(jobs: readonly JobRecords[], options: LimitOptions): PooledLimitVerdict {\n const { candidate, aa, jobs: each, trimmed, dropped } = pool(jobs, \"pooledLimitVerdict\");\n const pooled = limitVerdict(candidate, aa, options);\n const verdicts = each.map(({ job, candidate: c, aa: a }): JobLimitVerdict => ({ job, verdict: limitVerdict(c, a, options) }));\n const { outcome, opposed, reason } = guard(pooled, verdicts, limitOpposite);\n return { outcome, pooled, jobs: verdicts, opposed, trimmed, dropped, reason };\n}\n\n/** The pairs of a run: the records of each pair, by the id of the pair. */\nexport interface RunPairs {\n readonly pairs: Readonly<Record<string, readonly PairedTime[]>>;\n}\n\n/**\n * This function gives the verdict of each candidate pair of a run against its A/A pair `aaPair`.\n * It balances the records of each pair with `balanceOrders` first. It refuses an `aaPair` that the run does not have.\n */\nexport function armVerdicts(run: RunPairs, aaPair: string, options: VerdictOptions = {}): Record<string, TimeVerdict> {\n const aaRecords = run.pairs[aaPair];\n if (aaRecords === undefined) throw new RangeError(`armVerdicts: the run has no pair ${JSON.stringify(aaPair)}.`);\n const aa = balanceOrders(aaRecords).records;\n const out: Record<string, TimeVerdict> = {};\n for (const [pair, records] of Object.entries(run.pairs)) {\n if (pair !== aaPair) out[pair] = timeVerdict(balanceOrders(records).records, aa, options);\n }\n return out;\n}\n","/**\n * The power test of a time verdict. It draws seeded benches with a known speedup and counts the verdicts that find it.\n * It also draws benches with equal arms and counts the false verdicts.\n */\nimport { random } from \"@mark1russell7/rigor-generators\";\nimport { firstArm, type PairedTime } from \"./paired.ts\";\nimport { type TimeOutcome, type VerdictOptions, timeVerdict, verdictMargin } from \"./verdict.ts\";\n\n/** The options of a power test. */\nexport interface PowerOptions {\n /** The count of the cases of the candidate and of the A/A arm. */\n readonly cases: number;\n /** The count of the rounds of each case. The default is 5. */\n readonly rounds?: number;\n /** The true ratio of the candidate time to the baseline time, for example 0.93 for a speedup of 7%. */\n readonly ratio: number;\n /** The standard deviation of the log time of one arm in one round. The default is 0.1. */\n readonly sigma?: number;\n /** The count of the seeded benches. The default is 200. */\n readonly trials?: number;\n /** The seed of the draws. The default is 1. */\n readonly seed?: number | string;\n /** The options of the verdict of each bench. */\n readonly verdict?: VerdictOptions;\n}\n\n/** The result of a power test: the share of each outcome over the trials. */\nexport interface Power {\n readonly trials: number;\n readonly outcomes: Readonly<Record<TimeOutcome, number>>;\n /**\n * The share of the trials whose outcome is the true one. For the rule `difference`, a ratio in the margin of the verdict has the true outcome `same`.\n * That margin is the larger of `margin` and the noise of the pooled band. The pool is the same in each trial, so each trial has the same margin.\n * Out of that margin, and for the rule `band`, the true outcome is `faster` below 1, `slower` above 1 and `same` at 1.\n */\n readonly power: number;\n}\n\n/**\n * This function draws `trials` benches and gives the share of each verdict. Each case has its own log scale.\n * Each arm of each round adds a normal noise of `sigma` to the log time.\n */\nexport function verdictPower(options: PowerOptions): Power {\n const rounds = options.rounds ?? 5;\n const sigma = options.sigma ?? 0.1;\n const trials = options.trials ?? 200;\n if (!Number.isSafeInteger(options.cases) || options.cases < 1) throw new RangeError(`verdictPower: cases is a whole number of 1 or more, not ${options.cases}.`);\n if (!(options.ratio > 0 && Number.isFinite(options.ratio))) throw new RangeError(`verdictPower: the ratio is a finite number above 0, not ${options.ratio}.`);\n const r = random(options.seed ?? 1);\n const bench = (ratio: number, prefix: string): PairedTime[] => {\n const out: PairedTime[] = [];\n for (let c = 0; c < options.cases; c++) {\n const scale = 10 * Math.exp(r.normal());\n for (let round = 0; round < rounds; round++) {\n const a = scale * Math.exp(r.normal(0, sigma));\n const b = scale * ratio * Math.exp(r.normal(0, sigma));\n out.push({ case: `${prefix}${c}`, round, first: firstArm(round, c), a, b });\n }\n }\n return out;\n };\n const truth = trueOutcome(options.ratio, options.verdict);\n const counts: Record<TimeOutcome, number> = { faster: 0, slower: 0, same: 0, \"cannot decide\": 0 };\n for (let t = 0; t < trials; t++) {\n const aa = bench(1, \"aa\");\n const candidate = bench(options.ratio, \"c\");\n counts[timeVerdict(candidate, aa, options.verdict).outcome]++;\n }\n const outcomes = Object.fromEntries(Object.entries(counts).map(([k, n]) => [k, n / trials])) as Record<TimeOutcome, number>;\n return { trials, outcomes, power: outcomes[truth] };\n}\n\n/** This function gives the true outcome of a ratio. The margin of the rule `difference` comes first, as in `timeVerdict`. */\nfunction trueOutcome(ratio: number, verdict: VerdictOptions = {}): TimeOutcome {\n if ((verdict.rule ?? \"difference\") === \"difference\" && Math.abs(Math.log(ratio)) <= verdictMargin(verdict)) return \"same\";\n return ratio < 1 ? \"faster\" : ratio > 1 ? \"slower\" : \"same\";\n}\n","/**\n * The timed rounds of one case in one process. The two arms first run for a warm-up time. Then the count of calls of\n * one batch grows until a batch of arm `a` takes the target time. Each round times one batch of each arm, in the order\n * of `firstArm`. Start this function in a fresh process for each case.\n */\nimport { type Arm, type PairedTime, firstArm } from \"./paired.ts\";\n\n/** The options of the timed rounds of one case. */\nexport interface RoundsOptions {\n /** The name of the case. */\n readonly case: string;\n /** The count of the rounds. */\n readonly rounds: number;\n /** The offset of `firstArm`: give the index of the case, so half of the cases start with each arm. The default is 0. */\n readonly offset?: number;\n /** The least time in milliseconds of one batch of arm `a`. The default is 10. */\n readonly targetMs?: number;\n /** The time in milliseconds of the warm-up of the two arms. The default is 1000. */\n readonly warmMs?: number;\n /** The clock in milliseconds. The default is `performance.now` of the global `performance` of Node and of each browser. */\n readonly now?: () => number;\n}\n\n/** The result of the timed rounds of one case. */\nexport interface Rounds {\n readonly records: PairedTime[];\n /** The count of calls of each arm in one batch. */\n readonly count: number;\n /** The sum of the numbers that the arms gave. The engine cannot remove the work of a call, because this sum uses it. */\n readonly sink: number;\n}\n\n/**\n * This function times the arms `a` and `b` of one case, and gives one `PairedTime` record for each round. The time of\n * a record is the time of one batch, in milliseconds. Each call of an arm gives a number, and the sum of these numbers\n * keeps the work.\n */\nexport function timeRounds(arms: Readonly<Record<Arm, () => number>>, options: RoundsOptions): Rounds {\n const { rounds } = options;\n const offset = options.offset ?? 0;\n const targetMs = options.targetMs ?? 10;\n const warmMs = options.warmMs ?? 1000;\n const now = options.now ?? (() => performance.now());\n if (!Number.isSafeInteger(rounds) || rounds < 1) throw new RangeError(`timeRounds: the rounds are a whole number of 1 or more, not ${rounds}.`);\n if (!Number.isSafeInteger(offset)) throw new RangeError(`timeRounds: the offset is a whole number, not ${offset}.`);\n if (!(targetMs > 0) || !(warmMs >= 0)) throw new RangeError(\"timeRounds: the target time is above 0, and the warm-up time is 0 or more.\");\n let sink = 0;\n const batch = (arm: Arm, count: number): number => {\n const run = arms[arm];\n const start = now();\n for (let i = 0; i < count; i++) sink += run();\n return now() - start;\n };\n let count = 1;\n const warmUntil = now() + warmMs;\n while (now() < warmUntil) {\n if (batch(\"a\", count) < targetMs / 4) count *= 2;\n batch(\"b\", count);\n }\n while (batch(\"a\", count) < targetMs) count *= 2;\n const records: PairedTime[] = [];\n for (let round = 0; round < rounds; round++) {\n const first = firstArm(round, offset);\n const second: Arm = first === \"a\" ? \"b\" : \"a\";\n const times = { [first]: batch(first, count), [second]: batch(second, count) } as Record<Arm, number>;\n records.push({ case: options.case, round, first, a: times.a, b: times.b });\n }\n return { records, count, sink };\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.
|
|
4
|
+
"version": "0.2.9",
|
|
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": [
|
|
@@ -48,7 +52,7 @@
|
|
|
48
52
|
},
|
|
49
53
|
"dependencies": {
|
|
50
54
|
"@mark1russell7/rigor-generators": "^0.1.5",
|
|
51
|
-
"@mark1russell7/rigor-statistics": "^0.1.
|
|
55
|
+
"@mark1russell7/rigor-statistics": "^0.1.10"
|
|
52
56
|
},
|
|
53
57
|
"devDependencies": {
|
|
54
58
|
"@mark1russell7/cue": "^2.0.0",
|