Skip to content

Improve startup and test-run performance - #3460

Merged
novemberborn merged 3 commits into
mainfrom
improve-worker-startup-performance
Oct 18, 2026
Merged

novemberborn merged 3 commits into
mainfrom
improve-worker-startup-performance

Conversation

@sindresorhus

Copy link
Copy Markdown
Member

Profiling the worker and main processes under load surfaced overhead that scales with the number of test files and events rather than the work the tests actually do. This cuts that overhead substantially:

  • ~40% faster for many small test files (startup-bound)
  • ~30% faster for a single file with thousands of tests
  • ~50% faster for many files run concurrently

The gains come from reducing per-worker module loading and per-event main-process work.

Changes:

  • Defer heavy worker dependencies until actually needed. Workers loaded the glob stack (via the provider manager), the line-number parser (acorn) and write-file-atomic eagerly, though plain-JavaScript files with no line-number filter never use them. They now load lazily.

  • Load Concordance lazily. Concordance and its lodash dependency (~22ms to evaluate) were loaded in every worker via the assertions, test and snapshot modules, and in the main process via error serialization, even though passing simple assertions, plain t.log calls, snapshot-free runs and worker-level errors never touch it. A small shared module loads it on first use. This roughly halves the worker module-load tree or assertion-light files.

  • Keep the heavy assertion tree out of the main process. Error serialization pulled in all of assert.js (Concordance, lodash, the snapshot manager) only for the AssertionError class used in an instanceof check that is always false in the main process. The class now lives in its own tiny module.

  • Replace the stats deep-clone with a purpose-built copy. Run status cloned its stats object with a full v8.serialize/deserialize round-trip on every state change; the object is just flat counters and a map of flat counters, so a direct copy is ~20x faster.

  • Dispatch worker state-change events without Emittery. These are high-frequency, consumed synchronously and never awaited, so they bypass Emittery's per-emit async machinery. The rare shared-worker event still uses Emittery.

@novemberborn

Copy link
Copy Markdown
Member

@sindresorhus I'm fine with the changes, do you think you could rebase and fix formatting issues?

Profiling the worker and main processes under load surfaced overhead that scales with the number of test files and events rather than the work the tests actually do. This cuts that overhead substantially:

- ~40% faster for many small test files (startup-bound)
- ~30% faster for a single file with thousands of tests
- ~50% faster for many files run concurrently

The gains come from reducing per-worker module loading and per-event main-process work.

Changes:

- Defer heavy worker dependencies until actually needed. Workers loaded the glob stack (via the provider manager), the line-number parser (acorn) and write-file-atomic eagerly, though plain-JavaScript files with no line-number filter never use them. They now load lazily.

- Load Concordance lazily. Concordance and its lodash dependency (~22ms to evaluate) were loaded in every worker via the assertions, test and snapshot modules, and in the main process via error serialization, even though passing simple assertions, plain `t.log` calls, snapshot-free runs and worker-level errors never touch it. A small shared module loads it on first use. This roughly halves the worker module-load tree or assertion-light files.

- Keep the heavy assertion tree out of the main process. Error serialization pulled in all of assert.js (Concordance, lodash, the snapshot manager) only for the `AssertionError` class used in an `instanceof` check that is always false in the main process. The class now lives in its own tiny module.

- Replace the stats deep-clone with a purpose-built copy. Run status cloned its stats object with a full `v8.serialize`/`deserialize` round-trip on every state change; the object is just flat counters and a map of flat counters, so a direct copy is ~20x faster.

- Dispatch worker state-change events without Emittery. These are high-frequency, consumed synchronously and never awaited, so they bypass Emittery's per-emit async machinery. The rare shared-worker event still uses Emittery.
`RegExp.prototype.test()` advances `lastIndex` for a regex with the `g` or `y` flag, and the assertions called it on the caller's own regex, so a value that matched once failed the next time.

`t.regex('foo', /foo/g)` passed, then threw `Value must match expression: 'foo' / Regular expression: /foo/g` for the next identical string. The same happened for `t.notRegex()` and for `{message: /boom/g}` on `t.throwsAsync()`, which routes through `assertExpectations()`.

Reset `lastIndex` before each test so the same input always gets the same verdict.
@sindresorhus
sindresorhus force-pushed the improve-worker-startup-performance branch from c6cda2a to 5e60c85 Compare October 16, 2026 06:03
@sindresorhus

Copy link
Copy Markdown
Member Author

done

@novemberborn
novemberborn merged commit 0e89e38 into main Oct 18, 2026
16 checks passed
@novemberborn
novemberborn deleted the improve-worker-startup-performance branch October 18, 2026 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants