test-performance · git:20261015.dc45e41 · 2026-10-15 · sha256 8c537388db1126c5
test-performance git:20261015.dc45e41A
Immutable. This exact content never changes and is served at /api/v1/blob/8c537388db1126c5.
--- name: test-performance description: Use this when writing or changing a test, a test helper, or a vitest config in seedcord, and when a test is slow or fails only in the full suite. Covers the shared-worker setup every package runs on, cleaning up state with Disposable handles, keeping expensive setup rare, and checking a change for leaks. --- # Test performance Every package's tests run in shared workers, inside a workspace run that already uses most of the machine. A test that leaks state breaks a file that runs after it. A test that boots, builds, or typechecks more than it needs keeps every core busy for longer. Both show up in `pnpm prePush` as flakes and spinning fans. ## How the workers run `@seedcord/vitest-config` sets these for every package. Leave them alone in a package's own config. apps/docs is the one exception, with `isolate: true` until its tests stop mocking its own modules. - `isolate: false`. A worker process runs many test files in turn. - A setup file calls `vi.resetModules()` before each file. Each file gets fresh instances of the modules vitest runs, so a static cache or a module-level singleton stays inside its file. - `maxWorkers: '25%'` when `CI` is unset, with `concurrency: "2"` in `turbo.json`. Together they keep about half of a dev machine's cores free. CI runners keep vitest's default. The reset covers only modules vitest runs itself. Everything below survives into the next file in the same worker: - `globalThis` slots, like the ones a built bot or baked ids use - `process.env` and anything else on `process`, such as `cwd` - open servers, sockets, timers, and child processes - files written to disk, including copies under `tests/temp` - packages in `node_modules`, which Node loads once A package from `node_modules` that keeps state across calls goes in `server.deps.inline` in the shared config. vitest then runs it, and the reset reloads it for each file. envapt is there because it caches `process.env` on first read. `@testing-library/react` is there because it registers its auto cleanup when it loads. ## Mocks The reset leaves vitest's mock registry alone. A `vi.mock` in one test file replaces that module for every later file in the same worker, so `vi.mock` and `vi.doMock` go only in `tests/setup.ts`. That includes helpers a test file imports. - A module every test in the package needs faked goes in `tests/setup.ts`, mocked the same way for every file. See the REST fake in `packages/http/tests/setup.ts` and its `restClients()` helper. - A test that needs to read or change one call spies on the class with `vi.spyOn(SomeClass.prototype, 'method')` and restores it in `afterEach`. A spy patches the class this file loaded and leaves the module registry alone. - A test that needs its own behavior passes the fake in, through a constructor argument or a parameter. `TypeChecker` takes its `tsc` runner this way, and the create-seedcord steps take their prompt streams. ## Clean up with Disposable handles Anything a test creates outside its own module gets cleaned up by a handle the test holds with `using` or `await using`. - A helper that creates a resource returns it as a `Disposable` or `AsyncDisposable`, with cleanup in `[Symbol.dispose]` or `[Symbol.asyncDispose]`. See `registerBuiltFiles` in `packages/http/tests/helpers/builtFiles.ts`. - A helper with several setup steps builds an `AsyncDisposableStack`, `defer`s each cleanup as it goes, and returns `stack.move()` at the end. A step that throws then cleans up the steps before it. See `edgeProject` and `startedRuntime` in `cli/seedcord/tests/dev/edgeDevRuntime.ts`. - `using` disposes in reverse order of declaration. Declare the thing that writes into a resource after the resource, so it stops first. - `await using` disposes when the test body ends, which is before any `onTestFinished` hook runs. Cleanup that has to run before a `using` resource is released is itself a `using` declaration. - A `using` binding the test never reads takes a leading underscore, as in `await using _started = ...`. - Node objects that already implement disposal, like `http.Server`, need no wrapper. A package whose tests write a global slot also clears that slot in its setup file, so a test that forgot its handle still leaves the next file clean. `packages/http/tests/setup.ts` does this. ## Keep expensive setup rare Real builds, `tsc`, workerd boots, and typed lint programs cost seconds of CPU each. Pay for one only where the test checks what it produces. - Tests that only read a booted runtime or a built project share one per file, built in `beforeAll` and disposed by the function `beforeAll` returns. Tests that edit files, change env, or start and stop on purpose keep their own. - cli build tests use `quickBuildRunner()` from `cli/seedcord/tests/commands/build/quickBuildRunner.ts`, which skips `tsc`. `BuildRunner.create` runs the real check, for the type-error test, one passing build, and builds that fail before the check runs. - Work that only CI needs, like compiling a bun binary, checks `process.env.CI` through one helper (`compilesBunBinaries`). Move a test to CI only when a CI job already runs it on every push. - typescript-eslint's `RuleTester` clears the parser caches after every file. The typed rule testers replace `RuleTester.afterAll` with a no-op so each worker builds the typed program once. ## Write tests that hold under load The full suite runs with the machine busy, so a step that takes 3ms alone can take 30ms there. - Wait on the condition the test checks, with `vi.waitFor` or the cli's `settled` helper. A fixed sleep is too short under load and too slow when idle. - When a test masks output that varies, like a right-aligned duration, replace the value and its padding with a string of the same width. The column stays checked, and the value's length stops mattering. - To reproduce a flake that only shows under load, inject the delay where it happens, like a fake REST reply that waits 15ms. Saturating the CPU rarely reproduces it. - Ink draws at most one frame every 34ms, the throttle from its default `maxFps` of 30. A prompt test asserts the expected screen inside `settled`, which retries until that frame is drawn. ## Check a change Run these before calling a test change done: 1. `pnpm -C <pkg> exec vitest run --maxWorkers=1`. One worker runs every file in one process, which shows a cross-file leak in a single run. In packages/http, add `--project node`, since the workerd pool isolates its own files. 2. Run it with `--sequence.shuffle.files --sequence.seed=<n>` for two or three seeds. CI groups files into workers differently from your machine, and a leak that depends on file order shows up under a different order. 3. Confirm `tests/temp` is empty afterwards. `pnpm clean:scratch` clears what a killed run left behind. To find where a suite spends its time, read [`PROFILING.md`](PROFILING.md).