@ac-bench/measure-duration 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 (79) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +34 -0
  3. package/dist/_harness-overhead.d.ts +36 -0
  4. package/dist/_harness-overhead.d.ts.map +1 -0
  5. package/dist/_harness-overhead.js +62 -0
  6. package/dist/_harness-overhead.js.map +1 -0
  7. package/dist/_iteration-protocol.d.ts +18 -0
  8. package/dist/_iteration-protocol.d.ts.map +1 -0
  9. package/dist/_iteration-protocol.js +17 -0
  10. package/dist/_iteration-protocol.js.map +1 -0
  11. package/dist/_iteration-source.d.ts +64 -0
  12. package/dist/_iteration-source.d.ts.map +1 -0
  13. package/dist/_iteration-source.js +135 -0
  14. package/dist/_iteration-source.js.map +1 -0
  15. package/dist/_pool.d.ts +48 -0
  16. package/dist/_pool.d.ts.map +1 -0
  17. package/dist/_pool.js +219 -0
  18. package/dist/_pool.js.map +1 -0
  19. package/dist/_process-execution.d.ts +62 -0
  20. package/dist/_process-execution.d.ts.map +1 -0
  21. package/dist/_process-execution.js +142 -0
  22. package/dist/_process-execution.js.map +1 -0
  23. package/dist/_run-case.d.ts +32 -0
  24. package/dist/_run-case.d.ts.map +1 -0
  25. package/dist/_run-case.js +267 -0
  26. package/dist/_run-case.js.map +1 -0
  27. package/dist/_sampling-options.d.ts +61 -0
  28. package/dist/_sampling-options.d.ts.map +1 -0
  29. package/dist/_sampling-options.js +19 -0
  30. package/dist/_sampling-options.js.map +1 -0
  31. package/dist/_sampling-report.d.ts +44 -0
  32. package/dist/_sampling-report.d.ts.map +1 -0
  33. package/dist/_sampling-report.js +43 -0
  34. package/dist/_sampling-report.js.map +1 -0
  35. package/dist/_spawn-baseline.d.ts +35 -0
  36. package/dist/_spawn-baseline.d.ts.map +1 -0
  37. package/dist/_spawn-baseline.js +22 -0
  38. package/dist/_spawn-baseline.js.map +1 -0
  39. package/dist/_statistics-schema.d.ts +28 -0
  40. package/dist/_statistics-schema.d.ts.map +1 -0
  41. package/dist/_statistics-schema.js +59 -0
  42. package/dist/_statistics-schema.js.map +1 -0
  43. package/dist/_statistics.d.ts +25 -0
  44. package/dist/_statistics.d.ts.map +1 -0
  45. package/dist/_statistics.js +72 -0
  46. package/dist/_statistics.js.map +1 -0
  47. package/dist/_types.d.ts +302 -0
  48. package/dist/_types.d.ts.map +1 -0
  49. package/dist/_types.js +57 -0
  50. package/dist/_types.js.map +1 -0
  51. package/dist/_warnings.d.ts +62 -0
  52. package/dist/_warnings.d.ts.map +1 -0
  53. package/dist/_warnings.js +132 -0
  54. package/dist/_warnings.js.map +1 -0
  55. package/dist/decl.d.ts +21 -0
  56. package/dist/decl.d.ts.map +1 -0
  57. package/dist/decl.js +54 -0
  58. package/dist/decl.js.map +1 -0
  59. package/dist/index.d.ts +5 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +5 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/iteration-child.d.ts +2 -0
  64. package/dist/iteration-child.d.ts.map +1 -0
  65. package/dist/iteration-child.js +37 -0
  66. package/dist/iteration-child.js.map +1 -0
  67. package/dist/options.d.ts +59 -0
  68. package/dist/options.d.ts.map +1 -0
  69. package/dist/options.js +2 -0
  70. package/dist/options.js.map +1 -0
  71. package/dist/plugin.d.ts +3 -0
  72. package/dist/plugin.d.ts.map +1 -0
  73. package/dist/plugin.js +205 -0
  74. package/dist/plugin.js.map +1 -0
  75. package/dist/spawn-overhead.d.ts +59 -0
  76. package/dist/spawn-overhead.d.ts.map +1 -0
  77. package/dist/spawn-overhead.js +80 -0
  78. package/dist/spawn-overhead.js.map +1 -0
  79. package/package.json +77 -0
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024-2026 Anthony Champagne and contributors
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @ac-bench/measure-duration
2
+
3
+ How long an operation takes — measured properly, which means enough samples to
4
+ say something, and a warning when the number should not be trusted.
5
+
6
+ A single timing is noise. This samples adaptively until the distribution
7
+ stabilises, then reports the statistics alongside quality warnings: outliers, a
8
+ slow first run (JIT warm-up), high variance, harness overhead dominating the
9
+ measurement, or simply too few samples.
10
+
11
+ ```ts
12
+ import { durationCase, durationCondition } from "@ac-bench/measure-duration";
13
+
14
+ durationCondition("array sum", () => {
15
+ durationCase("for loop", () => {
16
+ let total = 0;
17
+ for (const value of values) total += value;
18
+ });
19
+
20
+ durationCase("reduce", () => {
21
+ values.reduce((total, value) => total + value, 0);
22
+ });
23
+ });
24
+ ```
25
+
26
+ A _condition_ groups cases that are alternatives to each other; a _case_ is one
27
+ implementation. Build the inputs outside the timed closure — anything inside it
28
+ is measured too.
29
+
30
+ Also exposes `measureSpawnOverhead` and `calibrateSpawnOverhead` for cases that
31
+ spawn a subprocess per iteration, and re-exports the `beforeAll`/`afterAll`
32
+ hooks from `@ac-bench/core`.
33
+
34
+ Run suites with the `measure` CLI from `@ac-bench/cli`.
@@ -0,0 +1,36 @@
1
+ import * as z from "zod/mini";
2
+ import { DurationStatistics } from "./_types.js";
3
+ /**
4
+ * The shape of a case body, which decides what its harness costs.
5
+ *
6
+ * `fn.constructor.name === "AsyncFunction"` alone is not a reliable signal — it
7
+ * misses a plain function returning a promise, which is the common shape for a
8
+ * case that delegates. What the body actually returned is.
9
+ */
10
+ export declare const harnessShapeSchema: z.ZodMiniEnum<{
11
+ "async-function": "async-function";
12
+ sync: "sync";
13
+ thenable: "thenable";
14
+ }>;
15
+ export type HarnessShape = z.infer<typeof harnessShapeSchema>;
16
+ export declare const harnessOverheadSchema: z.ZodMiniObject<{
17
+ shape: z.ZodMiniEnum<{
18
+ "async-function": "async-function";
19
+ sync: "sync";
20
+ thenable: "thenable";
21
+ }>;
22
+ medianMs: z.ZodMiniNumber<number>;
23
+ share: z.ZodMiniNumber<number>;
24
+ subtracted: z.ZodMiniBoolean<boolean>;
25
+ }, z.core.$strip>;
26
+ export type HarnessOverhead = z.infer<typeof harnessOverheadSchema>;
27
+ /** Fraction of a case's runtime above which its harness dominates the number. */
28
+ export declare const HARNESS_OVERHEAD_DOMINATES = 0.25;
29
+ export declare function detectHarnessShape(runFn: (signal: AbortSignal) => unknown, returned: unknown): HarnessShape;
30
+ /**
31
+ * Measures a no-op body of `shape` through the identical timed span the real
32
+ * case is measured through, so what is calibrated is the harness and not the
33
+ * benchmark.
34
+ */
35
+ export declare function calibrateHarnessOverhead(shape: HarnessShape, outlierThreshold: number, signal: AbortSignal): Promise<DurationStatistics>;
36
+ //# sourceMappingURL=_harness-overhead.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_harness-overhead.d.ts","sourceRoot":"","sources":["../src/_harness-overhead.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAG9B,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB;;;;EAI7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,qBAAqB;;;;;;;;;iBAOhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,iFAAiF;AACjF,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAK/C,wBAAgB,kBAAkB,CACjC,KAAK,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,EACvC,QAAQ,EAAE,OAAO,GACf,YAAY,CAQd;AAED;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC7C,KAAK,EAAE,YAAY,EACnB,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,WAAW,GACjB,OAAO,CAAC,kBAAkB,CAAC,CAa7B"}
@@ -0,0 +1,62 @@
1
+ import { isThenable } from "@ac-kit/core";
2
+ import * as z from "zod/mini";
3
+ import { summarise } from "./_statistics.js";
4
+ /**
5
+ * The shape of a case body, which decides what its harness costs.
6
+ *
7
+ * `fn.constructor.name === "AsyncFunction"` alone is not a reliable signal — it
8
+ * misses a plain function returning a promise, which is the common shape for a
9
+ * case that delegates. What the body actually returned is.
10
+ */
11
+ export const harnessShapeSchema = z.enum([
12
+ "sync",
13
+ "async-function",
14
+ "thenable",
15
+ ]);
16
+ export const harnessOverheadSchema = z.object({
17
+ shape: harnessShapeSchema,
18
+ medianMs: z.number(),
19
+ /** Overhead median as a fraction of the case median. */
20
+ share: z.number(),
21
+ /** Whether it was removed from the reported samples, or only reported. */
22
+ subtracted: z.boolean(),
23
+ });
24
+ /** Fraction of a case's runtime above which its harness dominates the number. */
25
+ export const HARNESS_OVERHEAD_DOMINATES = 0.25;
26
+ /** Enough to take a median of something this cheap, and no more. */
27
+ const CALIBRATION_SAMPLES_ = 50;
28
+ export function detectHarnessShape(runFn, returned) {
29
+ if (!isThenable(returned)) {
30
+ return "sync";
31
+ }
32
+ return runFn.constructor.name === "AsyncFunction"
33
+ ? "async-function"
34
+ : "thenable";
35
+ }
36
+ /**
37
+ * Measures a no-op body of `shape` through the identical timed span the real
38
+ * case is measured through, so what is calibrated is the harness and not the
39
+ * benchmark.
40
+ */
41
+ export async function calibrateHarnessOverhead(shape, outlierThreshold, signal) {
42
+ const noop = createNoop_(shape);
43
+ const spans = [];
44
+ for (let iteration = 0; iteration < CALIBRATION_SAMPLES_; iteration++) {
45
+ signal.throwIfAborted();
46
+ const begin = performance.now();
47
+ await noop();
48
+ spans.push(performance.now() - begin);
49
+ }
50
+ return summarise(spans, outlierThreshold);
51
+ }
52
+ function createNoop_(shape) {
53
+ switch (shape) {
54
+ case "sync":
55
+ return () => undefined;
56
+ case "async-function":
57
+ return async () => undefined;
58
+ case "thenable":
59
+ return () => Promise.resolve();
60
+ }
61
+ }
62
+ //# sourceMappingURL=_harness-overhead.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_harness-overhead.js","sourceRoot":"","sources":["../src/_harness-overhead.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAE9B,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAG7C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC;IACxC,MAAM;IACN,gBAAgB;IAChB,UAAU;CACV,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,KAAK,EAAE,kBAAkB;IACzB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,wDAAwD;IACxD,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,0EAA0E;IAC1E,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE;CACvB,CAAC,CAAC;AAGH,iFAAiF;AACjF,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC;AAE/C,oEAAoE;AACpE,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC,MAAM,UAAU,kBAAkB,CACjC,KAAuC,EACvC,QAAiB;IAEjB,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3B,OAAO,MAAM,CAAC;IACf,CAAC;IAED,OAAO,KAAK,CAAC,WAAW,CAAC,IAAI,KAAK,eAAe;QAChD,CAAC,CAAC,gBAAgB;QAClB,CAAC,CAAC,UAAU,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC7C,KAAmB,EACnB,gBAAwB,EACxB,MAAmB;IAEnB,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAChC,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,IAAI,SAAS,GAAG,CAAC,EAAE,SAAS,GAAG,oBAAoB,EAAE,SAAS,EAAE,EAAE,CAAC;QACvE,MAAM,CAAC,cAAc,EAAE,CAAC;QAExB,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAChC,MAAM,IAAI,EAAE,CAAC;QACb,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC;IACvC,CAAC;IAED,OAAO,SAAS,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC;AAC3C,CAAC;AAED,SAAS,WAAW,CAAC,KAAmB;IACvC,QAAQ,KAAK,EAAE,CAAC;QACf,KAAK,MAAM;YACV,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC;QAExB,KAAK,gBAAgB;YACpB,OAAO,KAAK,IAAI,EAAE,CAAC,SAAS,CAAC;QAE9B,KAAK,UAAU;YACd,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IACjC,CAAC;AACF,CAAC"}
@@ -0,0 +1,18 @@
1
+ import * as z from "zod/mini";
2
+ /**
3
+ * Measure option set on the one-shot invocation inside a spawned iteration.
4
+ *
5
+ * Same registration, same case body, different job: the spawned process must
6
+ * run the body exactly once instead of entering the sampler, or every iteration
7
+ * would recursively spawn a whole sampling run of its own.
8
+ */
9
+ export declare const SINGLE_ITERATION_OPTION = "singleIteration";
10
+ /** Sole argument of {@link iteration-child}, as one JSON argv string. */
11
+ export declare const iterationPayloadSchema: z.ZodMiniObject<{
12
+ file: z.ZodMiniString<string>;
13
+ conditionPath: z.ZodMiniArray<z.ZodMiniNumber<number>>;
14
+ conditionTitles: z.ZodMiniArray<z.ZodMiniString<string>>;
15
+ caseTitle: z.ZodMiniString<string>;
16
+ }, z.core.$strip>;
17
+ export type IterationPayload = z.infer<typeof iterationPayloadSchema>;
18
+ //# sourceMappingURL=_iteration-protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_iteration-protocol.d.ts","sourceRoot":"","sources":["../src/_iteration-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAE9B;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,oBAAoB,CAAC;AAEzD,yEAAyE;AACzE,eAAO,MAAM,sBAAsB;;;;;iBAKjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
@@ -0,0 +1,17 @@
1
+ import * as z from "zod/mini";
2
+ /**
3
+ * Measure option set on the one-shot invocation inside a spawned iteration.
4
+ *
5
+ * Same registration, same case body, different job: the spawned process must
6
+ * run the body exactly once instead of entering the sampler, or every iteration
7
+ * would recursively spawn a whole sampling run of its own.
8
+ */
9
+ export const SINGLE_ITERATION_OPTION = "singleIteration";
10
+ /** Sole argument of {@link iteration-child}, as one JSON argv string. */
11
+ export const iterationPayloadSchema = z.object({
12
+ file: z.string(),
13
+ conditionPath: z.array(z.number()),
14
+ conditionTitles: z.array(z.string()),
15
+ caseTitle: z.string(),
16
+ });
17
+ //# sourceMappingURL=_iteration-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_iteration-protocol.js","sourceRoot":"","sources":["../src/_iteration-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAE9B;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,iBAAiB,CAAC;AAEzD,yEAAyE;AACzE,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAClC,eAAe,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IACpC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CACrB,CAAC,CAAC"}
@@ -0,0 +1,64 @@
1
+ import { BenchCaseId } from "@ac-bench/core/runner";
2
+ import { ColdStartPhases, ProcessGroup } from "./_process-execution.js";
3
+ import { SpawnBaselineReport } from "./_spawn-baseline.js";
4
+ import { DurationStatistics } from "./_statistics-schema.js";
5
+ import { DurationCommandCaseOptions } from "./options.js";
6
+ /** Command a case executes once per iteration, with its do-nothing counterpart. */
7
+ export type IterationCommand = {
8
+ readonly command: string;
9
+ readonly args: readonly string[];
10
+ /** Invocation that starts the same program and does nothing, when known. */
11
+ readonly baselineArgs: readonly string[] | null;
12
+ readonly capturePhases: boolean;
13
+ readonly cwd?: string;
14
+ readonly env?: Readonly<Record<string, string>>;
15
+ };
16
+ /**
17
+ * Resolves what to execute for `execution: "per-iteration-process"`.
18
+ *
19
+ * The spawned process re-imports the bench file and replays the case identity,
20
+ * so it must be started by the same interpreter with the same loader flags — a
21
+ * bench file that only a TypeScript loader can import is the normal case, not
22
+ * the exception.
23
+ */
24
+ export declare function iterationProcessCommand(caseId: BenchCaseId): IterationCommand;
25
+ /** Resolves what to execute for a `durationCommandCase`. */
26
+ export declare function commandCaseCommand(command: string, args: readonly string[], options: DurationCommandCaseOptions | undefined): IterationCommand;
27
+ /**
28
+ * Runs one iteration and returns its wall time.
29
+ *
30
+ * @throws {Error} If the process failed, with its stderr tail attached — an
31
+ * exit code on its own is undiagnosable, and a failing program is the most
32
+ * likely thing to go wrong in this execution mode.
33
+ */
34
+ export declare function measureIteration(group: ProcessGroup, command: IterationCommand, phases: ColdStartPhases[], signal: AbortSignal): Promise<number>;
35
+ /**
36
+ * Measures what starting the program costs when it is asked to do nothing.
37
+ *
38
+ * Measured once per case rather than once per condition: a condition's cases
39
+ * routinely spawn different programs, or none at all, and one program's
40
+ * start-up cost is not another's.
41
+ *
42
+ * @param group Owns the spawned processes, so none outlives the case.
43
+ * @param command What the case executes, including its do-nothing arguments.
44
+ * @param counts Warm-up and measured invocation counts.
45
+ * @param signal Cancellation.
46
+ * @returns Statistics over the measured invocations, or `null` when the case
47
+ * declared no do-nothing invocation.
48
+ */
49
+ export declare function measureSpawnBaseline(group: ProcessGroup, command: IterationCommand, counts: {
50
+ readonly warmup: number;
51
+ readonly runs: number;
52
+ }, signal: AbortSignal): Promise<DurationStatistics | null>;
53
+ /**
54
+ * Assembles the reported baseline block.
55
+ *
56
+ * @param command What the case executes.
57
+ * @param statistics Baseline statistics, or `null` when none was measured.
58
+ * @param caseMedianMs The case's own median, for the share.
59
+ * @param subtracted Whether the baseline fed the `adjusted` figure.
60
+ */
61
+ export declare function spawnBaselineReport(command: IterationCommand, statistics: DurationStatistics | null, caseMedianMs: number, subtracted: boolean): SpawnBaselineReport | null;
62
+ /** Median of each phase, so one cold start's noise does not stand for all. */
63
+ export declare function summariseColdStart(phases: readonly ColdStartPhases[]): ColdStartPhases | null;
64
+ //# sourceMappingURL=_iteration-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_iteration-source.d.ts","sourceRoot":"","sources":["../src/_iteration-source.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGpD,OAAO,EACN,eAAe,EAEf,YAAY,EACZ,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAE1D,mFAAmF;AACnF,MAAM,MAAM,gBAAgB,GAAG;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IAChD,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAChD,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,WAAW,GAAG,gBAAgB,CAkB7E;AAED,4DAA4D;AAC5D,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,0BAA0B,GAAG,SAAS,GAC7C,gBAAgB,CASlB;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACrC,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,eAAe,EAAE,EACzB,MAAM,EAAE,WAAW,GACjB,OAAO,CAAC,MAAM,CAAC,CAqBjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,oBAAoB,CACzC,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAC1D,MAAM,EAAE,WAAW,GACjB,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAyBpC;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,gBAAgB,EACzB,UAAU,EAAE,kBAAkB,GAAG,IAAI,EACrC,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,OAAO,GACjB,mBAAmB,GAAG,IAAI,CAW5B;AAED,8EAA8E;AAC9E,wBAAgB,kBAAkB,CACjC,MAAM,EAAE,SAAS,eAAe,EAAE,GAChC,eAAe,GAAG,IAAI,CAUxB"}
@@ -0,0 +1,135 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { iterationPayloadSchema } from "./_iteration-protocol.js";
3
+ import { summarise } from "./_statistics.js";
4
+ /**
5
+ * Resolves what to execute for `execution: "per-iteration-process"`.
6
+ *
7
+ * The spawned process re-imports the bench file and replays the case identity,
8
+ * so it must be started by the same interpreter with the same loader flags — a
9
+ * bench file that only a TypeScript loader can import is the normal case, not
10
+ * the exception.
11
+ */
12
+ export function iterationProcessCommand(caseId) {
13
+ const payload = JSON.stringify(iterationPayloadSchema.parse({
14
+ file: caseId.file,
15
+ conditionPath: [...caseId.conditionPath],
16
+ conditionTitles: [...caseId.conditionTitles],
17
+ caseTitle: caseId.caseTitle,
18
+ }));
19
+ return {
20
+ command: process.execPath,
21
+ args: [...process.execArgv, iterationChildPath_(), payload],
22
+ // Same interpreter and same loader, without the bench file: everything
23
+ // this execution mode exists to measure, and nothing else.
24
+ baselineArgs: [...process.execArgv, "-e", ""],
25
+ capturePhases: true,
26
+ };
27
+ }
28
+ /** Resolves what to execute for a `durationCommandCase`. */
29
+ export function commandCaseCommand(command, args, options) {
30
+ return {
31
+ command,
32
+ args,
33
+ baselineArgs: options?.baselineArgs ? [...options.baselineArgs] : null,
34
+ capturePhases: false,
35
+ ...(options?.cwd === undefined ? {} : { cwd: options.cwd }),
36
+ ...(options?.env === undefined ? {} : { env: options.env }),
37
+ };
38
+ }
39
+ /**
40
+ * Runs one iteration and returns its wall time.
41
+ *
42
+ * @throws {Error} If the process failed, with its stderr tail attached — an
43
+ * exit code on its own is undiagnosable, and a failing program is the most
44
+ * likely thing to go wrong in this execution mode.
45
+ */
46
+ export async function measureIteration(group, command, phases, signal) {
47
+ const outcome = await group.run(command.command, command.args, {
48
+ capturePhases: command.capturePhases,
49
+ ...(command.cwd === undefined ? {} : { cwd: command.cwd }),
50
+ ...(command.env === undefined ? {} : { env: command.env }),
51
+ }, signal);
52
+ if (outcome.exitCode !== 0) {
53
+ throw new Error(describeFailure_(command, outcome));
54
+ }
55
+ if (outcome.phases !== null) {
56
+ phases.push(outcome.phases);
57
+ }
58
+ return outcome.wallMs;
59
+ }
60
+ /**
61
+ * Measures what starting the program costs when it is asked to do nothing.
62
+ *
63
+ * Measured once per case rather than once per condition: a condition's cases
64
+ * routinely spawn different programs, or none at all, and one program's
65
+ * start-up cost is not another's.
66
+ *
67
+ * @param group Owns the spawned processes, so none outlives the case.
68
+ * @param command What the case executes, including its do-nothing arguments.
69
+ * @param counts Warm-up and measured invocation counts.
70
+ * @param signal Cancellation.
71
+ * @returns Statistics over the measured invocations, or `null` when the case
72
+ * declared no do-nothing invocation.
73
+ */
74
+ export async function measureSpawnBaseline(group, command, counts, signal) {
75
+ if (command.baselineArgs === null) {
76
+ return null;
77
+ }
78
+ const baseline = {
79
+ ...command,
80
+ args: command.baselineArgs,
81
+ baselineArgs: null,
82
+ capturePhases: false,
83
+ };
84
+ const discard = [];
85
+ for (let iteration = 0; iteration < counts.warmup; iteration++) {
86
+ await measureIteration(group, baseline, discard, signal);
87
+ }
88
+ const samples = [];
89
+ for (let iteration = 0; iteration < counts.runs; iteration++) {
90
+ samples.push(await measureIteration(group, baseline, discard, signal));
91
+ }
92
+ return summarise(samples);
93
+ }
94
+ /**
95
+ * Assembles the reported baseline block.
96
+ *
97
+ * @param command What the case executes.
98
+ * @param statistics Baseline statistics, or `null` when none was measured.
99
+ * @param caseMedianMs The case's own median, for the share.
100
+ * @param subtracted Whether the baseline fed the `adjusted` figure.
101
+ */
102
+ export function spawnBaselineReport(command, statistics, caseMedianMs, subtracted) {
103
+ if (statistics === null) {
104
+ return null;
105
+ }
106
+ return {
107
+ command: [command.command, ...(command.baselineArgs ?? [])].join(" "),
108
+ statistics,
109
+ share: caseMedianMs > 0 ? statistics.medianMs / caseMedianMs : 1,
110
+ subtracted,
111
+ };
112
+ }
113
+ /** Median of each phase, so one cold start's noise does not stand for all. */
114
+ export function summariseColdStart(phases) {
115
+ if (phases.length === 0) {
116
+ return null;
117
+ }
118
+ return {
119
+ startupMs: summarise(phases.map((phase) => phase.startupMs)).medianMs,
120
+ importMs: summarise(phases.map((phase) => phase.importMs)).medianMs,
121
+ runMs: summarise(phases.map((phase) => phase.runMs)).medianMs,
122
+ };
123
+ }
124
+ function describeFailure_(command, outcome) {
125
+ const how = outcome.terminatedBy === null
126
+ ? `exited with code ${outcome.exitCode}`
127
+ : `was terminated by ${outcome.terminatedBy}`;
128
+ const invocation = [command.command, ...command.args.slice(0, 2)].join(" ");
129
+ const tail = outcome.stderrTail === "" ? "" : `\n${outcome.stderrTail.trimEnd()}`;
130
+ return `${invocation} ${how}${tail}`;
131
+ }
132
+ function iterationChildPath_() {
133
+ return fileURLToPath(import.meta.resolve("@ac-bench/measure-duration/iteration-child"));
134
+ }
135
+ //# sourceMappingURL=_iteration-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_iteration-source.js","sourceRoot":"","sources":["../src/_iteration-source.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { MeasureExecution } from "@ac-bench/core/plugin";
2
+ import { CentralEstimator } from "@ac-bench/core/runner";
3
+ import { ConfidenceInterval } from "@ac-kit/math-stats";
4
+ import * as z from "zod/mini";
5
+ import { DurationBenchRunCaseResult } from "./_types.js";
6
+ export declare const pooledEstimateSchema: z.ZodMiniObject<{
7
+ estimateMs: z.ZodMiniNumber<number>;
8
+ lowerMs: z.ZodMiniNumber<number>;
9
+ upperMs: z.ZodMiniNumber<number>;
10
+ betweenVarianceMs2: z.ZodMiniNumber<number>;
11
+ heterogeneity: z.ZodMiniNumber<number>;
12
+ executions: z.ZodMiniNumber<number>;
13
+ estimator: z.ZodMiniEnum<{
14
+ mean: "mean";
15
+ median: "median";
16
+ }>;
17
+ }, z.core.$strip>;
18
+ export type PooledEstimate = z.infer<typeof pooledEstimateSchema>;
19
+ export declare const contaminationSchema: z.ZodMiniObject<{
20
+ differenceMs: z.ZodMiniNumber<number>;
21
+ share: z.ZodMiniNumber<number>;
22
+ sharedExecutions: z.ZodMiniNumber<number>;
23
+ isolatedExecutions: z.ZodMiniNumber<number>;
24
+ }, z.core.$strip>;
25
+ export type Contamination = z.infer<typeof contaminationSchema>;
26
+ /**
27
+ * Combines every execution of one case into the result that answers for it.
28
+ *
29
+ * Executions come from different processes, and a process contributes an offset
30
+ * of its own — GC seed, address-space layout, which JIT tier the code settled
31
+ * in, the thermal state at that moment. Averaging the estimates would report an
32
+ * interval far too narrow, because it would treat that offset as if it did not
33
+ * exist. Random-effects pooling estimates it instead and widens the interval by
34
+ * it.
35
+ *
36
+ * The pooled interval is symmetric by construction, which is inverse-variance
37
+ * pooling's assumption rather than a property of the samples: a skewed median
38
+ * interval is symmetrised on the way in.
39
+ *
40
+ * @param executions At least one, in no particular order.
41
+ * @param estimator Which reported statistic to pool.
42
+ * @returns The pooled result, or the sole execution's own result when there is
43
+ * only one.
44
+ */
45
+ export declare function poolDurationExecutions(executions: readonly MeasureExecution<DurationBenchRunCaseResult>[], estimator?: CentralEstimator): DurationBenchRunCaseResult;
46
+ /** An interval a reader can quote, from the pooled estimate. */
47
+ export declare function pooledInterval(estimate: PooledEstimate): ConfidenceInterval;
48
+ //# sourceMappingURL=_pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_pool.d.ts","sourceRoot":"","sources":["../src/_pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EACN,kBAAkB,EAGlB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAI9B,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAQzD,eAAO,MAAM,oBAAoB;;;;;;;;;;;iBAa/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,eAAO,MAAM,mBAAmB;;;;;iBAQ9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CACrC,UAAU,EAAE,SAAS,gBAAgB,CAAC,0BAA0B,CAAC,EAAE,EACnE,SAAS,GAAE,gBAA2B,GACpC,0BAA0B,CAyC5B;AA6LD,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,cAAc,GAAG,kBAAkB,CAQ3E"}