diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40e3f26..89de8f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: jobs: build: permissions: - contents: read + contents: write id-token: write runs-on: ubuntu-latest steps: @@ -27,3 +27,13 @@ jobs: use_oidc: true - run: npm run build - run: npm run docs + - run: npm run benchmark + - uses: benchmark-action/github-action-benchmark@d48d326b4ca9ba73ca0cd0d59f108f9e02a381c7 #v1.20.4 + with: + name: Tansu benchmarks + tool: 'customBiggerIsBetter' + output-file-path: benchmarks.json + auto-push: ${{ github.event_name == 'push' }} + github-token: ${{ secrets.GITHUB_TOKEN }} + comment-on-alert: true + summary-always: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 23bb361..fefb367 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,8 +50,10 @@ jobs: - name: Publish documentation working-directory: gh-pages run: | + mv dev .. rm -rf * cp -a ../dist/docs/* . + mv ../dev . git add . git commit --allow-empty -a -m "Updating from ${{ github.sha }}" git push origin gh-pages diff --git a/.gitignore b/.gitignore index 7ed5884..f6af7e2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist temp coverage .angular +benchmarks.json diff --git a/benchmarks/basic.bench.ts b/benchmarks/basic.bench.ts new file mode 100644 index 0000000..aa8570a --- /dev/null +++ b/benchmarks/basic.bench.ts @@ -0,0 +1,82 @@ +import { bench, describe } from 'vitest'; +import type { ReadableSignal, Unsubscriber } from '../src'; +import { computed, derived, DerivedStore, readable, Store, writable } from '../src'; + +class StoreClass extends Store {} +class DoubleStoreClass extends DerivedStore> { + protected override derive(value: number): Unsubscriber | void { + this.set(2 * value); + } +} + +describe('creating base stores', () => { + bench('writable', () => { + writable(0); + }); + bench('readable', () => { + readable(0); + }); + + bench('new StoreClass', () => { + new StoreClass(0); + }); +}); + +describe('creating derived stores', () => { + const baseStore = writable(0); + bench('computed', () => { + computed(() => 2 * baseStore()); + }); + + bench('derived', () => { + derived(baseStore, (a) => 2 * a); + }); + + bench('new DoubleStoreClass', () => { + new DoubleStoreClass(baseStore, 0); + }); +}); + +describe('updating derived stores', () => { + const baseStore1 = writable(0); + computed(() => 2 * baseStore1()).subscribe(() => {}); + let count1 = 0; + bench('computed', () => { + count1++; + baseStore1.set(count1); + }); + + const baseStore2 = writable(0); + derived(baseStore2, (a) => 2 * a).subscribe(() => {}); + let count2 = 0; + bench('derived', () => { + count2++; + baseStore2.set(count2); + }); + + const baseStore3 = writable(0); + new DoubleStoreClass(baseStore3, 0).subscribe(() => {}); + let count3 = 0; + bench('DoubleStoreClass', () => { + count3++; + baseStore3.set(count3); + }); +}); + +describe('updating writable stores', () => { + const storeWithoutSubscriber = writable(0); + let count1 = 0; + + bench('without subscriber', () => { + count1++; + storeWithoutSubscriber.set(count1); + }); + + const storeWithSubscriber = writable(0); + storeWithSubscriber.subscribe(() => {}); + let count2 = 0; + bench('with subscriber', () => { + count2++; + storeWithSubscriber.set(count2); + }); +}); diff --git a/benchmarks/jsonArrayReporter.ts b/benchmarks/jsonArrayReporter.ts new file mode 100644 index 0000000..1957ce4 --- /dev/null +++ b/benchmarks/jsonArrayReporter.ts @@ -0,0 +1,38 @@ +import { RunnerTestFile } from 'vitest'; +import { Reporter } from 'vitest/reporters'; +import { writeFile } from 'fs/promises'; + +class JsonArrayReporter implements Reporter { + async onFinished(files: RunnerTestFile[]): Promise { + const results: { + name: string; + unit: string; + value: number; + }[] = []; + + function processTasks(tasks: RunnerTestFile['tasks'], name: string) { + for (const task of tasks) { + if (task.type === 'suite') { + processTasks(task.tasks, `${name} > ${task.name}`); + } else { + const value = task.result?.benchmark?.hz; + if (value) { + results.push({ + name: `${name} > ${task.name}`, + unit: 'Hz', + value, + }); + } + } + } + } + + for (const file of files) { + processTasks(file.tasks, file.name); + } + + await writeFile('benchmarks.json', JSON.stringify(results, null, ' ')); + } +} + +export default JsonArrayReporter; diff --git a/package.json b/package.json index c557d9d..4b65286 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "test": "vitest run", "tdd": "vitest", "tdd:ui": "vitest --ui", + "benchmark": "vitest bench --run", "clean": "rm -rf dist temp", "lint": "eslint src/{,**/}*.ts", "build:rollup": "rollup --failAfterWarnings -c", diff --git a/vitest.config.ts b/vitest.config.ts index 7ef946d..4188569 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,13 +1,19 @@ import { defineConfig } from 'vitest/config'; +import JsonArrayReporter from './benchmarks/jsonArrayReporter'; export default defineConfig({ test: { setupFiles: ['test.ts'], - include: ['./**/*.spec.ts'], + include: ['src/**/*.spec.ts'], environment: 'happy-dom', coverage: { provider: 'v8', reporter: ['lcov'], + include: ['src/**'], + }, + benchmark: { + include: ['benchmarks/**/*.bench.ts'], + reporters: [new JsonArrayReporter(), 'default'], }, }, });