@qvac/harness 1.0.0-rc.2 → 1.0.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -12
- package/bench/README.md +94 -0
- package/bench/bfcl/checker.ts +313 -0
- package/bench/bfcl/data.ts +93 -0
- package/bench/bfcl/observe.ts +93 -0
- package/bench/bfcl/run.ts +213 -0
- package/bench/bfcl/tools.ts +72 -0
- package/bench/bridge/bridge.ts +392 -0
- package/bench/bridge/openai.ts +150 -0
- package/bench/bridge/python/qvac_bench/__init__.py +3 -0
- package/bench/bridge/python/qvac_bench/airline.py +42 -0
- package/bench/bridge/python/qvac_bench/approval.py +26 -0
- package/bench/bridge/python/qvac_bench/bfcl_multi_turn.py +152 -0
- package/bench/bridge/python/qvac_bench/client.py +88 -0
- package/bench/bridge/python/qvac_bench/harbor_agent.py +85 -0
- package/bench/bridge/python/qvac_bench/session.py +107 -0
- package/bench/bridge/python/qvac_bench/speed.py +30 -0
- package/bench/bridge/python/qvac_bench/tau2_agent.py +106 -0
- package/bench/bridge/python/qvac_bench/tools.py +11 -0
- package/bench/bridge/python/run_bfcl_multi_turn.py +95 -0
- package/bench/bridge/python/run_tau2.py +150 -0
- package/bench/bridge/python/smoke.py +47 -0
- package/bench/bridge/python/test_airline.py +28 -0
- package/bench/bridge/python/test_approval.py +57 -0
- package/bench/bridge/python/test_bfcl_multi_turn.py +185 -0
- package/bench/bridge/python/test_harbor_agent.py +107 -0
- package/bench/bridge/serve.ts +52 -0
- package/bench/bridge/transcript.ts +52 -0
- package/bench/bun-tsconfig.json +15 -0
- package/bench/cpu-main.mjs +216 -0
- package/bench/cpu.ts +457 -0
- package/bench/evidence/2026-09-28-review15.md +225 -0
- package/bench/evidence/redact-bun1.3.14.json +92 -0
- package/bench/evidence/redact-node24.11.json +92 -0
- package/bench/graph-metrics.ts +43 -0
- package/bench/harness.ts +73 -0
- package/bench/issue129-matrix.mjs +722 -0
- package/bench/redact-adversarial.ts +256 -0
- package/bench/repeat-cpu.mjs +281 -0
- package/bench/report.ts +1075 -0
- package/bench/skill-eval/harness.ts +30 -0
- package/bench/skill-eval/laws.ts +37 -0
- package/bench/skill-eval/mermaid/client.ts +132 -0
- package/bench/skill-eval/mermaid/validate.mjs +115 -0
- package/bench/skill-eval/pack.ts +61 -0
- package/bench/skill-eval/packs/diagrams/cases.ts +281 -0
- package/bench/skill-eval/packs/diagrams/index.ts +50 -0
- package/bench/skill-eval/packs/diagrams/score.ts +74 -0
- package/bench/skill-eval/packs/index.ts +5 -0
- package/bench/skill-eval/packs/weather/cases.ts +279 -0
- package/bench/skill-eval/packs/weather/fixtures/wttr.json +566 -0
- package/bench/skill-eval/packs/weather/fixtures.ts +84 -0
- package/bench/skill-eval/packs/weather/index.ts +55 -0
- package/bench/skill-eval/packs/weather/score.ts +92 -0
- package/bench/skill-eval/report.ts +194 -0
- package/bench/skill-eval/run.ts +316 -0
- package/bench/speed.ts +80 -0
- package/package.json +4 -13
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
// Reproduce the 33-cell Issue 129 matrix; --dry-run and --prepare-only never measure.
|
|
2
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import {
|
|
5
|
+
cpSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
mkdtempSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
realpathSync,
|
|
12
|
+
renameSync,
|
|
13
|
+
symlinkSync,
|
|
14
|
+
writeFileSync
|
|
15
|
+
} from 'node:fs'
|
|
16
|
+
import os from 'node:os'
|
|
17
|
+
import path from 'node:path'
|
|
18
|
+
import { fileURLToPath } from 'node:url'
|
|
19
|
+
|
|
20
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
21
|
+
const baselineRevisions = [
|
|
22
|
+
'7d4a1ec282ed29b2480b551e6a4670d448441e63',
|
|
23
|
+
'8cbf419e15e6b13aee118f601b3282749cdf7e01'
|
|
24
|
+
]
|
|
25
|
+
const nodeDefault = '/tmp/node-v24.11.0-darwin-arm64/bin/node'
|
|
26
|
+
const omittedDirectories = new Set([
|
|
27
|
+
'.git',
|
|
28
|
+
'.turbo',
|
|
29
|
+
'.cache',
|
|
30
|
+
'node_modules',
|
|
31
|
+
'coverage',
|
|
32
|
+
'dist',
|
|
33
|
+
'dist-lib'
|
|
34
|
+
])
|
|
35
|
+
const options = parseOptions(process.argv.slice(2))
|
|
36
|
+
const schedule = makeSchedule(options.seed)
|
|
37
|
+
const nodeModules = realpathSync(
|
|
38
|
+
options.nodeModules ?? process.env.QVAC_MATRIX_NODE_MODULES ?? path.join(root, 'node_modules')
|
|
39
|
+
)
|
|
40
|
+
const nodeBinary = path.resolve(options.node ?? process.env.QVAC_NODE24_BIN ?? nodeDefault)
|
|
41
|
+
const bunBinary = options.bun ?? process.env.QVAC_BUN_BIN ?? 'bun'
|
|
42
|
+
const bareRuntimeBinary = path.resolve(
|
|
43
|
+
options.bareRuntime ??
|
|
44
|
+
process.env.QVAC_BARE_RUNTIME_BIN ??
|
|
45
|
+
path.join(nodeModules, 'bare-runtime/bin/bare')
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if (options.dryRun) {
|
|
49
|
+
console.log(JSON.stringify({ seed: options.seed, jobCount: schedule.length, schedule }))
|
|
50
|
+
process.exit(0)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const scratch = mkdtempSync(path.join(os.tmpdir(), 'qvac-issue129-matrix-'))
|
|
54
|
+
const candidateRoot = path.join(scratch, 'candidate')
|
|
55
|
+
const baselineRoots = new Map()
|
|
56
|
+
prepareCandidate(candidateRoot)
|
|
57
|
+
for (const revision of baselineRevisions) {
|
|
58
|
+
const baselineRoot = path.join(scratch, `baseline-${revision.slice(0, 7)}`)
|
|
59
|
+
prepareBaseline(revision, baselineRoot)
|
|
60
|
+
baselineRoots.set(revision, baselineRoot)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const runtimes = {
|
|
64
|
+
node: { executable: nodeBinary, version: versionOf(nodeBinary, ['--version']) },
|
|
65
|
+
bun: { executable: bunBinary, version: versionOf(bunBinary, ['--version']) },
|
|
66
|
+
bare: {
|
|
67
|
+
executable: bareRuntimeBinary,
|
|
68
|
+
version: versionOf(bareRuntimeBinary, ['--version'])
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const sourceManifests = {
|
|
72
|
+
candidate: sourceManifest(candidateRoot),
|
|
73
|
+
...Object.fromEntries(
|
|
74
|
+
[...baselineRoots].map(([revision, baselineRoot]) => [revision, sourceManifest(baselineRoot)])
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (options.prepareOnly) {
|
|
79
|
+
console.log(
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
scratch,
|
|
82
|
+
runtimes,
|
|
83
|
+
dependencyGraph: {
|
|
84
|
+
nodeModules,
|
|
85
|
+
packageManifestSha256: hashFile(path.resolve(nodeModules, 'package.json')),
|
|
86
|
+
lockfileSha256: hashFile(path.resolve(nodeModules, '../bun.lock')),
|
|
87
|
+
installedPackages: installedPackageVersions(nodeModules, [
|
|
88
|
+
'xstate',
|
|
89
|
+
'@qvac/sdk',
|
|
90
|
+
'@qvac/inference',
|
|
91
|
+
'b4a',
|
|
92
|
+
'bare-runtime',
|
|
93
|
+
'typescript'
|
|
94
|
+
])
|
|
95
|
+
},
|
|
96
|
+
sourceManifests
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
process.exit(0)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const outputPath = path.resolve(options.output)
|
|
103
|
+
const partialPath = `${outputPath}.partial.json`
|
|
104
|
+
mkdirSync(path.dirname(outputPath), { recursive: true })
|
|
105
|
+
|
|
106
|
+
const report = {
|
|
107
|
+
name: 'Issue 129 reproducible benchmark matrix',
|
|
108
|
+
status: 'running',
|
|
109
|
+
createdAt: new Date().toISOString(),
|
|
110
|
+
method: {
|
|
111
|
+
unit: 'process CPU milliseconds',
|
|
112
|
+
workload:
|
|
113
|
+
'tools: 101 messages (~200 KB), 3 rounds x 2 read tools x 2 KB results then answer; stream: 2,000 one-character tokens, 110 ms first-token gap, immediate setImmediate or timeout(0) spacing',
|
|
114
|
+
warmup: 'one untimed selected workload pass per process before three timed samples',
|
|
115
|
+
processReplicates: 3,
|
|
116
|
+
timedSamplesPerProcess: 3,
|
|
117
|
+
aggregate: 'pooled median of nine raw cpuSamplesMs values; process medians retained separately',
|
|
118
|
+
interleaving: `deterministic Fisher-Yates shuffle with seed ${options.seed}`
|
|
119
|
+
},
|
|
120
|
+
host: { platform: process.platform, architecture: process.arch, controller: process.version },
|
|
121
|
+
runtimes,
|
|
122
|
+
dependencyGraph: {
|
|
123
|
+
nodeModules,
|
|
124
|
+
packageManifestSha256: hashFile(path.resolve(nodeModules, 'package.json')),
|
|
125
|
+
lockfileSha256: hashFile(path.resolve(nodeModules, '../bun.lock')),
|
|
126
|
+
installedPackages: installedPackageVersions(nodeModules, [
|
|
127
|
+
'xstate',
|
|
128
|
+
'@qvac/sdk',
|
|
129
|
+
'@qvac/inference',
|
|
130
|
+
'b4a',
|
|
131
|
+
'bare-runtime',
|
|
132
|
+
'typescript'
|
|
133
|
+
]),
|
|
134
|
+
note: 'All snapshots and runtimes use the same selected frozen dependency graph, symlinked into each snapshot. Baseline source is historical, but baseline dependencies are not reinstalled from each historical lockfile.'
|
|
135
|
+
},
|
|
136
|
+
snapshots: {
|
|
137
|
+
directory: scratch,
|
|
138
|
+
candidate: sourceManifests.candidate,
|
|
139
|
+
baselines: Object.fromEntries(
|
|
140
|
+
baselineRevisions.map((revision) => [revision, sourceManifests[revision]])
|
|
141
|
+
),
|
|
142
|
+
overlay: [
|
|
143
|
+
'bench/cpu-main.mjs',
|
|
144
|
+
'bench/repeat-cpu.mjs',
|
|
145
|
+
'bench/bun-tsconfig.json',
|
|
146
|
+
"candidate-only generated Bare entrypoint: import 'bare-encoding/global'; import('./cpu.ts')"
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
schedule,
|
|
150
|
+
execution: [],
|
|
151
|
+
cells: []
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
writePartial()
|
|
155
|
+
for (let index = 0; index < schedule.length; index++) {
|
|
156
|
+
const job = schedule[index]
|
|
157
|
+
const result = runJob(job, index)
|
|
158
|
+
report.execution.push(result)
|
|
159
|
+
report.cells = aggregateCells(report.execution)
|
|
160
|
+
writePartial()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
report.cells.length !== 33 ||
|
|
165
|
+
report.cells.some((cell) => cell.processCount !== 3 || cell.rawSampleCount !== 9)
|
|
166
|
+
) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
'The matrix is incomplete: expected 33 cells with three processes and nine raw samples each'
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
report.status = 'complete'
|
|
173
|
+
report.completedAt = new Date().toISOString()
|
|
174
|
+
writePartial()
|
|
175
|
+
renameSync(partialPath, outputPath)
|
|
176
|
+
console.log(
|
|
177
|
+
JSON.stringify({ status: report.status, output: outputPath, jobCount: schedule.length })
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
function writePartial() {
|
|
181
|
+
writeFileSync(partialPath, `${JSON.stringify(report, null, 2)}\n`)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function prepareCandidate(destination) {
|
|
185
|
+
mkdirSync(destination, { recursive: true })
|
|
186
|
+
cpSync(root, destination, {
|
|
187
|
+
recursive: true,
|
|
188
|
+
filter(source) {
|
|
189
|
+
const relative = path.relative(root, source)
|
|
190
|
+
if (relative === '') {
|
|
191
|
+
return true
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const parts = relative.split(path.sep)
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
!parts.some((part) => omittedDirectories.has(part) || part.startsWith('.')) &&
|
|
198
|
+
!/\.(?:pem|key)$/i.test(relative)
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
writeFileSync(
|
|
203
|
+
path.join(destination, 'bench/cpu-bare-runner.mjs'),
|
|
204
|
+
"import 'bare-encoding/global'\nawait import('./cpu.ts')\n"
|
|
205
|
+
)
|
|
206
|
+
linkNodeModules(destination, nodeModules)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function prepareBaseline(revision, destination) {
|
|
210
|
+
mkdirSync(destination, { recursive: true })
|
|
211
|
+
const archive = execFileSync('git', ['archive', '--format=tar', revision], {
|
|
212
|
+
cwd: root,
|
|
213
|
+
maxBuffer: 512 * 1024 * 1024
|
|
214
|
+
})
|
|
215
|
+
const extracted = spawnSync('tar', ['-xf', '-', '-C', destination], {
|
|
216
|
+
input: archive,
|
|
217
|
+
encoding: 'utf8'
|
|
218
|
+
})
|
|
219
|
+
if (extracted.error) {
|
|
220
|
+
throw extracted.error
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (extracted.status !== 0) {
|
|
224
|
+
throw new Error(`Could not extract ${revision}: ${extracted.stderr}`)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const relative of [
|
|
228
|
+
'bench/cpu-main.mjs',
|
|
229
|
+
'bench/repeat-cpu.mjs',
|
|
230
|
+
'bench/bun-tsconfig.json'
|
|
231
|
+
]) {
|
|
232
|
+
const target = path.join(destination, relative)
|
|
233
|
+
mkdirSync(path.dirname(target), { recursive: true })
|
|
234
|
+
cpSync(path.join(root, relative), target)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
linkNodeModules(destination, nodeModules)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function linkNodeModules(destination, graphNodeModules) {
|
|
241
|
+
const target = path.join(destination, 'node_modules')
|
|
242
|
+
if (!existsSync(target)) {
|
|
243
|
+
symlinkSync(graphNodeModules, target, 'dir')
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function sourceManifest(directory) {
|
|
248
|
+
const files = []
|
|
249
|
+
listFiles(directory, '', files)
|
|
250
|
+
files.sort((left, right) => left.localeCompare(right))
|
|
251
|
+
const hash = createHash('sha256')
|
|
252
|
+
let byteCount = 0
|
|
253
|
+
const entries = []
|
|
254
|
+
for (const relative of files) {
|
|
255
|
+
const content = readFileSync(path.join(directory, relative))
|
|
256
|
+
const sha256 = createHash('sha256').update(content).digest('hex')
|
|
257
|
+
hash.update(relative).update('\0').update(sha256).update('\n')
|
|
258
|
+
byteCount += content.byteLength
|
|
259
|
+
if (relative.startsWith('bench/') || relative === 'package.json' || relative === 'bun.lock') {
|
|
260
|
+
entries.push({ path: relative, sha256 })
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
revision: directory === candidateRoot ? currentRevision() : null,
|
|
266
|
+
treeSha256: hash.digest('hex'),
|
|
267
|
+
fileCount: files.length,
|
|
268
|
+
byteCount,
|
|
269
|
+
benchmarkFiles: entries
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function listFiles(directory, relative, output) {
|
|
274
|
+
for (const entry of readdirSync(path.join(directory, relative), { withFileTypes: true })) {
|
|
275
|
+
if (entry.name.startsWith('.')) {
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const childRelative = path.join(relative, entry.name)
|
|
280
|
+
if (entry.isDirectory()) {
|
|
281
|
+
if (!omittedDirectories.has(entry.name)) {
|
|
282
|
+
listFiles(directory, childRelative, output)
|
|
283
|
+
}
|
|
284
|
+
} else if (entry.isFile()) {
|
|
285
|
+
output.push(childRelative.split(path.sep).join('/'))
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function currentRevision() {
|
|
291
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim()
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function hashFile(filename) {
|
|
295
|
+
return createHash('sha256').update(readFileSync(filename)).digest('hex')
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function makeSchedule(seed) {
|
|
299
|
+
const cells = [...baselineCells(), ...candidateCells(), ...bareCells()]
|
|
300
|
+
|
|
301
|
+
if (cells.length !== 33) {
|
|
302
|
+
throw new Error(`Expected 33 cells, got ${cells.length}`)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return shuffleJobs(
|
|
306
|
+
cells.flatMap((cell) => [1, 2, 3].map((replicate) => ({ ...cell, replicate }))),
|
|
307
|
+
seed
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function baselineCells() {
|
|
312
|
+
const cells = []
|
|
313
|
+
for (const revision of baselineRevisions) {
|
|
314
|
+
for (const runtime of ['node', 'bun']) {
|
|
315
|
+
cells.push({
|
|
316
|
+
kind: 'baseline',
|
|
317
|
+
revision,
|
|
318
|
+
runtime,
|
|
319
|
+
mode: 'main',
|
|
320
|
+
workload: 'tools',
|
|
321
|
+
spacing: null
|
|
322
|
+
})
|
|
323
|
+
for (const spacing of ['immediate', 'timeout']) {
|
|
324
|
+
cells.push({
|
|
325
|
+
kind: 'baseline',
|
|
326
|
+
revision,
|
|
327
|
+
runtime,
|
|
328
|
+
mode: 'main',
|
|
329
|
+
workload: 'stream',
|
|
330
|
+
spacing
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return cells
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function candidateCells() {
|
|
340
|
+
const cells = []
|
|
341
|
+
for (const runtime of ['node', 'bun']) {
|
|
342
|
+
for (const mode of ['plain', 'bench', 'production']) {
|
|
343
|
+
cells.push({
|
|
344
|
+
kind: 'candidate',
|
|
345
|
+
revision: 'candidate',
|
|
346
|
+
runtime,
|
|
347
|
+
mode,
|
|
348
|
+
workload: 'tools',
|
|
349
|
+
spacing: null
|
|
350
|
+
})
|
|
351
|
+
for (const spacing of ['immediate', 'timeout']) {
|
|
352
|
+
cells.push({
|
|
353
|
+
kind: 'candidate',
|
|
354
|
+
revision: 'candidate',
|
|
355
|
+
runtime,
|
|
356
|
+
mode,
|
|
357
|
+
workload: 'stream',
|
|
358
|
+
spacing
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return cells
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function bareCells() {
|
|
368
|
+
const cells = []
|
|
369
|
+
for (const workload of ['tools', 'stream']) {
|
|
370
|
+
const spacings = workload === 'tools' ? [null] : ['immediate', 'timeout']
|
|
371
|
+
for (const spacing of spacings) {
|
|
372
|
+
cells.push({
|
|
373
|
+
kind: 'candidate',
|
|
374
|
+
revision: 'candidate',
|
|
375
|
+
runtime: 'bare',
|
|
376
|
+
mode: 'production',
|
|
377
|
+
workload,
|
|
378
|
+
spacing
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return cells
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function shuffleJobs(jobs, seed) {
|
|
387
|
+
let state = seedHash(seed)
|
|
388
|
+
for (let index = jobs.length - 1; index > 0; index--) {
|
|
389
|
+
state ^= state << 13
|
|
390
|
+
state ^= state >>> 17
|
|
391
|
+
state ^= state << 5
|
|
392
|
+
const swapIndex = (state >>> 0) % (index + 1)
|
|
393
|
+
;[jobs[index], jobs[swapIndex]] = [jobs[swapIndex], jobs[index]]
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return jobs
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function seedHash(value) {
|
|
400
|
+
let hash = 2166136261
|
|
401
|
+
for (const character of value) {
|
|
402
|
+
hash = Math.imul(hash ^ character.charCodeAt(0), 16777619)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return hash >>> 0 || 1
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function runJob(job, scheduleIndex) {
|
|
409
|
+
const directory = job.kind === 'candidate' ? candidateRoot : baselineRoots.get(job.revision)
|
|
410
|
+
const invocation = createInvocation(job, directory)
|
|
411
|
+
const startedAt = new Date().toISOString()
|
|
412
|
+
const started = process.hrtime.bigint()
|
|
413
|
+
const child = spawnSync(invocation.executable, invocation.args, {
|
|
414
|
+
cwd: invocation.cwd,
|
|
415
|
+
encoding: 'utf8',
|
|
416
|
+
env: {
|
|
417
|
+
...process.env,
|
|
418
|
+
PATH: prependExecutableDirectory(job.runtime, process.env.PATH ?? ''),
|
|
419
|
+
QVAC_NO_SKILL_BIN_PROBE: '1'
|
|
420
|
+
},
|
|
421
|
+
maxBuffer: 32 * 1024 * 1024
|
|
422
|
+
})
|
|
423
|
+
const wallMs = Number(process.hrtime.bigint() - started) / 1e6
|
|
424
|
+
if (child.error) {
|
|
425
|
+
throw child.error
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (child.status !== 0) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`${invocation.executable} exited ${child.status} for ${cellKey(job)}:\n${child.stderr || child.stdout}`
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const parsed = parseLastJson(child.stdout)
|
|
435
|
+
const result =
|
|
436
|
+
job.runtime === 'bare' ? readBareResult(parsed, job) : readRepeatResult(parsed, job)
|
|
437
|
+
assertRuntimeVersion(parsed, job)
|
|
438
|
+
|
|
439
|
+
return reportJob({ job, invocation, scheduleIndex, startedAt, wallMs, parsed, result })
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function createInvocation(job, directory) {
|
|
443
|
+
const script = path.join(directory, 'bench/repeat-cpu.mjs')
|
|
444
|
+
if (job.runtime === 'bare') {
|
|
445
|
+
return {
|
|
446
|
+
executable: bareRuntimeBinary,
|
|
447
|
+
args: [
|
|
448
|
+
'bench/cpu-bare-runner.mjs',
|
|
449
|
+
'3',
|
|
450
|
+
`--mode=${job.mode}`,
|
|
451
|
+
`--spacing=${job.spacing ?? 'immediate'}`,
|
|
452
|
+
`--workload=${job.workload}`,
|
|
453
|
+
'--json'
|
|
454
|
+
],
|
|
455
|
+
cwd: directory
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const args = [
|
|
460
|
+
`--runtime=${job.runtime}`,
|
|
461
|
+
`--driver=${job.kind === 'baseline' ? 'main' : 'engine'}`,
|
|
462
|
+
'--processes=1',
|
|
463
|
+
'--runs=3',
|
|
464
|
+
`--spacing=${job.spacing ?? 'immediate'}`,
|
|
465
|
+
`--workload=${job.workload}`,
|
|
466
|
+
`--revision=${job.revision}`
|
|
467
|
+
]
|
|
468
|
+
if (job.kind !== 'baseline') {
|
|
469
|
+
args.push(`--mode=${job.mode}`)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
executable: nodeBinary,
|
|
474
|
+
args: ['--experimental-strip-types', script, ...args],
|
|
475
|
+
cwd: directory
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function assertRuntimeVersion(report, job) {
|
|
480
|
+
if (report.version !== runtimes[job.runtime].version) {
|
|
481
|
+
throw new Error(`Runtime version changed during ${cellKey(job)}: ${report.version}`)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function reportJob({ job, invocation, scheduleIndex, startedAt, wallMs, parsed, result }) {
|
|
486
|
+
return {
|
|
487
|
+
order: scheduleIndex + 1,
|
|
488
|
+
startedAt,
|
|
489
|
+
wallMs,
|
|
490
|
+
key: cellKey(job),
|
|
491
|
+
replicate: job.replicate,
|
|
492
|
+
command: { executable: invocation.executable, args: invocation.args, cwd: invocation.cwd },
|
|
493
|
+
runtime: parsed.runtime,
|
|
494
|
+
version: parsed.version,
|
|
495
|
+
rawCpuSamplesMs: result.cpuSamplesMs,
|
|
496
|
+
processMedianMs: result.cpuMs,
|
|
497
|
+
logBytes: result.logBytes ?? null,
|
|
498
|
+
records: result.records ?? null,
|
|
499
|
+
contentEvents: result.contentEvents
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function readRepeatResult(report, job) {
|
|
504
|
+
if (
|
|
505
|
+
report.runtime !== job.runtime ||
|
|
506
|
+
!Array.isArray(report.samples) ||
|
|
507
|
+
report.samples.length !== 1
|
|
508
|
+
) {
|
|
509
|
+
throw new Error(`Unexpected repeat-cpu report for ${cellKey(job)}`)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const process = report.samples[0]
|
|
513
|
+
const expectedMode = job.kind === 'baseline' ? 'main' : job.mode
|
|
514
|
+
const result = process?.results?.[0]
|
|
515
|
+
validateResult(result, job, expectedMode)
|
|
516
|
+
if (!Array.isArray(result.cpuSamplesMs) || result.cpuSamplesMs.length !== 3) {
|
|
517
|
+
throw new Error(`Expected three raw samples in ${cellKey(job)}`)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
validateSamples(result, cellKey(job))
|
|
521
|
+
|
|
522
|
+
return result
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function readBareResult(report, job) {
|
|
526
|
+
if (report.runtime !== 'bare' || !Array.isArray(report.results) || report.results.length !== 1) {
|
|
527
|
+
throw new Error(`Unexpected Bare report for ${cellKey(job)}`)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const result = report.results[0]
|
|
531
|
+
validateResult(result, job, job.mode)
|
|
532
|
+
if (!Array.isArray(result.cpuSamplesMs) || result.cpuSamplesMs.length !== 3) {
|
|
533
|
+
throw new Error(`Expected three raw Bare samples in ${cellKey(job)}`)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
validateSamples(result, cellKey(job))
|
|
537
|
+
|
|
538
|
+
return result
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function validateSamples(result, key) {
|
|
542
|
+
if (
|
|
543
|
+
!Array.isArray(result.cpuSamplesMs) ||
|
|
544
|
+
!result.cpuSamplesMs.every((sample) => Number.isFinite(sample) && sample >= 0) ||
|
|
545
|
+
result.cpuMs !== median(result.cpuSamplesMs)
|
|
546
|
+
) {
|
|
547
|
+
throw new Error(`Invalid per-process raw CPU samples in ${key}`)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function validateResult(result, job, mode) {
|
|
552
|
+
const carriesLogMetrics = job.kind !== 'baseline'
|
|
553
|
+
if (
|
|
554
|
+
!result ||
|
|
555
|
+
result.workload !== job.workload ||
|
|
556
|
+
result.mode !== mode ||
|
|
557
|
+
result.runs !== 3 ||
|
|
558
|
+
!Number.isFinite(result.cpuMs) ||
|
|
559
|
+
(carriesLogMetrics &&
|
|
560
|
+
(!Number.isFinite(result.logBytes) || !Number.isFinite(result.records))) ||
|
|
561
|
+
(!carriesLogMetrics && (result.logBytes !== undefined || result.records !== undefined))
|
|
562
|
+
) {
|
|
563
|
+
throw new Error(`Incomplete benchmark result for ${cellKey(job)}`)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (
|
|
567
|
+
job.workload === 'stream' &&
|
|
568
|
+
(result.contentEvents !== 2000 || result.spacing !== job.spacing)
|
|
569
|
+
) {
|
|
570
|
+
throw new Error(`Stream workload did not preserve events/spacing for ${cellKey(job)}`)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function parseLastJson(stdout) {
|
|
575
|
+
for (const line of stdout.trim().split(/\r?\n/).reverse()) {
|
|
576
|
+
try {
|
|
577
|
+
const value = JSON.parse(line)
|
|
578
|
+
if (value && typeof value === 'object') {
|
|
579
|
+
return value
|
|
580
|
+
}
|
|
581
|
+
} catch {}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
throw new Error('Benchmark process did not emit a JSON report')
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function aggregateCells(execution) {
|
|
588
|
+
const cells = new Map()
|
|
589
|
+
for (const sample of execution) {
|
|
590
|
+
let cell = cells.get(sample.key)
|
|
591
|
+
if (!cell) {
|
|
592
|
+
cell = { key: sample.key, processes: [] }
|
|
593
|
+
cells.set(sample.key, cell)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
cell.processes.push(sample)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return [...cells.values()].map((cell) => {
|
|
600
|
+
if (cell.processes.length > 3) {
|
|
601
|
+
throw new Error(`Too many process samples for ${cell.key}`)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const raw = cell.processes.flatMap((process) => process.rawCpuSamplesMs)
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
key: cell.key,
|
|
608
|
+
processCount: cell.processes.length,
|
|
609
|
+
rawSampleCount: raw.length,
|
|
610
|
+
rawCpuSamplesMs: raw,
|
|
611
|
+
cpuMs: raw.length === 9 ? median(raw) : null,
|
|
612
|
+
processMedianMs: cell.processes.map((process) => process.processMedianMs),
|
|
613
|
+
logBytes: medianOrNull(cell.processes.map((process) => process.logBytes)),
|
|
614
|
+
records: medianOrNull(cell.processes.map((process) => process.records)),
|
|
615
|
+
contentEvents: cell.processes[0]?.contentEvents
|
|
616
|
+
}
|
|
617
|
+
})
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function cellKey(job) {
|
|
621
|
+
return [job.kind, job.revision, job.runtime, job.mode, job.workload, job.spacing ?? ''].join(':')
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function median(values) {
|
|
625
|
+
const sorted = [...values].sort((left, right) => left - right)
|
|
626
|
+
return sorted[Math.floor(sorted.length / 2)]
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function medianOrNull(values) {
|
|
630
|
+
const finite = values.filter(Number.isFinite)
|
|
631
|
+
return finite.length ? median(finite) : null
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function installedPackageVersions(graphNodeModules, names) {
|
|
635
|
+
return Object.fromEntries(
|
|
636
|
+
names.map((name) => {
|
|
637
|
+
const manifestPath = path.join(graphNodeModules, name, 'package.json')
|
|
638
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
639
|
+
return [name, manifest.version]
|
|
640
|
+
})
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function prependExecutableDirectory(runtime, existingPath) {
|
|
645
|
+
if (runtime === 'node') {
|
|
646
|
+
return existingPath
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const executable = runtime === 'bun' ? bunBinary : bareRuntimeBinary
|
|
650
|
+
if (!executable.includes(path.sep)) {
|
|
651
|
+
return existingPath
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return `${path.dirname(path.resolve(executable))}${path.delimiter}${existingPath}`
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function versionOf(executable, args) {
|
|
658
|
+
const result = spawnSync(executable, args, { encoding: 'utf8', maxBuffer: 1024 * 1024 })
|
|
659
|
+
if (result.error) {
|
|
660
|
+
throw result.error
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (result.status !== 0) {
|
|
664
|
+
throw new Error(`Could not read runtime version from ${executable}`)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return result.stdout.trim().split(/\r?\n/)[0]
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function parseOptions(args) {
|
|
671
|
+
const result = {
|
|
672
|
+
seed: 'issue129-final-2026-09',
|
|
673
|
+
node: undefined,
|
|
674
|
+
bun: undefined,
|
|
675
|
+
nodeModules: undefined,
|
|
676
|
+
bareRuntime: undefined,
|
|
677
|
+
output: path.join(root, 'output/benchmarks/issue-129/final-matrix.json'),
|
|
678
|
+
dryRun: false,
|
|
679
|
+
prepareOnly: false
|
|
680
|
+
}
|
|
681
|
+
for (const arg of args) {
|
|
682
|
+
const [key, value] = arg.split('=', 2)
|
|
683
|
+
if (arg === '--dry-run') {
|
|
684
|
+
result.dryRun = true
|
|
685
|
+
} else if (arg === '--prepare-only') {
|
|
686
|
+
result.prepareOnly = true
|
|
687
|
+
} else if (key === '--seed' && value) {
|
|
688
|
+
result.seed = value
|
|
689
|
+
} else if (key === '--node' && value) {
|
|
690
|
+
result.node = value
|
|
691
|
+
} else if (key === '--bun' && value) {
|
|
692
|
+
result.bun = value
|
|
693
|
+
} else if (key === '--node-modules' && value) {
|
|
694
|
+
result.nodeModules = path.resolve(value)
|
|
695
|
+
} else if (key === '--bare-runtime' && value) {
|
|
696
|
+
result.bareRuntime = value
|
|
697
|
+
} else if (key === '--output' && value) {
|
|
698
|
+
result.output = value
|
|
699
|
+
} else {
|
|
700
|
+
throw new Error(`Unknown option: ${arg}`)
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (!existsSync(optionsNodePath(result))) {
|
|
705
|
+
throw new Error(`Node 24 binary not found: ${optionsNodePath(result)}`)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const graphNodeModules = path.resolve(
|
|
709
|
+
result.nodeModules ?? process.env.QVAC_MATRIX_NODE_MODULES ?? path.join(root, 'node_modules')
|
|
710
|
+
)
|
|
711
|
+
if (!existsSync(graphNodeModules)) {
|
|
712
|
+
throw new Error(`Validated dependency graph not found: ${graphNodeModules}`)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
result.nodeModules = realpathSync(graphNodeModules)
|
|
716
|
+
|
|
717
|
+
return result
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function optionsNodePath(result) {
|
|
721
|
+
return path.resolve(result.node ?? process.env.QVAC_NODE24_BIN ?? nodeDefault)
|
|
722
|
+
}
|