autor3search-typescript 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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/dist/benchproto/types.d.ts +32 -0
  4. package/dist/benchproto/types.js +28 -0
  5. package/dist/benchproto/types.js.map +1 -0
  6. package/dist/benchrun/child.d.ts +1 -0
  7. package/dist/benchrun/child.js +222 -0
  8. package/dist/benchrun/child.js.map +1 -0
  9. package/dist/benchrun/invoke.d.ts +61 -0
  10. package/dist/benchrun/invoke.js +152 -0
  11. package/dist/benchrun/invoke.js.map +1 -0
  12. package/dist/cli/cmd-baseline.d.ts +15 -0
  13. package/dist/cli/cmd-baseline.js +292 -0
  14. package/dist/cli/cmd-baseline.js.map +1 -0
  15. package/dist/cli/cmd-doctor.d.ts +11 -0
  16. package/dist/cli/cmd-doctor.js +26 -0
  17. package/dist/cli/cmd-doctor.js.map +1 -0
  18. package/dist/cli/cmd-eval.d.ts +30 -0
  19. package/dist/cli/cmd-eval.js +173 -0
  20. package/dist/cli/cmd-eval.js.map +1 -0
  21. package/dist/cli/cmd-init.d.ts +16 -0
  22. package/dist/cli/cmd-init.js +343 -0
  23. package/dist/cli/cmd-init.js.map +1 -0
  24. package/dist/cli/cmd-profile.d.ts +11 -0
  25. package/dist/cli/cmd-profile.js +140 -0
  26. package/dist/cli/cmd-profile.js.map +1 -0
  27. package/dist/cli/cmd-report.d.ts +15 -0
  28. package/dist/cli/cmd-report.js +70 -0
  29. package/dist/cli/cmd-report.js.map +1 -0
  30. package/dist/cli/cmd-status.d.ts +11 -0
  31. package/dist/cli/cmd-status.js +143 -0
  32. package/dist/cli/cmd-status.js.map +1 -0
  33. package/dist/cli/cmd-stop.d.ts +15 -0
  34. package/dist/cli/cmd-stop.js +143 -0
  35. package/dist/cli/cmd-stop.js.map +1 -0
  36. package/dist/cli/main.d.ts +31 -0
  37. package/dist/cli/main.js +133 -0
  38. package/dist/cli/main.js.map +1 -0
  39. package/dist/cli/runctx.d.ts +29 -0
  40. package/dist/cli/runctx.js +49 -0
  41. package/dist/cli/runctx.js.map +1 -0
  42. package/dist/cli/speedup.d.ts +14 -0
  43. package/dist/cli/speedup.js +25 -0
  44. package/dist/cli/speedup.js.map +1 -0
  45. package/dist/config/load.d.ts +6 -0
  46. package/dist/config/load.js +172 -0
  47. package/dist/config/load.js.map +1 -0
  48. package/dist/config/schema.d.ts +42 -0
  49. package/dist/config/schema.js +25 -0
  50. package/dist/config/schema.js.map +1 -0
  51. package/dist/discover/benchmarks.d.ts +26 -0
  52. package/dist/discover/benchmarks.js +88 -0
  53. package/dist/discover/benchmarks.js.map +1 -0
  54. package/dist/discover/files.d.ts +23 -0
  55. package/dist/discover/files.js +82 -0
  56. package/dist/discover/files.js.map +1 -0
  57. package/dist/doctor/doctor.d.ts +56 -0
  58. package/dist/doctor/doctor.js +255 -0
  59. package/dist/doctor/doctor.js.map +1 -0
  60. package/dist/freeze/freeze.d.ts +23 -0
  61. package/dist/freeze/freeze.js +107 -0
  62. package/dist/freeze/freeze.js.map +1 -0
  63. package/dist/freeze/manifest.d.ts +6 -0
  64. package/dist/freeze/manifest.js +9 -0
  65. package/dist/freeze/manifest.js.map +1 -0
  66. package/dist/freeze/safepath.d.ts +8 -0
  67. package/dist/freeze/safepath.js +22 -0
  68. package/dist/freeze/safepath.js.map +1 -0
  69. package/dist/gitx/git.d.ts +87 -0
  70. package/dist/gitx/git.js +235 -0
  71. package/dist/gitx/git.js.map +1 -0
  72. package/dist/measure/interleave.d.ts +32 -0
  73. package/dist/measure/interleave.js +42 -0
  74. package/dist/measure/interleave.js.map +1 -0
  75. package/dist/pipeline/eval.d.ts +67 -0
  76. package/dist/pipeline/eval.js +495 -0
  77. package/dist/pipeline/eval.js.map +1 -0
  78. package/dist/pm/detect.d.ts +17 -0
  79. package/dist/pm/detect.js +131 -0
  80. package/dist/pm/detect.js.map +1 -0
  81. package/dist/profile/profile.d.ts +53 -0
  82. package/dist/profile/profile.js +105 -0
  83. package/dist/profile/profile.js.map +1 -0
  84. package/dist/results/results.d.ts +65 -0
  85. package/dist/results/results.js +214 -0
  86. package/dist/results/results.js.map +1 -0
  87. package/dist/runner/exec.d.ts +69 -0
  88. package/dist/runner/exec.js +185 -0
  89. package/dist/runner/exec.js.map +1 -0
  90. package/dist/scope/scope.d.ts +20 -0
  91. package/dist/scope/scope.js +70 -0
  92. package/dist/scope/scope.js.map +1 -0
  93. package/dist/state/baseline.d.ts +35 -0
  94. package/dist/state/baseline.js +38 -0
  95. package/dist/state/baseline.js.map +1 -0
  96. package/dist/state/home.d.ts +9 -0
  97. package/dist/state/home.js +72 -0
  98. package/dist/state/home.js.map +1 -0
  99. package/dist/state/lock.d.ts +11 -0
  100. package/dist/state/lock.js +89 -0
  101. package/dist/state/lock.js.map +1 -0
  102. package/dist/state/runnaming.d.ts +27 -0
  103. package/dist/state/runnaming.js +47 -0
  104. package/dist/state/runnaming.js.map +1 -0
  105. package/dist/state/stop.d.ts +28 -0
  106. package/dist/state/stop.js +59 -0
  107. package/dist/state/stop.js.map +1 -0
  108. package/dist/stats/delta.d.ts +29 -0
  109. package/dist/stats/delta.js +41 -0
  110. package/dist/stats/delta.js.map +1 -0
  111. package/dist/stats/geomean.d.ts +4 -0
  112. package/dist/stats/geomean.js +21 -0
  113. package/dist/stats/geomean.js.map +1 -0
  114. package/dist/stats/mannwhitney.d.ts +27 -0
  115. package/dist/stats/mannwhitney.js +158 -0
  116. package/dist/stats/mannwhitney.js.map +1 -0
  117. package/dist/verdict/verdict.d.ts +46 -0
  118. package/dist/verdict/verdict.js +102 -0
  119. package/dist/verdict/verdict.js.map +1 -0
  120. package/dist/version.d.ts +2 -0
  121. package/dist/version.js +3 -0
  122. package/dist/version.js.map +1 -0
  123. package/package.json +35 -0
  124. package/templates/program.md +131 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gal Be
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # autor3search-typescript
2
+
3
+ Let an AI coding agent loose on your repository's performance, without letting it grade its own homework. `autor3search-typescript` freezes your tests and benchmarks, measures every change the agent proposes against a baseline it cannot touch, and only keeps a commit that is a real, statistically significant, material win.
4
+
5
+ Licensed under the [MIT License](./LICENSE). Copyright (c) 2026 Gal Be.
6
+
7
+ ## Start here
8
+
9
+ Open your coding agent inside the TypeScript repository you want to make faster, and
10
+ paste this:
11
+
12
+ ```text
13
+ Install and run autor3search-typescript on this repository, then optimize it.
14
+
15
+ Setup:
16
+ 1. npm install --save-dev autor3search-typescript
17
+ 2. npx autor3search-typescript init
18
+ Show me the benchmarks it discovered. If it reports none, STOP and tell me:
19
+ this tool can only optimize what it can measure.
20
+ 3. git add -A && git commit -m "autor3search-typescript init"
21
+ 4. npx autor3search-typescript doctor
22
+ Show me any warnings. If the machine looks unfit to measure, stop and ask me
23
+ before continuing.
24
+ 5. npx autor3search-typescript baseline -tag <today, e.g. sep7>
25
+
26
+ Then:
27
+ 6. Read program.md in this repository, in full. It is your instruction set for
28
+ the rest of this run. Follow it exactly.
29
+
30
+ Rules for the whole run:
31
+ - One hypothesis per commit. Commit before each experiment, then run
32
+ `npx autor3search-typescript eval --json` and apply its verdict before touching
33
+ anything else: KEEP means the commit stays; anything else (DISCARD, FAIL, CRASH)
34
+ means `git reset --hard HEAD~1`.
35
+ - Never edit program.md, .autor3search/config.yaml, results.tsv, any
36
+ *.test.*/*.spec.*/*.bench.* file, package.json, a lockfile, or tsconfig.json.
37
+ They are not yours.
38
+ - Never pass -force to any autor3search-typescript command. (I may run
39
+ `autor3search-typescript stop -force` myself; that one is mine, not yours.)
40
+ - Print one context line before each experiment, so I can see where you are:
41
+ [exp <n> | <branch> | vs <measure_commit> | stop: npx autor3search-typescript stop]
42
+
43
+ Run the loop until I stop you. I stop you by running
44
+ `npx autor3search-typescript stop` in my own terminal — you will see it as
45
+ "stop_requested": true in a verdict. When you do: apply that verdict, do not
46
+ start another experiment, run `npx autor3search-typescript report`, summarize
47
+ what you tried, and exit the loop.
48
+ ```
49
+
50
+ That's the whole handoff. The agent installs the tool, discovers your benchmarks,
51
+ freezes a baseline, and then follows `program.md` — generated for your repository by
52
+ `init` — which tells it how to run the keep-or-discard loop. `program.md` names the
53
+ benchmarks in scope, spells out the KEEP/DISCARD/FAIL/CRASH contract, lists everything
54
+ the agent must never touch, and ends with a bank of generic V8/TypeScript performance
55
+ ideas for when the agent is out of hypotheses.
56
+
57
+ Two things worth knowing before you start it:
58
+
59
+ - **It needs benchmarks.** `init` refuses to run without a discovered `bench*`
60
+ function in a `*.bench.ts` file — the tool optimizes what it can measure, and
61
+ refuses to guess.
62
+ - **Numbers are only as good as the machine.** Run `doctor` and read it. See
63
+ Limitations below for how much run-to-run noise a JS runtime can add even on an
64
+ idle machine.
65
+
66
+ ## What you get back
67
+
68
+ An unattended loop that, commit by commit:
69
+
70
+ - measures the agent's candidate against a frozen baseline with real, repeated, interleaved timing rounds and a rank-sum significance test — not a single before/after number;
71
+ - restores any test, spec or benchmark file the agent edited before running it, so a weakened test can never manufacture a KEEP;
72
+ - refuses (FAILs) a change that touches `package.json`, a lockfile, `tsconfig.json`, or a file outside the declared `scope`, before anything is even measured;
73
+ - refuses a change that adds a new test/bench file the baseline never saw, closing "add an easier benchmark";
74
+ - appends one row per experiment to `results.tsv`, and reports a cumulative, compounding speedup across the whole run.
75
+
76
+ ## Who owns what
77
+
78
+ | | you edit | the agent edits |
79
+ |---|---|---|
80
+ | `program.md` | yes, before handing it over — never after | never (enforced: editing it is a scope violation) |
81
+ | `.autor3search/config.yaml` | yes | never (enforced: a config hash mismatch FAILs the experiment) |
82
+ | source files under `scope` | rarely | yes — this is the whole point |
83
+ | `*.test.ts`, `*.spec.ts`, `*.bench.ts` | yes | never (enforced: frozen content is restored before every measurement) |
84
+ | `package.json`, lockfiles, `tsconfig.json` | yes | never (enforced: immutable regardless of `scope`) |
85
+ | `results.tsv` | never, ordinarily | not prevented, but pointless — it is gitignored and untracked, so no gate ever sees it as "changed," and it is never read back to decide a verdict; editing it corrupts a human-readable log, nothing more |
86
+ | baselines, locks, stop requests | never — this is what the agent cannot reach | never |
87
+
88
+ The measurement state — baselines, locks, stop requests — lives outside the repository entirely (see "Where run state lives" below). If the agent could write any of it, it could grade its own work.
89
+
90
+ ## Quick start
91
+
92
+ ```bash
93
+ npm install --save-dev autor3search-typescript
94
+ npx autor3search-typescript init
95
+ # review .autor3search/config.yaml and program.md, then:
96
+ git add .autor3search/config.yaml program.md .gitignore && git commit -m "chore: add autor3search-typescript"
97
+ npx autor3search-typescript baseline -tag <tag>
98
+ # hand the repo and program.md to your agent, using the prompt above
99
+ ```
100
+
101
+ `init` refuses to run if it finds no exported `bench*` function in a `*.bench.ts` file, or no `test` script in `package.json` — this tool has nothing to gate or measure without both.
102
+
103
+ To install from `main` ahead of a release instead of the last published version, use
104
+ `npm install --save-dev g4lb/autor3search-typescript` — the GitHub form works too,
105
+ just builds the tool on the way in instead of using a prebuilt tarball.
106
+
107
+ ## Watching and stopping a run
108
+
109
+ ```bash
110
+ npx autor3search-typescript status -tag <tag> # branch, commits, worktree, experiment counts, in-flight eval — read-only
111
+ npx autor3search-typescript stop -tag <tag> # ask the agent to stop after its current experiment
112
+ npx autor3search-typescript stop -tag <tag> -force # also signal the running eval to abandon it now
113
+ npx autor3search-typescript stop -tag <tag> -clear # cancel a pending stop
114
+ ```
115
+
116
+ `stop` never drops a commit for you — it only asks, or signals, and then prints the `git reset --hard HEAD~1` that would drop the abandoned experiment, for you to run yourself.
117
+
118
+ ## Commands
119
+
120
+ | command | what it does |
121
+ |---|---|
122
+ | `init` | Discover benchmarks and write `.autor3search/config.yaml` and `program.md` |
123
+ | `doctor` | Report whether this machine can measure reliably (informational, always exits 0) |
124
+ | `baseline` | Freeze tests/benchmarks, pin a worktree at HEAD, install and prove it can measure |
125
+ | `eval` | Run one experiment through the gate chain and report a verdict (0 KEEP, 1 DISCARD, 2 FAIL, 3 CRASH) |
126
+ | `status` | Report where a run is: branch, commits, worktree, experiment counts, in-flight eval, pending stop |
127
+ | `stop` | Ask the agent to stop after its current experiment; `-clear` cancels, `-force` also signals the running eval |
128
+ | `report` | Summarize `results.tsv`: counts by status, cumulative speedup, largest individual wins |
129
+ | `profile` | Run the declared benchmarks under Node's CPU profiler and print the hottest functions |
130
+
131
+ Every command accepts a leading `-C <dir>` to run as if invoked from `<dir>` (its git repository root is resolved from there).
132
+
133
+ ## Where run state lives
134
+
135
+ Everything the verdict depends on — the frozen manifest, the baseline record, the eval lock, stop requests — lives under the OS cache directory (`~/Library/Caches` on macOS, `$XDG_CACHE_HOME` or `~/.cache` on Linux, `%LOCALAPPDATA%` on Windows), keyed by the repository's own canonical path and the `-tag` you chose, never inside the repository itself. `results.tsv` and `run.log` also live in the repository but are gitignored and untracked — plain, human-readable output, not gated artifacts. `.autor3search/config.yaml` is the one exception: `init` writes `.autor3search/*` to `.gitignore` with a `!.autor3search/config.yaml` negation, so the run configuration itself is committed to version history (a KEEP has to stay reproducible and auditable later), while only its hash — not its content — is what the gate chain actually trusts; a hand-edit to it fails the next `eval` rather than silently loosening it.
136
+
137
+ ## The worked example
138
+
139
+ This is one real, unmodified run of this tool against the demo fixture shipped in `testdata/demo/`: a `countWords` function whose hot loop rebuilds a whole new array on every character —
140
+
141
+ ```ts
142
+ // before
143
+ chars = chars.concat([c]) // allocates and copies a new array per character: O(n^2) per word
144
+
145
+ // after
146
+ chars.push(c) // O(1) amortized per character: O(n) per word
147
+ ```
148
+
149
+ This is deliberately **not** a string-concatenation fix. Plain `+=` on a JS string is not the bug here — V8 represents concatenated strings as ropes, so it never re-copies on every append the way Go's naked string concatenation does. The bug is expressed with an array instead, because that is what actually costs O(n²) in this runtime.
150
+
151
+ Running `autor3search-typescript eval --json` against that one commit, on the machine this project was built on:
152
+
153
+ | | value |
154
+ |---|---|
155
+ | baseline | 3.59 ms/op |
156
+ | candidate | 268 µs/op |
157
+ | improvement | 92.55% |
158
+ | verdict | **KEEP** (exit 0) |
159
+
160
+ The same live session then correctly **DISCARDed** a genuine no-op commit (a comment-only change) measured against that *just-advanced* baseline — proving `measureCommit` really moved and the no-op was not coasting on the earlier win — and **FAILed** a scope violation. All three exit codes (0, 1, 2) were observed in that one live run, and the automated end-to-end test in `src/e2e/full-run.test.ts` reproduces the KEEP-then-DISCARD sequence on every run of this project's own test suite.
161
+
162
+ **How do we know the measurements themselves mean anything, not just that the harness runs?** The measurement layer was checked against a known quantity, not just against itself: a hand-timed 5 ms busy-spin measured back at **5.0002 ms/op**, and a hand-timed 1 ms spin at **1.0001 ms/op**. That is the strongest evidence in this project that the numbers are real — it is checkable, not merely self-consistent.
163
+
164
+ ## What the harness enforces
165
+
166
+ | if the agent tries to... | what happens |
167
+ |---|---|
168
+ | edit `package.json`, a lockfile, or `tsconfig.json` | FAIL, unconditionally — immutable regardless of `scope` |
169
+ | edit a file outside `scope` | FAIL — scope violation |
170
+ | edit a frozen test/spec/bench file to make it pass trivially | the real, original content is restored before the test ever runs; a `WARNING:` line reports what was restored |
171
+ | edit `.autor3search/config.yaml` (e.g. loosen `min_effect_pct`) | FAIL — the config hash no longer matches the baseline's |
172
+ | add a new, easier benchmark not present at baseline | FAIL — unmanifested file |
173
+ | commit source AND a slowed-down benchmark body together, hoping the slow body inflates its own future baseline | after a KEEP, the base worktree is repointed to the candidate commit and then the frozen benchmark bytes are immediately restored over it — so the tampered bench body never survives into the new baseline; every future comparison still measures the same original frozen bytes on both sides |
174
+ | let its own change fail the repo's typecheck, build, or test command | FAIL, before anything is measured |
175
+ | edit `program.md` | FAIL — scope violation, never in `scope` |
176
+ | edit `results.tsv` | nothing stops this, and nothing needs to: it is a gitignored, untracked, human-readable log the harness only ever appends to, never a gated or tamper-evident artifact, and no gate reads it back to make a decision |
177
+ | kill the running `eval` process (e.g. `stop -force`) while a benchmark is measuring | the detached measurement child (its own process group) is signalled and the eval lock is released before the process exits, so neither is left orphaned |
178
+ | kill and restart the measurement process to hide a crash | CRASH is reported (exit 3), distinct from FAIL — the harness does not let a crashed child silently become "no verdict" |
179
+
180
+ Anti-cheat evidence, not assertion: each row above was verified by mutation testing during development — the guard was removed, the attack was demonstrated to succeed, the guard was restored, and the attack failed again. Recorded examples: removing the measurement-commit advance made a comment-only no-op commit report KEEP; removing the post-KEEP worktree restore left a doctored benchmark in the base worktree, inflating every subsequent comparison; removing the symlink guard let a content-matching symlink survive `restore` and be mutated afterward; removing the process-group kill left a live orphaned benchmark process running after the harness exited.
181
+
182
+ ## Scoring
183
+
184
+ Each declared benchmark is measured `count` times per side (baseline and candidate), interleaved and alternating which side goes first each round to cancel ordering bias, using a Mann-Whitney rank-sum test per benchmark and a Bonferroni correction across however many benchmarks are compared. A commit KEEPs only if:
185
+
186
+ 1. at least one benchmark shows a significant (`p < ALPHA / k`) improvement, and
187
+ 2. no benchmark shows a significant regression past `max_regress_pct`, and
188
+ 3. the geometric mean of all candidate/baseline ratios beats `min_effect_pct`.
189
+
190
+ `count` below 4 is refused outright at config-load time: with fewer than 4 rounds per side, the exact rank-sum test cannot report `p < 0.05` no matter how large the true effect is, so every experiment would DISCARD regardless of what changed. The shipped default is 10.
191
+
192
+ ## Limitations — read this before trusting an overnight run
193
+
194
+ **A KEEP is evidence, not proof.** Any fixed statistical threshold admits false positives; a KEEP means "this cleared the bar the config set," not "this change is definitely faster in production."
195
+
196
+ **The candidate side's `node_modules` is not integrity-verified.** A Go project gets this for free from `go.sum`, which is checked on every build. npm verifies package integrity only at install time (against `package-lock.json`'s hashes) — once installed, nothing here re-checks that `node_modules` on disk still matches the lockfile before measuring against it.
197
+
198
+ **JavaScript measurement is noisier than Go's.** JIT warm-up state, garbage-collection timing, and (on Apple Silicon) P/E-core scheduling all add variance a Go binary's more uniform runtime does not have to the same degree. `doctor` reports what it can about the current machine's load, power source, and core mix, but cannot eliminate any of this.
199
+
200
+ **Concretely, on this project's own dev machine:** three independent measurements of the exact same fixture, on the exact same code, landed at 3.1, 3.198 and 3.59 ms/op — a spread of roughly 15% from run to run, with nothing else on the machine changed. That is the noise floor this tool exists to reason about statistically rather than paper over; it is also why the numbers above are presented as one coherent run, not averaged together into a false precision no single run actually produced.
201
+
202
+ **No benchmarks, no value.** This tool cannot invent something to measure. It only ever gates and reports what your own `*.bench.ts` files already exercise.
203
+
204
+ **Discovery is textual, not semantic.** It finds an exported `bench*` function in a `*.bench.ts` file. It does **not** find `export const benchX = someFactory()`, and it does not follow a cross-module re-export of a benchmark function — if your benchmark isn't a literal `export function bench...() { ... }` in the file that declares it, it will not be discovered.
205
+
206
+ **Microbenchmarks are not your application.** A benchmark that KEEPs proves the benchmark got faster under the exact conditions it constructs. Whether that improvement is visible in your actual application depends on how hot that code path really is there — something this tool has no way to know.
207
+
208
+ **`count` below 4 can never reach significance,** for the reason given under Scoring above, which is why it is refused at config-load time rather than silently producing a run that can only ever DISCARD.
209
+
210
+ A performance tool that oversells its own certainty is worse than useless: it launders noise into a verdict a human then trusts. Every claim above is stated as plainly as it can be for exactly that reason.
@@ -0,0 +1,32 @@
1
+ /** One benchmark measured once, as written by the child process. */
2
+ export type BenchResult = {
3
+ ok: true;
4
+ id: string;
5
+ /** Mean nanoseconds per operation across the measured window. */
6
+ nsPerOp: number;
7
+ iterations: number;
8
+ batches: number;
9
+ elapsedMs: number;
10
+ /** typeof the sink value, proving the return value was consumed. */
11
+ sinkType: string;
12
+ } | {
13
+ ok: false;
14
+ id: string;
15
+ error: string;
16
+ };
17
+ /**
18
+ * Parses one child result, validating just enough that a caller never
19
+ * silently treats corrupt data as a real measurement.
20
+ *
21
+ * Only `ok` and `id` were checked before this fix -- documented as an
22
+ * accepted limitation for a self-produced protocol, until the final
23
+ * whole-branch review reclassified it as a defect: a SIGKILL mid-`writeFile`
24
+ * (the timeout path, or an OS-level kill) can land after the JSON document
25
+ * is syntactically complete but before every field inside it was written --
26
+ * e.g. an atomic-rename race that leaves `"nsPerOp":` followed by a
27
+ * half-written number, or a field simply absent from an otherwise
28
+ * well-formed object. `JSON.parse` succeeds either way, so only an explicit
29
+ * check here stops `nsPerOp: undefined` (or `NaN`, or `-1`) from reaching
30
+ * the statistics layer looking like a legitimate zero-cost measurement.
31
+ */
32
+ export declare function parseBenchResult(text: string): BenchResult;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Parses one child result, validating just enough that a caller never
3
+ * silently treats corrupt data as a real measurement.
4
+ *
5
+ * Only `ok` and `id` were checked before this fix -- documented as an
6
+ * accepted limitation for a self-produced protocol, until the final
7
+ * whole-branch review reclassified it as a defect: a SIGKILL mid-`writeFile`
8
+ * (the timeout path, or an OS-level kill) can land after the JSON document
9
+ * is syntactically complete but before every field inside it was written --
10
+ * e.g. an atomic-rename race that leaves `"nsPerOp":` followed by a
11
+ * half-written number, or a field simply absent from an otherwise
12
+ * well-formed object. `JSON.parse` succeeds either way, so only an explicit
13
+ * check here stops `nsPerOp: undefined` (or `NaN`, or `-1`) from reaching
14
+ * the statistics layer looking like a legitimate zero-cost measurement.
15
+ */
16
+ export function parseBenchResult(text) {
17
+ const v = JSON.parse(text);
18
+ if (typeof v !== 'object' || v === null || !('ok' in v) || !('id' in v)) {
19
+ throw new Error(`malformed benchmark result: ${text.slice(0, 200)}`);
20
+ }
21
+ const r = v;
22
+ if (r.ok === true && !(typeof r.nsPerOp === 'number' && Number.isFinite(r.nsPerOp) && r.nsPerOp > 0)) {
23
+ throw new Error(`malformed benchmark result: ok:true but nsPerOp is not a finite positive number ` +
24
+ `(got ${JSON.stringify(v.nsPerOp)}): ${text.slice(0, 200)}`);
25
+ }
26
+ return r;
27
+ }
28
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/benchproto/types.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,CAAC,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACnC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CAAC,+BAA+B,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAA;IACtE,CAAC;IACD,MAAM,CAAC,GAAG,CAAgB,CAAA;IAC1B,IAAI,CAAC,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,CAAC;QACrG,MAAM,IAAI,KAAK,CACb,kFAAkF;YAChF,QAAQ,IAAI,CAAC,SAAS,CAAE,CAA2B,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CACzF,CAAA;IACH,CAAC;IACD,OAAO,CAAC,CAAA;AACV,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,222 @@
1
+ /**
2
+ * The measurement child.
3
+ *
4
+ * Runs exactly one benchmark function in a fresh process and writes one JSON
5
+ * result to --out. Never writes its result to stdout: the module under
6
+ * measurement is arbitrary repository code and is free to print (a stray
7
+ * console.log during import, module init, or the benchmark body itself), so
8
+ * a protocol that parsed stdout would be broken by one such line. --out is
9
+ * a file this process alone controls the contents of.
10
+ */
11
+ import { writeFile } from 'node:fs/promises';
12
+ import { parseArgs } from 'node:util';
13
+ import { pathToFileURL } from 'node:url';
14
+ /** Batches shorter than this are dominated by timer resolution. */
15
+ const MIN_BATCH_NS = 10000000n; // 10ms
16
+ /**
17
+ * Written on every call, read once at the end via `sinkType`. Consuming the
18
+ * return value this way is cheap insurance against an optimizer deciding an
19
+ * unread result is dead and eliding the work that produced it. Measured
20
+ * directly (see task-12-report.md): the specific V8 build and benchmark
21
+ * shapes tested so far did NOT eliminate the loop even with the sink
22
+ * removed, so this is not known to be load-bearing today -- it is kept
23
+ * because that is a fact about one V8 version and a handful of shapes, not
24
+ * a guarantee, and the cost of keeping it is one assignment and one read.
25
+ */
26
+ let sink;
27
+ /** True for a promise or any other thenable a benchmark might return. */
28
+ function isThenable(v) {
29
+ return v !== null && typeof v === 'object' && typeof v.then === 'function';
30
+ }
31
+ /**
32
+ * Arbitrary repository code can `throw` anything, not just an `Error` --
33
+ * `throw null` or `throw 'boom'` are both legal JS. `(e as Error).message`
34
+ * on a non-Error throws its own TypeError, which would crash this process
35
+ * with no --out file written at all: exactly the crash this module exists
36
+ * to avoid.
37
+ */
38
+ function messageOf(e) {
39
+ return e instanceof Error ? e.message : String(e);
40
+ }
41
+ function runBatchSync(fn, n) {
42
+ const start = process.hrtime.bigint();
43
+ for (let i = 0; i < n; i++)
44
+ sink = fn();
45
+ return process.hrtime.bigint() - start;
46
+ }
47
+ async function runBatchAsync(fn, n) {
48
+ const start = process.hrtime.bigint();
49
+ for (let i = 0; i < n; i++)
50
+ sink = await fn();
51
+ return process.hrtime.bigint() - start;
52
+ }
53
+ function isNonNegativeInteger(n) {
54
+ return Number.isFinite(n) && Number.isInteger(n) && n >= 0;
55
+ }
56
+ /**
57
+ * Parses and validates argv. --out and --id are checked first because they
58
+ * are what let any OTHER failure be reported at all -- without an output
59
+ * path or an id to put in it, an invalid-invocation result cannot be
60
+ * attributed to any benchmark, so their absence is fatal (caller exits
61
+ * non-zero) rather than reported as a benchmark result the way every other
62
+ * validation failure is.
63
+ */
64
+ function parseArgv() {
65
+ const { values } = parseArgs({
66
+ options: {
67
+ file: { type: 'string' },
68
+ fn: { type: 'string' },
69
+ id: { type: 'string' },
70
+ 'benchtime-ms': { type: 'string' },
71
+ 'warmup-ms': { type: 'string' },
72
+ out: { type: 'string' },
73
+ },
74
+ });
75
+ const out = values.out;
76
+ const id = values.id;
77
+ if (typeof out !== 'string' || out === '' || typeof id !== 'string' || id === '') {
78
+ return { ok: false, attributable: false, message: 'missing --out or --id' };
79
+ }
80
+ const file = values.file;
81
+ if (typeof file !== 'string' || file === '') {
82
+ return { ok: false, attributable: true, id, out, message: 'missing --file' };
83
+ }
84
+ const fnName = values.fn;
85
+ if (typeof fnName !== 'string' || fnName === '') {
86
+ return { ok: false, attributable: true, id, out, message: 'missing --fn' };
87
+ }
88
+ const benchtimeMs = Number(values['benchtime-ms']);
89
+ if (!isNonNegativeInteger(benchtimeMs)) {
90
+ return {
91
+ ok: false,
92
+ attributable: true,
93
+ id,
94
+ out,
95
+ message: `--benchtime-ms must be a non-negative integer, got ${JSON.stringify(values['benchtime-ms'])}`,
96
+ };
97
+ }
98
+ const warmupMs = Number(values['warmup-ms']);
99
+ if (!isNonNegativeInteger(warmupMs)) {
100
+ return {
101
+ ok: false,
102
+ attributable: true,
103
+ id,
104
+ out,
105
+ message: `--warmup-ms must be a non-negative integer, got ${JSON.stringify(values['warmup-ms'])}`,
106
+ };
107
+ }
108
+ return { ok: true, args: { file, fnName, id, benchtimeMs, warmupMs, out } };
109
+ }
110
+ async function measure(args) {
111
+ let fn;
112
+ try {
113
+ const mod = (await import(pathToFileURL(args.file).href));
114
+ const candidate = mod[args.fnName];
115
+ if (typeof candidate !== 'function') {
116
+ return { ok: false, id: args.id, error: `${args.file} does not export a function named ${args.fnName}` };
117
+ }
118
+ fn = candidate;
119
+ }
120
+ catch (e) {
121
+ return { ok: false, id: args.id, error: `import ${args.file}: ${messageOf(e)}` };
122
+ }
123
+ try {
124
+ // Probe once to learn whether the benchmark is async. Awaiting a
125
+ // synchronous function would fold microtask-queue overhead into every
126
+ // measurement, so the two cases get separate loops: runBatchSync never
127
+ // awaits, runBatchAsync always does.
128
+ const probe = fn();
129
+ const isAsync = isThenable(probe);
130
+ if (isAsync)
131
+ await probe;
132
+ sink = probe;
133
+ const batch = isAsync
134
+ ? (n) => runBatchAsync(fn, n)
135
+ : (n) => Promise.resolve(runBatchSync(fn, n));
136
+ // Warm up first: JIT tiering changes the speed we are about to
137
+ // calibrate to, so calibrating against a cold function would pick a
138
+ // batch size for code that no longer exists by the time we measure.
139
+ const warmDeadline = process.hrtime.bigint() + BigInt(args.warmupMs) * 1000000n;
140
+ while (process.hrtime.bigint() < warmDeadline)
141
+ await batch(16);
142
+ // Calibrate a batch size whose duration is well above timer resolution.
143
+ let n = 1;
144
+ for (;;) {
145
+ const took = await batch(n);
146
+ if (took >= MIN_BATCH_NS || n >= 1_000_000_000)
147
+ break;
148
+ n *= 2;
149
+ }
150
+ let totalNs = 0n;
151
+ let iterations = 0;
152
+ let batches = 0;
153
+ const deadline = process.hrtime.bigint() + BigInt(args.benchtimeMs) * 1000000n;
154
+ while (process.hrtime.bigint() < deadline) {
155
+ totalNs += await batch(n);
156
+ iterations += n;
157
+ batches++;
158
+ }
159
+ if (iterations === 0) {
160
+ // benchtime shorter than a single batch: still report one honest batch.
161
+ totalNs = await batch(n);
162
+ iterations = n;
163
+ batches = 1;
164
+ }
165
+ // The initial probe is a single, un-timed call: it decides which loop
166
+ // measures every batch for the rest of the run. A benchmark that took
167
+ // a synchronous fast path on that one call (a cache hit, an early
168
+ // return) but later starts returning promises (a cache miss, a lazily
169
+ // opened connection) would stay on runBatchSync, which never awaits --
170
+ // silently timing promise *construction* instead of the awaited work,
171
+ // and reporting a real-looking nsPerOp that is orders of magnitude too
172
+ // small. One check here, after the loop, is enough to catch it without
173
+ // adding a per-iteration cost to every measurement.
174
+ if (!isAsync && isThenable(sink)) {
175
+ return {
176
+ ok: false,
177
+ id: args.id,
178
+ error: `${args.fnName}: benchmark is conditionally async -- the initial probe call returned a plain value so it was measured synchronously, but a later call returned a promise; measured timings would reflect promise construction, not the awaited work`,
179
+ };
180
+ }
181
+ return {
182
+ ok: true,
183
+ id: args.id,
184
+ nsPerOp: Number(totalNs) / iterations,
185
+ iterations,
186
+ batches,
187
+ elapsedMs: Number(totalNs) / 1_000_000,
188
+ // typeof the value consumed above -- see the `sink` declaration for
189
+ // why it is read at all.
190
+ sinkType: typeof sink,
191
+ };
192
+ }
193
+ catch (e) {
194
+ return { ok: false, id: args.id, error: `${args.fnName}: ${messageOf(e)}` };
195
+ }
196
+ }
197
+ async function main() {
198
+ const parsed = parseArgv();
199
+ if (!parsed.ok && !parsed.attributable) {
200
+ // No --out and/or --id: there is nowhere to write a result and nothing
201
+ // to attribute it to, so this is the one failure mode that is not
202
+ // reported as a benchmark result.
203
+ process.stderr.write(`benchrun child: invalid invocation: ${parsed.message}\n`);
204
+ process.exit(1);
205
+ }
206
+ if (parsed.ok) {
207
+ const result = await measure(parsed.args);
208
+ await writeFile(parsed.args.out, JSON.stringify(result));
209
+ return;
210
+ }
211
+ await writeFile(parsed.out, JSON.stringify({ ok: false, id: parsed.id, error: `invalid invocation: ${parsed.message}` }));
212
+ }
213
+ await main();
214
+ // A benchmark module can leave the event loop non-empty on its way out (an
215
+ // open timer, a socket, a lingering connection pool) even though the result
216
+ // is already written. Without an explicit exit, node would wait for that
217
+ // handle and the parent would eventually kill this process on timeout,
218
+ // discarding a result that was complete on disk the whole time -- the same
219
+ // "arbitrary repository code" reasoning that justified --out in the first
220
+ // place.
221
+ process.exit(0);
222
+ //# sourceMappingURL=child.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"child.js","sourceRoot":"","sources":["../../src/benchrun/child.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAGxC,mEAAmE;AACnE,MAAM,YAAY,GAAG,SAAW,CAAA,CAAC,OAAO;AAExC;;;;;;;;;GASG;AACH,IAAI,IAAa,CAAA;AAIjB,yEAAyE;AACzE,SAAS,UAAU,CAAC,CAAU;IAC5B,OAAO,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAQ,CAA0B,CAAC,IAAI,KAAK,UAAU,CAAA;AACtG,CAAC;AAED;;;;;;GAMG;AACH,SAAS,SAAS,CAAC,CAAU;IAC3B,OAAO,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;AACnD,CAAC;AAED,SAAS,YAAY,CAAC,EAAS,EAAE,CAAS;IACxC,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,CAAA;IACrC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,IAAI,GAAG,EAAE,EAAE,CAAA;IACvC,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,KAAK,CAAA;AACxC,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,EAAS,EAAE,CAAS;IAC/C,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,CAAA;IACrC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,IAAI,GAAG,MAAM,EAAE,EAAE,CAAA;IAC7C,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,KAAK,CAAA;AACxC,CAAC;AAWD,SAAS,oBAAoB,CAAC,CAAS;IACrC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAC5D,CAAC;AAOD;;;;;;;GAOG;AACH,SAAS,SAAS;IAChB,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;QAC3B,OAAO,EAAE;YACP,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;YACxB,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;YACtB,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;YACtB,cAAc,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;YAClC,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;YAC/B,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SACxB;KACF,CAAC,CAAA;IAEF,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAA;IACtB,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,CAAA;IACpB,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,EAAE,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACjF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAA;IAC7E,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;IACxB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;QAC5C,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC9E,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,EAAE,CAAA;IACxB,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;QAChD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,cAAc,EAAE,CAAA;IAC5E,CAAC;IACD,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAA;IAClD,IAAI,CAAC,oBAAoB,CAAC,WAAW,CAAC,EAAE,CAAC;QACvC,OAAO;YACL,EAAE,EAAE,KAAK;YACT,YAAY,EAAE,IAAI;YAClB,EAAE;YACF,GAAG;YACH,OAAO,EAAE,sDAAsD,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,EAAE;SACxG,CAAA;IACH,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAA;IAC5C,IAAI,CAAC,oBAAoB,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpC,OAAO;YACL,EAAE,EAAE,KAAK;YACT,YAAY,EAAE,IAAI;YAClB,EAAE;YACF,GAAG;YACH,OAAO,EAAE,mDAAmD,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,EAAE;SAClG,CAAA;IACH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,CAAA;AAC7E,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,IAAU;IAC/B,IAAI,EAAS,CAAA;IACb,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAA4B,CAAA;QACpF,MAAM,SAAS,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAClC,IAAI,OAAO,SAAS,KAAK,UAAU,EAAE,CAAC;YACpC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,IAAI,qCAAqC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAA;QAC1G,CAAC;QACD,EAAE,GAAG,SAAkB,CAAA;IACzB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,UAAU,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAA;IAClF,CAAC;IAED,IAAI,CAAC;QACH,iEAAiE;QACjE,sEAAsE;QACtE,uEAAuE;QACvE,qCAAqC;QACrC,MAAM,KAAK,GAAG,EAAE,EAAE,CAAA;QAClB,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;QACjC,IAAI,OAAO;YAAE,MAAM,KAAK,CAAA;QACxB,IAAI,GAAG,KAAK,CAAA;QAEZ,MAAM,KAAK,GAAG,OAAO;YACnB,CAAC,CAAC,CAAC,CAAS,EAAmB,EAAE,CAAC,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC;YACtD,CAAC,CAAC,CAAC,CAAS,EAAmB,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAA;QAExE,+DAA+D;QAC/D,oEAAoE;QACpE,oEAAoE;QACpE,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,QAAU,CAAA;QACjF,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,YAAY;YAAE,MAAM,KAAK,CAAC,EAAE,CAAC,CAAA;QAE9D,wEAAwE;QACxE,IAAI,CAAC,GAAG,CAAC,CAAA;QACT,SAAS,CAAC;YACR,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAA;YAC3B,IAAI,IAAI,IAAI,YAAY,IAAI,CAAC,IAAI,aAAa;gBAAE,MAAK;YACrD,CAAC,IAAI,CAAC,CAAA;QACR,CAAC;QAED,IAAI,OAAO,GAAG,EAAE,CAAA;QAChB,IAAI,UAAU,GAAG,CAAC,CAAA;QAClB,IAAI,OAAO,GAAG,CAAC,CAAA;QACf,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,QAAU,CAAA;QAChF,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,QAAQ,EAAE,CAAC;YAC1C,OAAO,IAAI,MAAM,KAAK,CAAC,CAAC,CAAC,CAAA;YACzB,UAAU,IAAI,CAAC,CAAA;YACf,OAAO,EAAE,CAAA;QACX,CAAC;QACD,IAAI,UAAU,KAAK,CAAC,EAAE,CAAC;YACrB,wEAAwE;YACxE,OAAO,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAA;YACxB,UAAU,GAAG,CAAC,CAAA;YACd,OAAO,GAAG,CAAC,CAAA;QACb,CAAC;QAED,sEAAsE;QACtE,sEAAsE;QACtE,kEAAkE;QAClE,sEAAsE;QACtE,uEAAuE;QACvE,sEAAsE;QACtE,uEAAuE;QACvE,uEAAuE;QACvE,oDAAoD;QACpD,IAAI,CAAC,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACjC,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,EAAE,EAAE,IAAI,CAAC,EAAE;gBACX,KAAK,EAAE,GAAG,IAAI,CAAC,MAAM,sOAAsO;aAC5P,CAAA;QACH,CAAC;QAED,OAAO;YACL,EAAE,EAAE,IAAI;YACR,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,UAAU;YACrC,UAAU;YACV,OAAO;YACP,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,SAAS;YACtC,oEAAoE;YACpE,yBAAyB;YACzB,QAAQ,EAAE,OAAO,IAAI;SACtB,CAAA;IACH,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAA;IAC7E,CAAC;AACH,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAC1B,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;QACvC,uEAAuE;QACvE,kEAAkE;QAClE,kCAAkC;QAClC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uCAAuC,MAAM,CAAC,OAAO,IAAI,CAAC,CAAA;QAC/E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IACD,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;QACd,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACzC,MAAM,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAA;QACxD,OAAM;IACR,CAAC;IACD,MAAM,SAAS,CACb,MAAM,CAAC,GAAG,EACV,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,uBAAuB,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC,CAC7F,CAAA;AACH,CAAC;AAED,MAAM,IAAI,EAAE,CAAA;AACZ,2EAA2E;AAC3E,4EAA4E;AAC5E,yEAAyE;AACzE,uEAAuE;AACvE,2EAA2E;AAC3E,0EAA0E;AAC1E,SAAS;AACT,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA"}
@@ -0,0 +1,61 @@
1
+ import { type BenchResult } from '../benchproto/types.js';
2
+ import { type ExecResult } from '../runner/exec.js';
3
+ /**
4
+ * Picks the measurement child from a directory, preferring the built
5
+ * `child.js` and falling back to the source `child.ts`.
6
+ *
7
+ * Under a real install (`dist/`), only `child.js` exists next to this
8
+ * module -- that is the branch that actually runs for an installed user.
9
+ * Under Vitest, `import.meta.url` resolves to `src/benchrun/invoke.ts`,
10
+ * where only `child.ts` exists -- there is no build step before the test
11
+ * run. Either way the spawn below runs through the tsx loader, so a `.ts`
12
+ * path works identically to a `.js` one; this just picks whichever file is
13
+ * actually on disk.
14
+ *
15
+ * Pulled apart from `resolveChild` (a thin wrapper below) so both branches
16
+ * can be exercised directly with an injected existence check, since only
17
+ * one of the two ever exists in any single checkout.
18
+ */
19
+ export declare function pickChildPath(dir: string, exists: (p: string) => boolean): string;
20
+ export declare const CHILD: string;
21
+ /**
22
+ * Reads and parses the child's `--out` file, turning every failure mode
23
+ * into `{ ok: false }` instead of a rejection: a missing file (the child
24
+ * died before writing at all) and a present-but-malformed one (the file
25
+ * exists but doesn't parse, or doesn't have the right shape).
26
+ *
27
+ * The malformed case is not exotic: our own timeout path kills the child's
28
+ * process group with SIGKILL, which can land mid-`writeFile`, leaving a
29
+ * truncated-but-readable JSON document on disk. `readFile` succeeds on
30
+ * that; only `JSON.parse`/`parseBenchResult` would have caught it, and
31
+ * uncaught, that throw would have propagated out of `runChild` as a
32
+ * rejected promise -- exactly the "harness bug" failure mode `runChild`
33
+ * exists to avoid for a benchmark-level failure.
34
+ *
35
+ * Exported for testing: it lets a test write deliberately truncated JSON
36
+ * to a real, controlled `--out` path and assert `ok: false` directly,
37
+ * without needing to race a real subprocess's SIGKILL against its own
38
+ * `writeFile` to reproduce the truncation.
39
+ */
40
+ export declare function readChildResult(outFile: string, id: string, r: Pick<ExecResult, 'timedOut' | 'exitCode' | 'stderr'>, timeoutMs: number): Promise<BenchResult>;
41
+ export interface RunChildOptions {
42
+ /** The worktree to measure in. Sets module resolution and tsconfig context. */
43
+ cwd: string;
44
+ benchFileAbs: string;
45
+ fn: string;
46
+ id: string;
47
+ benchtimeMs: number;
48
+ warmupMs: number;
49
+ timeoutMs: number;
50
+ nodeArgs: string[];
51
+ log?: ((s: string) => void) | undefined;
52
+ }
53
+ /**
54
+ * Runs one benchmark, once, in a fresh child process.
55
+ *
56
+ * Never rejects for a benchmark-level failure: a timeout, a crash, or a
57
+ * child that exits without writing --out all come back as `{ ok: false }`
58
+ * with a diagnosis built from the child's own stderr, so a caller can
59
+ * record a CRASH verdict instead of aborting the whole run.
60
+ */
61
+ export declare function runChild(o: RunChildOptions): Promise<BenchResult>;