@mark1russell7/rigor-bench 0.2.12 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -265,7 +265,7 @@ const result = await superviseCases({
265
265
  The child calls `caseProgress(file).start(id, stage)` before each stage, and `end(id)` after the result is on the disk.
266
266
 
267
267
  - 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.
268
- - 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.
268
+ - 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. When cases have a `rank`, for example an input size, the count is for each rank. The stop is for that rank and above, and a smaller rank goes on. An overrun is measured, and a skip is not. Keep these tags different.
269
269
  - An overrun counts for the group of its stage only. A slow router thus does not count against the packer of its cases.
270
270
  - A case in `done` does not run, so a resume runs no case two times.
271
271
  - 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`.
@@ -12,6 +12,11 @@ export interface SupervisedCase {
12
12
  * A case with stages `pack` and `route` can thus count against a router, or against a packer, and not against both.
13
13
  */
14
14
  readonly groups?: Readonly<Record<string, string>>;
15
+ /**
16
+ * The place of the case in its groups, for example its input size. The default is 0.
17
+ * Overruns count for each group and rank. They stop the cases of that group at that rank and above, and a smaller rank goes on.
18
+ */
19
+ readonly rank?: number;
15
20
  }
16
21
  /** The end of a case that did not finish: an overrun, which is measured, or a skip, which is not. */
17
22
  export type SupervisedEnd = {
@@ -80,7 +85,9 @@ export declare function caseProgress(file: string, now?: () => number): CaseProg
80
85
  * This function runs the cases and holds each case to its budget.
81
86
  *
82
87
  * - 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.
88
+ * - After `overruns` overruns in one group at one rank, each other case of the group at that rank or above ends as `not-run`.
89
+ * The end has the ids of those overruns.
90
+ * - A case of a smaller rank goes on, and an overrun at another rank does not count. With no ranks, all cases of a group have one rank.
84
91
  * - 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
92
  * - An `over-budget` end of `earlier` counts as an overrun of this run. `onEnd` does not get the end again.
86
93
  * - A child that ends without a stop ends the run. Its exit code is the code of the result.
@@ -1 +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"}
1
+ {"version":3,"file":"supervise.d.ts","names":[],"sources":["../src/supervise.ts"],"mappings":";qBAYa;;qBAGA;;iBAGI;WACN;;WAEA;;;;;WAKA,SAAS,SAAS;;;;;WAKlB;;;YAIC;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;;;;;;;;;;;;;wBAgCpD,eAAe,SAAS,mBAAmB,QAAQ;;wBAwGzD,uBAAuB,yBAAyB;WAAoB;WAAuB,MAAM,OAAO;IAAoB"}
@@ -43,7 +43,9 @@ function progressReader(file) {
43
43
  * This function runs the cases and holds each case to its budget.
44
44
  *
45
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.
46
+ * - After `overruns` overruns in one group at one rank, each other case of the group at that rank or above ends as `not-run`.
47
+ * The end has the ids of those overruns.
48
+ * - A case of a smaller rank goes on, and an overrun at another rank does not count. With no ranks, all cases of a group have one rank.
47
49
  * - 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
50
  * - An `over-budget` end of `earlier` counts as an overrun of this run. `onEnd` does not get the end again.
49
51
  * - A child that ends without a stop ends the run. Its exit code is the code of the result.
@@ -54,6 +56,7 @@ async function superviseCases(options) {
54
56
  const limit = options.overruns ?? 2;
55
57
  const pollMs = options.pollMs ?? 1e3;
56
58
  const byId = new Map(cases.map((c) => [c.id, c]));
59
+ for (const c of cases) if (c.rank !== void 0 && !Number.isFinite(c.rank)) throw new RangeError(`The case "${c.id}" has the rank ${c.rank}. A rank must be a finite number.`);
57
60
  const finished = new Set(options.done ?? []);
58
61
  const groupOverruns = /* @__PURE__ */ new Map();
59
62
  const ends = [];
@@ -64,12 +67,16 @@ async function superviseCases(options) {
64
67
  onEnd(end);
65
68
  };
66
69
  const countOverrun = (id, stage) => {
67
- const group = byId.get(id)?.groups?.[stage];
70
+ const overrun = byId.get(id);
71
+ const group = overrun?.groups?.[stage];
68
72
  if (group === void 0) return;
69
- const ids = [...groupOverruns.get(group) ?? [], id];
70
- groupOverruns.set(group, ids);
73
+ const rank = overrun?.rank ?? 0;
74
+ const ranks = groupOverruns.get(group) ?? /* @__PURE__ */ new Map();
75
+ const ids = [...ranks.get(rank) ?? [], id];
76
+ ranks.set(rank, ids);
77
+ groupOverruns.set(group, ranks);
71
78
  if (ids.length < limit) return;
72
- for (const c of cases) if (!finished.has(c.id) && Object.values(c.groups ?? {}).includes(group)) report({
79
+ for (const c of cases) if (!finished.has(c.id) && (c.rank ?? 0) >= rank && Object.values(c.groups ?? {}).includes(group)) report({
73
80
  kind: "not-run",
74
81
  id: c.id,
75
82
  budgetMs: c.budgetMs,
@@ -1 +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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"}
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 * The place of the case in its groups, for example its input size. The default is 0.\n * Overruns count for each group and rank. They stop the cases of that group at that rank and above, and a smaller rank goes on.\n */\n readonly rank?: number;\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 at one rank, each other case of the group at that rank or above ends as `not-run`.\n * The end has the ids of those overruns.\n * - A case of a smaller rank goes on, and an overrun at another rank does not count. With no ranks, all cases of a group have one rank.\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 for (const c of cases) {\n if (c.rank !== undefined && !Number.isFinite(c.rank)) throw new RangeError(`The case \"${c.id}\" has the rank ${c.rank}. A rank must be a finite number.`);\n }\n const finished = new Set(options.done ?? []);\n const groupOverruns = new Map<string, Map<number, 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 overrun = byId.get(id);\n const group = overrun?.groups?.[stage];\n if (group === undefined) return;\n const rank = overrun?.rank ?? 0;\n const ranks = groupOverruns.get(group) ?? new Map<number, string[]>();\n const ids = [...(ranks.get(rank) ?? []), id];\n ranks.set(rank, ids);\n groupOverruns.set(group, ranks);\n if (ids.length < limit) return;\n for (const c of cases) {\n if (!finished.has(c.id) && (c.rank ?? 0) >= rank && 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/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.12",
4
+ "version": "0.2.13",
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",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@mark1russell7/rigor-generators": "^0.1.5",
55
- "@mark1russell7/rigor-statistics": "^0.1.10"
55
+ "@mark1russell7/rigor-statistics": "^0.1.11"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@mark1russell7/cue": "^2.0.0",