|
| 1 | +/* eslint-disable n/no-process-exit */ |
| 2 | +/** |
| 3 | + * Benchmark comparison script using mitata. |
| 4 | + * |
| 5 | + * Copies the base branch's source to a temp directory, installs its |
| 6 | + * dependencies, then runs the mitata bench script with --control-dir so that |
| 7 | + * both control (base) and experiment (current) plugins are benchmarked in the |
| 8 | + * same process — giving mitata a fair, head-to-head comparison with built-in |
| 9 | + * summary tables and boxplots. |
| 10 | + * |
| 11 | + * Usage: |
| 12 | + * node scripts/bench-compare.mjs [--base <branch>] |
| 13 | + * |
| 14 | + * Options: |
| 15 | + * --base <branch> Branch to compare against (default: master) |
| 16 | + */ |
| 17 | + |
| 18 | +import { execSync, spawnSync } from 'node:child_process'; |
| 19 | +import { existsSync, mkdirSync, rmSync } from 'node:fs'; |
| 20 | +import { tmpdir } from 'node:os'; |
| 21 | +import { join } from 'node:path'; |
| 22 | + |
| 23 | +// --------------------------------------------------------------------------- |
| 24 | +// CLI args |
| 25 | +// --------------------------------------------------------------------------- |
| 26 | + |
| 27 | +const args = process.argv.slice(2); |
| 28 | +const baseIdx = args.indexOf('--base'); |
| 29 | +const BASE_BRANCH = baseIdx !== -1 ? args[baseIdx + 1] : 'master'; |
| 30 | + |
| 31 | +// --------------------------------------------------------------------------- |
| 32 | +// Helpers |
| 33 | +// --------------------------------------------------------------------------- |
| 34 | + |
| 35 | +function run(cmd, opts = {}) { |
| 36 | + return execSync(cmd, { stdio: 'inherit', ...opts }); |
| 37 | +} |
| 38 | + |
| 39 | +/** |
| 40 | + * Resolve a branch name to a commit SHA. Tries `origin/<branch>` first (for CI |
| 41 | + * where only the PR branch is checked out locally), then falls back to `<branch>`. |
| 42 | + */ |
| 43 | +function resolveRef(branch) { |
| 44 | + for (const candidate of [`origin/${branch}`, branch]) { |
| 45 | + const result = spawnSync('git', ['rev-parse', '--verify', candidate], { |
| 46 | + encoding: 'utf8', |
| 47 | + stdio: ['pipe', 'pipe', 'pipe'], |
| 48 | + }); |
| 49 | + if (result.status === 0) return result.stdout.trim(); |
| 50 | + } |
| 51 | + throw new Error(`Could not resolve ref for branch "${branch}". Is it fetched?`); |
| 52 | +} |
| 53 | + |
| 54 | +// --------------------------------------------------------------------------- |
| 55 | +// Main |
| 56 | +// --------------------------------------------------------------------------- |
| 57 | + |
| 58 | +const ROOT = process.cwd(); |
| 59 | +const CONTROL_DIR = join(tmpdir(), `bench-control-${BASE_BRANCH}-${Date.now()}`); |
| 60 | + |
| 61 | +console.error(`\n🔧 Setting up control (${BASE_BRANCH}) in ${CONTROL_DIR}\n`); |
| 62 | + |
| 63 | +const BASE_REF = resolveRef(BASE_BRANCH); |
| 64 | +console.error(` Resolved ${BASE_BRANCH} → ${BASE_REF.slice(0, 10)}\n`); |
| 65 | + |
| 66 | +// Clean up temp dir on exit |
| 67 | +function cleanup() { |
| 68 | + if (existsSync(CONTROL_DIR)) { |
| 69 | + try { |
| 70 | + rmSync(CONTROL_DIR, { recursive: true, force: true }); |
| 71 | + } catch { |
| 72 | + // best-effort cleanup |
| 73 | + } |
| 74 | + } |
| 75 | +} |
| 76 | +process.on('exit', cleanup); |
| 77 | +process.on('SIGINT', () => process.exit(130)); |
| 78 | +process.on('SIGTERM', () => process.exit(143)); |
| 79 | + |
| 80 | +try { |
| 81 | + // ── 1. Export base branch source to temp dir ───────────────────────────── |
| 82 | + mkdirSync(CONTROL_DIR, { recursive: true }); |
| 83 | + |
| 84 | + // Copy package manifests and source (use resolved SHA for reliability) |
| 85 | + run( |
| 86 | + `git archive ${BASE_REF} -- package.json pnpm-lock.yaml .npmrc lib/ | tar -x -C "${CONTROL_DIR}"` |
| 87 | + ); |
| 88 | + |
| 89 | + // ── 2. Install dependencies in control dir ─────────────────────────────── |
| 90 | + console.error(`\n📦 Installing dependencies for control (${BASE_BRANCH})…\n`); |
| 91 | + try { |
| 92 | + run('pnpm install --frozen-lockfile', { |
| 93 | + cwd: CONTROL_DIR, |
| 94 | + stdio: ['inherit', 'pipe', 'inherit'], |
| 95 | + }); |
| 96 | + } catch { |
| 97 | + console.error('⚠️ Frozen install failed, retrying without --frozen-lockfile…\n'); |
| 98 | + run('pnpm install --no-frozen-lockfile', { |
| 99 | + cwd: CONTROL_DIR, |
| 100 | + stdio: ['inherit', 'pipe', 'inherit'], |
| 101 | + }); |
| 102 | + } |
| 103 | + |
| 104 | + // ── 3. Run mitata bench with --control-dir ─────────────────────────────── |
| 105 | + console.error(`\n🏎️ Running benchmarks (experiment vs control)…\n`); |
| 106 | + |
| 107 | + const benchScript = join(ROOT, 'tests/lint.bench.mjs'); |
| 108 | + const benchArgs = [ |
| 109 | + '--expose-gc', |
| 110 | + '--max-old-space-size=4096', |
| 111 | + benchScript, |
| 112 | + '--control-dir', |
| 113 | + CONTROL_DIR, |
| 114 | + ]; |
| 115 | + |
| 116 | + // CPU pinning on Linux to reduce cross-core migration variance |
| 117 | + const IS_LINUX = process.platform === 'linux'; |
| 118 | + const HAS_TASKSET = IS_LINUX && spawnSync('which', ['taskset'], { stdio: 'pipe' }).status === 0; |
| 119 | + |
| 120 | + let cmd = 'node'; |
| 121 | + let fullArgs = benchArgs; |
| 122 | + |
| 123 | + if (HAS_TASKSET) { |
| 124 | + cmd = 'taskset'; |
| 125 | + fullArgs = ['-c', '0', 'node', ...benchArgs]; |
| 126 | + console.error('📌 CPU pinning enabled (taskset -c 0)\n'); |
| 127 | + } |
| 128 | + |
| 129 | + const result = spawnSync(cmd, fullArgs, { |
| 130 | + stdio: 'inherit', |
| 131 | + cwd: ROOT, |
| 132 | + env: { ...process.env }, |
| 133 | + }); |
| 134 | + |
| 135 | + if (result.status !== 0) { |
| 136 | + console.error('\n❌ Benchmark run failed.'); |
| 137 | + process.exit(1); |
| 138 | + } |
| 139 | + |
| 140 | + console.error('\n✅ Benchmark comparison complete.\n'); |
| 141 | +} catch (e) { |
| 142 | + console.error('❌ Error:', e.message); |
| 143 | + process.exit(1); |
| 144 | +} |
0 commit comments