Skip to content

Investigate options to improve pnpm check performance #44787

Description

@jamietanna

Given we're now running pnpm check / pnpm check --no-test much more often - in agent loops or as humans - there may be some areas we can improve performance.

Claude Opus 4.8 suggests:

Here's the full profiling report — no changes made.

# `pnpm check --no-test` performance report

## Baseline (warm, 12-core machine)

Full run: **13.0s**. Cold first run is ~worse (~20s) due to tsc/oxlint building their caches; all numbers below are warm/steady-state, which is what the agent loop actually hits.

Per-check, run in isolation:

| Check              | Time | Notes                                                       |
| ------------------ | ---: | ----------------------------------------------------------- |
| **oxlint**         | 6.7s | Rust binary, saturates all cores, **no file cache**         |
| **type-check**     | 6.4s | `generate:*` (2.8s) → `tsc --noEmit` (4.2s), **sequential** |
| lint-other         | 5.1s | vitest process (4 spec files)                               |
| biome              | 3.0s |                                                             |
| markdown-lint      | 2.4s |                                                             |
| prettier           | 2.0s | has `--cache`                                               |
| doc-fence-check    | 1.8s |                                                             |
| lint-documentation | 1.8s | vitest process (1 spec file)                                |
| test-schema        | 1.8s | vitest-ish + validate-schema                                |
| ls-lint            | 1.2s |                                                             |
| git-check          | 1.1s |                                                             |

## The two bottlenecks

**1. CPU contention (~6s of waste).** All 11 checks launch simultaneously via `Promise.all` (`execution.ts:35`). The theoretical floor is the slowest single check (oxlint, 6.7s), but the actual run is 13s. Measured proof:
- oxlint + type-check *alone* in parallel → 8.3s (each individually 6.7/6.4, so contention already inflates them)
- The other 9 checks in parallel → 7.5s

oxlint is the primary offender: a native multithreaded binary that grabs all 12 cores, so everything scheduled alongside it (tsc, 3× vitest, biome, prettier) is starved.

**2. `generate:*` runs on the type-check critical path every time (2.8s, mostly waste).** `type-check` = `run-s 'generate:*' 'tsc --noEmit'` (package.json). `generate:imports` globs `lib/**`, hashes every manager, and does a runtime `import` of `lib/modules/manager/api.ts` — but `updateFile()` only *writes* generated files when content changed (`generate-imports.mjs:22-29`). So in the agent edit→check→edit loop, when managers/datasources/versioning/data aren't touched, those 2.8s produce zero file changes. Incremental tsc itself already has a hard ~4.2s floor even with a no-op change (1.6M `.cache/tsbuildinfo`, worsened by `checkJs: true` scanning all `.js`) — that part is largely intrinsic to tsc.

## Recommended changes, ranked

**A. Cache `generate:*` with a fingerprint early-exit***highest leverage for agents, ~2.8s off type-check.*
Add a guard at the top of `generate-imports.mjs`: hash/stat its inputs (`lib/**/*.ts`, `data/**`, and the list of existing `*.generated.ts`), store the fingerprint in `.cache/`, and `process.exit(0)` early if unchanged *and* all generated files exist. Since type-check runs generate→tsc sequentially, this drops type-check from 6.4s to ~4.2s. Risk: staleness if the fingerprint misses an input — mitigated by fingerprinting the same globs the script already reads. (Note: generated files aren't committed — 0 in git — so the cache must never skip when a generated file is missing.)

**B. Cut contention***reclaims part of the ~6s tax.*
Options, in order of simplicity:
- Cap oxlint with `--threads` (e.g. 4–6) so it stops starving the others. Counterintuitively can lower *wall-clock* of the whole batch even though oxlint alone gets slower.
- Or give the two tent-poles (oxlint, type-check) their own phase and run the 9 light checks in the other phase, rather than one flat `Promise.all` — schedule so two 6s jobs don't also fight nine smaller jobs. Needs per-machine tuning; the scheduler in `execution.ts` would need a concurrency/grouping concept it doesn't currently have.

**C. Merge the vitest checks***modest, low risk.*
`lint-documentation` + `lint-other` (+ the vitest part of `test-schema`) are 3 separate vitest boots, each spinning up its own worker pool. A single `vitest run test/docs test/other` shares startup and one pool. Saves ~1–2s and removes 2 competitors from the contention pool.

## Realistic target
A (generate cache) + B (contention) together should bring warm `--no-test` from **13s → ~7–8s**, floored by oxlint at 6.7s. Beyond that you'd need oxlint itself to get faster (no cache flag exists in 1.72; narrowing `ignorePatterns` or dropping the slower JS-resolving `import` plugin rules are the only levers, both with correctness tradeoffs).

Tell me which of A/B/C to implement and I'll do it.

One aside — `a.json` and `out.json` are untracked in your working tree (the latter is a GHSA security-advisory dump); unrelated to this work, just flagging so they don't get committed accidentally.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    None yet

    Datasource

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions