initial commit
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
// Verify that a Bun binary doesn't use CPU instructions beyond its baseline target.
|
||||
//
|
||||
// Detects the platform and chooses the appropriate emulator:
|
||||
// Linux x64: QEMU with Nehalem CPU (no AVX)
|
||||
// Linux arm64: QEMU with Cortex-A53 (no LSE/SVE)
|
||||
// Windows x64: Intel SDE with -nhm (no AVX)
|
||||
//
|
||||
// Usage:
|
||||
// bun scripts/verify-baseline.ts --binary ./bun --arch x64 --emulator /usr/bin/qemu-x86_64
|
||||
// bun scripts/verify-baseline.ts --binary ./bun.exe --arch x64 --emulator ./sde.exe
|
||||
|
||||
import { readdirSync } from "node:fs";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
// @ts-ignore — utils.mjs has JSDoc types but no .d.ts
|
||||
import { markBuildkiteStepReported } from "./utils.mjs";
|
||||
|
||||
const { parseArgs } = require("node:util");
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
binary: { type: "string" },
|
||||
// Target arch of --binary ("x64" | "aarch64"). The host may differ (e.g.
|
||||
// verifying an x64 build from an arm64 CI host), so process.arch is only a
|
||||
// fallback for local dev.
|
||||
arch: { type: "string" },
|
||||
emulator: { type: "string" },
|
||||
"jit-stress": { type: "boolean", default: false },
|
||||
"skip-emulation": { type: "boolean", default: false },
|
||||
},
|
||||
strict: true,
|
||||
});
|
||||
|
||||
const binary = resolve(values.binary!);
|
||||
|
||||
function resolveEmulator(name: string): string {
|
||||
const found = Bun.which(name);
|
||||
if (found) return found;
|
||||
// Try without -static suffix (e.g. qemu-aarch64 instead of qemu-aarch64-static)
|
||||
if (name.endsWith("-static")) {
|
||||
const fallback = Bun.which(name.slice(0, -"-static".length));
|
||||
if (fallback) return fallback;
|
||||
}
|
||||
// Last resort: resolve as a path (absolute paths like C:\intel-sde\sde.exe
|
||||
// pass through unchanged, relative paths resolve against cwd)
|
||||
return resolve(name);
|
||||
}
|
||||
|
||||
const emulatorPath = resolveEmulator(values.emulator!);
|
||||
|
||||
const scriptDir = dirname(import.meta.path);
|
||||
const repoRoot = resolve(scriptDir, "..");
|
||||
const fixturesDir = join(repoRoot, "test", "js", "bun", "jsc-stress", "fixtures");
|
||||
const wasmFixturesDir = join(fixturesDir, "wasm");
|
||||
const preloadPath = join(repoRoot, "test", "js", "bun", "jsc-stress", "preload.js");
|
||||
|
||||
// Host OS (getVerifyBaselineHost() guarantees host OS == target OS here).
|
||||
const isWindows = process.platform === "win32";
|
||||
// Target arch (host arch may differ; --arch is explicit).
|
||||
const targetArch = values.arch ?? (process.arch === "arm64" ? "aarch64" : "x64");
|
||||
if (targetArch !== "x64" && targetArch !== "aarch64") {
|
||||
throw new Error(`--arch must be "x64" or "aarch64", got: ${targetArch}`);
|
||||
}
|
||||
const isAarch64 = targetArch === "aarch64";
|
||||
|
||||
// SDE outputs this when a chip-check violation occurs
|
||||
const SDE_VIOLATION_PATTERN = /SDE-ERROR:.*not valid for specified chip/i;
|
||||
|
||||
// Configure emulator based on platform
|
||||
const config = isWindows
|
||||
? {
|
||||
runnerCmd: [emulatorPath, "-nhm", "--"],
|
||||
cpuDesc: "Nehalem (SSE4.2, no AVX/AVX2/AVX512)",
|
||||
// SDE must run from its own directory for Pin DLL resolution
|
||||
cwd: dirname(emulatorPath),
|
||||
}
|
||||
: isAarch64
|
||||
? {
|
||||
runnerCmd: [emulatorPath, "-cpu", "cortex-a53"],
|
||||
cpuDesc: "Cortex-A53 (ARMv8.0-A+CRC, no LSE/SVE)",
|
||||
cwd: undefined,
|
||||
}
|
||||
: {
|
||||
runnerCmd: [emulatorPath, "-cpu", "Nehalem"],
|
||||
cpuDesc: "Nehalem (SSE4.2, no AVX/AVX2/AVX512)",
|
||||
cwd: undefined,
|
||||
};
|
||||
|
||||
function isInstructionViolation(signalCode: NodeJS.Signals | null, output: string): boolean {
|
||||
if (isWindows) return SDE_VIOLATION_PATTERN.test(output);
|
||||
// qemu-user re-raises the guest's fatal signal on the host, so the process is WIFSIGNALED:
|
||||
// `proc.exitCode` is null and only `proc.signalCode` carries the verdict.
|
||||
return signalCode === "SIGILL";
|
||||
}
|
||||
|
||||
console.log(`--- Verifying ${basename(binary)} on ${config.cpuDesc}`);
|
||||
console.log(` Binary: ${binary}`);
|
||||
console.log(` Emulator: ${config.runnerCmd.join(" ")}`);
|
||||
console.log();
|
||||
|
||||
let instructionFailures = 0;
|
||||
let otherFailures = 0;
|
||||
let passed = 0;
|
||||
const failedTests: string[] = [];
|
||||
|
||||
interface RunTestOptions {
|
||||
cwd?: string;
|
||||
/** Tee output live to the console while still capturing it for analysis */
|
||||
live?: boolean;
|
||||
}
|
||||
|
||||
/** Read a stream, write each chunk to a writable, and return the full text. */
|
||||
async function teeStream(stream: ReadableStream<Uint8Array>, output: NodeJS.WriteStream): Promise<string> {
|
||||
const chunks: Uint8Array[] = [];
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk);
|
||||
output.write(chunk);
|
||||
}
|
||||
return Buffer.concat(chunks).toString();
|
||||
}
|
||||
|
||||
async function runTest(label: string, binaryArgs: string[], options?: RunTestOptions): Promise<boolean> {
|
||||
console.log(`+++ ${label}`);
|
||||
|
||||
const start = performance.now();
|
||||
const live = options?.live ?? false;
|
||||
const proc = Bun.spawn([...config.runnerCmd, binary, ...binaryArgs], {
|
||||
// config.cwd takes priority — SDE on Windows must run from its own directory for Pin DLL resolution
|
||||
cwd: config.cwd ?? options?.cwd,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
let stdout: string;
|
||||
let stderr: string;
|
||||
if (live) {
|
||||
[stdout, stderr] = await Promise.all([
|
||||
teeStream(proc.stdout as ReadableStream<Uint8Array>, process.stdout),
|
||||
teeStream(proc.stderr as ReadableStream<Uint8Array>, process.stderr),
|
||||
proc.exited,
|
||||
]);
|
||||
} else {
|
||||
[stdout, stderr] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
}
|
||||
|
||||
const exitCode = proc.exitCode;
|
||||
const signalCode = proc.signalCode;
|
||||
const elapsed = ((performance.now() - start) / 1000).toFixed(1);
|
||||
const output = stdout + "\n" + stderr;
|
||||
|
||||
if (exitCode === 0) {
|
||||
if (!live && stdout.trim()) console.log(stdout.trim());
|
||||
console.log(` PASS (${elapsed}s)`);
|
||||
passed++;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isInstructionViolation(signalCode, output)) {
|
||||
if (!live && output.trim()) console.log(output.trim());
|
||||
console.log();
|
||||
console.log(` FAIL: CPU instruction violation detected (${elapsed}s)`);
|
||||
if (isAarch64) {
|
||||
console.log(" The aarch64 build targets Cortex-A53 (ARMv8.0-A+CRC).");
|
||||
console.log(" LSE atomics, SVE, and dotprod instructions are not allowed.");
|
||||
} else {
|
||||
console.log(" The baseline x64 build targets Nehalem (SSE4.2).");
|
||||
console.log(" AVX, AVX2, and AVX512 instructions are not allowed.");
|
||||
}
|
||||
instructionFailures++;
|
||||
failedTests.push(label);
|
||||
} else {
|
||||
if (!live && output.trim()) console.log(output.trim());
|
||||
const how = exitCode === null ? `signal ${signalCode}` : `exit code ${exitCode}`;
|
||||
console.log(` FAIL: ${how} (${elapsed}s, not a CPU instruction issue)`);
|
||||
otherFailures++;
|
||||
failedTests.push(label);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Phase 0: Static instruction scan (no emulation — disassembles the binary).
|
||||
// Catches instructions in code paths the emulator test below never executes.
|
||||
// Soft-skipped if the checker isn't built (dev without cargo).
|
||||
const staticCheckerExe = isWindows ? "verify-baseline-static.exe" : "verify-baseline-static";
|
||||
const staticChecker = join(scriptDir, "verify-baseline-static", "target", "release", staticCheckerExe);
|
||||
const staticAllowlistName = isWindows
|
||||
? "allowlist-x64-windows.txt"
|
||||
: isAarch64
|
||||
? "allowlist-aarch64.txt"
|
||||
: "allowlist-x64.txt";
|
||||
const staticAllowlist = join(scriptDir, "verify-baseline-static", staticAllowlistName);
|
||||
let staticViolations = "";
|
||||
|
||||
if (await Bun.file(staticChecker).exists()) {
|
||||
console.log("+++ Static instruction scan");
|
||||
const start = performance.now();
|
||||
const proc = Bun.spawn([staticChecker, "--binary", binary, "--allowlist", staticAllowlist], {
|
||||
stdout: "pipe",
|
||||
stderr: "inherit",
|
||||
});
|
||||
const [stdout, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
||||
const elapsed = ((performance.now() - start) / 1000).toFixed(1);
|
||||
console.log(stdout);
|
||||
|
||||
if (code === 0) {
|
||||
console.log(` PASS (${elapsed}s)`);
|
||||
passed++;
|
||||
} else if (code === 1) {
|
||||
// Pull just the VIOLATIONS block for the annotation — full stdout is in the log.
|
||||
const m = stdout.match(/^VIOLATIONS[^\n]*\n([\s\S]*?)\n(?:ALLOWLISTED|STALE|SUMMARY)/m);
|
||||
staticViolations = m ? m[1].trim() : stdout;
|
||||
console.log(` FAIL: static scan found post-baseline instructions outside the allowlist (${elapsed}s)`);
|
||||
instructionFailures++;
|
||||
failedTests.push("Static instruction scan");
|
||||
} else {
|
||||
console.log(` FAIL: checker exited ${code} (${elapsed}s, tool error)`);
|
||||
otherFailures++;
|
||||
failedTests.push("Static instruction scan (tool error)");
|
||||
}
|
||||
console.log();
|
||||
} else {
|
||||
console.log("--- Skipping static instruction scan (not built)");
|
||||
console.log(" cargo build --release --manifest-path scripts/verify-baseline-static/Cargo.toml");
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Phase 1/2: emulated runs. --skip-emulation (CI sets this for Android, whose
|
||||
// binary needs /system/bin/linker64 and has no sysroot on the build host)
|
||||
// leaves only the static scan.
|
||||
if (values["skip-emulation"]) {
|
||||
console.log("--- Skipping emulated runs (--skip-emulation)");
|
||||
} else {
|
||||
// Phase 1: SIMD code path verification
|
||||
const simdTestPath = join(repoRoot, "test", "js", "bun", "jsc-stress", "fixtures", "simd-baseline.test.ts");
|
||||
await runTest("SIMD baseline tests", ["test", simdTestPath], { live: true });
|
||||
}
|
||||
|
||||
// Phase 2: JIT stress fixtures (only with --jit-stress, e.g. on WebKit changes)
|
||||
if (values["skip-emulation"]) {
|
||||
// already reported the skip above
|
||||
} else if (values["jit-stress"]) {
|
||||
const jsFixtures = readdirSync(fixturesDir)
|
||||
.filter(f => f.endsWith(".js"))
|
||||
.sort();
|
||||
console.log();
|
||||
console.log(`--- JS fixtures (DFG/FTL) — ${jsFixtures.length} tests`);
|
||||
for (let i = 0; i < jsFixtures.length; i++) {
|
||||
const fixture = jsFixtures[i];
|
||||
await runTest(`[${i + 1}/${jsFixtures.length}] ${fixture}`, ["--preload", preloadPath, join(fixturesDir, fixture)]);
|
||||
}
|
||||
|
||||
const wasmFixtures = readdirSync(wasmFixturesDir)
|
||||
.filter(f => f.endsWith(".js"))
|
||||
.sort();
|
||||
console.log();
|
||||
console.log(`--- Wasm fixtures (BBQ/OMG) — ${wasmFixtures.length} tests`);
|
||||
for (let i = 0; i < wasmFixtures.length; i++) {
|
||||
const fixture = wasmFixtures[i];
|
||||
await runTest(
|
||||
`[${i + 1}/${wasmFixtures.length}] ${fixture}`,
|
||||
["--preload", preloadPath, join(wasmFixturesDir, fixture)],
|
||||
{ cwd: wasmFixturesDir },
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log();
|
||||
console.log("--- Skipping JIT stress fixtures (pass --jit-stress to enable)");
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log();
|
||||
console.log("--- Summary");
|
||||
console.log(` Passed: ${passed}`);
|
||||
console.log(` Instruction failures: ${instructionFailures}`);
|
||||
console.log(` Other failures: ${otherFailures} (not CPU instruction issues)`);
|
||||
console.log();
|
||||
|
||||
const platform = isWindows
|
||||
? isAarch64
|
||||
? "Windows aarch64"
|
||||
: "Windows x64"
|
||||
: isAarch64
|
||||
? "Linux aarch64"
|
||||
: "Linux x64";
|
||||
|
||||
function annotate(html: string) {
|
||||
Bun.spawnSync(["buildkite-agent", "annotate", "--append", "--style", "error", "--context", "verify-baseline"], {
|
||||
stdin: new Blob([html]),
|
||||
});
|
||||
// Suppress the generic fallback in .buildkite/hooks/pre-exit; this script
|
||||
// owns its failure annotation.
|
||||
markBuildkiteStepReported();
|
||||
}
|
||||
|
||||
if (instructionFailures > 0) {
|
||||
console.error(" FAILED: Code uses unsupported CPU instructions.");
|
||||
|
||||
const parts = [
|
||||
`<details open>`,
|
||||
`<summary>❌ CPU instruction violation on <b>${platform}</b> — ${instructionFailures} check(s) failed</summary>`,
|
||||
`<p>The baseline build contains instructions not available on <code>${config.cpuDesc}</code>.</p>`,
|
||||
`<ul>${failedTests.map(t => `<li><code>${t}</code></li>`).join("")}</ul>`,
|
||||
];
|
||||
if (staticViolations) {
|
||||
// Cap the inline block so a -march= leak (thousands of symbols) doesn't
|
||||
// produce a multi-MB annotation. Full output is in the step log.
|
||||
const lines = staticViolations.split("\n");
|
||||
const shown =
|
||||
lines.length > 80 ? [...lines.slice(0, 80), `... ${lines.length - 80} more lines (see step log)`] : lines;
|
||||
parts.push(
|
||||
`<h4>Static scan violations</h4>`,
|
||||
`<pre><code>${shown.join("\n").replace(/</g, "<")}</code></pre>`,
|
||||
`<p><b>If these are runtime-dispatched behind a CPUID gate:</b> add each symbol to`,
|
||||
`<code>scripts/verify-baseline-static/${staticAllowlistName}</code> with a comment pointing at the gate.`,
|
||||
`Feature ceilings (the <code>[FEAT, ...]</code> bracket) should list what the gate checks.</p>`,
|
||||
`<p><b>If there's no gate:</b> this is a real bug — a <code>-march</code> leaked into a subbuild.`,
|
||||
`Find the translation unit and fix its compile flags.</p>`,
|
||||
);
|
||||
}
|
||||
parts.push(`</details>`);
|
||||
annotate(parts.join("\n"));
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (otherFailures > 0) {
|
||||
console.error(` FAILED: ${otherFailures} check(s) failed under emulation on ${config.cpuDesc}.`);
|
||||
|
||||
annotate(
|
||||
[
|
||||
`<details open>`,
|
||||
`<summary>❌ Baseline verification failed on <b>${platform}</b> — ${otherFailures} check(s)</summary>`,
|
||||
`<p>The baseline build crashed or failed tests under <code>${config.cpuDesc}</code> emulation ` +
|
||||
`(not an unsupported-instruction fault). See the step log for the crash output.</p>`,
|
||||
`<ul>${failedTests.map(t => `<li><code>${t}</code></li>`).join("")}</ul>`,
|
||||
`</details>`,
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(` All baseline verification passed on ${config.cpuDesc}.`);
|
||||
Reference in New Issue
Block a user