diff --git a/.github/workflows/bench-main.yml b/.github/workflows/bench-main.yml new file mode 100644 index 000000000..0180c96e4 --- /dev/null +++ b/.github/workflows/bench-main.yml @@ -0,0 +1,38 @@ +name: Benchmark Main Branch + +on: + push: + branches: [main] + paths: + - 'src/**/*.ts' + +jobs: + benchmark: + name: Run Benchmarks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22.x' + cache: pnpm + + - name: Install dependencies + run: | + pnpm install -C scripts/benchmarks + pnpm install -C scripts/radashi-db + + - uses: awalsh128/cache-apt-pkgs-action@v1.4.2 + with: + packages: valgrind + version: 1 # The “cache version” for cache-busting + + - name: Run benchmarks + env: + SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} + run: | + # https://pythonspeed.com/articles/consistent-benchmarking-in-ci/ + valgrind --tool=cachegrind ./scripts/benchmarks/node_modules/.bin/tsx ./scripts/benchmarks/ci-bench-main.ts diff --git a/.github/workflows/bench-pr.yml b/.github/workflows/bench-pr.yml new file mode 100644 index 000000000..1c4b86f59 --- /dev/null +++ b/.github/workflows/bench-pr.yml @@ -0,0 +1,39 @@ +name: Benchmark Pull Request + +on: + pull_request: + branches: [main, next] + paths: + - 'src/**/*.ts' + +jobs: + benchmark: + name: Run Benchmarks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22.x' + cache: pnpm + + - name: Install dependencies + run: | + pnpm install -C scripts/benchmarks + pnpm install -C scripts/radashi-db + + - uses: awalsh128/cache-apt-pkgs-action@v1.4.2 + with: + packages: valgrind + version: 1 # The “cache version” for cache-busting + + - name: Run benchmarks + env: + RADASHI_BOT_TOKEN: ${{ secrets.RADASHI_BOT_TOKEN }} + SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} + run: | + # https://pythonspeed.com/articles/consistent-benchmarking-in-ci/ + valgrind --tool=cachegrind ./scripts/benchmarks/node_modules/.bin/tsx ./scripts/benchmarks/ci-bench-pr.ts ${{ github.base_ref }} ${{ github.event.number }} diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml deleted file mode 100644 index 5311c4f67..000000000 --- a/.github/workflows/codspeed.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: codspeed-benchmarks - -on: - # pull_request: - # branches: - # - "main" - # `workflow_dispatch` allows CodSpeed to trigger backtest - # performance analysis in order to generate initial data. - workflow_dispatch: - -jobs: - benchmarks: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - registry-url: 'https://registry.npmjs.org' - node-version: '22.x' - cache: pnpm - - run: pnpm install - - name: Run benchmarks - uses: CodSpeedHQ/action@v2 - with: - token: ${{ secrets.CODSPEED_TOKEN }} - run: pnpm bench diff --git a/.github/workflows/seed-benchmarks.yml b/.github/workflows/seed-benchmarks.yml new file mode 100644 index 000000000..907fbfca0 --- /dev/null +++ b/.github/workflows/seed-benchmarks.yml @@ -0,0 +1,32 @@ +name: Seed Benchmarks + +on: + workflow_dispatch: + +jobs: + seed-benchmarks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22.x' + cache: pnpm + + - name: Install dependencies + run: | + pnpm install -C scripts/benchmarks + pnpm install -C scripts/radashi-db + + - uses: awalsh128/cache-apt-pkgs-action@v1.4.2 + with: + packages: valgrind + version: 1 # The “cache version” for cache-busting + + - name: Run benchmarks and seed database + env: + SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} + run: | + # https://pythonspeed.com/articles/consistent-benchmarking-in-ci/ + valgrind --tool=cachegrind ./scripts/benchmarks/node_modules/.bin/tsx ./scripts/benchmarks/seed-benchmarks.ts diff --git a/package.json b/package.json index f0d77c692..570dde8b6 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ }, "devDependencies": { "@biomejs/biome": "^1.8.3", - "@codspeed/vitest-plugin": "^3.1.0", "@typescript-eslint/parser": "^7.16.1", "@vitest/coverage-v8": "2.0.3", "concurrently": "^8.2.2", @@ -66,10 +65,5 @@ "browserslist": [ ">0.1% and not dead", "node >= 16" - ], - "pnpm": { - "patchedDependencies": { - "@codspeed/vitest-plugin@3.1.0": "patches/@codspeed__vitest-plugin@3.1.0.patch" - } - } + ] } diff --git a/patches/@codspeed__vitest-plugin@3.1.0.patch b/patches/@codspeed__vitest-plugin@3.1.0.patch deleted file mode 100644 index 2fd1f71e3..000000000 --- a/patches/@codspeed__vitest-plugin@3.1.0.patch +++ /dev/null @@ -1,16 +0,0 @@ -diff --git a/dist/index.d.ts b/dist/index.d.mts -similarity index 100% -rename from dist/index.d.ts -rename to dist/index.d.mts -diff --git a/package.json b/package.json -index f51062b7f85397bd10b24cfd01e4316c410e8fd7..cb5ed57516f899119d08bca600a3a36dab97a9d4 100644 ---- a/package.json -+++ b/package.json -@@ -12,6 +12,7 @@ - "types": "./dist/index.d.ts", - "exports": { - ".": { -+ "types": "./dist/index.d.mts", - "import": "./dist/index.mjs" - } - }, diff --git a/scripts/benchmarks/ci-bench-main.ts b/scripts/benchmarks/ci-bench-main.ts new file mode 100644 index 000000000..c7a314715 --- /dev/null +++ b/scripts/benchmarks/ci-bench-main.ts @@ -0,0 +1,96 @@ +import { execa } from 'execa' +import { existsSync } from 'node:fs' +import { supabase } from 'radashi-db/supabase.js' +import { createVitest } from 'vitest/node' +import { getChangedFiles } from './src/get-changed.js' +import { type Benchmark, reportToBenchmarkHandler } from './src/reporter.js' + +main() + +async function main() { + if (!process.env.SUPABASE_KEY) { + throw new Error('SUPABASE_KEY is not set') + } + + // Get the last benched SHA + const metaResult = await supabase + .from('meta') + .select('value') + .eq('id', 'last_benched_sha') + .limit(1) + .single() + + if (metaResult.error) { + console.error('Error fetching last benched SHA:', metaResult.error) + return + } + + const lastBenchedSha = metaResult.data.value as string + const currentSha = await execa('git', ['rev-parse', 'HEAD']).then( + result => result.stdout, + ) + + if (lastBenchedSha === currentSha) { + console.log('No changes since last benched SHA') + return + } + + const files = await getChangedFiles(lastBenchedSha, ['src/**/*.ts']) + + const results: Benchmark[] = [] + + const vitest = await createVitest('benchmark', { + benchmark: { + reporters: [ + reportToBenchmarkHandler(benchmark => { + results.push(benchmark) + }), + ], + }, + }) + + for (const file of files) { + // Run benchmarks for modified or added source files in a function group + if ( + (file.status === 'M' || file.status === 'A') && + /^src\/.+?\//.test(file.name) + ) { + const benchFile = file.name + .replace('src', 'benchmarks') + .replace(/\.ts$/, '.bench.ts') + + if (existsSync(benchFile)) { + console.log(`Running benchmarks in ./${benchFile}`) + await vitest.start([benchFile]) + } + } + } + + console.log('Results', results) + + const { error: insertError } = await supabase.from('benchmarks').insert( + results.map(result => ({ + ...result, + sha: currentSha, + })), + ) + + if (insertError) { + insertError.message = + 'Error inserting benchmark results: ' + insertError.message + throw insertError + } + + const { error: updateError } = await supabase + .from('meta') + .update({ value: currentSha }) + .eq('id', 'last_benched_sha') + + if (updateError) { + updateError.message = + 'Error updating last benched SHA: ' + updateError.message + throw updateError + } + + console.log('Completed', results.length, 'benchmarks') +} diff --git a/scripts/benchmarks/ci-bench-pr.ts b/scripts/benchmarks/ci-bench-pr.ts new file mode 100644 index 000000000..de09d189d --- /dev/null +++ b/scripts/benchmarks/ci-bench-pr.ts @@ -0,0 +1,143 @@ +import { Octokit } from '@octokit/rest' +import mri from 'mri' +import { benchAddedFiles } from './src/bench-added.js' +import { benchChangedFiles } from './src/bench-changed.js' + +const octokit = new Octokit({ + auth: process.env.RADASHI_BOT_TOKEN, +}) + +main() + +async function main() { + const { baseRef, prNumber } = parseArgv(process.argv.slice(2)) + + // Run the benchmarks + const addedBenchmarks = await benchAddedFiles(baseRef) + const changedBenchmarks = await benchChangedFiles(baseRef) + + console.log('Added', addedBenchmarks) + console.log('Changed', changedBenchmarks) + + const columnNames = ['Name', 'Current'] + if (changedBenchmarks.some(b => b.baseline)) { + columnNames.push('Baseline', 'Change') + } + + // Create the comment body + let comment = '## Benchmark Results\n\n' + comment += `| ${columnNames.join(' | ')} |\n` + comment += `| ${columnNames.map(name => '-'.repeat(name.length)).join(' | ')} |\n` + + for (const { result, baseline } of changedBenchmarks) { + if (!baseline) { + addedBenchmarks.push(result) + continue + } + + const change = ((result.hz - baseline.hz) / baseline.hz) * 100 + const columns = [ + `${result.func}: ${result.name}`, + `${formatNumber(result.hz)} ops/sec ±${formatNumber(result.rme)}%`, + `${formatNumber(baseline.hz)} ops/sec ±${formatNumber(baseline.rme)}%`, + formatChange(change), + ] + + comment += `| ${columns.join(' | ')} |\n` + } + + for (const result of addedBenchmarks) { + const columns = [ + `${result.func}: ${result.name}`, + `${formatNumber(result.hz)} ops/sec ±${formatNumber(result.rme)}%`, + ] + + if (columnNames.length > 2) { + columns.push('', '') + } + + comment += `| ${columns.join(' | ')} |\n` + } + + // Delete the previous benchmark comment if it exists + try { + const { data: comments } = await octokit.rest.issues.listComments({ + owner: 'radashi-org', + repo: 'radashi', + issue_number: prNumber, + per_page: 100, + }) + + const benchmarkComment = comments.find( + comment => + comment.body?.startsWith('## Benchmark Results') && + comment.user?.login === 'radashi-bot', + ) + + if (benchmarkComment) { + await octokit.rest.issues.deleteComment({ + owner: 'radashi-org', + repo: 'radashi', + comment_id: benchmarkComment.id, + }) + console.log('Successfully deleted previous benchmark comment.') + } + } catch (error) { + console.error('Error deleting previous benchmark comment:', error) + } + + // Create a comment in the PR with the benchmark results + try { + const { data: pullRequest } = await octokit.rest.pulls.get({ + owner: 'radashi-org', + repo: 'radashi', + pull_number: prNumber, + }) + + await octokit.rest.issues.createComment({ + owner: 'radashi-org', + repo: 'radashi', + issue_number: pullRequest.number, + body: comment, + }) + + console.log('Successfully posted benchmark results as a comment.') + } catch (error: any) { + error.message = `Failed to create comment in PR: ${error.message}` + throw error + } +} + +function formatNumber(n: number) { + return new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 }).format(n) +} + +function formatChange(change: number) { + return `${change >= 0 ? '🚀' : '🐢'} ${change >= 0 ? '+' : ''}${formatNumber(change)}%` +} + +function parseArgv(argv: string[]) { + if (!process.env.SUPABASE_KEY) { + throw new Error('SUPABASE_KEY is not set') + } + if (!process.env.RADASHI_BOT_TOKEN) { + throw new Error('RADASHI_BOT_TOKEN is not set') + } + + // Prevent access to secrets from the benchmarks. + process.env.SUPABASE_KEY = '' + process.env.RADASHI_BOT_TOKEN = '' + + const { + _: [baseRef, prNumber], + } = mri(argv) + + if (!baseRef || Number.isNaN(+prNumber)) { + throw new Error('Invalid arguments') + } + + return { + baseRef, + prNumber: +prNumber, + } +} diff --git a/scripts/benchmarks/package.json b/scripts/benchmarks/package.json new file mode 100644 index 000000000..d61c39be3 --- /dev/null +++ b/scripts/benchmarks/package.json @@ -0,0 +1,14 @@ +{ + "private": true, + "type": "module", + "dependencies": { + "@octokit/rest": "^21.0.1", + "@types/node": "^22.2.0", + "execa": "^9.3.0", + "fast-glob": "^3.3.2", + "mri": "^1.2.0", + "radashi": "link:../../src", + "radashi-db": "link:../radashi-db", + "tsx": "^4.17.0" + } +} diff --git a/scripts/benchmarks/pnpm-lock.yaml b/scripts/benchmarks/pnpm-lock.yaml new file mode 100644 index 000000000..6a50c0bcd --- /dev/null +++ b/scripts/benchmarks/pnpm-lock.yaml @@ -0,0 +1,754 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@octokit/rest': + specifier: ^21.0.1 + version: 21.0.1 + '@types/node': + specifier: ^22.2.0 + version: 22.2.0 + execa: + specifier: ^9.3.0 + version: 9.3.0 + fast-glob: + specifier: ^3.3.2 + version: 3.3.2 + mri: + specifier: ^1.2.0 + version: 1.2.0 + radashi: + specifier: link:../../src + version: link:../../src + radashi-db: + specifier: link:../radashi-db + version: link:../radashi-db + tsx: + specifier: ^4.17.0 + version: 4.17.0 + +packages: + + '@esbuild/aix-ppc64@0.23.0': + resolution: {integrity: sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.23.0': + resolution: {integrity: sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.23.0': + resolution: {integrity: sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.23.0': + resolution: {integrity: sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.23.0': + resolution: {integrity: sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.23.0': + resolution: {integrity: sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.23.0': + resolution: {integrity: sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.23.0': + resolution: {integrity: sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.23.0': + resolution: {integrity: sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.23.0': + resolution: {integrity: sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.23.0': + resolution: {integrity: sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.23.0': + resolution: {integrity: sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.23.0': + resolution: {integrity: sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.23.0': + resolution: {integrity: sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.23.0': + resolution: {integrity: sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.23.0': + resolution: {integrity: sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.23.0': + resolution: {integrity: sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.23.0': + resolution: {integrity: sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.0': + resolution: {integrity: sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.23.0': + resolution: {integrity: sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.23.0': + resolution: {integrity: sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.23.0': + resolution: {integrity: sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.23.0': + resolution: {integrity: sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.23.0': + resolution: {integrity: sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@octokit/auth-token@5.1.1': + resolution: {integrity: sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==} + engines: {node: '>= 18'} + + '@octokit/core@6.1.2': + resolution: {integrity: sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==} + engines: {node: '>= 18'} + + '@octokit/endpoint@10.1.1': + resolution: {integrity: sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==} + engines: {node: '>= 18'} + + '@octokit/graphql@8.1.1': + resolution: {integrity: sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@22.2.0': + resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==} + + '@octokit/plugin-paginate-rest@11.3.3': + resolution: {integrity: sha512-o4WRoOJZlKqEEgj+i9CpcmnByvtzoUYC6I8PD2SA95M+BJ2x8h7oLcVOg9qcowWXBOdcTRsMZiwvM3EyLm9AfA==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-request-log@5.3.1': + resolution: {integrity: sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@13.2.4': + resolution: {integrity: sha512-gusyAVgTrPiuXOdfqOySMDztQHv6928PQ3E4dqVGEtOvRXAKRbJR4b1zQyniIT9waqaWk/UDaoJ2dyPr7Bk7Iw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/request-error@6.1.4': + resolution: {integrity: sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==} + engines: {node: '>= 18'} + + '@octokit/request@9.1.3': + resolution: {integrity: sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==} + engines: {node: '>= 18'} + + '@octokit/rest@21.0.1': + resolution: {integrity: sha512-RWA6YU4CqK0h0J6tfYlUFnH3+YgBADlxaHXaKSG+BVr2y4PTfbU2tlKuaQoQZ83qaTbi4CUxLNAmbAqR93A6mQ==} + engines: {node: '>= 18'} + + '@octokit/types@13.5.0': + resolution: {integrity: sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@types/node@22.2.0': + resolution: {integrity: sha512-bm6EG6/pCpkxDf/0gDNDdtDILMOHgaQBVOJGdwsqClnxA3xL6jtMv76rLBc006RVMWbmaf0xbmom4Z/5o2nRkQ==} + + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + esbuild@0.23.0: + resolution: {integrity: sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==} + engines: {node: '>=18'} + hasBin: true + + execa@9.3.0: + resolution: {integrity: sha512-l6JFbqnHEadBoVAVpN5dl2yCyfX28WoBAGaoQcNmLLSedOxTxcn2Qa83s8I/PA5i56vWru2OHOtrwF7Om2vqlg==} + engines: {node: ^18.19.0 || >=20.5.0} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-tsconfig@4.7.6: + resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + human-signals@7.0.0: + resolution: {integrity: sha512-74kytxOUSvNbjrT9KisAbaTZ/eJwD/LrbM/kh5j0IhPuJzwuA19dWvniFGwBzN9rVjg+O/e+F310PjObDXS+9Q==} + engines: {node: '>=18.18.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.0.0: + resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pretty-ms@9.1.0: + resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} + engines: {node: '>=18'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tsx@4.17.0: + resolution: {integrity: sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==} + engines: {node: '>=18.0.0'} + hasBin: true + + undici-types@6.13.0: + resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==} + + universal-user-agent@7.0.2: + resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + +snapshots: + + '@esbuild/aix-ppc64@0.23.0': + optional: true + + '@esbuild/android-arm64@0.23.0': + optional: true + + '@esbuild/android-arm@0.23.0': + optional: true + + '@esbuild/android-x64@0.23.0': + optional: true + + '@esbuild/darwin-arm64@0.23.0': + optional: true + + '@esbuild/darwin-x64@0.23.0': + optional: true + + '@esbuild/freebsd-arm64@0.23.0': + optional: true + + '@esbuild/freebsd-x64@0.23.0': + optional: true + + '@esbuild/linux-arm64@0.23.0': + optional: true + + '@esbuild/linux-arm@0.23.0': + optional: true + + '@esbuild/linux-ia32@0.23.0': + optional: true + + '@esbuild/linux-loong64@0.23.0': + optional: true + + '@esbuild/linux-mips64el@0.23.0': + optional: true + + '@esbuild/linux-ppc64@0.23.0': + optional: true + + '@esbuild/linux-riscv64@0.23.0': + optional: true + + '@esbuild/linux-s390x@0.23.0': + optional: true + + '@esbuild/linux-x64@0.23.0': + optional: true + + '@esbuild/netbsd-x64@0.23.0': + optional: true + + '@esbuild/openbsd-arm64@0.23.0': + optional: true + + '@esbuild/openbsd-x64@0.23.0': + optional: true + + '@esbuild/sunos-x64@0.23.0': + optional: true + + '@esbuild/win32-arm64@0.23.0': + optional: true + + '@esbuild/win32-ia32@0.23.0': + optional: true + + '@esbuild/win32-x64@0.23.0': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@octokit/auth-token@5.1.1': {} + + '@octokit/core@6.1.2': + dependencies: + '@octokit/auth-token': 5.1.1 + '@octokit/graphql': 8.1.1 + '@octokit/request': 9.1.3 + '@octokit/request-error': 6.1.4 + '@octokit/types': 13.5.0 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.2 + + '@octokit/endpoint@10.1.1': + dependencies: + '@octokit/types': 13.5.0 + universal-user-agent: 7.0.2 + + '@octokit/graphql@8.1.1': + dependencies: + '@octokit/request': 9.1.3 + '@octokit/types': 13.5.0 + universal-user-agent: 7.0.2 + + '@octokit/openapi-types@22.2.0': {} + + '@octokit/plugin-paginate-rest@11.3.3(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/types': 13.5.0 + + '@octokit/plugin-request-log@5.3.1(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + + '@octokit/plugin-rest-endpoint-methods@13.2.4(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/types': 13.5.0 + + '@octokit/request-error@6.1.4': + dependencies: + '@octokit/types': 13.5.0 + + '@octokit/request@9.1.3': + dependencies: + '@octokit/endpoint': 10.1.1 + '@octokit/request-error': 6.1.4 + '@octokit/types': 13.5.0 + universal-user-agent: 7.0.2 + + '@octokit/rest@21.0.1': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/plugin-paginate-rest': 11.3.3(@octokit/core@6.1.2) + '@octokit/plugin-request-log': 5.3.1(@octokit/core@6.1.2) + '@octokit/plugin-rest-endpoint-methods': 13.2.4(@octokit/core@6.1.2) + + '@octokit/types@13.5.0': + dependencies: + '@octokit/openapi-types': 22.2.0 + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@types/node@22.2.0': + dependencies: + undici-types: 6.13.0 + + before-after-hook@3.0.2: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + esbuild@0.23.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.0 + '@esbuild/android-arm': 0.23.0 + '@esbuild/android-arm64': 0.23.0 + '@esbuild/android-x64': 0.23.0 + '@esbuild/darwin-arm64': 0.23.0 + '@esbuild/darwin-x64': 0.23.0 + '@esbuild/freebsd-arm64': 0.23.0 + '@esbuild/freebsd-x64': 0.23.0 + '@esbuild/linux-arm': 0.23.0 + '@esbuild/linux-arm64': 0.23.0 + '@esbuild/linux-ia32': 0.23.0 + '@esbuild/linux-loong64': 0.23.0 + '@esbuild/linux-mips64el': 0.23.0 + '@esbuild/linux-ppc64': 0.23.0 + '@esbuild/linux-riscv64': 0.23.0 + '@esbuild/linux-s390x': 0.23.0 + '@esbuild/linux-x64': 0.23.0 + '@esbuild/netbsd-x64': 0.23.0 + '@esbuild/openbsd-arm64': 0.23.0 + '@esbuild/openbsd-x64': 0.23.0 + '@esbuild/sunos-x64': 0.23.0 + '@esbuild/win32-arm64': 0.23.0 + '@esbuild/win32-ia32': 0.23.0 + '@esbuild/win32-x64': 0.23.0 + + execa@9.3.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.3 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 7.0.0 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 5.3.0 + pretty-ms: 9.1.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.1 + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.7 + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.0.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + fsevents@2.3.3: + optional: true + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-tsconfig@4.7.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + human-signals@7.0.0: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-plain-obj@4.1.0: {} + + is-stream@4.0.1: {} + + is-unicode-supported@2.0.0: {} + + isexe@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.7: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mri@1.2.0: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + parse-ms@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + picomatch@2.3.1: {} + + pretty-ms@9.1.0: + dependencies: + parse-ms: 4.0.0 + + queue-microtask@1.2.3: {} + + resolve-pkg-maps@1.0.0: {} + + reusify@1.0.4: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + strip-final-newline@4.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tsx@4.17.0: + dependencies: + esbuild: 0.23.0 + get-tsconfig: 4.7.6 + optionalDependencies: + fsevents: 2.3.3 + + undici-types@6.13.0: {} + + universal-user-agent@7.0.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + yoctocolors@2.1.1: {} diff --git a/scripts/benchmarks/seed-benchmarks.ts b/scripts/benchmarks/seed-benchmarks.ts new file mode 100644 index 000000000..05736e730 --- /dev/null +++ b/scripts/benchmarks/seed-benchmarks.ts @@ -0,0 +1,71 @@ +import { execa } from 'execa' +import glob from 'fast-glob' +import { existsSync } from 'node:fs' +import { supabase } from 'radashi-db/supabase.js' +import { createVitest } from 'vitest/node' +import { type Benchmark, reportToBenchmarkHandler } from './src/reporter.js' + +async function main() { + if (!process.env.SUPABASE_KEY) { + throw new Error('SUPABASE_KEY is not set') + } + + const results: Benchmark[] = [] + + const vitest = await createVitest('benchmark', { + benchmark: { + reporters: [ + reportToBenchmarkHandler(benchmark => { + results.push(benchmark) + }), + ], + }, + }) + + for (const file of await glob('src/**/*.ts')) { + if (!/^src\/.+?\//.test(file)) { + continue + } + + const benchFile = file + .replace('src', 'benchmarks') + .replace(/\.ts$/, '.bench.ts') + + if (existsSync(benchFile)) { + console.log(`Running benchmarks in ./${benchFile}`) + await vitest.start([benchFile]) + } + } + + const { stdout: currentSha } = await execa('git', ['rev-parse', 'HEAD']) + + const { error: insertError } = await supabase.from('benchmarks').insert( + results.map(result => ({ + ...result, + sha: currentSha, + })), + ) + + if (insertError) { + insertError.message = + 'Error inserting benchmark results: ' + insertError.message + throw insertError + } + + const { error: updateError } = await supabase + .from('meta') + .upsert({ id: 'last_benched_sha', value: currentSha }) + .eq('id', 'last_benched_sha') + + if (updateError) { + console.error('Error updating last benched SHA:', updateError) + } else { + console.log('Updated "last_benched_sha" in meta table') + } + + console.log( + `Seeded ${results.length} benchmark results for SHA: ${currentSha}`, + ) +} + +main().catch(console.error) diff --git a/scripts/benchmarks/src/bench-added.ts b/scripts/benchmarks/src/bench-added.ts new file mode 100644 index 000000000..2e13062aa --- /dev/null +++ b/scripts/benchmarks/src/bench-added.ts @@ -0,0 +1,46 @@ +/** + * This script checks which functions have been added and runs the + * benchmarks for them. + */ +import { existsSync } from 'node:fs' +import { createVitest } from 'vitest/node' +import { getChangedFiles } from './get-changed.js' +import { type Benchmark, reportToBenchmarkHandler } from './reporter.js' + +/** + * Given a target branch, run the benchmarks for any source files that have + * been modified. It returns the results of the benchmarks. + */ +export async function benchAddedFiles(targetBranch: string) { + const files = await getChangedFiles(targetBranch, ['src/**/*.ts']) + + const results: Benchmark[] = [] + + const vitest = await createVitest('benchmark', { + benchmark: { + reporters: [ + reportToBenchmarkHandler(benchmark => { + results.push(benchmark) + }), + ], + }, + }) + + for (const file of files) { + // Only run benchmarks for added source files in a function group. + if (file.status !== 'A' || !/^src\/.+?\//.test(file.name)) { + continue + } + + const benchFile = file.name + .replace('src', 'benchmarks') + .replace(/\.ts$/, '.bench.ts') + + if (existsSync(benchFile)) { + console.log(`Running benchmarks in ./${benchFile}`) + await vitest.start([benchFile]) + } + } + + return results +} diff --git a/scripts/benchmarks/src/bench-changed.ts b/scripts/benchmarks/src/bench-changed.ts new file mode 100644 index 000000000..083cfed74 --- /dev/null +++ b/scripts/benchmarks/src/bench-changed.ts @@ -0,0 +1,56 @@ +/** + * This script checks which functions have been modified and runs the + * benchmarks for them. + */ +import { existsSync } from 'node:fs' +import { createVitest } from 'vitest/node' +import { supabase } from '../../radashi-db/supabase.js' +import { getChangedFiles } from './get-changed.js' +import { type Benchmark, reportToBenchmarkHandler } from './reporter.js' + +/** + * Given a target branch, run the benchmarks for any source files that have + * been modified. It returns the results of the benchmarks. + */ +export async function benchChangedFiles(targetBranch: string) { + const files = await getChangedFiles(targetBranch, ['src/**/*.ts']) + + const results: { result: Benchmark; baseline: Benchmark | null }[] = [] + + const vitest = await createVitest('benchmark', { + benchmark: { + reporters: [ + reportToBenchmarkHandler(async result => { + const { data: baseline } = await supabase + .from('benchmarks') + .select('*') + .eq('func', result.func) + .eq('name', result.name) + .order('created_at', { ascending: false }) + .limit(1) + .single() + + results.push({ result, baseline }) + }), + ], + }, + }) + + for (const file of files) { + // Only run benchmarks for modified source files in a function group. + if (file.status !== 'M' || !/^src\/.+?\//.test(file.name)) { + continue + } + + const benchFile = file.name + .replace('src', 'benchmarks') + .replace(/\.ts$/, '.bench.ts') + + if (existsSync(benchFile)) { + console.log(`Running benchmarks in ./${benchFile}`) + await vitest.start([benchFile]) + } + } + + return results +} diff --git a/scripts/benchmarks/src/get-changed.ts b/scripts/benchmarks/src/get-changed.ts new file mode 100644 index 000000000..3ff99f0c0 --- /dev/null +++ b/scripts/benchmarks/src/get-changed.ts @@ -0,0 +1,21 @@ +import { execa } from 'execa' +import { cluster } from 'radashi/array/cluster.js' + +export async function getChangedFiles( + targetBranch: string, + globs: [string, ...string[]], +) { + const { stdout } = await execa('git', [ + 'diff', + '--name-status', + `origin/${targetBranch}`, + 'HEAD', + '--', + ...globs, + ]) + + return cluster(stdout.trim().split(/[\r\n\t]+/), 2).map(([status, name]) => ({ + status, + name, + })) +} diff --git a/scripts/benchmarks/src/reporter.ts b/scripts/benchmarks/src/reporter.ts new file mode 100644 index 000000000..b244fcc2b --- /dev/null +++ b/scripts/benchmarks/src/reporter.ts @@ -0,0 +1,39 @@ +import path from 'node:path' +import type { Reporter } from 'vitest' + +export interface Benchmark { + func: string + name: string + /** Cycles per second */ + hz: number + /** Standard deviation */ + sd: number + /** Relative mean error */ + rme: number +} + +export function reportToBenchmarkHandler( + handler: (benchmark: Benchmark) => void, +): Reporter { + return { + async onFinished(files) { + for (const file of files) { + const func = path.basename(file.filepath).replace(/\.bench\.ts$/, '') + for (const task of file.tasks) { + const benchmark = task.result?.benchmark + if (!benchmark) { + continue + } + + handler({ + func, + name: task.name, + hz: benchmark.hz, + sd: benchmark.sd, + rme: benchmark.rme, + }) + } + } + }, + } +} diff --git a/vitest.config.ts b/vitest.config.ts index 086e895a7..a6f97f621 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,10 +1,9 @@ -import codspeed from '@codspeed/vitest-plugin' import { defineConfig } from 'vitest/config' const resolve = (specifier: string) => new URL(import.meta.resolve(specifier)).pathname -export default defineConfig(({ mode }) => ({ +export default defineConfig({ test: { globals: true, coverage: { @@ -17,5 +16,4 @@ export default defineConfig(({ mode }) => ({ radashi: resolve('./src/mod.js'), }, }, - plugins: [mode === 'benchmark' && process.env.CI ? codspeed() : []], -})) +})