Files
1a028f7f03 [internal] Add a skill for benching changes in a sandbox (#95943)
Vibed. This should help with running React PRs on sandboxes to measure
impact on `bench/`.

## Usage

```
use bench skill to to measure impact of https://github.com/react/react/pull/37066
```

- Should be able to point at React or Next PRs
- Compares against mergebase
- Grabs React builds from CI when possible
- Before benching, asserts both React builds are green, or that tests
pass
- Runs on many sandboxes with interleaved runs
- Caches reusable stuff (builds) between sandboxes
- Uses `bench/` examples with realistic payloads

## Test Plan

Running a bench:

<img width="1445" height="584" alt="Screenshot 2026-07-20 at 03 08 02"
src="https://github.com/user-attachments/assets/ac8b6aac-1e62-4695-9a59-fd0577a8a441"
/>

Bench that shows good results:

<img width="1078" height="725" alt="Screenshot 2026-07-20 at 03 46 20"
src="https://github.com/user-attachments/assets/ce49a806-f1b1-497c-a3dd-e5f8d08ea25b"
/>

Neutral bench:

<img width="1032" height="455" alt="Screenshot 2026-07-20 at 03 47 27"
src="https://github.com/user-attachments/assets/de3b700c-4875-4c70-b1c1-55cdddd73a1a"
/>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-25 17:46:38 +01:00

87 lines
2.9 KiB
JavaScript

// List/remove leaked bench sandboxes. Every VM the skill's scripts
// create is named sbench-* and tagged purpose=sandbox-bench; the
// harnesses remove their VMs in finally blocks, but a killed local
// process leaks them until their timeout.
//
// The project may be shared, so removal is deliberately conservative:
// a VM is only listed when its name matches, its tag matches, AND it is
// older than --min-age-hours (default 3 — longer than any healthy run),
// and removal is per exact listed name.
//
// node sandbox-sweep.mjs # list what would be removed
// node sandbox-sweep.mjs --yes # remove
// node sandbox-sweep.mjs --min-age-hours 0 # include fresh VMs (danger:
// # only when nothing is active)
import { execFile } from 'child_process'
import { promisify } from 'util'
import { loadConfig, sandboxScope } from './config.mjs'
const execFileP = promisify(execFile)
const CONFIG = loadConfig()
const SCOPE = sandboxScope(CONFIG)
const argv = process.argv.slice(2)
const minAgeHours = argv.includes('--min-age-hours')
? Number(argv[argv.indexOf('--min-age-hours') + 1])
: 3
// Age from the CLI's "CREATED" column ("32 minutes ago", "2 hours ago").
function ageHours(words) {
const i = words.findIndex((w) => w === 'ago')
if (i < 2) return Infinity
const n = Number(words[i - 2])
const unit = words[i - 1]
if (!Number.isFinite(n)) return Infinity
if (unit.startsWith('second')) return n / 3600
if (unit.startsWith('minute')) return n / 60
if (unit.startsWith('hour')) return n
return n * 24
}
// Paginate: the CLI prints a cursor hint line when more pages exist.
const found = []
let cursor
for (let page = 0; page < 50; page++) {
const args = ['sandbox', 'list', '--limit', '100', ...SCOPE]
if (cursor) args.push('--cursor', cursor)
const { stdout, stderr } = await execFileP(CONFIG.vercelBin, args, {
maxBuffer: 1 << 24,
})
for (const line of stdout.split('\n')) {
const words = line.trim().split(/\s+/)
const name = words[0]
if (!/^sbench-/.test(name)) continue
if (!line.includes('purpose:sandbox-bench')) continue
const age = ageHours(words)
if (age < minAgeHours) {
console.log(
`skipping ${name} (${age.toFixed(1)}h old < --min-age-hours ${minAgeHours})`
)
continue
}
found.push(name)
}
cursor = `${stdout}\n${stderr}`.match(/--cursor\s+(\S+)/)?.[1]
if (!cursor) break
}
if (!found.length) {
console.log('no leaked bench sandboxes found')
process.exit(0)
}
const doIt = argv.includes('--yes')
let failures = 0
for (const name of found) {
if (doIt) {
try {
await execFileP(CONFIG.vercelBin, ['sandbox', 'rm', name, ...SCOPE])
console.log(`removed ${name}`)
} catch (e) {
failures++
console.error(`could not remove ${name}: ${e.message.split('\n')[0]}`)
}
} else {
console.log(`would remove ${name} (pass --yes)`)
}
}
process.exit(failures ? 1 : 0)