Files
pnpm/.github/scripts/compare-integrated-benchmarks.test.mjs
Zoltan Kochan adb6411d85 ci(benchmark): add the store-only install against a mostly reusable lockfile (#16895)
None of the integrated benchmark's scenarios resolved against a lockfile
the walk could mostly reuse, installed store-only (`enableModulesDir:
false`), or read the per-file store index. The install regressions fixed
in pnpm/pnpm#16886, pnpm/pnpm#16888 and pnpm/pnpm#16890 all sat in that shape; the
benchmark would have missed each of them, and would miss their reversal.

The new `store-only.unlocked-direct-deps.cold-cache.hot-store` scenario
seeds the static fixture's lockfile with the direct dependencies whose
locked version carries a peer suffix left out of the importer, while
`packages` and `snapshots` keep the whole graph. That is `pnpm install`
after dependencies were added to `package.json` by hand: the missing
direct dependencies resolve from the registry, land on versions the
graph holds, and the walk reuses their subtrees. The choice of the
peered ones is deliberate: a dependency without peers is taken from the
lockfile's package list without a registry request, and a dependency
whose specifier merely changed is reused as long as the recorded version
satisfies the new range, so neither exercises the fresh pick that the
subtree reuse then follows. The metadata cache is wiped per iteration,
so every package the walk does resolve costs a round trip over the
emulated registry link; with a hot cache on local disk the whole
speculative warm-up costs about 25 ms and sits inside the noise. The
lockfile is trusted (`trustLockfile: true`): its supply-chain
verification reads every entry's packument and left nothing for the
resolution to fetch, hiding the difference entirely. With
`enableModulesDir: false` nothing is linked, so the rest of the timed
work is the check that the store holds every package, read through
`storeIndexFormat: files`.
`--no-frozen-lockfile` is passed explicitly because the runners set
`CI`, where a plain `install` is frozen and would reject the lockfile.
Only `cache-dir` is removed and the lockfile restored between
iterations; `node_modules` is never created and the warm store stays.

The scenario runs direct targets only, like the workspace fixtures: a
pnpr server would take over the resolution being measured. The
comparison script and its matrix-coverage test, the Bencher upload and
the summary list it alongside the others. Every `cleanup` arm now goes
through a `Cleanup` constructor, which keeps the method within the
function-length limit with the new arm.

Measured locally (hyperfine, 5 runs, main vs the commit before pnpm/pnpm#16886):
    main                     1.008 s +- 0.025 s   (0.991 .. 1.052 s), 513 packuments
    before pnpm/pnpm#16886            1.148 s +- 0.026 s   (1.131 .. 1.194 s), 840 packuments
2026-10-13 08:53:08 +02:00

130 lines
6.5 KiB
JavaScript

import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { test } from 'node:test'
import { compare, compareConfirmed, renderComparison, scenarios } from './compare-integrated-benchmarks.mjs'
function command (name, times) {
return { command: name, times, exit_codes: times.map(() => 0) }
}
function report (head, base) {
return { results: [command('pacquet@HEAD', head), command('pacquet@main', base)] }
}
const stable = Array(10).fill(1)
test('a slower machine affects both revisions without causing an alert', () => {
assert.equal(compare(report(stable.map(x => x * 3.1), stable.map(x => x * 3)), 'pacquet').status, 'Within tolerance')
})
test('a consistent regression fails, an improvement passes', () => {
assert.equal(compare(report(Array(9).fill(1.3), Array(9).fill(1)), 'pacquet').status, 'Regression')
assert.equal(compare(report(stable, stable.map(x => x * 1.3)), 'pacquet').status, 'Within tolerance')
})
test('multi-second scenarios catch consistent slowdowns below 20%', () => {
const result = compare(report(Array(10).fill(3.18), Array(10).fill(3)), 'pacquet')
assert.equal(result.status, 'Regression')
assert.equal(result.tolerance, 3 * 0.05)
})
test('short scenarios tolerate up to 2 ms but still catch larger slowdowns', () => {
const base = Array(10).fill(0.008)
assert.equal(compare(report(Array(10).fill(0.009), base), 'pacquet').status, 'Within tolerance')
assert.equal(compare(report(Array(10).fill(0.010), base), 'pacquet').status, 'Within tolerance')
const result = compare(report(Array(10).fill(0.011), base), 'pacquet')
assert.equal(result.status, 'Regression')
assert.equal(result.tolerance, 0.002)
})
test('exactly 5% is tolerated when the relative threshold is larger', () => {
assert.equal(compare(report(Array(10).fill(1.05), stable), 'pacquet').status, 'Within tolerance')
})
test('a noisy slowdown is explicitly inconclusive', () => {
assert.equal(compare(report([1, 1, 1.3, 1.3, 1.3, 1.3, 1.4, 1.4, 2, 3], stable), 'pacquet').status, 'Inconclusive (overlapping samples)')
})
test('isolated outliers neither create nor conceal a regression', () => {
assert.equal(compare(report([...stable.slice(1), 10], stable), 'pacquet').status, 'Within tolerance')
assert.equal(compare(report([0.5, ...Array(9).fill(1.3)], [...stable.slice(1), 10]), 'pacquet').status, 'Regression')
})
test('missing, duplicate, failed, undersampled and invalid data fail closed', () => {
const valid = report(stable, stable)
const invalid = [
{ results: [] },
{ results: [...valid.results, valid.results[0]] },
report(stable.slice(0, 8), stable),
report([...stable.slice(1), NaN], stable),
report([...stable.slice(1), 0], stable),
{ results: [{ ...valid.results[0], exit_codes: [0] }, valid.results[1]] },
{ results: [{ ...valid.results[0], exit_codes: [1, ...Array(9).fill(0)] }, valid.results[1]] },
]
for (const input of invalid) assert.throws(() => compare(input, 'pacquet'))
})
test('all scenarios and both engines are checked, and missing reports fail', async () => {
const directory = await mkdtemp(join(tmpdir(), 'benchmark-comparison-'))
try {
for (const scenario of scenarios) {
const input = report(stable, stable)
if (!scenario.includes('PEER_HEAVY') && !scenario.includes('LINKED_WORKSPACE') && !scenario.includes('INJECTED_WORKSPACE') && !scenario.includes('STORE_ONLY')) {
input.results.push(command('pnpr@HEAD', Array(10).fill(1.4)), command('pnpr@main', stable))
}
input.confirmation = { results: input.results }
await writeFile(join(directory, `BENCHMARK_REPORT_${scenario}.json`), JSON.stringify(input))
}
const result = await renderComparison(directory)
assert.equal(result.failed, true)
assert.equal(result.markdown.match(/\| Regression \|/g).length, 10)
assert.equal(result.markdown.match(/\| Within tolerance \|/g).length, 14)
const cli = new URL('./compare-integrated-benchmarks.mjs', import.meta.url)
const run = spawnSync(process.execPath, [fileURLToPath(cli), directory], { encoding: 'utf8' })
assert.equal(run.status, 1)
assert.match(run.stdout, /Regression/)
assert.equal(run.stderr, '')
for (const scenario of scenarios) {
const input = { results: ['pacquet', 'pnpr'].flatMap(engine => [command(`${engine}@HEAD`, stable), command(`${engine}@main`, stable)]) }
input.confirmation = { results: input.results }
await writeFile(join(directory, `BENCHMARK_REPORT_${scenario}.json`), JSON.stringify(input))
}
assert.equal(spawnSync(process.execPath, [fileURLToPath(cli), directory]).status, 0)
await rm(join(directory, `BENCHMARK_REPORT_${scenarios[0]}.json`))
await assert.rejects(renderComparison(directory), /ENOENT/)
} finally {
await rm(directory, { recursive: true, force: true })
}
})
test('comparison and Bencher uploads cover every matrix scenario', async () => {
const workflow = await readFile(new URL('../workflows/pacquet-integrated-benchmark.yml', import.meta.url), 'utf8')
const tags = [...workflow.matchAll(/'([A-Z_]+):[a-z][^']+'/g)].map(match => match[1])
const matrix = workflow.match(/ scenario:\n((?: - [^\n]+\n)+)/)[1]
const matrixTags = [...matrix.matchAll(/- ([a-z][\w.-]+)/g)].map(match => match[1]
.replace('-linker.', '_').replaceAll(/[.-]/g, '_').toUpperCase())
assert.deepEqual([...new Set(tags)].sort(), [...matrixTags].sort())
assert.deepEqual([...scenarios].sort(), [...matrixTags].sort())
})
test('hyperfine command names identify targets when commands are shell scripts', () => {
const input = report(stable, stable)
input.results = input.results.map(result => ({ ...result, command_name: result.command, command: '/tmp/run.sh' }))
assert.equal(compare(input, 'pacquet').status, 'Within tolerance')
})
test('regressions must reproduce in the confirmation run', () => {
const input = report(Array(10).fill(1.3), stable)
assert.throws(() => compareConfirmed(input, 'pacquet'), /Missing confirmation/)
input.confirmation = report(stable, stable)
assert.equal(compareConfirmed(input, 'pacquet').status, 'Inconclusive (not reproduced)')
input.confirmation = report(Array(10).fill(1.3), stable)
assert.equal(compareConfirmed(input, 'pacquet').status, 'Regression')
input.confirmation = report([], stable)
assert.throws(() => compareConfirmed(input, 'pacquet'), /samples/)
})