Skip to content

Conversation

fasttime
Copy link
Member

@fasttime fasttime commented Sep 3, 2025

Prerequisites checklist

What is the purpose of this pull request? (put an "X" next to an item)

[ ] Documentation update
[ ] Bug fix (template)
[ ] New rule (template)
[ ] Changes an existing rule (template)
[ ] Add autofix to a rule
[ ] Add a CLI option
[ ] Add something to the core
[x] Other, please explain:

Improve worker count calculation for "auto" concurrency.

refs #20040 (comment)

What changes did you make? (Give an overview)

This pull request makes some improvements to the thread count calculation that is done when the concurrency is "auto".

  1. Ignored files are no longer considered in the thread count calculation. Determining if a file is ignored can be done very efficiently now because config arrays are pre-loaded for all files when the files are enumerated.
  2. When using the cache with the default "metadata" strategy, cached files with unchanged results are no longer considered in the thread count calculation. This means that only files which need to be read, parsed and processed by the linter will be counted. If all cached results are valid, multithreading will not be used.
  3. Increased the AUTO_FILES_PER_WORKER constant from 35 to 50. This constant value affects the thread count calculation when only a few files are linted. The most visible effect is that multithreading will be no longer turned on when linting less than 51 files (this is an empiric value and possibly still too low on some machines). I realized that some of the projects I used in my early tests were biased by ignored files. After repeating the tests with ignored files not counted, the larger value seems more appropriate.

Is there anything you'd like reviewers to focus on?

The additional time to count files that need processing (excluding ignored and cached files) is printed in a debug line, for example:

  eslint:eslint 351 file(s) to process counted in 18 ms +19ms

In practice, the effective overhead seems to be even lower, possibly because of cross-thread caching by the OS, which results in subsequent access to file metadata being faster.

Benchmarks

I tested the implementation on a fork of the OpenUI5 repo which contains 12,500 lintable files and uses the --cache option: https://github.com/mauriciolauffer/openui5/tree/eslint-v9.

These are the results when the cache file is outdated:

> time DEBUG=eslint:eslint eslint ./src --quiet --cache --concurrency=auto
  eslint:eslint Using config loader LegacyConfigLoader +0ms
  eslint:eslint Using file patterns: ./src +0ms
  eslint:eslint 12500 file(s) found in 4009 ms +4s
  eslint:eslint 151 file(s) to process counted in 3 ms +3ms
  eslint:eslint Linting using 4 worker thread(s). +0ms
DEBUG=eslint:eslint eslint ./src --quiet --cache --concurrency=auto  139.43s user 8.25s system 526% cpu 28.039 total

It takes just 3 milliseconds to determine that the maximum number of threads (i.e. 4 for an 8 core CPU) should be used.

Now, here are the results when the cache file is present and all cached results are valid:

> time DEBUG=eslint:eslint eslint ./src --quiet --cache --concurrency=auto 
  eslint:eslint Using config loader LegacyConfigLoader +0ms
  eslint:eslint Using file patterns: ./src +1ms
  eslint:eslint 12500 file(s) found in 3987 ms +4s
  eslint:eslint 0 file(s) to process counted in 56 ms +56ms
  eslint:eslint Linting in single-thread mode. +0ms
DEBUG=eslint:eslint eslint ./src --quiet --cache --concurrency=auto  4.46s user 0.36s system 108% cpu 4.452 total

In this case, it takes 56 milliseconds to determine that no concurrency should be used. The process is slower compared to the previous run because a much larger number of file metadata is checked, until it is determined that there aren't enough files to process for multithread mode.

I also repeated the same tests on a slower Windows machine with 16 cores, with comparable results:

No files cached

> $env:DEBUG="eslint:eslint"; Measure-Command { eslint ./src --quiet --cache --concurrency=auto }
  eslint:eslint Using config loader LegacyConfigLoader +0ms
  eslint:eslint Using file patterns: ./src +3ms
  eslint:eslint 12500 file(s) found in 8384 ms +8s
  eslint:eslint 351 file(s) to process counted in 8 ms +9ms
  eslint:eslint Linting using 8 worker thread(s). +0ms


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 39
Milliseconds      : 356
Ticks             : 393564490
TotalDays         : 0.000455514456018518
TotalHours        : 0.0109323469444444
TotalMinutes      : 0.655940816666667
TotalSeconds      : 39.356449
TotalMilliseconds : 39356.449

All files cached

> $env:DEBUG="eslint:eslint"; Measure-Command { eslint ./src --quiet --cache --concurrency=auto }
  eslint:eslint Using config loader LegacyConfigLoader +0ms
  eslint:eslint Using file patterns: ./src +2ms
  eslint:eslint 12500 file(s) found in 8150 ms +8s
  eslint:eslint 0 file(s) to process counted in 197 ms +199ms
  eslint:eslint Linting in single-thread mode. +0ms


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 10
Milliseconds      : 574
Ticks             : 105746003
TotalDays         : 0.000122391207175926
TotalHours        : 0.00293738897222222
TotalMinutes      : 0.176243338333333
TotalSeconds      : 10.5746003
TotalMilliseconds : 10574.6003

As already mentioned, the actual time overhead seems to be even lower than the figures suggests because of the reduced time for other operation, to the extent that there is no noticeable difference between --concurrency=auto and --concurrency=off when most of the files are cached:

> hyperfine "./bin/eslint.js --cache --concurrency=auto" -n 5 
Benchmark 1: 5
  Time (mean ± σ):     577.8 ms ±   9.5 ms    [User: 762.1 ms, System: 65.9 ms]
  Range (min … max):   562.8 ms … 597.1 ms    10 runs
 
> hyperfine "./bin/eslint.js --cache --concurrency=off" -n 5 
Benchmark 1: 5
  Time (mean ± σ):     582.3 ms ±  11.0 ms    [User: 769.0 ms, System: 66.4 ms]
  Range (min … max):   558.4 ms … 600.4 ms    10 runs

@github-project-automation github-project-automation bot moved this to Needs Triage in Triage Sep 3, 2025
Copy link

netlify bot commented Sep 3, 2025

Deploy Preview for docs-eslint canceled.

Name Link
🔨 Latest commit 16d8260
🔍 Latest deploy log https://app.netlify.com/projects/docs-eslint/deploys/68d0d9fe3220040008b2fd0d

@eslint-github-bot eslint-github-bot bot added the chore This change is not user-facing label Sep 3, 2025
@github-actions github-actions bot added cli Relates to ESLint's command-line interface core Relates to ESLint's core APIs and features labels Sep 3, 2025
@fasttime fasttime force-pushed the improve-auto-concurrency branch from 357e07f to 4230d0b Compare September 4, 2025 12:28
@fasttime fasttime marked this pull request as ready for review September 8, 2025 10:02
@fasttime fasttime requested a review from a team as a code owner September 8, 2025 10:02
//-----------------------------------------------------------------------------

module.exports = {
createDebug,
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the same as the debug utility's main export, but it makes sure that the %t formatter is registered. I'm using this to format differences of high-resolution timer values (process.hrtime.bigint()). I'm not sure if this should be made more explicit with a different name, a comment, etc.

Comment on lines 13476 to 13477

describe("caclulateWorkerCount", () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

I moved these tests here so they will be repeated with and without the v10_config_lookup_from_file flag. This is because calculateWorkerCount now uses the config loader.

const fCache = require("file-entry-cache");
const sinon = require("sinon");
const proxyquire = require("proxyquire").noCallThru().noPreserveCache();
const proxyquire = require("proxyquire").noCallThru();
Copy link
Member Author

Choose a reason for hiding this comment

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

This was causing test failures because of ESLint objects being created from a mocked version of lib/eslint/eslint.js. With noPreserveCache set, a mocked version of the lib/eslint/eslint.js module was being kept in the require cache, with its own version of privateMembers, and that did no longer match the privateMembers in the scope of the caclulateWorkerCount function, which is imported from the original module.

{ WarningService } = require("../../lib/services/warning-service");

const proxyquire = require("proxyquire").noCallThru().noPreserveCache();
const proxyquire = require("proxyquire").noCallThru();
Copy link
Member Author

Choose a reason for hiding this comment

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

As above, this was causing inconsistencies in Node.js' require cache, resulting in surprising test failures.

Copy link

Hi everyone, it looks like we lost track of this pull request. Please review and see what the next steps are. This pull request will auto-close in 7 days without an update.

@github-actions github-actions bot added the Stale label Sep 18, 2025
@fasttime
Copy link
Member Author

This is ready for review.

@fasttime fasttime removed the Stale label Sep 19, 2025
Copy link
Member

@mdjermanovic mdjermanovic left a comment

Choose a reason for hiding this comment

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

Just a few minor suggestions, then LGTM.

fasttime and others added 2 commits September 22, 2025 06:54
Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
Copy link
Member

@mdjermanovic mdjermanovic left a comment

Choose a reason for hiding this comment

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

LGTM, thanks! Would like @nzakas to review before merging.

@mdjermanovic mdjermanovic moved this from Needs Triage to Second Review Needed in Triage Sep 22, 2025
Copy link
Member

@nzakas nzakas left a comment

Choose a reason for hiding this comment

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

LGTM. Just waiting for the patch release window to close.

@nzakas nzakas moved this from Second Review Needed to Merge Candidates in Triage Sep 22, 2025
@mdjermanovic mdjermanovic merged commit 177f669 into main Sep 22, 2025
30 checks passed
@mdjermanovic mdjermanovic deleted the improve-auto-concurrency branch September 22, 2025 19:27
@github-project-automation github-project-automation bot moved this from Merge Candidates to Complete in Triage Sep 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
chore This change is not user-facing cli Relates to ESLint's command-line interface core Relates to ESLint's core APIs and features
Projects
Status: Complete
Development

Successfully merging this pull request may close these issues.

3 participants