Restore sandbox test performance (#86)

* Restore sandbox test performance

- Re-enable secure native compiler caching for repo-local test artifacts.
- Isolate strict cache coverage while keeping it off the sandbox critical path.

* fix: respect sandbox test runtime limits
This commit is contained in:
Chris Tate
2026-08-03 01:03:34 -05:00
committed by GitHub
parent 52459a86d4
commit 22666584df
4 changed files with 100 additions and 14 deletions
+17
View File
@@ -29,6 +29,23 @@ export function sandboxRunnerConfig(env) {
};
}
/** Reserve one Vitest worker for each concurrently scheduled side process
* without ever starving the case-sharded corpus or exceeding the configured
* per-Sandbox worker budget. Extra side processes share the reserved slots. */
export function sandboxTestWorkerAllocation(workerCount, sideTaskCount) {
if (!Number.isInteger(workerCount) || workerCount < 1) {
throw new Error("workerCount must be a positive integer");
}
if (!Number.isInteger(sideTaskCount) || sideTaskCount < 0) {
throw new Error("sideTaskCount must be a non-negative integer");
}
const sideConcurrency = Math.min(sideTaskCount, Math.max(0, workerCount - 1));
return {
caseWorkers: workerCount - sideConcurrency,
sideConcurrency,
};
}
export function sandboxImageConfig() {
loadLocalEnv();
// Keep tenancy explicit: a public clone must use a Vercel project its
+47 -12
View File
@@ -10,6 +10,7 @@ import { fileURLToPath } from "node:url";
import {
sandboxImageConfig,
sandboxRunnerConfig,
sandboxTestWorkerAllocation,
} from "./sandbox-config.mjs";
import {
sandboxHostSchedule,
@@ -32,9 +33,14 @@ const laneCaseShardedFiles = [
// checked, but running the same sweep in the second lane adds no coverage.
const invariantCaseShardedFiles = ["tests/harness/coverage.test.ts"];
// Native-cache invalidation cases mutate process-wide compiler inputs and
// therefore stay serial inside a worker. They are sanitizer-invariant, but
// use their own shard variable so the independent cases can spread across the
// once lane's Sandboxes without colliding with corpus case selection.
// deliberately disable the immutable-toolchain memo used by the differential
// corpus. Keep them in their own Vitest process, matching CI: co-scheduling
// this file with the corpus can consume one of the corpus workers with the
// strict (and intentionally expensive) production probe path.
//
// They are sanitizer-invariant and use their own shard variable so the
// independent cases can spread across the once lane's Sandboxes without
// colliding with corpus case selection.
const cacheCaseShardedFiles = ["packages/compiler/src/backend/cc-cache.test.ts"];
const caseShardedFiles = [
...laneCaseShardedFiles,
@@ -192,7 +198,6 @@ const remoteWorkerCount = Number(testWorkers);
if (!Number.isInteger(remoteWorkerCount) || remoteWorkerCount < 1) {
throw new Error(`SCRIPTC_TEST_WORKERS must be a positive integer (got ${testWorkers})`);
}
const caseWorkers = String(Math.max(1, remoteWorkerCount - 1));
const fileWorkers = "1";
const localCaseShardCount = Number(localCaseShards);
if (!Number.isInteger(localCaseShardCount) || localCaseShardCount < 1) {
@@ -512,6 +517,12 @@ async function allWorkers(phase, task, concurrency = workers.length) {
console.log(`${phase} completed in ${((Date.now() - started) / 1000).toFixed(1)}s`);
}
async function runTaskQueue(tasks, concurrency) {
for (let offset = 0; offset < tasks.length; offset += concurrency) {
await Promise.all(tasks.slice(offset, offset + concurrency).map((task) => task()));
}
}
async function cleanup() {
if (values.keep || created.size === 0) return;
if (!cleanupPromise) {
@@ -619,10 +630,13 @@ try {
};
const workerCaseFiles = [
...laneCaseShardedFiles,
...(worker.lane === onceLane
? [...invariantCaseShardedFiles, ...cacheCaseShardedFiles]
: []),
...(worker.lane === onceLane ? invariantCaseShardedFiles : []),
];
const runCacheCases = worker.lane === onceLane;
const { caseWorkers, sideConcurrency } = sandboxTestWorkerAllocation(
remoteWorkerCount,
runCacheCases ? 2 : 1,
);
const cases = () =>
execIn(
worker,
@@ -631,8 +645,7 @@ try {
{
...sharedTestEnv,
SCRIPTC_TEST_SHARD: `${worker.shard}/${shardCount}`,
SCRIPTC_CACHE_TEST_SHARD: `${worker.shard}/${shardCount}`,
SCRIPTC_TEST_WORKERS: caseWorkers,
SCRIPTC_TEST_WORKERS: String(caseWorkers),
},
"cases",
);
@@ -662,11 +675,33 @@ try {
},
"files",
);
if (remoteWorkerCount === 1) {
const cacheCases = () =>
execIn(
worker,
"pnpm",
["test", "--reporter=dot", ...cacheCaseShardedFiles],
{
...sharedTestEnv,
SCRIPTC_CACHE_TEST_SHARD: `${worker.shard}/${shardCount}`,
// This is the strict production-path contract. Keep it out of
// the memoized corpus process and pin the opt-out explicitly,
// just as the GitHub Actions matrix does.
SCRIPTC_TEST_STABLE_TOOLCHAIN: "0",
SCRIPTC_TEST_WORKERS: "1",
},
"cache",
);
// The corpus owns the remaining pool while file/cache processes share
// the reserved side slots (serially when only one slot is available).
const sideTasks = [files, ...(runCacheCases ? [cacheCases] : [])];
if (sideConcurrency === 0) {
await cases();
await files();
await runTaskQueue(sideTasks, 1);
} else {
await Promise.all([cases(), files()]);
await Promise.all([
cases(),
runTaskQueue(sideTasks, sideConcurrency),
]);
}
});