zerospikes

Limits

Each scenario gets a limit per metric the first time it runs, from its slowest run plus room for noise. They're saved in __limits__/<spec>.json, next to the spec.

Lowering and accepting

Limits only go down on their own terms:

You made it…RunOr in npx zerospikes
Fasternpx playwright test -ut
Slower, on purposeZS_ALLOW_WORSE=1 npx playwright test -uw

-u alone never raises a limit, so it can't hide a slowdown by accident.

Other machines

Limits record the machine they were measured on: CPU, cores, platform, browser version, headless or not. On another machine they aren't checked: the scenario shows SKIP and passes, and -u records that machine's own.

Upgrading Playwright changes the browser version, so the same happens: run -u once after an upgrade to check against limits again.

So commit __limits__ when one machine records them for everyone. On hosted CI runners, whose hardware changes, use zerospikes compare instead.

Fixed limits

A limit of your own, checked on every machine on top of the recorded ones:

await zs.measure("open menu", open, { limits: { "frame.max": 50 } });
await zs.cycles("open and close", cycle, { limits: { "memory.nodes": 0 } });

Errors in the page

A scenario that throws halfway can look faster than one that works. If the page logs a console.error or an uncaught exception while it runs, the scenario shows as ERROR: the test fails with the errors, and its numbers neither pass nor set limits.

Leave out the errors you expect, by message or file:

playwright.config.ts
import { defineConfig } from "@playwright/test";
import type { ZeroSpikesOptions } from "zerospikes";

export default defineConfig<ZeroSpikesOptions>({
  use: { zsIgnoreErrors: ["ResizeObserver loop", /^Warning: Each child in a list/] },
});