@ac-bench/util 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.
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,36 @@
1
+ # @ac-bench/util
2
+
3
+ The fixtures a benchmark needs before it can measure anything honestly.
4
+
5
+ Two things go wrong most often. Inputs generated with `Math.random()` differ
6
+ between runs, so two runs are not comparable; and an "optimised" implementation
7
+ that quietly returns the wrong answer wins every benchmark. These helpers
8
+ address both: every generator is seeded, and `checksum` gives you a cheap way to
9
+ assert two implementations agreed.
10
+
11
+ ```ts
12
+ import { checksum, DEFAULT_SEED, randomFloat64Values } from "@ac-bench/util";
13
+
14
+ // Same values on every run and every machine.
15
+ const values = randomFloat64Values(10_000, DEFAULT_SEED);
16
+
17
+ const expected = checksum(reference(values));
18
+ // ... assert each candidate reproduces `expected` before trusting its timing.
19
+ ```
20
+
21
+ ## What it exposes
22
+
23
+ - **Seeded workloads** — `randomFloat64Values`, `randomUint32Values`,
24
+ `randomInts`, `words`, `skewedIndices`, `distinctKeys`, and `DEFAULT_SEED`.
25
+ - **Equivalence** — `checksum`, `sequentialChecksum`, `hashString`,
26
+ `stringHash32`: order-sensitive digests for proving two implementations
27
+ produced the same output.
28
+ - **Contention** — `EventLoopLoad`, a controllable background load, for
29
+ measuring the same operation idle and under pressure.
30
+ - **Native toolchains** — `prepareNativeToolchains`, for suites comparing
31
+ against a reference implementation in another language.
32
+
33
+ Generic process helpers (`execAsync`, `hasBinary`, `createTempDir`) are
34
+ `@ac-kit/node`'s — they carry no measurement-specific semantics. Spawn-overhead
35
+ calibration lives with the measure that needs it, in
36
+ `@ac-bench/measure-duration`.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Keeps the event loop busy until stopped.
3
+ *
4
+ * Timer benchmarks run on an idle loop measure a best case nobody experiences:
5
+ * the loop only checks its timer list once per iteration, so lateness is
6
+ * bounded below by whatever else is already queued. Measuring a condition both
7
+ * idle and loaded, and comparing the two, shows how much of the result belongs
8
+ * to the runtime rather than to the timer.
9
+ */
10
+ export declare class EventLoopLoad {
11
+ private running;
12
+ private handle;
13
+ start(): void;
14
+ stop(): void;
15
+ private schedule;
16
+ }
17
+ //# sourceMappingURL=event-loop-load.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-loop-load.d.ts","sourceRoot":"","sources":["../src/event-loop-load.ts"],"names":[],"mappings":"AAKA;;;;;;;;GAQG;AACH,qBAAa,aAAa;IACzB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAiC;IAE/C,KAAK,IAAI,IAAI,CAMZ;IAED,IAAI,IAAI,IAAI,CAMX;IAED,OAAO,CAAC,QAAQ;CAShB"}
@@ -0,0 +1,40 @@
1
+ import { busyWaitSync } from "@ac-kit/test-util";
2
+ /** Milliseconds of CPU burned per scheduled slice. */
3
+ const SLICE_MS = 2;
4
+ /**
5
+ * Keeps the event loop busy until stopped.
6
+ *
7
+ * Timer benchmarks run on an idle loop measure a best case nobody experiences:
8
+ * the loop only checks its timer list once per iteration, so lateness is
9
+ * bounded below by whatever else is already queued. Measuring a condition both
10
+ * idle and loaded, and comparing the two, shows how much of the result belongs
11
+ * to the runtime rather than to the timer.
12
+ */
13
+ export class EventLoopLoad {
14
+ running = false;
15
+ handle = null;
16
+ start() {
17
+ if (this.running) {
18
+ return;
19
+ }
20
+ this.running = true;
21
+ this.schedule();
22
+ }
23
+ stop() {
24
+ this.running = false;
25
+ if (this.handle) {
26
+ clearImmediate(this.handle);
27
+ this.handle = null;
28
+ }
29
+ }
30
+ schedule() {
31
+ if (!this.running) {
32
+ return;
33
+ }
34
+ this.handle = setImmediate(() => {
35
+ busyWaitSync(SLICE_MS);
36
+ this.schedule();
37
+ });
38
+ }
39
+ }
40
+ //# sourceMappingURL=event-loop-load.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-loop-load.js","sourceRoot":"","sources":["../src/event-loop-load.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,sDAAsD;AACtD,MAAM,QAAQ,GAAG,CAAC,CAAC;AAEnB;;;;;;;;GAQG;AACH,MAAM,OAAO,aAAa;IACjB,OAAO,GAAG,KAAK,CAAC;IAChB,MAAM,GAA4B,IAAI,CAAC;IAE/C,KAAK;QACJ,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,OAAO;QACR,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC,QAAQ,EAAE,CAAC;IACjB,CAAC;IAED,IAAI;QACH,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACrB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC5B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACpB,CAAC;IACF,CAAC;IAEO,QAAQ;QACf,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YACnB,OAAO;QACR,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE;YAC/B,YAAY,CAAC,QAAQ,CAAC,CAAC;YACvB,IAAI,CAAC,QAAQ,EAAE,CAAC;QACjB,CAAC,CAAC,CAAC;IACJ,CAAC;CACD"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Fixtures shared by every benchmark package.
3
+ *
4
+ * Contenders inside a group must see identical input and produce the same
5
+ * answer; otherwise the ranking measures the fixture rather than the code. The
6
+ * generators here are deterministic so a run can be reproduced, and the
7
+ * reference programs in other languages can generate the same values.
8
+ */
9
+ /** Default seed, shared so unrelated packages produce comparable fixtures. */
10
+ export declare const DEFAULT_SEED = 625341585;
11
+ /**
12
+ * `count` pseudo-random unsigned 32-bit values.
13
+ *
14
+ * Recovers the exact 32-bit state {@link xorshift32} divides down to a float:
15
+ * multiplying back by 2^32 is lossless for a value that started as an integer
16
+ * with 32 significant bits or fewer.
17
+ */
18
+ export declare function randomUint32Values(count: number, seed?: number): number[];
19
+ /** `count` pseudo-random floats in `[0, 1)`. */
20
+ export declare function randomFloat64Values(count: number, seed?: number): number[];
21
+ /** Sum of `values`, truncated to 32 bits. */
22
+ export declare function checksum(values: Iterable<number>): number;
23
+ /** Sum of `0..count-1`, the answer every FIFO workload must reach. */
24
+ export declare function sequentialChecksum(count: number): number;
25
+ /** Cheap order-sensitive hash of a string, used to verify text workloads. */
26
+ export declare function hashString(text: string): number;
27
+ /**
28
+ * Seeded 32-bit hash of a string, for structures that take a hash function as a
29
+ * parameter (Bloom filters, Count-Min sketches, HyperLogLog, …).
30
+ *
31
+ * Distinct from {@link hashString}, which is an unseeded checksum used to
32
+ * _verify_ a workload. This one is an input to the code under test, so it has
33
+ * to avalanche: the error bounds those structures document assume a uniformly
34
+ * distributed hash, and a weak one would make a suite measure the hash instead
35
+ * of the structure.
36
+ *
37
+ * Its shape matches `@ac-kit/data`'s `Hash32<string>` structurally, so it can
38
+ * be passed straight in without this package depending on `@ac-kit/data`.
39
+ */
40
+ export declare function stringHash32(text: string, seed: number): number;
41
+ /** `count` distinct keys, deterministic so every run compares like with like. */
42
+ export declare function distinctKeys(count: number, prefix?: string): string[];
43
+ /** `count` pseudo-random integers in `[0, bound)`. */
44
+ export declare function randomInts(count: number, bound: number, seed?: number): number[];
45
+ /**
46
+ * A skewed access pattern over `[0, universe)`: low indices drawn far more
47
+ * often than high ones.
48
+ *
49
+ * A uniform pattern makes every cache look alike, because a replacement policy
50
+ * only shows itself when some entries are genuinely hotter than others.
51
+ * Squaring a uniform draw is the cheapest way to get that skew and stays
52
+ * reproducible.
53
+ */
54
+ export declare function skewedIndices(count: number, universe: number, seed?: number): number[];
55
+ /** `count` short lowercase words, deterministic. */
56
+ export declare function words(count: number, seed?: number): string[];
57
+ //# sourceMappingURL=fixtures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../src/fixtures.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,8EAA8E;AAC9E,eAAO,MAAM,YAAY,YAAa,CAAC;AAEvC;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CACjC,KAAK,EAAE,MAAM,EACb,IAAI,SAAe,GACjB,MAAM,EAAE,CAKV;AAED,gDAAgD;AAChD,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,MAAM,EACb,IAAI,SAAe,GACjB,MAAM,EAAE,CAGV;AAED,6CAA6C;AAC7C,wBAAgB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,CAMzD;AAED,sEAAsE;AACtE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMxD;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,SAAQ,GAAG,MAAM,EAAE,CAEpE;AAED,sDAAsD;AACtD,wBAAgB,UAAU,CACzB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,SAAe,GACjB,MAAM,EAAE,CAGV;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC5B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,SAAe,GACjB,MAAM,EAAE,CAMV;AAED,oDAAoD;AACpD,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,SAAe,GAAG,MAAM,EAAE,CAclE"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Fixtures shared by every benchmark package.
3
+ *
4
+ * Contenders inside a group must see identical input and produce the same
5
+ * answer; otherwise the ranking measures the fixture rather than the code. The
6
+ * generators here are deterministic so a run can be reproduced, and the
7
+ * reference programs in other languages can generate the same values.
8
+ */
9
+ import { encodeTextUtf8 } from "@ac-kit/core";
10
+ import { xorshift32 } from "@ac-kit/math-random";
11
+ import { fnv1a_32, murmur3_32 } from "@ac-kit/noncrypto-hash";
12
+ /** Default seed, shared so unrelated packages produce comparable fixtures. */
13
+ export const DEFAULT_SEED = 0x2545f491;
14
+ /**
15
+ * `count` pseudo-random unsigned 32-bit values.
16
+ *
17
+ * Recovers the exact 32-bit state {@link xorshift32} divides down to a float:
18
+ * multiplying back by 2^32 is lossless for a value that started as an integer
19
+ * with 32 significant bits or fewer.
20
+ */
21
+ export function randomUint32Values(count, seed = DEFAULT_SEED) {
22
+ const rand = xorshift32(seed);
23
+ return Array.from({ length: count }, () => Math.round(rand() * 4294967296));
24
+ }
25
+ /** `count` pseudo-random floats in `[0, 1)`. */
26
+ export function randomFloat64Values(count, seed = DEFAULT_SEED) {
27
+ const rand = xorshift32(seed);
28
+ return Array.from({ length: count }, () => rand());
29
+ }
30
+ /** Sum of `values`, truncated to 32 bits. */
31
+ export function checksum(values) {
32
+ let sum = 0;
33
+ for (const value of values) {
34
+ sum = (sum + value) >>> 0;
35
+ }
36
+ return sum;
37
+ }
38
+ /** Sum of `0..count-1`, the answer every FIFO workload must reach. */
39
+ export function sequentialChecksum(count) {
40
+ let sum = 0;
41
+ for (let index = 0; index < count; index++) {
42
+ sum = (sum + index) >>> 0;
43
+ }
44
+ return sum;
45
+ }
46
+ /** Cheap order-sensitive hash of a string, used to verify text workloads. */
47
+ export function hashString(text) {
48
+ return fnv1a_32(encodeTextUtf8(text));
49
+ }
50
+ /**
51
+ * Seeded 32-bit hash of a string, for structures that take a hash function as a
52
+ * parameter (Bloom filters, Count-Min sketches, HyperLogLog, …).
53
+ *
54
+ * Distinct from {@link hashString}, which is an unseeded checksum used to
55
+ * _verify_ a workload. This one is an input to the code under test, so it has
56
+ * to avalanche: the error bounds those structures document assume a uniformly
57
+ * distributed hash, and a weak one would make a suite measure the hash instead
58
+ * of the structure.
59
+ *
60
+ * Its shape matches `@ac-kit/data`'s `Hash32<string>` structurally, so it can
61
+ * be passed straight in without this package depending on `@ac-kit/data`.
62
+ */
63
+ export function stringHash32(text, seed) {
64
+ return murmur3_32(encodeTextUtf8(text), seed);
65
+ }
66
+ /** `count` distinct keys, deterministic so every run compares like with like. */
67
+ export function distinctKeys(count, prefix = "key") {
68
+ return Array.from({ length: count }, (_, index) => `${prefix}-${index}`);
69
+ }
70
+ /** `count` pseudo-random integers in `[0, bound)`. */
71
+ export function randomInts(count, bound, seed = DEFAULT_SEED) {
72
+ const rand = xorshift32(seed);
73
+ return Array.from({ length: count }, () => Math.floor(rand() * bound));
74
+ }
75
+ /**
76
+ * A skewed access pattern over `[0, universe)`: low indices drawn far more
77
+ * often than high ones.
78
+ *
79
+ * A uniform pattern makes every cache look alike, because a replacement policy
80
+ * only shows itself when some entries are genuinely hotter than others.
81
+ * Squaring a uniform draw is the cheapest way to get that skew and stays
82
+ * reproducible.
83
+ */
84
+ export function skewedIndices(count, universe, seed = DEFAULT_SEED) {
85
+ const rand = xorshift32(seed);
86
+ return Array.from({ length: count }, () => {
87
+ const value = rand();
88
+ return Math.floor(value * value * universe);
89
+ });
90
+ }
91
+ /** `count` short lowercase words, deterministic. */
92
+ export function words(count, seed = DEFAULT_SEED) {
93
+ const rand = xorshift32(seed);
94
+ const alphabet = "abcdefghijklmnopqrstuvwxyz";
95
+ return Array.from({ length: count }, () => {
96
+ const length = 3 + Math.floor(rand() * 6);
97
+ let word = "";
98
+ for (let index = 0; index < length; index++) {
99
+ word += alphabet[Math.floor(rand() * alphabet.length)];
100
+ }
101
+ return word;
102
+ });
103
+ }
104
+ //# sourceMappingURL=fixtures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixtures.js","sourceRoot":"","sources":["../src/fixtures.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export * from "./event-loop-load.js";
2
+ export * from "./fixtures.js";
3
+ export * from "./native-toolchain.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./event-loop-load.js";
2
+ export * from "./fixtures.js";
3
+ export * from "./native-toolchain.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Detects and, where needed, compiles a runnable reference program per
3
+ * toolchain spec — the machinery shared by every cross-language benchmark
4
+ * fixture in this workspace (a duration comparison is only meaningful once a
5
+ * program to compare against exists and actually runs).
6
+ */
7
+ import type { ArtifactCache } from "@ac-kit/app-system";
8
+ /** One candidate reference implementation, interpreted or compiled from source. */
9
+ export interface NativeToolchainSpec {
10
+ /** Attribution label carried into `NativeProgram.language`, e.g. `"C++"`. */
11
+ language: string;
12
+ /** Absolute path to the reference program's source file. */
13
+ sourcePath: string;
14
+ /**
15
+ * Binaries tried in order; the first one found on `PATH` is used. For an
16
+ * interpreted toolchain this is the interpreter itself (e.g. `["python3"]`);
17
+ * for a compiled one, the candidate compilers (e.g. `["g++", "clang++"]`).
18
+ */
19
+ candidates: readonly string[];
20
+ /**
21
+ * Builds the compile command for whichever candidate was found. Omit for an
22
+ * interpreted toolchain, where `sourcePath` is invoked directly as the
23
+ * program's first argument instead.
24
+ */
25
+ compile?: (compiler: string, sourcePath: string, outBinary: string) => {
26
+ command: string;
27
+ args: readonly string[];
28
+ };
29
+ }
30
+ /**
31
+ * A reference program ready to spawn: `command` with `baseArgs` prepended to
32
+ * any call.
33
+ */
34
+ export interface NativeProgram {
35
+ language: string;
36
+ command: string;
37
+ baseArgs: readonly string[];
38
+ }
39
+ export interface PrepareNativeToolchainsOptions {
40
+ /**
41
+ * Keeps only the programs that pass, e.g. a smoke test comparing against a
42
+ * known result. Every detected/compiled program is kept when omitted.
43
+ */
44
+ verify?: (program: NativeProgram) => Promise<boolean>;
45
+ /**
46
+ * Where compiled programs are cached. Supply the runner's own cache to share
47
+ * one compile across every process of a run; omitted, a persistent cache
48
+ * under `node_modules/.cache/ac-bench` is used.
49
+ */
50
+ cache?: ArtifactCache;
51
+ }
52
+ /**
53
+ * Detects, and where a `compile` step is declared compiles, one
54
+ * {@link NativeProgram} per spec whose first matching candidate is found on
55
+ * `PATH` — specs with no candidate found are silently skipped, and a spec whose
56
+ * compile step fails is reported to `stderr` and skipped rather than failing
57
+ * the whole batch, since a benchmark should degrade to comparing fewer
58
+ * languages rather than not comparing at all when one toolchain is missing.
59
+ *
60
+ * Compiled outputs are content-addressed by source text, compiler, compiler
61
+ * version and compile flags, so a program survives across runs and is rebuilt
62
+ * exactly when one of those changes. Nothing is written outside the cache
63
+ * root.
64
+ *
65
+ * Call once per distinct spec set and memoize the result in the caller (a lazy
66
+ * module-level singleton, as every consumer here already does) — the artifact
67
+ * cache spares the compile, not the detection work.
68
+ */
69
+ export declare function prepareNativeToolchains(specs: readonly NativeToolchainSpec[], options?: PrepareNativeToolchainsOptions): Promise<readonly NativeProgram[]>;
70
+ //# sourceMappingURL=native-toolchain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-toolchain.d.ts","sourceRoot":"","sources":["../src/native-toolchain.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,oBAAoB,CAAC;AAKrE,mFAAmF;AACnF,MAAM,WAAW,mBAAmB;IACnC,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IAEjB,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IAEnB;;;;OAIG;IACH,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAE9B;;;;OAIG;IACH,OAAO,CAAC,EAAE,CACT,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,KACb;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,CAAC;CAClD;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,8BAA8B;IAC9C;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAEtD;;;;OAIG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC;CACtB;AAaD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,uBAAuB,CAC5C,KAAK,EAAE,SAAS,mBAAmB,EAAE,EACrC,OAAO,GAAE,8BAAmC,GAC1C,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAiEnC"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Detects and, where needed, compiles a runnable reference program per
3
+ * toolchain spec — the machinery shared by every cross-language benchmark
4
+ * fixture in this workspace (a duration comparison is only meaningful once a
5
+ * program to compare against exists and actually runs).
6
+ */
7
+ import { readFile } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+ import { createArtifactCache, hasBinary } from "@ac-kit/app-system";
10
+ import { slugify } from "@ac-kit/core";
11
+ import { spawnProcess } from "@ac-kit/node";
12
+ const DEFAULT_CACHE_ROOT = join("node_modules", ".cache", "ac-bench");
13
+ /** Best-effort, for the artifact identity: a compiler upgrade must invalidate. */
14
+ async function probeVersion(compiler) {
15
+ try {
16
+ return (await spawnProcess(compiler, ["--version"])).stdout;
17
+ }
18
+ catch {
19
+ return "";
20
+ }
21
+ }
22
+ /**
23
+ * Detects, and where a `compile` step is declared compiles, one
24
+ * {@link NativeProgram} per spec whose first matching candidate is found on
25
+ * `PATH` — specs with no candidate found are silently skipped, and a spec whose
26
+ * compile step fails is reported to `stderr` and skipped rather than failing
27
+ * the whole batch, since a benchmark should degrade to comparing fewer
28
+ * languages rather than not comparing at all when one toolchain is missing.
29
+ *
30
+ * Compiled outputs are content-addressed by source text, compiler, compiler
31
+ * version and compile flags, so a program survives across runs and is rebuilt
32
+ * exactly when one of those changes. Nothing is written outside the cache
33
+ * root.
34
+ *
35
+ * Call once per distinct spec set and memoize the result in the caller (a lazy
36
+ * module-level singleton, as every consumer here already does) — the artifact
37
+ * cache spares the compile, not the detection work.
38
+ */
39
+ export async function prepareNativeToolchains(specs, options = {}) {
40
+ const cache = options.cache ?? createArtifactCache({ root: DEFAULT_CACHE_ROOT });
41
+ const programs = [];
42
+ for (const spec of specs) {
43
+ let found = null;
44
+ for (const candidate of spec.candidates) {
45
+ if (await hasBinary(candidate)) {
46
+ found = candidate;
47
+ break;
48
+ }
49
+ }
50
+ if (found === null) {
51
+ continue;
52
+ }
53
+ if (!spec.compile) {
54
+ programs.push({
55
+ language: spec.language,
56
+ command: found,
57
+ baseArgs: [spec.sourcePath],
58
+ });
59
+ continue;
60
+ }
61
+ const compile = spec.compile;
62
+ const binaryName = slugify(spec.language, { fallback: "program" });
63
+ try {
64
+ const directory = await cache.ensure(await buildArtifactKey(spec, compile, found, binaryName), async (staging, signal) => {
65
+ const { command, args } = compile(found, spec.sourcePath, join(staging, binaryName));
66
+ await spawnProcess(command, args, { signal });
67
+ });
68
+ programs.push({
69
+ language: spec.language,
70
+ command: join(directory, binaryName),
71
+ baseArgs: [],
72
+ });
73
+ }
74
+ catch (error) {
75
+ process.stderr.write(`${spec.language} reference not built: ${String(error)}\n`);
76
+ }
77
+ }
78
+ if (!options.verify) {
79
+ return programs;
80
+ }
81
+ const usable = [];
82
+ for (const program of programs) {
83
+ if (await options.verify(program)) {
84
+ usable.push(program);
85
+ }
86
+ }
87
+ return usable;
88
+ }
89
+ async function buildArtifactKey(spec, compile, compiler, binaryName) {
90
+ // The real output path differs per attempt, so the identity uses a fixed
91
+ // placeholder — otherwise no two calls could ever agree on a key.
92
+ const { command, args } = compile(compiler, spec.sourcePath, join("<artifact>", binaryName));
93
+ return {
94
+ name: spec.language,
95
+ inputs: [
96
+ await readSourceOrPath(spec.sourcePath),
97
+ compiler,
98
+ await probeVersion(compiler),
99
+ command,
100
+ ...args,
101
+ ],
102
+ };
103
+ }
104
+ /**
105
+ * An unreadable source still gets an identity: the compile is about to fail on
106
+ * it anyway, and a failed build is never cached.
107
+ */
108
+ async function readSourceOrPath(sourcePath) {
109
+ try {
110
+ return await readFile(sourcePath, "utf8");
111
+ }
112
+ catch {
113
+ return `\u0000unreadable:${sourcePath}`;
114
+ }
115
+ }
116
+ //# sourceMappingURL=native-toolchain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-toolchain.js","sourceRoot":"","sources":["../src/native-toolchain.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@ac-bench/util",
3
+ "version": "0.1.0",
4
+ "description": "Helpers for writing benchmarks: deterministic PRNG workloads, an order-sensitive output hash, an equivalence guard, a controllable event-loop load, and spawn-overhead measurement.",
5
+ "keywords": [
6
+ "benchmark",
7
+ "fixture",
8
+ "prng",
9
+ "workload"
10
+ ],
11
+ "homepage": "https://github.com/anthochamp/node-essentials/tree/main/bench/util#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/util"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "main": "./dist/index.js",
28
+ "publishConfig": {
29
+ "main": "./dist/index.js",
30
+ "access": "public"
31
+ },
32
+ "scripts": {
33
+ "compile": "tsc -p ./tsconfig.build.json --outDir dist",
34
+ "compile:check": "tsc -noEmit -p ./tsconfig.json",
35
+ "clean": "rimraf dist",
36
+ "build": "yarn clean && yarn compile",
37
+ "test": "yarn run -T vitest run",
38
+ "prepack": "yarn build"
39
+ },
40
+ "dependencies": {
41
+ "@ac-kit/app-system": "^0.1.0",
42
+ "@ac-kit/core": "^0.1.0",
43
+ "@ac-kit/math-random": "^0.1.0",
44
+ "@ac-kit/math-stats": "^0.1.0",
45
+ "@ac-kit/node": "^0.1.0",
46
+ "@ac-kit/noncrypto-hash": "^0.1.0",
47
+ "@ac-kit/test-util": "^0.1.0"
48
+ },
49
+ "devDependencies": {
50
+ "@ac-kit/tsconfig": "0.1.0",
51
+ "@ac-kit/typedoc-config": "0.1.0",
52
+ "@ac-kit/vitest-config": "0.1.0",
53
+ "@types/node": "26.5.1",
54
+ "@typescript/native": "typescript@7.0.2",
55
+ "rimraf": "6.1.3",
56
+ "typedoc": "0.28.20",
57
+ "typescript": "@typescript/typescript6@6.0.2",
58
+ "vite": "8.3.0",
59
+ "vitest": "5.0.0"
60
+ },
61
+ "engines": {
62
+ "node": ">=24"
63
+ }
64
+ }