-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
feat: multithread linting #19794
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: multithread linting #19794
Conversation
✅ Deploy Preview for docs-eslint canceled.
|
Would it make sense to link this issue to #3565? |
Sure! I forgot to add the link in the description. |
I did some initial testing, and on my machine (Windows 11, 8 logical cores), concurrency seems to be slower. I just ran ESLint on itself. Resultsnzaka@X1-Laptop MINGW64 ~/projects/eslint/eslint (rfc129)
$ time node bin/eslint.js .
real 1m24.218s
user 0m0.514s
sys 0m1.014s
nzaka@X1-Laptop MINGW64 ~/projects/eslint/eslint (rfc129)
$ time node bin/eslint.js . --concurrency auto
real 2m2.273s
user 0m0.544s
sys 0m1.639s
nzaka@X1-Laptop MINGW64 ~/projects/eslint/eslint (rfc129)
$ time node bin/eslint.js . --concurrency auto
real 1m43.752s
user 0m0.468s
sys 0m1.872s
nzaka@X1-Laptop MINGW64 ~/projects/eslint/eslint (rfc129)
$ time node bin/eslint.js . --concurrency 2
real 1m34.724s
user 0m0.607s
sys 0m1.966s
$ time node bin/eslint.js . --concurrency 3
real 1m31.344s
user 0m0.622s
sys 0m1.232s
$ time node bin/eslint.js . --concurrency 4
real 1m46.929s
user 0m0.529s
sys 0m1.170s
$ time node bin/eslint.js . --concurrency 6
real 1m43.638s
user 0m0.592s
sys 0m1.451s Would be good to get some benchmarking on other machines as well. |
Benchmarked runnning on my M2 Max laptop (12 logical cores, 8 performance, 4 efficiency): Results
As expected, using about half of the available cores ended up being the fastest overall execution. Not using the |
@fasttime it may be helpful to add in some debug statements with profiling data to see where the time is being spent. |
@faultyserver Indeed yes, it's expected. That's the reason why @nzakas I've added some debug messages, all details are in the PR description. Please check if this helps to make sense of the slow results. Thanks for testing! |
With the latest changes, concurrency=auto runs in roughly the same time as running without concurrency (1m55s), so that's an improvement. As an experiment, I eliminated the use of the Retrier completely and that brought the time down to 1m30s. I don't think removing the Retrier completely will work on larger projects, but it at least gives us an idea of where some of the time is being spent. |
Maybe there is room for improvement in the Retrier logic, as that should be a thin wrapper around the main functionality, I guess. In multithread mode, each worker thread will read at most one file at a time, such that no limited concurrency is necessary even when using the Retrier. This significantly lowers the chances of running into EMFILE and ENFILE errors. I think these errors could still occur if a plugin opens many files at the same time, or if the ESLint API is used to lint files in a process that has many other files open. |
I agree, the chances are diminished inside of workers but aren't 0. Because removing the retrier sped things up significantly, it seems worth investigating a lighter-weight option to use inside of workers. |
As you asked for further benchmarks, I also gave the current version a try:
Results$ hyperfine --runs 3 --parameter-list threads 1,2,4,6,8,10,auto 'node ./node_modules/.bin/eslint --concurrency {threads}' Benchmark 1: node ./node_modules/.bin/eslint --concurrency 1 Time (mean ± σ): 77.193 s ± 0.800 s [User: 106.509 s, System: 5.332 s] Range (min … max): 76.318 s … 77.885 s 3 runsBenchmark 2: node ./node_modules/.bin/eslint --concurrency 2 Benchmark 3: node ./node_modules/.bin/eslint --concurrency 4 Benchmark 4: node ./node_modules/.bin/eslint --concurrency 6 Benchmark 5: node ./node_modules/.bin/eslint --concurrency 8 Benchmark 6: node ./node_modules/.bin/eslint --concurrency 10 Benchmark 7: node ./node_modules/.bin/eslint --concurrency auto Summary $ hyperfine --runs 3 'node ./node_modules/.bin/eslint' |
Benchmark from https://github.com/mauriciolauffer/openui5/tree/eslint-v9 Results > concurrency ONhyperfine --runs 5 --parameter-list threads 2,3,4,auto 'node .\node_modules\eslint\bin\eslint.js src --quiet --concurrency {threads}'... Benchmark 1: node .\node_modules\eslint\bin\eslint.js src --quiet --concurrency 2 Benchmark 2: node .\node_modules\eslint\bin\eslint.js src --quiet --concurrency 3 Benchmark 3: node .\node_modules\eslint\bin\eslint.js src --quiet --concurrency 4 Benchmark 4: node .\node_modules\eslint\bin\eslint.js src --quiet --concurrency auto Summary Results > concurrency OFFhyperfine --runs 5 'node .\node_modules\eslint\bin\eslint.js src --quiet'... Benchmark 1: node .\node_modules\eslint\bin\eslint.js src --quiet |
For this react project:
Example ESLint config{
"settings": {
"import-x/resolver-next": [
{
"interfaceVersion": 3,
"name": "eslint-import-resolver-typescript"
}
],
"polyfills": [
"navigator.serviceWorker",
"Notification",
"requestIdleCallback"
],
"react": {
"version": "18"
},
"import-x/extensions": [
".ts",
".tsx",
".cts",
".mts",
".js",
".jsx",
".cjs",
".mjs"
],
"import-x/external-module-folders": [
"node_modules",
"node_modules/@types"
],
"import-x/parsers": {
"@typescript-eslint/parser": [
".ts",
".tsx",
".cts",
".mts"
]
},
"import-x/resolver": {
"typescript": true
}
},
"linterOptions": {
"reportUnusedDisableDirectives": 1
},
"rules": {
"constructor-super": [
0
],
"for-direction": [
2
],
"getter-return": [
0,
{
"allowImplicit": false
}
],
"no-async-promise-executor": [
2
],
"no-case-declarations": [
2
],
"no-class-assign": [
0
],
"no-compare-neg-zero": [
2
],
"no-cond-assign": [
2,
"except-parens"
],
"no-const-assign": [
0
],
"no-constant-binary-expression": [
2
],
"no-constant-condition": [
2,
{
"checkLoops": "allExceptWhileTrue"
}
],
"no-control-regex": [
2
],
"no-debugger": [
2
],
"no-delete-var": [
2
],
"no-dupe-args": [
0
],
"no-dupe-class-members": [
0
],
"no-dupe-else-if": [
2
],
"no-dupe-keys": [
0
],
"no-duplicate-case": [
2
],
"no-empty": [
2,
{
"allowEmptyCatch": false
}
],
"no-empty-character-class": [
2
],
"no-empty-pattern": [
2,
{
"allowObjectPatternsAsParameters": false
}
],
"no-empty-static-block": [
2
],
"no-ex-assign": [
2
],
"no-extra-boolean-cast": [
2,
{}
],
"no-fallthrough": [
2,
{
"allowEmptyCase": false,
"reportUnusedFallthroughComment": false
}
],
"no-func-assign": [
0
],
"no-global-assign": [
2,
{
"exceptions": []
}
],
"no-import-assign": [
0
],
"no-invalid-regexp": [
2,
{}
],
"no-irregular-whitespace": [
2,
{
"skipComments": false,
"skipJSXText": false,
"skipRegExps": false,
"skipStrings": true,
"skipTemplates": false
}
],
"no-loss-of-precision": [
2
],
"no-misleading-character-class": [
2
],
"no-new-native-nonconstructor": [
0
],
"no-nonoctal-decimal-escape": [
2
],
"no-obj-calls": [
0
],
"no-octal": [
2
],
"no-prototype-builtins": [
2
],
"no-redeclare": [
0,
{
"builtinGlobals": true
}
],
"no-regex-spaces": [
2
],
"no-self-assign": [
2,
{
"props": true
}
],
"no-setter-return": [
0
],
"no-shadow-restricted-names": [
2,
{
"reportGlobalThis": false
}
],
"no-sparse-arrays": [
2
],
"no-this-before-super": [
0
],
"no-undef": [
0,
{
"typeof": false
}
],
"no-unexpected-multiline": [
2
],
"no-unreachable": [
0
],
"no-unsafe-finally": [
2
],
"no-unsafe-negation": [
0,
{
"enforceForOrderingRelations": false
}
],
"no-unsafe-optional-chaining": [
2,
{
"disallowArithmeticOperators": false
}
],
"no-unused-labels": [
2
],
"no-unused-private-class-members": [
2
],
"no-unused-vars": [
0,
{
"ignoreRestSiblings": true
}
],
"no-useless-backreference": [
2
],
"no-useless-catch": [
2
],
"no-useless-escape": [
2,
{
"allowRegexCharacters": []
}
],
"no-with": [
0
],
"require-yield": [
2
],
"use-isnan": [
2,
{
"enforceForIndexOf": false,
"enforceForSwitchCase": true
}
],
"valid-typeof": [
2,
{
"requireStringLiterals": false
}
],
"accessor-pairs": [
2,
{
"enforceForClassMembers": true,
"getWithoutSet": false,
"setWithoutGet": true
}
],
"array-callback-return": [
2,
{
"allowImplicit": false,
"checkForEach": false,
"allowVoid": false
}
],
"block-scoped-var": [
2
],
"class-methods-use-this": [
0,
{
"enforceForClassFields": true,
"exceptMethods": [],
"ignoreOverrideMethods": false
}
],
"consistent-return": [
2,
{
"treatUndefinedAsUnspecified": false
}
],
"default-case-last": [
2
],
"default-case": [
2,
{}
],
"default-param-last": [
0
],
"eqeqeq": [
2,
"always",
{
"null": "ignore"
}
],
"func-name-matching": [
2
],
"func-names": [
2,
"as-needed",
{}
],
"grouped-accessor-pairs": [
2,
"anyOrder"
],
"no-alert": [
2
],
"no-array-constructor": [
0
],
"no-caller": [
2
],
"no-console": [
1,
{}
],
"no-constructor-return": [
2
],
"no-div-regex": [
2
],
"no-duplicate-imports": [
0,
{
"includeExports": false
}
],
"no-empty-function": [
2,
{
"allow": []
}
],
"no-eq-null": [
2
],
"no-eval": [
2,
{
"allowIndirect": false
}
],
"no-extend-native": [
2,
{
"exceptions": []
}
],
"no-implicit-coercion": [
2,
{
"allow": [],
"boolean": true,
"disallowTemplateShorthand": false,
"number": true,
"string": true
}
],
"no-implicit-globals": [
0,
{
"lexicalBindings": false
}
],
"no-implied-eval": [
0
],
"no-invalid-this": [
0,
{
"capIsConstructor": true
}
],
"no-iterator": [
2
],
"no-label-var": [
2
],
"no-loop-func": [
2
],
"no-new-func": [
2
],
"no-new-wrappers": [
2
],
"no-new": [
2
],
"no-octal-escape": [
2
],
"no-param-reassign": [
2
],
"no-proto": [
2
],
"no-return-assign": [
2,
"except-parens"
],
"no-script-url": [
2
],
"no-self-compare": [
2
],
"no-sequences": [
2,
{
"allowInParentheses": true
}
],
"no-shadow": [
0,
{
"allow": [],
"builtinGlobals": false,
"hoist": "functions",
"ignoreOnInitialization": false,
"ignoreTypeValueShadow": true,
"ignoreFunctionTypeParameterNameValueShadow": true
}
],
"no-template-curly-in-string": [
2
],
"no-throw-literal": [
0
],
"no-unmodified-loop-condition": [
2
],
"no-unused-expressions": [
0,
{
"allowShortCircuit": false,
"allowTernary": false,
"allowTaggedTemplates": false,
"enforceForJSX": false,
"ignoreDirectives": false
}
],
"no-use-before-define": [
0,
{
"classes": true,
"functions": false,
"variables": true,
"allowNamedExports": false,
"enums": true,
"typedefs": true,
"ignoreTypeReferences": true
}
],
"no-useless-call": [
2
],
"no-useless-computed-key": [
2,
{
"enforceForClassMembers": true
}
],
"no-useless-concat": [
2
],
"no-useless-constructor": [
0
],
"no-useless-rename": [
2,
{
"ignoreDestructuring": false,
"ignoreImport": false,
"ignoreExport": false
}
],
"no-useless-return": [
2
],
"no-var": [
2
],
"no-void": [
2,
{
"allowAsStatement": true
}
],
"object-shorthand": [
2,
"always",
{
"avoidQuotes": true
}
],
"prefer-numeric-literals": [
2
],
"prefer-promise-reject-errors": [
0,
{
"allowEmptyReject": false
}
],
"prefer-rest-params": [
2
],
"prefer-spread": [
2
],
"prefer-template": [
2
],
"radix": [
2,
"always"
],
"require-await": [
0
],
"symbol-description": [
2
],
"vars-on-top": [
2
],
"import/no-unresolved": [
2
],
"import/named": [
0
],
"import/namespace": [
2
],
"import/default": [
2
],
"import/export": [
2
],
"import/first": [
2
],
"import/no-absolute-path": [
2
],
"import/no-amd": [
2
],
"import/no-anonymous-default-export": [
2
],
"import/no-commonjs": [
2
],
"import/no-cycle": [
0
],
"import/no-deprecated": [
1
],
"import/no-duplicates": [
2
],
"import/no-dynamic-require": [
2
],
"import/no-empty-named-blocks": [
2
],
"import/no-extraneous-dependencies": [
2
],
"import/no-import-module-exports": [
2
],
"import/no-mutable-exports": [
2
],
"import/no-named-as-default-member": [
2
],
"import/no-named-as-default": [
2
],
"import/no-relative-packages": [
1
],
"import/no-self-import": [
2
],
"import/no-useless-path-segments": [
2
],
"import/no-webpack-loader-syntax": [
2
],
"compat/compat": [
2
],
"perfectionist/sort-imports": [
1,
{
"internalPattern": [
"^@mw/.+",
"^@masterworks/.+"
],
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"object",
"style",
"side-effect",
"side-effect-style",
"unknown"
]
}
],
"no-new-symbol": [
0
],
"prefer-const": [
2,
{
"destructuring": "any",
"ignoreReadBeforeAssign": false
}
],
"@typescript-eslint/await-thenable": [
2
],
"@typescript-eslint/ban-ts-comment": [
2,
{
"minimumDescriptionLength": 10
}
],
"@typescript-eslint/no-array-constructor": [
2
],
"@typescript-eslint/no-array-delete": [
2
],
"@typescript-eslint/no-base-to-string": [
0
],
"@typescript-eslint/no-duplicate-enum-values": [
2
],
"@typescript-eslint/no-duplicate-type-constituents": [
2
],
"@typescript-eslint/no-empty-object-type": [
2
],
"@typescript-eslint/no-explicit-any": [
2
],
"@typescript-eslint/no-extra-non-null-assertion": [
2
],
"@typescript-eslint/no-floating-promises": [
2
],
"@typescript-eslint/no-for-in-array": [
2
],
"@typescript-eslint/no-implied-eval": [
2
],
"@typescript-eslint/no-misused-new": [
2
],
"@typescript-eslint/no-misused-promises": [
2
],
"@typescript-eslint/no-namespace": [
2
],
"@typescript-eslint/no-non-null-asserted-optional-chain": [
2
],
"@typescript-eslint/no-redundant-type-constituents": [
2
],
"@typescript-eslint/no-require-imports": [
2
],
"@typescript-eslint/no-this-alias": [
2
],
"@typescript-eslint/no-unnecessary-type-assertion": [
2
],
"@typescript-eslint/no-unnecessary-type-constraint": [
2
],
"@typescript-eslint/no-unsafe-argument": [
2
],
"@typescript-eslint/no-unsafe-assignment": [
2
],
"@typescript-eslint/no-unsafe-call": [
2
],
"@typescript-eslint/no-unsafe-declaration-merging": [
2
],
"@typescript-eslint/no-unsafe-enum-comparison": [
2
],
"@typescript-eslint/no-unsafe-function-type": [
2
],
"@typescript-eslint/no-unsafe-member-access": [
2
],
"@typescript-eslint/no-unsafe-return": [
2
],
"@typescript-eslint/no-unsafe-unary-minus": [
2
],
"@typescript-eslint/no-unused-expressions": [
2,
{
"allowShortCircuit": false,
"allowTaggedTemplates": false,
"allowTernary": false
}
],
"@typescript-eslint/no-unused-vars": [
2
],
"@typescript-eslint/no-wrapper-object-types": [
2
],
"@typescript-eslint/only-throw-error": [
2
],
"@typescript-eslint/prefer-as-const": [
2
],
"@typescript-eslint/prefer-namespace-keyword": [
2
],
"@typescript-eslint/prefer-promise-reject-errors": [
2
],
"@typescript-eslint/require-await": [
2
],
"@typescript-eslint/restrict-plus-operands": [
2,
{
"allowAny": false,
"allowBoolean": false,
"allowNullish": false,
"allowNumberAndString": false,
"allowRegExp": false
}
],
"@typescript-eslint/restrict-template-expressions": [
2,
{
"allowNumber": true
}
],
"@typescript-eslint/triple-slash-reference": [
2
],
"@typescript-eslint/unbound-method": [
2,
{
"ignoreStatic": true
}
],
"@typescript-eslint/class-methods-use-this": [
2
],
"@typescript-eslint/default-param-last": [
2
],
"@typescript-eslint/no-dupe-class-members": [
2
],
"@typescript-eslint/no-use-before-define": [
2
],
"@typescript-eslint/parameter-properties": [
2
],
"@typescript-eslint/require-array-sort-compare": [
2
],
"@typescript-eslint/no-confusing-void-expression": [
2
],
"@typescript-eslint/no-deprecated": [
1
],
"@typescript-eslint/no-dynamic-delete": [
2
],
"@typescript-eslint/no-extraneous-class": [
2
],
"@typescript-eslint/no-invalid-void-type": [
2,
{
"allowAsThisParameter": true,
"allowInGenericTypeArguments": true
}
],
"@typescript-eslint/no-meaningless-void-operator": [
2
],
"@typescript-eslint/no-misused-spread": [
2
],
"@typescript-eslint/no-mixed-enums": [
2
],
"@typescript-eslint/no-non-null-asserted-nullish-coalescing": [
2
],
"@typescript-eslint/no-non-null-assertion": [
2
],
"@typescript-eslint/no-unnecessary-boolean-literal-compare": [
2
],
"@typescript-eslint/no-unnecessary-condition": [
2
],
"@typescript-eslint/no-unnecessary-template-expression": [
2
],
"@typescript-eslint/no-unnecessary-type-arguments": [
2
],
"@typescript-eslint/no-unnecessary-type-parameters": [
0
],
"@typescript-eslint/no-useless-constructor": [
2
],
"@typescript-eslint/prefer-literal-enum-member": [
2
],
"@typescript-eslint/prefer-reduce-type-parameter": [
2
],
"@typescript-eslint/prefer-return-this-type": [
2
],
"@typescript-eslint/related-getter-setter-pairs": [
2
],
"no-return-await": [
0
],
"@typescript-eslint/return-await": [
2,
"error-handling-correctness-only"
],
"@typescript-eslint/unified-signatures": [
2
],
"@typescript-eslint/use-unknown-in-catch-callback-variable": [
1
],
"@typescript-eslint/adjacent-overload-signatures": [
1
],
"@typescript-eslint/explicit-function-return-type": [
2,
{
"allowConciseArrowFunctionExpressionsStartingWithVoid": true,
"allowExpressions": true,
"allowIIFEs": true
}
],
"@typescript-eslint/no-shadow": [
2
],
"@typescript-eslint/prefer-enum-initializers": [
2
],
"@typescript-eslint/promise-function-async": [
2
],
"@typescript-eslint/switch-exhaustiveness-check": [
2
],
"react/display-name": [
2
],
"react/jsx-key": [
2,
{
"checkFragmentShorthand": true,
"checkKeyMustBeforeSpread": false,
"warnOnDuplicates": false
}
],
"react/jsx-no-comment-textnodes": [
2
],
"react/jsx-no-duplicate-props": [
2
],
"react/jsx-no-target-blank": [
2
],
"react/jsx-no-undef": [
2
],
"react/jsx-uses-react": [
0
],
"react/jsx-uses-vars": [
2
],
"react/no-children-prop": [
2
],
"react/no-danger-with-children": [
2
],
"react/no-deprecated": [
1
],
"react/no-direct-mutation-state": [
2
],
"react/no-find-dom-node": [
2
],
"react/no-is-mounted": [
2
],
"react/no-render-return-value": [
2
],
"react/no-string-refs": [
2
],
"react/no-unescaped-entities": [
2,
{
"forbid": [
{
"char": ">",
"alternatives": [
">"
]
},
{
"char": "}",
"alternatives": [
"}"
]
}
]
}
],
"react/no-unknown-property": [
2
],
"react/no-unsafe": [
0
],
"react/prop-types": [
0
],
"react/react-in-jsx-scope": [
0
],
"react/require-render-return": [
2
],
"react-hooks/exhaustive-deps": [
2
],
"react-hooks/rules-of-hooks": [
2
],
"react/button-has-type": [
2
],
"react/jsx-no-bind": [
2,
{
"allowArrowFunctions": true,
"allowBind": false,
"allowFunctions": false,
"ignoreRefs": false,
"ignoreDOMComponents": false
}
],
"react/no-access-state-in-setstate": [
2
],
"react/no-array-index-key": [
2
],
"react/no-did-mount-set-state": [
2
],
"react/no-did-update-set-state": [
2
],
"react/no-redundant-should-component-update": [
2
],
"react/no-this-in-sfc": [
2
],
"react/no-typos": [
2
],
"react/no-unused-prop-types": [
2
],
"react/no-unused-state": [
2
],
"react/no-will-update-set-state": [
2
],
"react/prefer-es6-class": [
2
],
"react/prefer-stateless-function": [
2
],
"react/self-closing-comp": [
2
],
"react/style-prop-object": [
2,
{
"allow": [
"FormattedNumber"
]
}
],
"react/void-dom-elements-no-children": [
2
],
"react/default-props-match-prop-types": [
0
],
"react/forbid-foreign-prop-types": [
0
],
"jsx-a11y/alt-text": [
2
],
"jsx-a11y/anchor-ambiguous-text": [
0
],
"jsx-a11y/anchor-has-content": [
2
],
"jsx-a11y/anchor-is-valid": [
2
],
"jsx-a11y/aria-activedescendant-has-tabindex": [
2
],
"jsx-a11y/aria-props": [
2
],
"jsx-a11y/aria-proptypes": [
2
],
"jsx-a11y/aria-role": [
2
],
"jsx-a11y/aria-unsupported-elements": [
2
],
"jsx-a11y/autocomplete-valid": [
2
],
"jsx-a11y/click-events-have-key-events": [
2
],
"jsx-a11y/control-has-associated-label": [
0,
{
"ignoreElements": [
"audio",
"canvas",
"embed",
"input",
"textarea",
"tr",
"video"
],
"ignoreRoles": [
"grid",
"listbox",
"menu",
"menubar",
"radiogroup",
"row",
"tablist",
"toolbar",
"tree",
"treegrid"
],
"includeRoles": [
"alert",
"dialog"
]
}
],
"jsx-a11y/heading-has-content": [
2
],
"jsx-a11y/html-has-lang": [
2
],
"jsx-a11y/iframe-has-title": [
2
],
"jsx-a11y/img-redundant-alt": [
2
],
"jsx-a11y/interactive-supports-focus": [
2,
{
"tabbable": [
"button",
"checkbox",
"link",
"searchbox",
"spinbutton",
"switch",
"textbox"
]
}
],
"jsx-a11y/label-has-associated-control": [
2,
{
"labelComponents": [
"Label"
],
"labelAttributes": [
"label"
],
"controlComponents": [
"Input",
"Field"
],
"depth": 3
}
],
"jsx-a11y/label-has-for": [
0
],
"jsx-a11y/media-has-caption": [
2
],
"jsx-a11y/mouse-events-have-key-events": [
2
],
"jsx-a11y/no-access-key": [
2
],
"jsx-a11y/no-autofocus": [
2
],
"jsx-a11y/no-distracting-elements": [
2
],
"jsx-a11y/no-interactive-element-to-noninteractive-role": [
2,
{
"tr": [
"none",
"presentation"
],
"canvas": [
"img"
]
}
],
"jsx-a11y/no-noninteractive-element-interactions": [
2,
{
"handlers": [
"onClick",
"onError",
"onLoad",
"onMouseDown",
"onMouseUp",
"onKeyPress",
"onKeyDown",
"onKeyUp"
],
"alert": [
"onKeyUp",
"onKeyDown",
"onKeyPress"
],
"body": [
"onError",
"onLoad"
],
"dialog": [
"onKeyUp",
"onKeyDown",
"onKeyPress"
],
"iframe": [
"onError",
"onLoad"
],
"img": [
"onError",
"onLoad"
]
}
],
"jsx-a11y/no-noninteractive-element-to-interactive-role": [
2,
{
"ul": [
"listbox",
"menu",
"menubar",
"radiogroup",
"tablist",
"tree",
"treegrid"
],
"ol": [
"listbox",
"menu",
"menubar",
"radiogroup",
"tablist",
"tree",
"treegrid"
],
"li": [
"menuitem",
"menuitemradio",
"menuitemcheckbox",
"option",
"row",
"tab",
"treeitem"
],
"table": [
"grid"
],
"td": [
"gridcell"
],
"fieldset": [
"radiogroup",
"presentation"
]
}
],
"jsx-a11y/no-noninteractive-tabindex": [
2,
{
"tags": [],
"roles": [
"tabpanel"
],
"allowExpressionValues": true
}
],
"jsx-a11y/no-redundant-roles": [
2,
{
"ul": [
"list"
],
"ol": [
"list"
],
"li": [
"listitem"
]
}
],
"jsx-a11y/no-static-element-interactions": [
2,
{
"allowExpressionValues": true,
"handlers": [
"onClick",
"onMouseDown",
"onMouseUp",
"onKeyPress",
"onKeyDown",
"onKeyUp"
]
}
],
"jsx-a11y/role-has-required-aria-props": [
2
],
"jsx-a11y/role-supports-aria-props": [
2
],
"jsx-a11y/scope": [
2
],
"jsx-a11y/tabindex-no-positive": [
2
],
"arrow-body-style": [
2,
"as-needed"
],
"camelcase": [
2,
{
"allow": [],
"ignoreDestructuring": true,
"ignoreGlobals": true,
"ignoreImports": true,
"properties": "never"
}
],
"capitalized-comments": [
2,
"always",
{
"ignoreConsecutiveComments": true,
"ignoreInlineComments": true
}
],
"dot-notation": [
2,
{
"allowKeywords": true,
"allowPattern": ""
}
],
"no-else-return": [
2,
{
"allowElseIf": true
}
],
"no-extra-bind": [
2
],
"no-extra-label": [
2
],
"prefer-arrow-callback": [
2,
{
"allowNamedFunctions": false,
"allowUnboundThis": true
}
],
"yoda": [
2,
"never",
{
"exceptRange": false,
"onlyEquality": false
}
],
"import/consistent-type-specifier-style": [
0,
"prefer-top-level"
],
"import/order": [
0
],
"@stylistic/arrow-parens": [
2,
"always"
],
"@stylistic/arrow-spacing": [
2
],
"@stylistic/block-spacing": [
2
],
"@stylistic/dot-location": [
2,
"property"
],
"@stylistic/no-floating-decimal": [
2
],
"@stylistic/no-multi-spaces": [
2
],
"@stylistic/rest-spread-spacing": [
2
],
"@stylistic/template-curly-spacing": [
2
],
"@stylistic/wrap-iife": [
2,
"any"
],
"@stylistic/yield-star-spacing": [
2,
"after"
],
"@typescript-eslint/consistent-generic-constructors": [
2
],
"@typescript-eslint/consistent-indexed-object-style": [
2,
"record"
],
"@typescript-eslint/consistent-type-assertions": [
2
],
"@typescript-eslint/consistent-type-exports": [
2
],
"@typescript-eslint/consistent-type-imports": [
2,
{
"prefer": "type-imports",
"fixStyle": "separate-type-imports"
}
],
"@typescript-eslint/member-ordering": [
1
],
"@typescript-eslint/no-inferrable-types": [
1
],
"@typescript-eslint/prefer-for-of": [
2
],
"@typescript-eslint/prefer-includes": [
2
],
"@typescript-eslint/prefer-nullish-coalescing": [
2,
{
"ignoreConditionalTests": true,
"ignorePrimitives": true,
"ignoreTernaryTests": true
}
],
"@typescript-eslint/prefer-optional-chain": [
2
],
"@typescript-eslint/prefer-readonly": [
2
],
"@typescript-eslint/prefer-string-starts-ends-with": [
2
],
"@typescript-eslint/sort-type-constituents": [
2
],
"react/jsx-boolean-value": [
2,
"never"
],
"react/jsx-closing-bracket-location": [
2
],
"react/jsx-closing-tag-location": [
2
],
"react/jsx-curly-spacing": [
2
],
"react/jsx-equals-spacing": [
2,
"never"
],
"react/jsx-fragments": [
2,
"syntax"
],
"react/jsx-no-useless-fragment": [
2
],
"react/jsx-props-no-multi-spaces": [
2
],
"react/jsx-tag-spacing": [
2
],
"react/jsx-wrap-multilines": [
2,
{
"arrow": "parens-new-line",
"assignment": "parens-new-line",
"condition": "parens-new-line",
"declaration": "parens-new-line",
"logical": "parens-new-line",
"prop": "parens-new-line",
"return": "parens-new-line"
}
]
},
"plugins": [
"@",
"import:eslint-plugin-import-x@4.15.0",
"compat:eslint-plugin-compat@6.0.2",
"perfectionist:eslint-plugin-perfectionist@4.13.0",
"@typescript-eslint:@typescript-eslint/eslint-plugin@8.33.1",
"react",
"react-hooks:eslint-plugin-react-hooks",
"jsx-a11y:eslint-plugin-jsx-a11y@6.10.2",
"@stylistic"
],
"language": "@/js",
"languageOptions": {
"sourceType": "module",
"ecmaVersion": 2025,
"parser": "typescript-eslint/parser@8.33.1",
"parserOptions": {
"projectService": true,
"tsconfigRootDir": "/Users/sam/Code/masterworks/frontend/",
"ecmaFeatures": {
"jsx": true
},
"jsxPragma": null
},
"globals": {
"AbortController": false,
"AbortSignal": false,
"AbsoluteOrientationSensor": false,
"AbstractRange": false,
"Accelerometer": false,
"addEventListener": false,
"ai": false,
"AI": false,
"AICreateMonitor": false,
"AITextSession": false,
"alert": false,
"AnalyserNode": false,
"Animation": false,
"AnimationEffect": false,
"AnimationEvent": false,
"AnimationPlaybackEvent": false,
"AnimationTimeline": false,
"AsyncDisposableStack": false,
"atob": false,
"Attr": false,
"Audio": false,
"AudioBuffer": false,
"AudioBufferSourceNode": false,
"AudioContext": false,
"AudioData": false,
"AudioDecoder": false,
"AudioDestinationNode": false,
"AudioEncoder": false,
"AudioListener": false,
"AudioNode": false,
"AudioParam": false,
"AudioParamMap": false,
"AudioProcessingEvent": false,
"AudioScheduledSourceNode": false,
"AudioSinkInfo": false,
"AudioWorklet": false,
"AudioWorkletGlobalScope": false,
"AudioWorkletNode": false,
"AudioWorkletProcessor": false,
"AuthenticatorAssertionResponse": false,
"AuthenticatorAttestationResponse": false,
"AuthenticatorResponse": false,
"BackgroundFetchManager": false,
"BackgroundFetchRecord": false,
"BackgroundFetchRegistration": false,
"BarcodeDetector": false,
"BarProp": false,
"BaseAudioContext": false,
"BatteryManager": false,
"BeforeUnloadEvent": false,
"BiquadFilterNode": false,
"Blob": false,
"BlobEvent": false,
"Bluetooth": false,
"BluetoothCharacteristicProperties": false,
"BluetoothDevice": false,
"BluetoothRemoteGATTCharacteristic": false,
"BluetoothRemoteGATTDescriptor": false,
"BluetoothRemoteGATTServer": false,
"BluetoothRemoteGATTService": false,
"BluetoothUUID": false,
"blur": false,
"BroadcastChannel": false,
"BrowserCaptureMediaStreamTrack": false,
"btoa": false,
"ByteLengthQueuingStrategy": false,
"Cache": false,
"caches": false,
"CacheStorage": false,
"cancelAnimationFrame": false,
"cancelIdleCallback": false,
"CanvasCaptureMediaStream": false,
"CanvasCaptureMediaStreamTrack": false,
"CanvasGradient": false,
"CanvasPattern": false,
"CanvasRenderingContext2D": false,
"CaptureController": false,
"CaretPosition": false,
"CDATASection": false,
"ChannelMergerNode": false,
"ChannelSplitterNode": false,
"ChapterInformation": false,
"CharacterBoundsUpdateEvent": false,
"CharacterData": false,
"clearInterval": false,
"clearTimeout": false,
"clientInformation": false,
"Clipboard": false,
"ClipboardEvent": false,
"ClipboardItem": false,
"close": false,
"closed": false,
"CloseEvent": false,
"CloseWatcher": false,
"CommandEvent": false,
"Comment": false,
"CompositionEvent": false,
"CompressionStream": false,
"confirm": false,
"console": false,
"ConstantSourceNode": false,
"ContentVisibilityAutoStateChangeEvent": false,
"ConvolverNode": false,
"CookieChangeEvent": false,
"CookieDeprecationLabel": false,
"cookieStore": false,
"CookieStore": false,
"CookieStoreManager": false,
"CountQueuingStrategy": false,
"createImageBitmap": false,
"Credential": false,
"credentialless": false,
"CredentialsContainer": false,
"CropTarget": false,
"crossOriginIsolated": false,
"crypto": false,
"Crypto": false,
"CryptoKey": false,
"CSPViolationReportBody": false,
"CSS": false,
"CSSAnimation": false,
"CSSConditionRule": false,
"CSSContainerRule": false,
"CSSCounterStyleRule": false,
"CSSFontFaceRule": false,
"CSSFontFeatureValuesRule": false,
"CSSFontPaletteValuesRule": false,
"CSSGroupingRule": false,
"CSSImageValue": false,
"CSSImportRule": false,
"CSSKeyframeRule": false,
"CSSKeyframesRule": false,
"CSSKeywordValue": false,
"CSSLayerBlockRule": false,
"CSSLayerStatementRule": false,
"CSSMarginRule": false,
"CSSMathClamp": false,
"CSSMathInvert": false,
"CSSMathMax": false,
"CSSMathMin": false,
"CSSMathNegate": false,
"CSSMathProduct": false,
"CSSMathSum": false,
"CSSMathValue": false,
"CSSMatrixComponent": false,
"CSSMediaRule": false,
"CSSNamespaceRule": false,
"CSSNestedDeclarations": false,
"CSSNumericArray": false,
"CSSNumericValue": false,
"CSSPageDescriptors": false,
"CSSPageRule": false,
"CSSPerspective": false,
"CSSPositionTryDescriptors": false,
"CSSPositionTryRule": false,
"CSSPositionValue": false,
"CSSPropertyRule": false,
"CSSRotate": false,
"CSSRule": false,
"CSSRuleList": false,
"CSSScale": false,
"CSSScopeRule": false,
"CSSSkew": false,
"CSSSkewX": false,
"CSSSkewY": false,
"CSSStartingStyleRule": false,
"CSSStyleDeclaration": false,
"CSSStyleRule": false,
"CSSStyleSheet": false,
"CSSStyleValue": false,
"CSSSupportsRule": false,
"CSSTransformComponent": false,
"CSSTransformValue": false,
"CSSTransition": false,
"CSSTranslate": false,
"CSSUnitValue": false,
"CSSUnparsedValue": false,
"CSSVariableReferenceValue": false,
"CSSViewTransitionRule": false,
"currentFrame": false,
"currentTime": false,
"CustomElementRegistry": false,
"customElements": false,
"CustomEvent": false,
"CustomStateSet": false,
"DataTransfer": false,
"DataTransferItem": false,
"DataTransferItemList": false,
"DecompressionStream": false,
"DelayNode": false,
"DelegatedInkTrailPresenter": false,
"DeviceMotionEvent": false,
"DeviceMotionEventAcceleration": false,
"DeviceMotionEventRotationRate": false,
"DeviceOrientationEvent": false,
"devicePixelRatio": false,
"DevicePosture": false,
"dispatchEvent": false,
"DisposableStack": false,
"document": false,
"Document": false,
"DocumentFragment": false,
"documentPictureInPicture": false,
"DocumentPictureInPicture": false,
"DocumentPictureInPictureEvent": false,
"DocumentTimeline": false,
"DocumentType": false,
"DOMError": false,
"DOMException": false,
"DOMImplementation": false,
"DOMMatrix": false,
"DOMMatrixReadOnly": false,
"DOMParser": false,
"DOMPoint": false,
"DOMPointReadOnly": false,
"DOMQuad": false,
"DOMRect": false,
"DOMRectList": false,
"DOMRectReadOnly": false,
"DOMStringList": false,
"DOMStringMap": false,
"DOMTokenList": false,
"DragEvent": false,
"DynamicsCompressorNode": false,
"EditContext": false,
"Element": false,
"ElementInternals": false,
"EncodedAudioChunk": false,
"EncodedVideoChunk": false,
"ErrorEvent": false,
"event": false,
"Event": false,
"EventCounts": false,
"EventSource": false,
"EventTarget": false,
"external": false,
"External": false,
"EyeDropper": false,
"FeaturePolicy": false,
"FederatedCredential": false,
"fence": false,
"Fence": false,
"FencedFrameConfig": false,
"fetch": false,
"fetchLater": false,
"FetchLaterResult": false,
"File": false,
"FileList": false,
"FileReader": false,
"FileSystem": false,
"FileSystemDirectoryEntry": false,
"FileSystemDirectoryHandle": false,
"FileSystemDirectoryReader": false,
"FileSystemEntry": false,
"FileSystemFileEntry": false,
"FileSystemFileHandle": false,
"FileSystemHandle": false,
"FileSystemObserver": false,
"FileSystemWritableFileStream": false,
"find": false,
"focus": false,
"FocusEvent": false,
"FontData": false,
"FontFace": false,
"FontFaceSet": false,
"FontFaceSetLoadEvent": false,
"FormData": false,
"FormDataEvent": false,
"FragmentDirective": false,
"frameElement": false,
"frames": false,
"GainNode": false,
"Gamepad": false,
"GamepadAxisMoveEvent": false,
"GamepadButton": false,
"GamepadButtonEvent": false,
"GamepadEvent": false,
"GamepadHapticActuator": false,
"GamepadPose": false,
"Geolocation": false,
"GeolocationCoordinates": false,
"GeolocationPosition": false,
"GeolocationPositionError": false,
"getComputedStyle": false,
"getScreenDetails": false,
"getSelection": false,
"GPU": false,
"GPUAdapter": false,
"GPUAdapterInfo": false,
"GPUBindGroup": false,
"GPUBindGroupLayout": false,
"GPUBuffer": false,
"GPUBufferUsage": false,
"GPUCanvasContext": false,
"GPUColorWrite": false,
"GPUCommandBuffer": false,
"GPUCommandEncoder": false,
"GPUCompilationInfo": false,
"GPUCompilationMessage": false,
"GPUComputePassEncoder": false,
"GPUComputePipeline": false,
"GPUDevice": false,
"GPUDeviceLostInfo": false,
"GPUError": false,
"GPUExternalTexture": false,
"GPUInternalError": false,
"GPUMapMode": false,
"GPUOutOfMemoryError": false,
"GPUPipelineError": false,
"GPUPipelineLayout": false,
"GPUQuerySet": false,
"GPUQueue": false,
"GPURenderBundle": false,
"GPURenderBundleEncoder": false,
"GPURenderPassEncoder": false,
"GPURenderPipeline": false,
"GPUSampler": false,
"GPUShaderModule": false,
"GPUShaderStage": false,
"GPUSupportedFeatures": false,
"GPUSupportedLimits": false,
"GPUTexture": false,
"GPUTextureUsage": false,
"GPUTextureView": false,
"GPUUncapturedErrorEvent": false,
"GPUValidationError": false,
"GravitySensor": false,
"Gyroscope": false,
"HashChangeEvent": false,
"Headers": false,
"HID": false,
"HIDConnectionEvent": false,
"HIDDevice": false,
"HIDInputReportEvent": false,
"Highlight": false,
"HighlightRegistry": false,
"history": false,
"History": false,
"HTMLAllCollection": false,
"HTMLAnchorElement": false,
"HTMLAreaElement": false,
"HTMLAudioElement": false,
"HTMLBaseElement": false,
"HTMLBodyElement": false,
"HTMLBRElement": false,
"HTMLButtonElement": false,
"HTMLCanvasElement": false,
"HTMLCollection": false,
"HTMLDataElement": false,
"HTMLDataListElement": false,
"HTMLDetailsElement": false,
"HTMLDialogElement": false,
"HTMLDirectoryElement": false,
"HTMLDivElement": false,
"HTMLDListElement": false,
"HTMLDocument": false,
"HTMLElement": false,
"HTMLEmbedElement": false,
"HTMLFencedFrameElement": false,
"HTMLFieldSetElement": false,
"HTMLFontElement": false,
"HTMLFormControlsCollection": false,
"HTMLFormElement": false,
"HTMLFrameElement": false,
"HTMLFrameSetElement": false,
"HTMLHeadElement": false,
"HTMLHeadingElement": false,
"HTMLHRElement": false,
"HTMLHtmlElement": false,
"HTMLIFrameElement": false,
"HTMLImageElement": false,
"HTMLInputElement": false,
"HTMLLabelElement": false,
"HTMLLegendElement": false,
"HTMLLIElement": false,
"HTMLLinkElement": false,
"HTMLMapElement": false,
"HTMLMarqueeElement": false,
"HTMLMediaElement": false,
"HTMLMenuElement": false,
"HTMLMetaElement": false,
"HTMLMeterElement": false,
"HTMLModElement": false,
"HTMLObjectElement": false,
"HTMLOListElement": false,
"HTMLOptGroupElement": false,
"HTMLOptionElement": false,
"HTMLOptionsCollection": false,
"HTMLOutputElement": false,
"HTMLParagraphElement": false,
"HTMLParamElement": false,
"HTMLPictureElement": false,
"HTMLPreElement": false,
"HTMLProgressElement": false,
"HTMLQuoteElement": false,
"HTMLScriptElement": false,
"HTMLSelectedContentElement": false,
"HTMLSelectElement": false,
"HTMLSlotElement": false,
"HTMLSourceElement": false,
"HTMLSpanElement": false,
"HTMLStyleElement": false,
"HTMLTableCaptionElement": false,
"HTMLTableCellElement": false,
"HTMLTableColElement": false,
"HTMLTableElement": false,
"HTMLTableRowElement": false,
"HTMLTableSectionElement": false,
"HTMLTemplateElement": false,
"HTMLTextAreaElement": false,
"HTMLTimeElement": false,
"HTMLTitleElement": false,
"HTMLTrackElement": false,
"HTMLUListElement": false,
"HTMLUnknownElement": false,
"HTMLVideoElement": false,
"IDBCursor": false,
"IDBCursorWithValue": false,
"IDBDatabase": false,
"IDBFactory": false,
"IDBIndex": false,
"IDBKeyRange": false,
"IDBObjectStore": false,
"IDBOpenDBRequest": false,
"IDBRequest": false,
"IDBTransaction": false,
"IDBVersionChangeEvent": false,
"IdentityCredential": false,
"IdentityCredentialError": false,
"IdentityProvider": false,
"IdleDeadline": false,
"IdleDetector": false,
"IIRFilterNode": false,
"Image": false,
"ImageBitmap": false,
"ImageBitmapRenderingContext": false,
"ImageCapture": false,
"ImageData": false,
"ImageDecoder": false,
"ImageTrack": false,
"ImageTrackList": false,
"indexedDB": false,
"Ink": false,
"innerHeight": false,
"innerWidth": false,
"InputDeviceCapabilities": false,
"InputDeviceInfo": false,
"InputEvent": false,
"IntersectionObserver": false,
"IntersectionObserverEntry": false,
"isSecureContext": false,
"Keyboard": false,
"KeyboardEvent": false,
"KeyboardLayoutMap": false,
"KeyframeEffect": false,
"LanguageDetector": false,
"LargestContentfulPaint": false,
"LaunchParams": false,
"launchQueue": false,
"LaunchQueue": false,
"LayoutShift": false,
"LayoutShiftAttribution": false,
"length": false,
"LinearAccelerationSensor": false,
"localStorage": false,
"location": true,
"Location": false,
"locationbar": false,
"Lock": false,
"LockManager": false,
"matchMedia": false,
"MathMLElement": false,
"MediaCapabilities": false,
"MediaCapabilitiesInfo": false,
"MediaDeviceInfo": false,
"MediaDevices": false,
"MediaElementAudioSourceNode": false,
"MediaEncryptedEvent": false,
"MediaError": false,
"MediaKeyError": false,
"MediaKeyMessageEvent": false,
"MediaKeys": false,
"MediaKeySession": false,
"MediaKeyStatusMap": false,
"MediaKeySystemAccess": false,
"MediaList": false,
"MediaMetadata": false,
"MediaQueryList": false,
"MediaQueryListEvent": false,
"MediaRecorder": false,
"MediaRecorderErrorEvent": false,
"MediaSession": false,
"MediaSource": false,
"MediaSourceHandle": false,
"MediaStream": false,
"MediaStreamAudioDestinationNode": false,
"MediaStreamAudioSourceNode": false,
"MediaStreamEvent": false,
"MediaStreamTrack": false,
"MediaStreamTrackAudioSourceNode": false,
"MediaStreamTrackAudioStats": false,
"MediaStreamTrackEvent": false,
"MediaStreamTrackGenerator": false,
"MediaStreamTrackProcessor": false,
"MediaStreamTrackVideoStats": false,
"menubar": false,
"MessageChannel": false,
"MessageEvent": false,
"MessagePort": false,
"MIDIAccess": false,
"MIDIConnectionEvent": false,
"MIDIInput": false,
"MIDIInputMap": false,
"MIDIMessageEvent": false,
"MIDIOutput": false,
"MIDIOutputMap": false,
"MIDIPort": false,
"MimeType": false,
"MimeTypeArray": false,
"model": false,
"ModelGenericSession": false,
"ModelManager": false,
"MouseEvent": false,
"moveBy": false,
"moveTo": false,
"MutationEvent": false,
"MutationObserver": false,
"MutationRecord": false,
"name": false,
"NamedNodeMap": false,
"NavigateEvent": false,
"navigation": false,
"Navigation": false,
"NavigationActivation": false,
"NavigationCurrentEntryChangeEvent": false,
"NavigationDestination": false,
"NavigationHistoryEntry": false,
"NavigationPreloadManager": false,
"NavigationTransition": false,
"navigator": false,
"Navigator": false,
"NavigatorLogin": false,
"NavigatorManagedData": false,
"NavigatorUAData": false,
"NetworkInformation": false,
"Node": false,
"NodeFilter": false,
"NodeIterator": false,
"NodeList": false,
"Notification": false,
"NotifyPaintEvent": false,
"NotRestoredReasonDetails": false,
"NotRestoredReasons": false,
"Observable": false,
"OfflineAudioCompletionEvent": false,
"OfflineAudioContext": false,
"offscreenBuffering": false,
"OffscreenCanvas": false,
"OffscreenCanvasRenderingContext2D": false,
"onabort": true,
"onafterprint": true,
"onanimationcancel": true,
"onanimationend": true,
"onanimationiteration": true,
"onanimationstart": true,
"onappinstalled": true,
"onauxclick": true,
"onbeforeinput": true,
"onbeforeinstallprompt": true,
"onbeforematch": true,
"onbeforeprint": true,
"onbeforetoggle": true,
"onbeforeunload": true,
"onbeforexrselect": true,
"onblur": true,
"oncancel": true,
"oncanplay": true,
"oncanplaythrough": true,
"onchange": true,
"onclick": true,
"onclose": true,
"oncommand": true,
"oncontentvisibilityautostatechange": true,
"oncontextlost": true,
"oncontextmenu": true,
"oncontextrestored": true,
"oncopy": true,
"oncuechange": true,
"oncut": true,
"ondblclick": true,
"ondevicemotion": true,
"ondeviceorientation": true,
"ondeviceorientationabsolute": true,
"ondrag": true,
"ondragend": true,
"ondragenter": true,
"ondragleave": true,
"ondragover": true,
"ondragstart": true,
"ondrop": true,
"ondurationchange": true,
"onemptied": true,
"onended": true,
"onerror": true,
"onfocus": true,
"onformdata": true,
"ongamepadconnected": true,
"ongamepaddisconnected": true,
"ongotpointercapture": true,
"onhashchange": true,
"oninput": true,
"oninvalid": true,
"onkeydown": true,
"onkeypress": true,
"onkeyup": true,
"onlanguagechange": true,
"onload": true,
"onloadeddata": true,
"onloadedmetadata": true,
"onloadstart": true,
"onlostpointercapture": true,
"onmessage": true,
"onmessageerror": true,
"onmousedown": true,
"onmouseenter": true,
"onmouseleave": true,
"onmousemove": true,
"onmouseout": true,
"onmouseover": true,
"onmouseup": true,
"onmousewheel": true,
"onoffline": true,
"ononline": true,
"onpagehide": true,
"onpagereveal": true,
"onpageshow": true,
"onpageswap": true,
"onpaste": true,
"onpause": true,
"onplay": true,
"onplaying": true,
"onpointercancel": true,
"onpointerdown": true,
"onpointerenter": true,
"onpointerleave": true,
"onpointermove": true,
"onpointerout": true,
"onpointerover": true,
"onpointerrawupdate": true,
"onpointerup": true,
"onpopstate": true,
"onprogress": true,
"onratechange": true,
"onrejectionhandled": true,
"onreset": true,
"onresize": true,
"onscroll": true,
"onscrollend": true,
"onscrollsnapchange": true,
"onscrollsnapchanging": true,
"onsearch": true,
"onsecuritypolicyviolation": true,
"onseeked": true,
"onseeking": true,
"onselect": true,
"onselectionchange": true,
"onselectstart": true,
"onslotchange": true,
"onstalled": true,
"onstorage": true,
"onsubmit": true,
"onsuspend": true,
"ontimeupdate": true,
"ontoggle": true,
"ontransitioncancel": true,
"ontransitionend": true,
"ontransitionrun": true,
"ontransitionstart": true,
"onunhandledrejection": true,
"onunload": true,
"onvolumechange": true,
"onwaiting": true,
"onwheel": true,
"open": false,
"opener": false,
"Option": false,
"OrientationSensor": false,
"origin": false,
"originAgentCluster": false,
"OscillatorNode": false,
"OTPCredential": false,
"outerHeight": false,
"outerWidth": false,
"OverconstrainedError": false,
"PageRevealEvent": false,
"PageSwapEvent": false,
"PageTransitionEvent": false,
"pageXOffset": false,
"pageYOffset": false,
"PannerNode": false,
"parent": false,
"PasswordCredential": false,
"Path2D": false,
"PaymentAddress": false,
"PaymentManager": false,
"PaymentMethodChangeEvent": false,
"PaymentRequest": false,
"PaymentRequestUpdateEvent": false,
"PaymentResponse": false,
"performance": false,
"Performance": false,
"PerformanceElementTiming": false,
"PerformanceEntry": false,
"PerformanceEventTiming": false,
"PerformanceLongAnimationFrameTiming": false,
"PerformanceLongTaskTiming": false,
"PerformanceMark": false,
"PerformanceMeasure": false,
"PerformanceNavigation": false,
"PerformanceNavigationTiming": false,
"PerformanceObserver": false,
"PerformanceObserverEntryList": false,
"PerformancePaintTiming": false,
"PerformanceResourceTiming": false,
"PerformanceScriptTiming": false,
"PerformanceServerTiming": false,
"PerformanceTiming": false,
"PeriodicSyncManager": false,
"PeriodicWave": false,
"Permissions": false,
"PermissionStatus": false,
"PERSISTENT": false,
"personalbar": false,
"PictureInPictureEvent": false,
"PictureInPictureWindow": false,
"Plugin": false,
"PluginArray": false,
"PointerEvent": false,
"PopStateEvent": false,
"postMessage": false,
"Presentation": false,
"PresentationAvailability": false,
"PresentationConnection": false,
"PresentationConnectionAvailableEvent": false,
"PresentationConnectionCloseEvent": false,
"PresentationConnectionList": false,
"PresentationReceiver": false,
"PresentationRequest": false,
"PressureObserver": false,
"PressureRecord": false,
"print": false,
"ProcessingInstruction": false,
"Profiler": false,
"ProgressEvent": false,
"PromiseRejectionEvent": false,
"prompt": false,
"ProtectedAudience": false,
"PublicKeyCredential": false,
"PushManager": false,
"PushSubscription": false,
"PushSubscriptionOptions": false,
"queryLocalFonts": false,
"queueMicrotask": false,
"RadioNodeList": false,
"Range": false,
"ReadableByteStreamController": false,
"ReadableStream": false,
"ReadableStreamBYOBReader": false,
"ReadableStreamBYOBRequest": false,
"ReadableStreamDefaultController": false,
"ReadableStreamDefaultReader": false,
"registerProcessor": false,
"RelativeOrientationSensor": false,
"RemotePlayback": false,
"removeEventListener": false,
"ReportBody": false,
"reportError": false,
"ReportingObserver": false,
"Request": false,
"requestAnimationFrame": false,
"requestIdleCallback": false,
"resizeBy": false,
"ResizeObserver": false,
"ResizeObserverEntry": false,
"ResizeObserverSize": false,
"resizeTo": false,
"Response": false,
"RestrictionTarget": false,
"RTCCertificate": false,
"RTCDataChannel": false,
"RTCDataChannelEvent": false,
"RTCDtlsTransport": false,
"RTCDTMFSender": false,
"RTCDTMFToneChangeEvent": false,
"RTCEncodedAudioFrame": false,
"RTCEncodedVideoFrame": false,
"RTCError": false,
"RTCErrorEvent": false,
"RTCIceCandidate": false,
"RTCIceTransport": false,
"RTCPeerConnection": false,
"RTCPeerConnectionIceErrorEvent": false,
"RTCPeerConnectionIceEvent": false,
"RTCRtpReceiver": false,
"RTCRtpScriptTransform": false,
"RTCRtpSender": false,
"RTCRtpTransceiver": false,
"RTCSctpTransport": false,
"RTCSessionDescription": false,
"RTCStatsReport": false,
"RTCTrackEvent": false,
"sampleRate": false,
"scheduler": false,
"Scheduler": false,
"Scheduling": false,
"screen": false,
"Screen": false,
"ScreenDetailed": false,
"ScreenDetails": false,
"screenLeft": false,
"ScreenOrientation": false,
"screenTop": false,
"screenX": false,
"screenY": false,
"ScriptProcessorNode": false,
"scroll": false,
"scrollbars": false,
"scrollBy": false,
"ScrollTimeline": false,
"scrollTo": false,
"scrollX": false,
"scrollY": false,
"SecurityPolicyViolationEvent": false,
"Selection": false,
"self": false,
"Sensor": false,
"SensorErrorEvent": false,
"Serial": false,
"SerialPort": false,
"ServiceWorker": false,
"ServiceWorkerContainer": false,
"ServiceWorkerRegistration": false,
"sessionStorage": false,
"setInterval": false,
"setTimeout": false,
"ShadowRoot": false,
"sharedStorage": false,
"SharedStorage": false,
"SharedStorageAppendMethod": false,
"SharedStorageClearMethod": false,
"SharedStorageDeleteMethod": false,
"SharedStorageModifierMethod": false,
"SharedStorageSetMethod": false,
"SharedStorageWorklet": false,
"SharedWorker": false,
"showDirectoryPicker": false,
"showOpenFilePicker": false,
"showSaveFilePicker": false,
"SnapEvent": false,
"SourceBuffer": false,
"SourceBufferList": false,
"speechSynthesis": false,
"SpeechSynthesis": false,
"SpeechSynthesisErrorEvent": false,
"SpeechSynthesisEvent": false,
"SpeechSynthesisUtterance": false,
"SpeechSynthesisVoice": false,
"StaticRange": false,
"status": false,
"statusbar": false,
"StereoPannerNode": false,
"stop": false,
"Storage": false,
"StorageBucket": false,
"StorageBucketManager": false,
"StorageEvent": false,
"StorageManager": false,
"structuredClone": false,
"styleMedia": false,
"StylePropertyMap": false,
"StylePropertyMapReadOnly": false,
"StyleSheet": false,
"StyleSheetList": false,
"SubmitEvent": false,
"Subscriber": false,
"SubtleCrypto": false,
"SuppressedError": false,
"SVGAElement": false,
"SVGAngle": false,
"SVGAnimatedAngle": false,
"SVGAnimatedBoolean": false,
"SVGAnimatedEnumeration": false,
"SVGAnimatedInteger": false,
"SVGAnimatedLength": false,
"SVGAnimatedLengthList": false,
"SVGAnimatedNumber": false,
"SVGAnimatedNumberList": false,
"SVGAnimatedPreserveAspectRatio": false,
"SVGAnimatedRect": false,
"SVGAnimatedString": false,
"SVGAnimatedTransformList": false,
"SVGAnimateElement": false,
"SVGAnimateMotionElement": false,
"SVGAnimateTransformElement": false,
"SVGAnimationElement": false,
"SVGCircleElement": false,
"SVGClipPathElement": false,
"SVGComponentTransferFunctionElement": false,
"SVGDefsElement": false,
"SVGDescElement": false,
"SVGElement": false,
"SVGEllipseElement": false,
"SVGFEBlendElement": false,
"SVGFEColorMatrixElement": false,
"SVGFEComponentTransferElement": false,
"SVGFECompositeElement": false,
"SVGFEConvolveMatrixElement": false,
"SVGFEDiffuseLightingElement": false,
"SVGFEDisplacementMapElement": false,
"SVGFEDistantLightElement": false,
"SVGFEDropShadowElement": false,
"SVGFEFloodElement": false,
"SVGFEFuncAElement": false,
"SVGFEFuncBElement": false,
"SVGFEFuncGElement": false,
"SVGFEFuncRElement": false,
"SVGFEGaussianBlurElement": false,
"SVGFEImageElement": false,
"SVGFEMergeElement": false,
"SVGFEMergeNodeElement": false,
"SVGFEMorphologyElement": false,
"SVGFEOffsetElement": false,
"SVGFEPointLightElement": false,
"SVGFESpecularLightingElement": false,
"SVGFESpotLightElement": false,
"SVGFETileElement": false,
"SVGFETurbulenceElement": false,
"SVGFilterElement": false,
"SVGForeignObjectElement": false,
"SVGGElement": false,
"SVGGeometryElement": false,
"SVGGradientElement": false,
"SVGGraphicsElement": false,
"SVGImageElement": false,
"SVGLength": false,
"SVGLengthList": false,
"SVGLinearGradientElement": false,
"SVGLineElement": false,
"SVGMarkerElement": false,
"SVGMaskElement": false,
"SVGMatrix": false,
"SVGMetadataElement": false,
"SVGMPathElement": false,
"SVGNumber": false,
"SVGNumberList": false,
"SVGPathElement": false,
"SVGPatternElement": false,
"SVGPoint": false,
"SVGPointList": false,
"SVGPolygonElement": false,
"SVGPolylineElement": false,
"SVGPreserveAspectRatio": false,
"SVGRadialGradientElement": false,
"SVGRect": false,
"SVGRectElement": false,
"SVGScriptElement": false,
"SVGSetElement": false,
"SVGStopElement": false,
"SVGStringList": false,
"SVGStyleElement": false,
"SVGSVGElement": false,
"SVGSwitchElement": false,
"SVGSymbolElement": false,
"SVGTextContentElement": false,
"SVGTextElement": false,
"SVGTextPathElement": false,
"SVGTextPositioningElement": false,
"SVGTitleElement": false,
"SVGTransform": false,
"SVGTransformList": false,
"SVGTSpanElement": false,
"SVGUnitTypes": false,
"SVGUseElement": false,
"SVGViewElement": false,
"SyncManager": false,
"TaskAttributionTiming": false,
"TaskController": false,
"TaskPriorityChangeEvent": false,
"TaskSignal": false,
"TEMPORARY": false,
"Text": false,
"TextDecoder": false,
"TextDecoderStream": false,
"TextEncoder": false,
"TextEncoderStream": false,
"TextEvent": false,
"TextFormat": false,
"TextFormatUpdateEvent": false,
"TextMetrics": false,
"TextTrack": false,
"TextTrackCue": false,
"TextTrackCueList": false,
"TextTrackList": false,
"TextUpdateEvent": false,
"TimeEvent": false,
"TimeRanges": false,
"ToggleEvent": false,
"toolbar": false,
"top": false,
"Touch": false,
"TouchEvent": false,
"TouchList": false,
"TrackEvent": false,
"TransformStream": false,
"TransformStreamDefaultController": false,
"TransitionEvent": false,
"TreeWalker": false,
"TrustedHTML": false,
"TrustedScript": false,
"TrustedScriptURL": false,
"TrustedTypePolicy": false,
"TrustedTypePolicyFactory": false,
"trustedTypes": false,
"UIEvent": false,
"URL": false,
"URLPattern": false,
"URLSearchParams": false,
"USB": false,
"USBAlternateInterface": false,
"USBConfiguration": false,
"USBConnectionEvent": false,
"USBDevice": false,
"USBEndpoint": false,
"USBInterface": false,
"USBInTransferResult": false,
"USBIsochronousInTransferPacket": false,
"USBIsochronousInTransferResult": false,
"USBIsochronousOutTransferPacket": false,
"USBIsochronousOutTransferResult": false,
"USBOutTransferResult": false,
"UserActivation": false,
"ValidityState": false,
"VideoColorSpace": false,
"VideoDecoder": false,
"VideoEncoder": false,
"VideoFrame": false,
"VideoPlaybackQuality": false,
"ViewTimeline": false,
"ViewTransition": false,
"ViewTransitionTypeSet": false,
"VirtualKeyboard": false,
"VirtualKeyboardGeometryChangeEvent": false,
"VisibilityStateEntry": false,
"visualViewport": false,
"VisualViewport": false,
"VTTCue": false,
"VTTRegion": false,
"WakeLock": false,
"WakeLockSentinel": false,
"WaveShaperNode": false,
"WebAssembly": false,
"WebGL2RenderingContext": false,
"WebGLActiveInfo": false,
"WebGLBuffer": false,
"WebGLContextEvent": false,
"WebGLFramebuffer": false,
"WebGLObject": false,
"WebGLProgram": false,
"WebGLQuery": false,
"WebGLRenderbuffer": false,
"WebGLRenderingContext": false,
"WebGLSampler": false,
"WebGLShader": false,
"WebGLShaderPrecisionFormat": false,
"WebGLSync": false,
"WebGLTexture": false,
"WebGLTransformFeedback": false,
"WebGLUniformLocation": false,
"WebGLVertexArrayObject": false,
"WebSocket": false,
"WebSocketError": false,
"WebSocketStream": false,
"WebTransport": false,
"WebTransportBidirectionalStream": false,
"WebTransportDatagramDuplexStream": false,
"WebTransportError": false,
"WebTransportReceiveStream": false,
"WebTransportSendStream": false,
"WGSLLanguageFeatures": false,
"WheelEvent": false,
"when": false,
"window": false,
"Window": false,
"WindowControlsOverlay": false,
"WindowControlsOverlayGeometryChangeEvent": false,
"Worker": false,
"Worklet": false,
"WorkletGlobalScope": false,
"WritableStream": false,
"WritableStreamDefaultController": false,
"WritableStreamDefaultWriter": false,
"XMLDocument": false,
"XMLHttpRequest": false,
"XMLHttpRequestEventTarget": false,
"XMLHttpRequestUpload": false,
"XMLSerializer": false,
"XPathEvaluator": false,
"XPathExpression": false,
"XPathResult": false,
"XRAnchor": false,
"XRAnchorSet": false,
"XRBoundedReferenceSpace": false,
"XRCamera": false,
"XRCPUDepthInformation": false,
"XRDepthInformation": false,
"XRDOMOverlayState": false,
"XRFrame": false,
"XRHand": false,
"XRHitTestResult": false,
"XRHitTestSource": false,
"XRInputSource": false,
"XRInputSourceArray": false,
"XRInputSourceEvent": false,
"XRInputSourcesChangeEvent": false,
"XRJointPose": false,
"XRJointSpace": false,
"XRLayer": false,
"XRLightEstimate": false,
"XRLightProbe": false,
"XRPose": false,
"XRRay": false,
"XRReferenceSpace": false,
"XRReferenceSpaceEvent": false,
"XRRenderState": false,
"XRRigidTransform": false,
"XRSession": false,
"XRSessionEvent": false,
"XRSpace": false,
"XRSystem": false,
"XRTransientInputHitTestResult": false,
"XRTransientInputHitTestSource": false,
"XRView": false,
"XRViewerPose": false,
"XRViewport": false,
"XRWebGLBinding": false,
"XRWebGLDepthInformation": false,
"XRWebGLLayer": false,
"XSLTProcessor": false,
"process": "readonly"
}
}
} Top-10 rules
Benchmark results$pkgx hyperfine --runs 3 --parameter-list threads off,2,3,4,auto './node_modules/.bin/eslint --quiet --concurrency {threads}'
Benchmark 1: ./node_modules/.bin/eslint --quiet --concurrency off
Time (mean ± σ): 79.499 s ± 0.637 s [User: 112.108 s, System: 6.522 s]
Range (min … max): 79.015 s … 80.220 s 3 runs
Benchmark 2: ./node_modules/.bin/eslint --quiet --concurrency 2
Time (mean ± σ): 57.637 s ± 0.571 s [User: 157.154 s, System: 12.519 s]
Range (min … max): 57.104 s … 58.239 s 3 runs
Benchmark 3: ./node_modules/.bin/eslint --quiet --concurrency 3
Time (mean ± σ): 53.158 s ± 0.627 s [User: 205.728 s, System: 21.050 s]
Range (min … max): 52.579 s … 53.825 s 3 runs
Benchmark 4: ./node_modules/.bin/eslint --quiet --concurrency 4
Time (mean ± σ): 59.152 s ± 3.709 s [User: 276.589 s, System: 33.123 s]
Range (min … max): 56.210 s … 63.318 s 3 runs
Benchmark 5: ./node_modules/.bin/eslint --quiet --concurrency auto
Time (mean ± σ): 62.648 s ± 0.432 s [User: 299.816 s, System: 33.271 s]
Range (min … max): 62.158 s … 62.975 s 3 runs
Summary
./node_modules/.bin/eslint --quiet --concurrency 3 ran
1.08 ± 0.02 times faster than ./node_modules/.bin/eslint --quiet --concurrency 2
1.11 ± 0.07 times faster than ./node_modules/.bin/eslint --quiet --concurrency 4
1.18 ± 0.02 times faster than ./node_modules/.bin/eslint --quiet --concurrency auto
1.50 ± 0.02 times faster than ./node_modules/.bin/eslint --quiet --concurrency off Caveat: my M2 Macbook Air temp-throttled during the benchmark. Qualitatively, Amazing! 😍 |
Thanks a lot for the benchmarks @DreierF, @mauriciolauffer and @stefanmaric! It would be interesting to see how those results compare with what you get when you run ESLint with |
I already had a measurement without |
Thanks everyone for the benchmark results! This confirms my theory that the effects of concurrency can't be felt on smaller projects whereas larger projects can see some significant gains. It will be interested to see what the cutoff is and we might want to consider outputting a warning if ESLint is run on a small number of files with concurrency. |
To provide some additional data around a possible cutoff point, here are benchmark results including Environment: M4 Max, 12 performance, 4 efficiencyI'm using an M4 Max MacBook Pro with 12 performance cores and four efficiency cores.
File counts: 1071 total
Top-10 rules: all pluginsMost of these rules require an upfront information-gathering pass for types or imports.
Benchmark results: 4-6 are fastest, 1.33x faster than off
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still looking for feedback on #19794 (comment).
Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
I made some benchmarks on the getsentry/sentry codebase, which has over 1M lines of TypeScript to lint. Our eslint config is here. Machine stats:
before, with eslint 9.32.0:
with this PR:
concurrency 3 performed best, and the savings are substantial - about 35% 🎉 It’s also interesting that from the concurrency setting, |
This comment was marked as off-topic.
This comment was marked as off-topic.
@kachkaev that's off-topic for this PR. If you'd like to discuss sharding further, please open a separate issue. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Accepting we'll probably get some bug reports once I released, I think this is a good starting point. Nice work!
Holding off on merging for @mdjermanovic to verify.
Thanks for the results @TkDodo. For large projects, concurrency |
FWIW, I've been using this branch on a large codebase and it makes a huge difference and I've had no issues with it so far. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, great work!
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [eslint](https://eslint.org) ([source](https://github.com/eslint/eslint)) | devDependencies | minor | [`9.33.0` -> `9.34.0`](https://renovatebot.com/diffs/npm/eslint/9.33.0/9.34.0) | --- ### Release Notes <details> <summary>eslint/eslint (eslint)</summary> ### [`v9.34.0`](https://github.com/eslint/eslint/releases/tag/v9.34.0) [Compare Source](eslint/eslint@v9.33.0...v9.34.0) #### Features - [`0bb777a`](eslint/eslint@0bb777a) feat: multithread linting ([#​19794](eslint/eslint#19794)) (Francesco Trotta) - [`43a5f9e`](eslint/eslint@43a5f9e) feat: add eslint-plugin-regexp to eslint-config-eslint base config ([#​19951](eslint/eslint#19951)) (Pixel998) #### Bug Fixes - [`9b89903`](eslint/eslint@9b89903) fix: default value of accessor-pairs option in rule.d.ts file ([#​20024](eslint/eslint#20024)) (Tanuj Kanti) - [`6c07420`](eslint/eslint@6c07420) fix: fix spurious failure in neostandard integration test ([#​20023](eslint/eslint#20023)) (Kirk Waiblinger) - [`676f4ac`](eslint/eslint@676f4ac) fix: allow scientific notation with trailing zeros matching exponent ([#​20002](eslint/eslint#20002)) (Sweta Tanwar) #### Documentation - [`0b4a590`](eslint/eslint@0b4a590) docs: make rulesdir deprecation clearer ([#​20018](eslint/eslint#20018)) (Domenico Gemoli) - [`327c672`](eslint/eslint@327c672) docs: Update README (GitHub Actions Bot) - [`bf26229`](eslint/eslint@bf26229) docs: Fix typo in core-concepts/index.md ([#​20009](eslint/eslint#20009)) (Tobias Hernstig) - [`2309327`](eslint/eslint@2309327) docs: fix typo in the "Configuring Rules" section ([#​20001](eslint/eslint#20001)) (ghazi-git) - [`2b87e21`](eslint/eslint@2b87e21) docs: \[no-else-return] clarify sample code. ([#​19991](eslint/eslint#19991)) (Yuki Takada (Yukinosuke Takada)) - [`c36570c`](eslint/eslint@c36570c) docs: Update README (GitHub Actions Bot) #### Chores - [`f19ad94`](eslint/eslint@f19ad94) chore: upgrade to `@eslint/js@9.34.0` ([#​20030](eslint/eslint#20030)) (Francesco Trotta) - [`b48fa20`](eslint/eslint@b48fa20) chore: package.json update for [@​eslint/js](https://github.com/eslint/js) release (Jenkins) - [`4bce8a2`](eslint/eslint@4bce8a2) chore: package.json update for eslint-config-eslint release (Jenkins) - [`0c9999c`](eslint/eslint@0c9999c) refactor: prefer default options in `grouped-accessor-pairs` ([#​20028](eslint/eslint#20028)) (루밀LuMir) - [`d503f19`](eslint/eslint@d503f19) ci: fix `stale.yml` ([#​20010](eslint/eslint#20010)) (루밀LuMir) - [`e2dc67d`](eslint/eslint@e2dc67d) ci: centralize `stale.yml` ([#​19994](eslint/eslint#19994)) (루밀LuMir) - [`7093cb8`](eslint/eslint@7093cb8) ci: bump actions/checkout from 4 to 5 ([#​20005](eslint/eslint#20005)) (dependabot\[bot]) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS42MS4wIiwidXBkYXRlZEluVmVyIjoiNDEuNjEuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Reviewed-on: https://git.robbevp.be/robbevp/website-robbevanpetegem/pulls/495 Co-authored-by: Renovate Bot <renovate@robbevp.be> Co-committed-by: Renovate Bot <renovate@robbevp.be>
Sorry if this is creating noise, at storyblok - we managed to shave ~ 40% time on our CI with eslint by setting concurrency auto on our main frontend monorepo. Just wanted to say thank you for the hard work. (We have a strict setup with vue-eslint-parser which leads to relatively slow runs)
|
@jeremycolin would you mind sharing that on this discussion? #20040 |
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
[X] Add something to the core
[ ] Other, please explain:
closes #3565
What changes did you make? (Give an overview)
This pull request implements eslint/rfcs#129, featuring multithread linting and option modules.
Is there anything you'd like reviewers to focus on?
Testing the prototype
To test this in a local package:
Debug Messages
These are some interesting debug messages.
Number of enumerated files, time spent to enumerate files in the main thread, and effective number of worker threads:
Alternatively, if no concurrency is used:
Time to load dependencies in a worker thread:
Additional time to initialize a worker thread after dependencies are loaded:
Time to load a config array in a worker thread. It takes long to load a config array the first time, successive loads use the cache and are much faster:
Time to read a file to be linted:
Time spent by the linter on a file:
Total time spent on a file in a worker thread. This includes loading the config array, reading the file, linting the file, and a little overhead:
Benchmark References
ESLint
Repo: https://github.com/eslint/eslint
Files: <1000
Commands:
git clone --no-tags --single-branch https://github.com/eslint/eslint -b rfc129 cd eslint npm i npm --prefix docs i node bin/eslint.js --concurrency=auto
OpenUI5
Repo: https://github.com/mauriciolauffer/openui5/tree/eslint-v9
Files: ~12500
Commands:
git clone --no-tags --single-branch https://github.com/mauriciolauffer/openui5 -b eslint-v9 cd openui5 npm i -D eslint/eslint#rfc129 node node_modules/eslint/bin/eslint.js --concurrency=auto src --quiet
core-js
Repo: https://github.com/zloirock/core-js
Files: ~8000
Commands:
Archived Topics
This section describes three different solutions, each implemented in its own branch, for the handling of
ESLint#getRulesMetaForResults()
as discussed in the RFC (seeESLint#getRulesMetaForResults()
).To recap, the
findFiles
function currently loads config files for files it finds by resolving glob patterns, and it will continue to do so in multithread mode. But if a file path is passed explicitly (i.e. not matched by a glob pattern), thenfindFiles
does not try to load a config file for it. The problem with thegetRulesMetaForResults
method in its present implementation, is that this method requires config files for all linted files to have been loaded in the current thread. This means that in order forgetRulesMetaForResults
to continue working in multithread mode, either config files for all explicitly passed files must be loaded in the controlling thread — additionally to worker threads — during thelintFiles
invocation, orgetRulesMetaForResults
must be changed to have the information it requires calculated by worker threads.rfc129 (this pull request)
This is based on Solution 1 shown in the RFC. This implementation ensures that the config files for all enumerated files are loaded in the controlling thread while worker threads are running. If
getRulesMetaForResults
is not called afterlintFiles
, it's possible that some config files will be loaded unnecessarily.rfc129-b
Based on eslint/rfcs#129 (comment). All config files are loaded by
findFiles
before the worker threads are created, without using limited concurrency. This is similar to the previous implementation in terms of performance, but it simplifies the code and ensures that later access to config files can be done synchronously.rfc129-c
Based on Solution 2 shown in the RFC.
Rules
meta
objects andusedDeprecatedRules
for all results are precalculated in the worker threads. This solution requires some important refactiongs and it adds complexity to the existing logic, for example an additional field on theESLint
instance to store rulesmeta
objects or aBroadcastChannel
to send warnings to the main thread. The advantage is that config files for explicitly passed filenames don't need to be loaded in the controlling thread. This yields a measurable performance improvement especially when filenames are passed explicitly tolintFiles
.Design Questions
Handling--concurrency=1
Running ESLint from the CLI with
--concurrency=1
performs worse than linting in the main thread (i.e. without the--concurrency
flag or with--concurrency=off
) because spinning up a worker thread adds extra overhead.That said, a concurrency value of 1 can be useful for testing or debugging, and it could make sense when using the using the Node.js API, because if other asynchronous tasks are running in the main thread, linting in a separate thread can improve performance. However, the CLI already launches ESLint in its own dedicated process with no competing async work, so specifying
--concurrency=1
there will always degrade performance.To avoid confusion, we could forbid--concurrency=1
when ESLint is invoked via the CLI. Another option could be emitting a warning wheneverconcurrency
is set to 1, regardless of how ESLint is started. What do you think is the best way to handle this case?ShouldESLint.fromOptionModule()
accept Unix pathnames?Per the RFC,
ESLint.fromOptionModule()
accepts only a URL. I have realized that the original design could be adapted to also accept a Unix pathname like other APIs do. For example, dynamic imports can use absolute paths (import("/path/to/file.js")
) or relative paths (import("./file.js")
) besides file URLs. This would be just a minor enhancement to the developer experience, as file paths would continue to be converted to file URLs behind the scenes. ShouldESLint.fromOptionModule()
accept Unix pathnames besides URLs? If so, should relative filenames be allowed and resolved from the current working directory?Should TypeScript option modules be loaded like TypeScript config files?Thanks to built-in type stripping, Node.js 24 permits loading TypeScript files as option modules out of the box, with a URL like
file:///path/to/eslint-options.ts
. Bun and Deno recognize TypeScript syntax natively. Nonetheless, if we wish to align the way option modules are loaded with how config files are loaded, we could consider allowing native loading of TypeScript files behind a new feature flag, similar tounstable_native_nodejs_ts_config
, and using jiti otherwise. I'm not sure how useful this approach would be in practice, and I doubt it's worth the added complexity, but it's good to discuss it in advance. Once option modules are released, changing the way files are loaded will be probably a breaking change.Should theconcurrency
option be forbidden in theESLint
constructor?Another point raised in the RFC was the strategy to guarantee that all options are serializable when
concurrency
is enabled. One solution that we did not consider is to completely disallow theconcurrency
option in theESLint
constructor for users. This would imply that the only way to specifyconcurrency
would be through an option module. While this would eliminate the need to check the options whenconcurrency
is specified, it would also sacrifice usability, so I'm not sure if we want to do this.