Skip to content

fix(lint): run single-threaded by default and share one command with CI - #416

Merged
lucas77778 merged 2 commits into
masterfrom
xuan/lint-concurrency
Aug 6, 2026
Merged

fix(lint): run single-threaded by default and share one command with CI#416
lucas77778 merged 2 commits into
masterfrom
xuan/lint-concurrency

Conversation

@AprilNEA

@AprilNEA AprilNEA commented Aug 3, 2026

Copy link
Copy Markdown
Member

Problem

pnpm lint used --concurrency=auto, which opens availableParallelism() >> 1 workers (9 on an 18-core host). Each worker independently rebuilds the typescript-eslint program set instead of sharing it, so cost scales with worker count rather than being divided by it.

Measured over this repo (1225 files, cold cache, 18-core host):

--concurrency wall CPU peak RSS
off, default 4 GB heap 40.6 s 88.7 s 4.74 GB OOM
2, default heap 35.7 s 139.1 s 9.46 GB OOM
4, default heap 48.4 s 294.2 s 18.62 GB ok
auto (9 workers), default heap 32.5 s 339.9 s 38.70 GB ok
off, 8 GB heap 48.2 s 70.5 s 7.40 GB ok

Peak RSS scales linearly at a constant ~4.7 GB per worker, and CPU time grows 4.8x from off to auto while wall time barely moves.

Two consequences:

  • The default wanted ~39 GB. On a machine without that much free it degrades into swap thrash — the reported symptom was a lint that sat for half an hour. A 36 GB machine cannot run it at all.
  • Narrowing to 2 is not the safe direction. Fewer workers means more files, and more accumulated type information, per heap: 2 OOMs where 4 survives. This is the same failure as the ERR_WORKER_OUT_OF_MEMORY that broke every branch in 2026-07 (ci(lint): fix the intermittent ESLint worker OOM #312), and it exits non-zero fast enough to look like a fast successful run.

Concurrency only pays on a cold full run with genuinely free RAM. On a warm cache — the common case, and what the pre-commit hook hits on every commit — single-threaded is 1.9 s / 0.5 GB against 3.3 s / 3.8 GB at auto, because nine workers each pay startup and each read the whole cache file.

Change

  • lint / lint:fix use a cross-platform Node launcher that defaults to --concurrency=off, starts ESLint with --max-old-space-size=8192, and reads LINT_CONCURRENCY itself. The npm scripts contain no POSIX-only environment assignment or parameter expansion.
  • lint:ci is removed; CI runs pnpm lint, the identical command developers run.
  • LINT_CONCURRENCY overrides the default for anyone wanting to trade RAM for a faster cold run; set it to 4 in the invoking shell. ~4.7 GB per worker.
  • docs/DEVELOPMENT.md said concurrency "splits the type-aware program across workers"; ci.yml said it "duplicates ... instead of splitting". The measurements confirm ci.yml — the doc is corrected and now carries the table.
  • The workflow-level NODE_OPTIONS: --max-old-space-size=6144 stays for the non-lint Node steps; lint no longer depends on it.

Verification

  • pnpm check:ci (format + lint + typecheck) green in a full checkout.
  • pnpm lint cold: exit 0, 56.5 s, peak RSS 7.30 GB — no OOM, which is itself the proof the launcher heap limit reaches ESLint.
  • pnpm lint warm: 1.9 s, peak RSS 0.50 GB.
  • LINT_CONCURRENCY=4 with pnpm lint warm: 4.9 s — the override reaches ESLint (an invalid value fails ESLint's option validation loudly).

`--concurrency=auto` opened `availableParallelism() >> 1` workers, and each one
independently rebuilds the typescript-eslint program set rather than sharing it.
Measured over this repo (1225 files, cold cache, 18-core host), peak RSS scales
linearly at a constant ~4.7 GB per worker:

  off  (4 GB heap)   40.6 s   4.74 GB   OOM
  2    (4 GB heap)   35.7 s   9.46 GB   OOM
  4    (4 GB heap)   48.4 s  18.62 GB   ok
  auto (9 workers)   32.5 s  38.70 GB   ok
  off  (8 GB heap)   48.2 s   7.40 GB   ok

So the default asked for ~39 GB and, on a machine without that much free, fell
into swap thrash; narrowing it to 2 did not help but OOM'd instead, because
fewer workers means more files -- and more accumulated type information -- per
heap.

Concurrency only wins on a cold full run with genuinely free RAM. On a warm
cache, which is the common case and what the pre-commit hook hits, single
threaded is 1.9 s / 0.5 GB against 3.3 s / 3.8 GB at `auto`.

Default to `--concurrency=off` with an inline 8 GB heap (pnpm 11 ignores
`node-options` in `.npmrc`, so the script is the only place that reaches both
local and CI), and drop `lint:ci` so both run the identical command.
`LINT_CONCURRENCY` overrides it for anyone who wants to trade RAM for a faster
cold run.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2ee5531a2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread package.json Outdated
Comment thread .github/workflows/ci.yml Outdated
@lucas77778
lucas77778 self-requested a review August 6, 2026 04:17
@lucas77778
lucas77778 merged commit 1fb5474 into master Aug 6, 2026
10 checks passed
@lucas77778
lucas77778 deleted the xuan/lint-concurrency branch August 6, 2026 05:40
@linear-code

linear-code Bot commented Aug 6, 2026

Copy link
Copy Markdown

CODE-573

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