-
Notifications
You must be signed in to change notification settings - Fork 31
feat: add performance-testing package #3024
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
Open
JoshuaKGoldberg
wants to merge
19
commits into
main
Choose a base branch
from
perf-comparisons
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 7 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
c407a90
[WIP] feat: add performance comparisons package
JoshuaKGoldberg 7b24685
Merge branch 'main'
JoshuaKGoldberg 6153d4c
--skip-formatting
JoshuaKGoldberg d1963eb
Merge branch 'main' into perf-comparisons
JoshuaKGoldberg 7e65b24
feat: make ESLint and Flint measurements directly comparable
JoshuaKGoldberg f2751e3
fix: get performance-testing past CI
JoshuaKGoldberg 51b7e2e
chore: apply performance testing review feedback
JoshuaKGoldberg 04a80b5
initial points
JoshuaKGoldberg ed391cc
Apply suggestions from code review
JoshuaKGoldberg bfb15ab
lil fix: ordering
JoshuaKGoldberg 48373f0
added some basic unit tests
JoshuaKGoldberg 990bb81
Merge branch 'main' into perf-comparisons
JoshuaKGoldberg 89bdba0
fix: get performance-testing past knip and flint after merging main
JoshuaKGoldberg b0e55f0
Apply suggestions from code review
JoshuaKGoldberg 5f2c1ef
path.join in writeStructure
JoshuaKGoldberg dd0863d
also normalize in writeFile.test.ts
JoshuaKGoldberg feddd2e
I thought I removed the results section...
JoshuaKGoldberg 6aaf8f1
Corrected Knip config
JoshuaKGoldberg 01867fe
lil touchups
JoshuaKGoldberg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,4 +2,5 @@ | |
| packages/*/.astro | ||
| packages/*/dist | ||
| packages/*/lib | ||
| packages/performance-testing/cases | ||
| /pnpm-lock.yaml | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /cases |
|
JoshuaKGoldberg marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| <h1 align="center"><code>@flint.fyi/performance-testing</code></h1> | ||
|
|
||
| <p align="center"> | ||
| Runs performance measurements for Flint and other linters. | ||
| ❤️🔥 | ||
| </p> | ||
|
|
||
| ```shell | ||
| pnpm generate | ||
| DEBUG=*runInHyperfine pnpm measure | ||
|
JoshuaKGoldberg marked this conversation as resolved.
Outdated
|
||
| ``` | ||
|
|
||
| ## Results | ||
|
|
||
| Measured with Hyperfine 1.20.0 on an Apple Silicon Mac running Node 24.13.1, against ESLint 10.8.0 and typescript-eslint 8.66.0: | ||
|
|
||
| ```text | ||
| ┌───────────────────────┬───────┬───────────────────────┬───────┐ | ||
| │ eslint │ files │ flint │ rules │ | ||
| ├───────────────────────┼───────┼───────────────────────┼───────┤ | ||
| │ '543.0 ms ± 24.1 ms' │ 2 │ '791.5 ms ± 23.3 ms' │ 1 │ | ||
| │ '781.0 ms ± 29.1 ms' │ 2 │ '844.7 ms ± 46.8 ms' │ 119 │ | ||
| │ '854.7 ms ± 56.7 ms' │ 2 │ '827.4 ms ± 38.6 ms' │ 272 │ | ||
| │ '966.5 ms ± 22.7 ms' │ 256 │ '1.060 s ± 0.054 s' │ 1 │ | ||
| │ '1.301 s ± 0.037 s' │ 256 │ '1.234 s ± 0.043 s' │ 119 │ | ||
| │ '1.458 s ± 0.049 s' │ 256 │ '1.322 s ± 0.032 s' │ 272 │ | ||
| └───────────────────────┴───────┴───────────────────────┴───────┘ | ||
| ``` | ||
|
|
||
| Numbers from any one machine are only meaningful next to the other linter measured on that same machine, so treat the columns as a comparison rather than as absolute costs. | ||
|
|
||
| ## Comparability | ||
|
|
||
| `pnpm generate` writes one test case directory per (files, rules) pair, each containing an `eslint.config.js` and a `flint.config.ts` meant to ask for the same work: | ||
|
|
||
| - **Same files.** Both configs lint `src/**/*.ts` and ignore `node_modules` and `*.config.*`, so neither linter is charged for the other's config file. | ||
| - **Same rules.** Every enabled rule comes from a `@flint.fyi/rule-data` entry that maps a Flint rule to an ESLint rule, so the counts match exactly. | ||
| Flint rules that map to several overlapping ESLint rules — typically a core rule and its typescript-eslint extension — are paired with just one of them, preferring the typescript-eslint rule, then the core rule, then the plugin rule that sorts first. | ||
| - **Same type information.** Both run type-aware: ESLint through `projectService`, Flint through the case's `tsconfig.json`. | ||
| - **Same cold start.** Flint runs with `--cache-ignore` so Hyperfine's repeated runs re-lint from scratch, matching ESLint's lack of a cache. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| { | ||
| "name": "@flint.fyi/performance-testing", | ||
| "version": "0.0.0", | ||
| "private": true, | ||
| "description": "Runs performance measurements for Flint and other linters.", | ||
| "homepage": "https://flint.fyi", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/flint-fyi/flint.git", | ||
| "directory": "packages/performance-testing" | ||
| }, | ||
| "license": "MIT", | ||
| "author": { | ||
| "name": "Flint Team", | ||
| "url": "https://flint.fyi/team" | ||
| }, | ||
| "sideEffects": false, | ||
| "type": "module", | ||
| "scripts": { | ||
| "generate": "node src/generate.ts", | ||
| "measure": "DEBUG=\"*runInHyperfine\" node src/measure.ts" | ||
| }, | ||
| "dependencies": { | ||
| "@flint.fyi/rule-data": "workspace:^", | ||
| "console-table-without-index": "^0.1.1", | ||
| "debug-for-file": "^0.4.0", | ||
| "execa": "catalog:dev", | ||
| "flint": "workspace:^", | ||
| "prettier": "catalog:dev" | ||
| }, | ||
| "devDependencies": { | ||
| "@flint.fyi/build": "workspace:^", | ||
| "eslint": "catalog:dev", | ||
| "eslint-plugin-import": "catalog:dev", | ||
| "eslint-plugin-regexp": "catalog:dev", | ||
| "eslint-plugin-unicorn": "catalog:dev", | ||
| "typescript": "catalog:dev", | ||
| "typescript-eslint": "catalog:dev" | ||
| }, | ||
| "engines": { | ||
| "node": ">=26.1.0" | ||
| } | ||
| } |
89 changes: 89 additions & 0 deletions
89
packages/performance-testing/src/creators/createCaseFiles.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import type { TestCase } from "../testCases.ts"; | ||
| import type { Structure } from "../writing/writeStructure.ts"; | ||
| import { createESLintConfigFile } from "./files/createESLintConfigFile.ts"; | ||
| import { createFlintConfigFile } from "./files/createFlintConfigFile.ts"; | ||
| import { createStandardTSConfigFile } from "./files/createStandardTSConfigFile.ts"; | ||
| import { range } from "./utils.ts"; | ||
|
|
||
| export function countCaseFiles(testCase: TestCase): number { | ||
| return countStructureFiles(createSourceFiles(testCase)); | ||
| } | ||
|
|
||
| export function createCaseFiles(testCase: TestCase): Structure { | ||
| return { | ||
| "eslint.config.js": [createESLintConfigFile(testCase.rules), "typescript"], | ||
| "flint.config.ts": [createFlintConfigFile(testCase.rules), "typescript"], | ||
| src: createSourceFiles(testCase), | ||
| "tsconfig.json": [createStandardTSConfigFile(), "json"], | ||
| }; | ||
| } | ||
|
|
||
| function countStructureFiles(structure: Structure): number { | ||
| return Object.values(structure).reduce( | ||
| (total, value) => | ||
| total + (Array.isArray(value) ? 1 : countStructureFiles(value)), | ||
| 0, | ||
| ); | ||
| } | ||
|
|
||
| function createExampleDirectory(index: number): Structure { | ||
| return { | ||
| "index.ts": [createExampleFile(index), "typescript"], | ||
| ...(index > 2 && | ||
| Object.fromEntries( | ||
| range(1, index).map((i) => [ | ||
| `nested${i}`, | ||
| createExampleDirectory(i - 1), | ||
| ]), | ||
| )), | ||
| }; | ||
| } | ||
|
|
||
| function createExampleFile(index: number) { | ||
| return [ | ||
| index > 1 && | ||
| range(1, index) | ||
| .map((i) => `export * as nested${i} from "./nested${i}/index.js";`) | ||
|
JoshuaKGoldberg marked this conversation as resolved.
Outdated
|
||
| .join("\n\t\t"), | ||
| ` | ||
| export async function example${index}(prefix: string) { | ||
| await Promise.resolve(); | ||
| return [prefix + "", ${index}]; | ||
| } | ||
| `, | ||
| ] | ||
| .filter(Boolean) | ||
| .join("\n\n"); | ||
| } | ||
|
|
||
| function createIndexFile(topLevelWidth: number) { | ||
| const indices = range(0, topLevelWidth); | ||
|
|
||
| return ` | ||
| import { example0 } from "./example0/index.ts"; | ||
|
|
||
| export async function root() { | ||
| // Lint report: ts/forInArrays | ||
| for (const i in await example0("")) {} | ||
|
|
||
| // No lint report | ||
| for (const i of await example0("")) {} | ||
| } | ||
|
|
||
| ${indices.map((index) => `export { example${index} } from "./example${index}/index.js";`).join("\n\t\t")} | ||
|
JoshuaKGoldberg marked this conversation as resolved.
Outdated
|
||
| `; | ||
| } | ||
|
|
||
| function createSourceFiles(testCase: TestCase): Structure { | ||
| const topLevelWidth = Math.max(1, Math.floor(Math.log(testCase.files) * 1.7)); | ||
|
|
||
| return { | ||
| "index.ts": [createIndexFile(topLevelWidth), "typescript"], | ||
| ...Object.fromEntries( | ||
| Array.from({ length: topLevelWidth }, (_, index) => [ | ||
| `example${index}`, | ||
| createExampleDirectory(index), | ||
| ]), | ||
| ), | ||
| }; | ||
| } | ||
72 changes: 72 additions & 0 deletions
72
packages/performance-testing/src/creators/files/createESLintConfigFile.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import type { TestCaseRules } from "../../testCases.ts"; | ||
| import { comparedRules } from "./rules.ts"; | ||
|
|
||
| interface PluginPackage { | ||
| alias: string; | ||
| specifier: string; | ||
| } | ||
|
|
||
| const preregisteredPlugins = new Set(["@typescript-eslint"]); | ||
|
|
||
| const pluginPackages = new Map<string, PluginPackage>([ | ||
| ["import", { alias: "importPlugin", specifier: "eslint-plugin-import" }], | ||
| ["regexp", { alias: "regexp", specifier: "eslint-plugin-regexp" }], | ||
| ["unicorn", { alias: "unicorn", specifier: "eslint-plugin-unicorn" }], | ||
| ]); | ||
|
|
||
| export function createESLintConfigFile(rules: TestCaseRules): string { | ||
| const enabled = comparedRules[rules]; | ||
| const used = new Map<string, PluginPackage>(); | ||
|
|
||
| for (const { eslint } of enabled) { | ||
| const pluginName = getPluginName(eslint); | ||
|
|
||
| if (pluginName === undefined || preregisteredPlugins.has(pluginName)) { | ||
| continue; | ||
| } | ||
|
|
||
| const pluginPackage = pluginPackages.get(pluginName); | ||
|
|
||
| if (!pluginPackage) { | ||
| throw new Error(`No ESLint plugin package is known for ${eslint}.`); | ||
| } | ||
|
|
||
| used.set(pluginName, pluginPackage); | ||
| } | ||
|
|
||
| const plugins = Array.from(used, ([name, pluginPackage]) => ({ | ||
| ...pluginPackage, | ||
| name, | ||
| })).sort((a, b) => a.name.localeCompare(b.name)); | ||
|
|
||
| return ` | ||
| import { defineConfig, globalIgnores } from "eslint/config"; | ||
| ${plugins.map(({ alias, specifier }) => `import ${alias} from "${specifier}";`).join("\n")} | ||
| import tseslint from "typescript-eslint"; | ||
|
|
||
| export default defineConfig( | ||
| globalIgnores(["node_modules", "*.config.*"]), | ||
| tseslint.configs.base, | ||
| { | ||
| files: ["src/**/*.ts"], | ||
| languageOptions: { | ||
| parserOptions: { | ||
| projectService: true, | ||
| }, | ||
| }, | ||
| plugins: { | ||
| ${plugins.map(({ alias, name }) => `"${name}": ${alias}`).join(",\n")} | ||
| }, | ||
| rules: { | ||
| ${enabled.map(({ eslint }) => `"${eslint}": "error"`).join(",\n")} | ||
| }, | ||
| }, | ||
| ); | ||
| `; | ||
| } | ||
|
|
||
| function getPluginName(ruleName: string) { | ||
| const separator = ruleName.indexOf("/"); | ||
|
|
||
| return separator === -1 ? undefined : ruleName.slice(0, separator); | ||
| } |
22 changes: 22 additions & 0 deletions
22
packages/performance-testing/src/creators/files/createFlintConfigFile.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import type { TestCaseRules } from "../../testCases.ts"; | ||
| import { comparedRules } from "./rules.ts"; | ||
|
|
||
| export function createFlintConfigFile(rules: TestCaseRules): string { | ||
| return ` | ||
| import { defineConfig, ts } from "flint"; | ||
|
|
||
| export default defineConfig({ | ||
| ignore: ["node_modules", "*.config.*"], | ||
| use: [ | ||
| { | ||
| files: ["src/**/*.ts"], | ||
| rules: [ | ||
| ts.rules({ | ||
| ${comparedRules[rules].map(({ flint }) => `${flint}: true`).join(",\n")} | ||
| }), | ||
| ], | ||
| }, | ||
| ], | ||
| }); | ||
| `; | ||
| } |
10 changes: 10 additions & 0 deletions
10
packages/performance-testing/src/creators/files/createPackageFile.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import type { TestCase } from "../../testCases.ts"; | ||
| import { createTestCaseSlug } from "../../utils.ts"; | ||
|
|
||
| export function createPackageFile(data: TestCase): object { | ||
| return { | ||
| name: createTestCaseSlug(data), | ||
| private: true, | ||
| type: "module", | ||
| }; | ||
| } |
13 changes: 13 additions & 0 deletions
13
packages/performance-testing/src/creators/files/createStandardTSConfigFile.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| export function createStandardTSConfigFile(): object { | ||
| return { | ||
| compilerOptions: { | ||
| allowImportingTsExtensions: true, | ||
| module: "NodeNext", | ||
| noEmit: true, | ||
| skipLibCheck: true, | ||
| strict: true, | ||
| target: "ESNext", | ||
| }, | ||
| include: ["src"], | ||
| }; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.