@ac-bench/lib 0.1.0

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.
Files changed (63) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +41 -0
  3. package/dist/child/channel.d.ts +28 -0
  4. package/dist/child/channel.d.ts.map +1 -0
  5. package/dist/child/channel.js +68 -0
  6. package/dist/child/channel.js.map +1 -0
  7. package/dist/child/event-sink.d.ts +13 -0
  8. package/dist/child/event-sink.d.ts.map +1 -0
  9. package/dist/child/event-sink.js +34 -0
  10. package/dist/child/event-sink.js.map +1 -0
  11. package/dist/child/main.d.ts +2 -0
  12. package/dist/child/main.d.ts.map +1 -0
  13. package/dist/child/main.js +157 -0
  14. package/dist/child/main.js.map +1 -0
  15. package/dist/cooldown.d.ts +51 -0
  16. package/dist/cooldown.d.ts.map +1 -0
  17. package/dist/cooldown.js +118 -0
  18. package/dist/cooldown.js.map +1 -0
  19. package/dist/current-environment.d.ts +8 -0
  20. package/dist/current-environment.d.ts.map +1 -0
  21. package/dist/current-environment.js +10 -0
  22. package/dist/current-environment.js.map +1 -0
  23. package/dist/environment-monitor.d.ts +59 -0
  24. package/dist/environment-monitor.d.ts.map +1 -0
  25. package/dist/environment-monitor.js +163 -0
  26. package/dist/environment-monitor.js.map +1 -0
  27. package/dist/escalation.d.ts +24 -0
  28. package/dist/escalation.d.ts.map +1 -0
  29. package/dist/escalation.js +73 -0
  30. package/dist/escalation.js.map +1 -0
  31. package/dist/fork-runner.d.ts +52 -0
  32. package/dist/fork-runner.d.ts.map +1 -0
  33. package/dist/fork-runner.js +348 -0
  34. package/dist/fork-runner.js.map +1 -0
  35. package/dist/index.d.ts +12 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +12 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/ipc-report-receiver.d.ts +47 -0
  40. package/dist/ipc-report-receiver.d.ts.map +1 -0
  41. package/dist/ipc-report-receiver.js +149 -0
  42. package/dist/ipc-report-receiver.js.map +1 -0
  43. package/dist/isolated-runner.d.ts +67 -0
  44. package/dist/isolated-runner.d.ts.map +1 -0
  45. package/dist/isolated-runner.js +537 -0
  46. package/dist/isolated-runner.js.map +1 -0
  47. package/dist/measure-diagnostic-proxy.d.ts +15 -0
  48. package/dist/measure-diagnostic-proxy.d.ts.map +1 -0
  49. package/dist/measure-diagnostic-proxy.js +50 -0
  50. package/dist/measure-diagnostic-proxy.js.map +1 -0
  51. package/dist/measurement-plan.d.ts +50 -0
  52. package/dist/measurement-plan.d.ts.map +1 -0
  53. package/dist/measurement-plan.js +2 -0
  54. package/dist/measurement-plan.js.map +1 -0
  55. package/dist/reporter-context.d.ts +19 -0
  56. package/dist/reporter-context.d.ts.map +1 -0
  57. package/dist/reporter-context.js +21 -0
  58. package/dist/reporter-context.js.map +1 -0
  59. package/dist/run-bench.d.ts +61 -0
  60. package/dist/run-bench.d.ts.map +1 -0
  61. package/dist/run-bench.js +151 -0
  62. package/dist/run-bench.js.map +1 -0
  63. package/package.json +79 -0
@@ -0,0 +1,50 @@
1
+ import { formatError } from "@ac-kit/core";
2
+ /**
3
+ * Derives the generic `diagnostic` events from a measure's typed warnings, in
4
+ * the parent.
5
+ *
6
+ * Warnings live in the typed result, where the plugin renders them inline per
7
+ * row; generic sinks need them as severity-carrying events. Emitting both from
8
+ * the child would give one fact two sources of truth, so the translation
9
+ * happens here instead — right after the `data` event that carries the result
10
+ * and before the case's `scope-end`.
11
+ */
12
+ export function createMeasureDiagnosticProxy(inner, registry) {
13
+ return {
14
+ enabled: (probe) => inner.enabled?.(probe) ?? true,
15
+ async write(event, signal) {
16
+ await inner.write(event, signal);
17
+ if (event.kind !== "data" || event.data.kind !== "case-result") {
18
+ return;
19
+ }
20
+ const { measure, caseTitle, result } = event.data;
21
+ for (const diagnostic of resolveWarnings_(registry, measure, result)) {
22
+ await inner.write({
23
+ kind: "diagnostic",
24
+ timestamp: event.timestamp,
25
+ scopeId: event.scopeId,
26
+ ...diagnostic,
27
+ // The scope this lands on is the condition's, not the case's.
28
+ attributes: { ...diagnostic.attributes, "bench.case": caseTitle },
29
+ }, signal);
30
+ }
31
+ },
32
+ flush: (signal) => inner.flush(signal),
33
+ close: (signal) => inner.close(signal),
34
+ };
35
+ }
36
+ function resolveWarnings_(registry, measure, result) {
37
+ try {
38
+ return registry.resolve(measure).warnings(result);
39
+ }
40
+ catch (error) {
41
+ return [
42
+ {
43
+ severity: "error",
44
+ code: "invalid-case-result",
45
+ message: `measure "${measure}" rejected its own case result: ${formatError(error)}`,
46
+ },
47
+ ];
48
+ }
49
+ }
50
+ //# sourceMappingURL=measure-diagnostic-proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"measure-diagnostic-proxy.js","sourceRoot":"","sources":["../src/measure-diagnostic-proxy.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C;;;;;;;;;GASG;AACH,MAAM,UAAU,4BAA4B,CAC3C,KAAyB,EACzB,QAAyB;IAEzB,OAAO;QACN,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,IAAI,IAAI;QAElD,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM;YACxB,MAAM,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAEjC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;gBAChE,OAAO;YACR,CAAC;YAED,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC;YAElD,KAAK,MAAM,UAAU,IAAI,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC;gBACtE,MAAM,KAAK,CAAC,KAAK,CAChB;oBACC,IAAI,EAAE,YAAY;oBAClB,SAAS,EAAE,KAAK,CAAC,SAAS;oBAC1B,OAAO,EAAE,KAAK,CAAC,OAAO;oBACtB,GAAG,UAAU;oBACb,8DAA8D;oBAC9D,UAAU,EAAE,EAAE,GAAG,UAAU,CAAC,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE;iBACjE,EACD,MAAM,CACN,CAAC;YACH,CAAC;QACF,CAAC;QAED,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC;QACtC,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC;KACtC,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB,CACxB,QAAyB,EACzB,OAAe,EACf,MAAe;IAEf,IAAI,CAAC;QACJ,OAAO,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACnD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO;YACN;gBACC,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,qBAAqB;gBAC3B,OAAO,EAAE,YAAY,OAAO,mCAAmC,WAAW,CAAC,KAAK,CAAC,EAAE;aACnF;SACD,CAAC;IACH,CAAC;AACF,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * How many contexts a condition is measured in, and under what conditions.
3
+ *
4
+ * Sampling (how many samples one case needs in one process) is the layer below
5
+ * and is decided per case by the sampler. This is the layer above: how many
6
+ * processes there are and what runs in each.
7
+ *
8
+ * Parameters, not an intent: naming an intent (`quick`, `rigorous`) and turning
9
+ * it into these numbers is configuration, and lives in `@ac-bench/cli`.
10
+ */
11
+ export type MeasurementPlan = {
12
+ /**
13
+ * `P`: independent processes running the whole condition, cases together.
14
+ *
15
+ * Differences between cases measured in the same process are paired, so the
16
+ * process-level offset cancels. Replicating gives that offset a distribution
17
+ * instead of one unknown draw.
18
+ */
19
+ readonly replicates: number;
20
+ /**
21
+ * Ceiling on `P` when the run escalates.
22
+ *
23
+ * A plan that starts cheap buys more processes only for a comparison its
24
+ * intervals have not settled, and only up to here.
25
+ */
26
+ readonly maxReplicates: number;
27
+ /**
28
+ * `Q`: additional processes running one case alone, with the full ancestor
29
+ * hook chain.
30
+ *
31
+ * The only way to tell "this case is slow" from "this case is slow after the
32
+ * one before it". Costs an execution of every ancestor hook per case, so it
33
+ * stays small and off by default.
34
+ */
35
+ readonly isolatedRuns: number;
36
+ /**
37
+ * `R`: how many turns each case takes within one process.
38
+ *
39
+ * Above 1 the cases interleave, one measured slice each in turn, so none of
40
+ * them owns a single stretch of the process's life. At 1 they run to
41
+ * completion one after another, and the first case measured gets the cold
42
+ * interpreter while the last gets the warm one.
43
+ */
44
+ readonly rounds: number;
45
+ /** Wait for the machine to settle before each attempt. */
46
+ readonly cooldown: boolean;
47
+ /** Re-runs of a condition whose environment verdict was `"unstable"`. */
48
+ readonly retryOnInstability: number;
49
+ };
50
+ //# sourceMappingURL=measurement-plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"measurement-plan.d.ts","sourceRoot":"","sources":["../src/measurement-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,MAAM,eAAe,GAAG;IAC7B;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAE5B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAE/B;;;;;;;OAOG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAE9B;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAE3B,yEAAyE;IACzE,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACpC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=measurement-plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"measurement-plan.js","sourceRoot":"","sources":["../src/measurement-plan.ts"],"names":[],"mappings":""}
@@ -0,0 +1,19 @@
1
+ import type { MeasureRegistry, ReporterContext } from "@ac-bench/core/plugin";
2
+ import type { Terminal } from "@ac-kit/app-terminal";
3
+ export type ReporterContextOptions = {
4
+ readonly registry: MeasureRegistry;
5
+ /** Base name for output files, for a reporter given no explicit one. */
6
+ readonly defaultOutput: string;
7
+ /** Defaults to `process.stdout`, wrapped so a sink cannot close it. */
8
+ readonly stdout?: WritableStream<string>;
9
+ /** Defaults to whatever `process.stdout` reports. */
10
+ readonly terminal?: Terminal;
11
+ /** Show what a run normally hides: child output, full stack traces. */
12
+ readonly verbose?: boolean;
13
+ };
14
+ /**
15
+ * Assembles the host-bound half of reporting, so a reporter package needs no
16
+ * `node:` import of its own.
17
+ */
18
+ export declare function createReporterContext(options: ReporterContextOptions): ReporterContext;
19
+ //# sourceMappingURL=reporter-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter-context.d.ts","sourceRoot":"","sources":["../src/reporter-context.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE9E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGrD,MAAM,MAAM,sBAAsB,GAAG;IACpC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,wEAAwE;IACxE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACzC,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAE7B,uEAAuE;IACvE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,sBAAsB,GAC7B,eAAe,CAYjB"}
@@ -0,0 +1,21 @@
1
+ import { createWriteStream } from "node:fs";
2
+ import { Writable } from "node:stream";
3
+ import { nodeTerminal } from "@ac-kit/app-system";
4
+ import { nonClosingWritableStream } from "@ac-kit/node";
5
+ /**
6
+ * Assembles the host-bound half of reporting, so a reporter package needs no
7
+ * `node:` import of its own.
8
+ */
9
+ export function createReporterContext(options) {
10
+ return {
11
+ registry: options.registry,
12
+ terminal: options.terminal ?? nodeTerminal(process.stdout),
13
+ defaultOutput: options.defaultOutput,
14
+ verbose: options.verbose === true,
15
+ // Never closes process.stdout — closing a TTY-backed WHATWG writable never
16
+ // resolves.
17
+ stdout: options.stdout ?? nonClosingWritableStream(process.stdout),
18
+ openOutput: (fileName) => Writable.toWeb(createWriteStream(fileName)),
19
+ };
20
+ }
21
+ //# sourceMappingURL=reporter-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter-context.js","sourceRoot":"","sources":["../src/reporter-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGvC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAexD;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACpC,OAA+B;IAE/B,OAAO;QACN,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC;QAC1D,aAAa,EAAE,OAAO,CAAC,aAAa;QACpC,OAAO,EAAE,OAAO,CAAC,OAAO,KAAK,IAAI;QACjC,2EAA2E;QAC3E,YAAY;QACZ,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,wBAAwB,CAAC,OAAO,CAAC,MAAM,CAAC;QAClE,UAAU,EAAE,CAAC,QAAQ,EAAE,EAAE,CACxB,QAAQ,CAAC,KAAK,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAA2B;KACtE,CAAC;AACH,CAAC"}
@@ -0,0 +1,61 @@
1
+ import type { MeasurePlugin, ReporterPlugin } from "@ac-bench/core/plugin";
2
+ import type { Terminal } from "@ac-kit/app-terminal";
3
+ import { MeasurementPlan } from "./measurement-plan.js";
4
+ /**
5
+ * Everything a run needs, already resolved.
6
+ *
7
+ * Nothing here is an intent: naming one (`--mode rigorous`, a reporter id, a
8
+ * glob) and turning it into these values is configuration, and belongs to
9
+ * whoever parses it.
10
+ */
11
+ export type BenchRunOptions = {
12
+ /** Bench files to run, already discovered. */
13
+ readonly files: readonly string[];
14
+ /** Registered for the measures `files` declare conditions for. */
15
+ readonly measures: readonly MeasurePlugin[];
16
+ /** Every reporter the run reports to; each sink is closed when it ends. */
17
+ readonly reporters: readonly ReporterPlugin[];
18
+ /** Base name for output files, for a reporter given no explicit one. */
19
+ readonly output: string;
20
+ /** How many contexts each condition is measured in. */
21
+ readonly plan: MeasurementPlan;
22
+ /** Seeds the replicate and case orderings. */
23
+ readonly orderSeed: number;
24
+ readonly signal: AbortSignal;
25
+ /** Case-insensitive substring patterns, matched on a case or condition title. */
26
+ readonly filter?: readonly string[];
27
+ /** Print each file's condition tree instead of measuring anything. */
28
+ readonly list?: boolean;
29
+ /** Imported by every child before the bench file, in order. */
30
+ readonly setupFiles?: readonly string[];
31
+ /** Attach each case's raw samples, which are copied across the boundary. */
32
+ readonly emitSamples?: boolean;
33
+ /**
34
+ * Report every child's output as it happens. Off, a child's output reaches
35
+ * the reporters only for a scope that ended badly.
36
+ */
37
+ readonly verbose?: boolean;
38
+ readonly heartbeatTimeoutMs?: number;
39
+ /** Wall-clock cap on one fork unit. `0` disables it. */
40
+ readonly conditionTimeoutMs?: number;
41
+ /** Wall-clock cap on the whole run, cooldown included. `0` disables it. */
42
+ readonly maxTotalTimeMs?: number;
43
+ /** Defaults to `process.stdout`; an override exists for tests. */
44
+ readonly stdout?: WritableStream<string>;
45
+ /** Defaults to whatever `process.stdout` reports. */
46
+ readonly terminal?: Terminal;
47
+ };
48
+ /**
49
+ * Discovers every bench file's condition tree in a child of its own, then runs
50
+ * each fork unit in a child of its own, closing every sink once the last one
51
+ * finishes.
52
+ *
53
+ * When `signal` aborts, the in-flight child is terminated, every condition not
54
+ * yet started is skipped, and the sinks are still closed.
55
+ *
56
+ * A file that fails to load is reported to `stderr` as a `load-error` and
57
+ * skipped — the run continues with the next file rather than crashing over one
58
+ * broken bench file.
59
+ */
60
+ export declare function runBench(options: BenchRunOptions): Promise<void>;
61
+ //# sourceMappingURL=run-bench.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-bench.d.ts","sourceRoot":"","sources":["../src/run-bench.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAI3E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAgBrD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAUxD;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG;IAC7B,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAC5C,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;IAC9C,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,iFAAiF;IACjF,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,sEAAsE;IACtE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,wDAAwD;IACxD,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACzC,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;CAC7B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAyHtE"}
@@ -0,0 +1,151 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { MeasureRegistry } from "@ac-bench/core/plugin";
3
+ import { createFanOutSink, TailWindowProxy } from "@ac-kit/app-report";
4
+ import { formatError } from "@ac-kit/core";
5
+ import { captureEnvironmentBaseline, CooldownGate } from "./cooldown.js";
6
+ import { EnvironmentMonitor, nodeEnvironmentProbe, } from "./environment-monitor.js";
7
+ import { discoverConditionsInFile } from "./fork-runner.js";
8
+ import { defaultArtifactCacheRoot, runFileIsolated, toForkOptions, } from "./isolated-runner.js";
9
+ import { createMeasureDiagnosticProxy } from "./measure-diagnostic-proxy.js";
10
+ import { createReporterContext } from "./reporter-context.js";
11
+ /** Settle window before the first measurement, when cooling is on. */
12
+ const BASELINE_WINDOW_MS = 3000;
13
+ /** Per gate, and across the whole run. Exhausting either proceeds anyway. */
14
+ const MAX_COOLDOWN_MS = 30000;
15
+ const TOTAL_COOLDOWN_BUDGET_MS = 300000;
16
+ /**
17
+ * Discovers every bench file's condition tree in a child of its own, then runs
18
+ * each fork unit in a child of its own, closing every sink once the last one
19
+ * finishes.
20
+ *
21
+ * When `signal` aborts, the in-flight child is terminated, every condition not
22
+ * yet started is skipped, and the sinks are still closed.
23
+ *
24
+ * A file that fails to load is reported to `stderr` as a `load-error` and
25
+ * skipped — the run continues with the next file rather than crashing over one
26
+ * broken bench file.
27
+ */
28
+ export async function runBench(options) {
29
+ const registry = new MeasureRegistry();
30
+ for (const plugin of options.measures) {
31
+ registry.register(plugin);
32
+ }
33
+ const reporterContext = createReporterContext({
34
+ registry,
35
+ defaultOutput: options.output,
36
+ ...(options.verbose ? { verbose: true } : {}),
37
+ ...(options.stdout ? { stdout: options.stdout } : {}),
38
+ ...(options.terminal ? { terminal: options.terminal } : {}),
39
+ });
40
+ const reported = createFanOutSink(options.reporters.map((reporter) => reporter.createSink(reporterContext)));
41
+ const sink = createMeasureDiagnosticProxy(
42
+ // A healthy child's chatter is noise; the same chatter from a child that
43
+ // died is the only account of what it was doing. Only `output` is held
44
+ // back — every other kind is the report itself.
45
+ options.verbose
46
+ ? reported
47
+ : new TailWindowProxy(reported, {
48
+ alwaysForwardKinds: [
49
+ "scope-start",
50
+ "scope-attributes",
51
+ "scope-progress",
52
+ "scope-heartbeat",
53
+ "scope-end",
54
+ "diagnostic",
55
+ "attachment",
56
+ "data",
57
+ ],
58
+ }), registry);
59
+ // One measuring child exists at a time, so one slot is enough to point the
60
+ // probe at whichever one is currently running.
61
+ const measuringChild = { pid: undefined };
62
+ const probe = nodeEnvironmentProbe({ pidOf: () => measuringChild.pid });
63
+ const maxTotalTimeMs = options.maxTotalTimeMs ?? 0;
64
+ const { plan } = options;
65
+ // One signal for both reasons, so every existing abort check honours the
66
+ // budget too; the two are told apart afterwards for the exit code.
67
+ const budget = maxTotalTimeMs > 0 ? AbortSignal.timeout(maxTotalTimeMs) : null;
68
+ const runSignal = budget === null
69
+ ? options.signal
70
+ : AbortSignal.any([options.signal, budget]);
71
+ const runOptions = {
72
+ sink,
73
+ registry,
74
+ signal: runSignal,
75
+ runId: randomUUID(),
76
+ artifactCacheRoot: defaultArtifactCacheRoot(),
77
+ monitor: new EnvironmentMonitor(probe),
78
+ plan,
79
+ orderSeed: options.orderSeed,
80
+ retryOnInstability: plan.retryOnInstability,
81
+ onChildPid: (pid) => {
82
+ measuringChild.pid = pid;
83
+ },
84
+ ...(options.filter ? { filter: options.filter } : {}),
85
+ ...(options.emitSamples ? { emitSamples: true } : {}),
86
+ ...(options.setupFiles ? { setupFiles: options.setupFiles } : {}),
87
+ ...(options.heartbeatTimeoutMs !== undefined
88
+ ? { heartbeatTimeoutMs: options.heartbeatTimeoutMs }
89
+ : {}),
90
+ ...(options.conditionTimeoutMs !== undefined
91
+ ? { conditionTimeoutMs: options.conditionTimeoutMs }
92
+ : {}),
93
+ ...(plan.cooldown
94
+ ? {
95
+ cooldown: new CooldownGate(probe, await captureEnvironmentBaseline(probe, BASELINE_WINDOW_MS, 500, runSignal), {
96
+ maxCooldownMs: MAX_COOLDOWN_MS,
97
+ totalBudgetMs: TOTAL_COOLDOWN_BUDGET_MS,
98
+ }),
99
+ }
100
+ : {}),
101
+ };
102
+ for (const file of options.files) {
103
+ if (runSignal.aborted) {
104
+ break;
105
+ }
106
+ if (options.list === true) {
107
+ await listFile_(file, runOptions);
108
+ continue;
109
+ }
110
+ await runFileIsolated(file, runOptions);
111
+ }
112
+ if (budget?.aborted === true && !options.signal.aborted) {
113
+ await sink.write({
114
+ kind: "diagnostic",
115
+ timestamp: Date.now(),
116
+ scopeId: null,
117
+ severity: "warning",
118
+ code: "max-total-time-exhausted",
119
+ message: `the run was stopped after its ${maxTotalTimeMs}ms budget; some conditions were never measured`,
120
+ });
121
+ }
122
+ await sink.close();
123
+ }
124
+ /** Prints a discovered condition tree in declaration order, running nothing. */
125
+ function listCondition_(condition, registry, depth) {
126
+ const indent = " ".repeat(depth);
127
+ const { label } = registry.resolve(condition.measure);
128
+ process.stdout.write(`${indent}${condition.title} (${label})\n`);
129
+ for (const entry of condition.entries) {
130
+ if (entry.kind === "case") {
131
+ process.stdout.write(`${indent} ${entry.title}\n`);
132
+ }
133
+ else {
134
+ listCondition_(entry.condition, registry, depth + 1);
135
+ }
136
+ }
137
+ }
138
+ async function listFile_(file, options) {
139
+ let conditions;
140
+ try {
141
+ conditions = await discoverConditionsInFile(file, toForkOptions(options));
142
+ }
143
+ catch (error) {
144
+ process.stderr.write(`measure: failed to load ${file}: ${formatError(error)}\n`);
145
+ return;
146
+ }
147
+ for (const condition of conditions) {
148
+ listCondition_(condition, options.registry, 0);
149
+ }
150
+ }
151
+ //# sourceMappingURL=run-bench.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-bench.js","sourceRoot":"","sources":["../src/run-bench.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@ac-bench/lib",
3
+ "version": "0.1.0",
4
+ "description": "Programmatic benchmark API for @ac-bench: fork orchestration and protocol, the environment monitor and cooldown gate, and the escalation decision.",
5
+ "keywords": [
6
+ "benchmark",
7
+ "fork",
8
+ "measurement",
9
+ "performance"
10
+ ],
11
+ "homepage": "https://github.com/anthochamp/node-essentials/tree/main/bench/lib#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/anthochamp/node-essentials/issues"
14
+ },
15
+ "license": "BSD-3-Clause",
16
+ "author": "Anthony Champagne <dev@anthonychampagne.fr>",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/anthochamp/node-essentials.git",
20
+ "directory": "bench/lib"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "main": "./dist/index.js",
28
+ "exports": {
29
+ ".": "./dist/index.js",
30
+ "./child": "./dist/child/main.js"
31
+ },
32
+ "publishConfig": {
33
+ "main": "./dist/index.js",
34
+ "exports": {
35
+ ".": "./dist/index.js",
36
+ "./child": "./dist/child/main.js"
37
+ },
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "compile": "tsc -p ./tsconfig.build.json --outDir dist",
42
+ "compile:check": "tsc -noEmit -p ./tsconfig.json",
43
+ "clean": "rimraf dist",
44
+ "build": "yarn clean && yarn compile",
45
+ "test": "yarn run -T vitest run",
46
+ "prepack": "yarn build"
47
+ },
48
+ "dependencies": {
49
+ "@ac-bench/core": "^0.1.0",
50
+ "@ac-kit/algo": "^0.1.0",
51
+ "@ac-kit/app-report": "^0.1.0",
52
+ "@ac-kit/app-system": "^0.1.0",
53
+ "@ac-kit/app-terminal": "^0.1.0",
54
+ "@ac-kit/core": "^0.1.0",
55
+ "@ac-kit/format-ansi": "^0.1.0",
56
+ "@ac-kit/format-monospace": "^0.1.0",
57
+ "@ac-kit/math-random": "^0.1.0",
58
+ "@ac-kit/math-stats": "^0.1.0",
59
+ "@ac-kit/model-dataset": "^0.1.0",
60
+ "@ac-kit/node": "^0.1.0",
61
+ "tsx": "4.23.12"
62
+ },
63
+ "devDependencies": {
64
+ "@ac-kit/async": "0.1.0",
65
+ "@ac-kit/tsconfig": "0.1.0",
66
+ "@ac-kit/typedoc-config": "0.1.0",
67
+ "@ac-kit/vitest-config": "0.1.0",
68
+ "@types/node": "26.5.1",
69
+ "@typescript/native": "typescript@7.0.2",
70
+ "rimraf": "6.1.3",
71
+ "typedoc": "0.28.20",
72
+ "typescript": "@typescript/typescript6@6.0.2",
73
+ "vite": "8.3.0",
74
+ "vitest": "5.0.0"
75
+ },
76
+ "engines": {
77
+ "node": ">=24"
78
+ }
79
+ }