diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0647a22..3b849f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,3 +62,27 @@ jobs: - name: Type-check run: pnpm type-check + + benchmark: + name: Benchmarks + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4.0.0 + + - name: Set node version to 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org/ + cache: pnpm + + - name: Install deps + run: pnpm install + + - name: Run benchmarks + run: pnpm benchmark diff --git a/benchmarks/completion.bench.ts b/benchmarks/completion.bench.ts new file mode 100644 index 0000000..e84fa4d --- /dev/null +++ b/benchmarks/completion.bench.ts @@ -0,0 +1,62 @@ +import { Bench } from 'tinybench'; +import { promisify } from 'node:util'; +import { exec as execCb } from 'node:child_process'; + +const exec = promisify(execCb); + +const bench = new Bench({ time: 2000 }); + +const cmdPrefix = `${process.execPath} ./dist/examples/demo.t.js complete --`; + +async function run(cmd: string) { + await exec(cmd); +} + +bench.add('command completion', async () => { + await run(`${cmdPrefix} d`); +}); + +bench.add('option completion', async () => { + await run(`${cmdPrefix} dev --p`); +}); + +bench.add('option value completion', async () => { + await run(`${cmdPrefix} dev --port ""`); +}); + +bench.add('config value completion', async () => { + await run(`${cmdPrefix} --config ""`); +}); + +bench.add('no match', async () => { + await run(`${cmdPrefix} xyz`); +}); + +async function runBenchmarks() { + await bench.run(); + + console.table( + bench.tasks.map((task) => { + const hz = task.result?.hz; + const derivedMs = + typeof hz === 'number' && hz > 0 ? 1000 / hz : undefined; + const mean = task.result?.mean; + return { + name: task.name, + 'ops/sec': hz ? Math.round(hz).toLocaleString() : 'N/A', + 'avg (ms)': + derivedMs !== undefined + ? derivedMs.toFixed(3) + : mean !== undefined + ? (mean * 1000).toFixed(3) + : 'N/A', + }; + }) + ); +} + +if (process.argv[1]?.endsWith('completion.bench.ts')) { + runBenchmarks().catch(console.error); +} + +export { runBenchmarks }; diff --git a/package.json b/package.json index b7b3b9f..065cfe3 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "build": "tsdown", "prepare": "pnpm build", "lint": "eslint src \"./*.ts\"", - "test-cli": "tsx bin/cli.ts" + "benchmark": "tsx benchmarks/completion.bench.ts" }, "files": [ "dist" @@ -30,6 +30,7 @@ "commander": "^13.1.0", "eslint-config-prettier": "^10.0.1", "prettier": "^3.5.2", + "tinybench": "^4.0.1", "tsdown": "^0.9.7", "tsx": "^4.19.1", "typescript": "^5.7.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78da1d2..d963698 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: prettier: specifier: ^3.5.2 version: 3.5.2 + tinybench: + specifier: ^4.0.1 + version: 4.0.1 tsdown: specifier: ^0.9.7 version: 0.9.7(typescript@5.7.3) @@ -1383,6 +1386,10 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinybench@4.0.1: + resolution: {integrity: sha512-Nb1srn7dvzkVx0J5h1vq8f48e3TIcbrS7e/UfAI/cDSef/n8yLh4zsAEsFkfpw6auTY+ZaspEvam/xs8nMnotQ==} + engines: {node: '>=18.0.0'} + tinyexec@0.3.1: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} @@ -2708,6 +2715,8 @@ snapshots: tinybench@2.9.0: {} + tinybench@4.0.1: {} + tinyexec@0.3.1: {} tinyexec@1.0.1: {} diff --git a/tsdown.config.ts b/tsdown.config.ts index a29c942..e12fc42 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -7,6 +7,10 @@ export default defineConfig({ 'src/cac.ts', 'src/commander.ts', 'bin/cli.ts', + 'examples/demo.t.ts', + 'examples/demo.cac.ts', + 'examples/demo.citty.ts', + 'examples/demo.commander.ts', ], format: ['esm'], dts: true,