diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89de8f1..9c2eace 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: - uses: benchmark-action/github-action-benchmark@d48d326b4ca9ba73ca0cd0d59f108f9e02a381c7 #v1.20.4 with: name: Tansu benchmarks - tool: 'customBiggerIsBetter' + tool: 'customSmallerIsBetter' output-file-path: benchmarks.json auto-push: ${{ github.event_name == 'push' }} github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index f6af7e2..4def6a3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ dist temp coverage .angular -benchmarks.json +*benchmarks.json diff --git a/benchmarks/js-reactivity-benchmark/adapter.ts b/benchmarks/js-reactivity-benchmark/adapter.ts new file mode 100644 index 0000000..3b72dde --- /dev/null +++ b/benchmarks/js-reactivity-benchmark/adapter.ts @@ -0,0 +1,22 @@ +import type { ReactiveFramework } from 'js-reactivity-benchmark'; +import { writable, computed, batch } from '../../src/index'; + +export const tansuFramework: ReactiveFramework = { + name: '@amadeus-it-group/tansu', + signal: (initialValue) => { + const w = writable(initialValue); + return { + write: w.set, + read: w, + }; + }, + computed: (fn) => { + const c = computed(fn); + return { + read: c, + }; + }, + effect: (fn) => computed(fn).subscribe(() => {}), + withBatch: batch, + withBuild: (fn) => fn(), +}; diff --git a/benchmarks/js-reactivity-benchmark/js-reactivity-benchmark.ts b/benchmarks/js-reactivity-benchmark/js-reactivity-benchmark.ts new file mode 100644 index 0000000..1f85f23 --- /dev/null +++ b/benchmarks/js-reactivity-benchmark/js-reactivity-benchmark.ts @@ -0,0 +1,18 @@ +import { writeFile } from 'fs/promises'; +import { + runTests, + formatPerfResultStrings, + formatPerfResult, + perfResultHeaders, +} from 'js-reactivity-benchmark'; +import { tansuFramework } from './adapter'; + +(async () => { + console.log(formatPerfResultStrings(perfResultHeaders())); + const results: { name: string; value: number; unit: string }[] = []; + await runTests([{ framework: tansuFramework, testPullCounts: true }], (result) => { + console.log(formatPerfResult(result)); + results.push({ name: result.test, value: result.time, unit: 'ms' }); + }); + await writeFile('js-reactivity-benchmarks.json', JSON.stringify(results, null, ' ')); +})(); diff --git a/benchmarks/js-reactivity-benchmarks/cellxBench.bench.ts b/benchmarks/js-reactivity-benchmarks/cellxBench.bench.ts deleted file mode 100644 index 1804de2..0000000 --- a/benchmarks/js-reactivity-benchmarks/cellxBench.bench.ts +++ /dev/null @@ -1,94 +0,0 @@ -// adapted from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/cellxBench.ts - -import { bench, expect } from 'vitest'; -import { batch, computed, writable } from '../../src'; -import type { ReadableSignal } from '../../src'; -import { setup } from '../gc'; - -// The following is an implementation of the cellx benchmark https://github.com/Riim/cellx/blob/master/perf/perf.html - -const cellx = ( - layers: number, - expectedBefore: readonly [number, number, number, number], - expectedAfter: readonly [number, number, number, number] -) => { - const start = { - prop1: writable(1), - prop2: writable(2), - prop3: writable(3), - prop4: writable(4), - }; - - let layer: { - prop1: ReadableSignal; - prop2: ReadableSignal; - prop3: ReadableSignal; - prop4: ReadableSignal; - } = start; - - for (let i = layers; i > 0; i--) { - const m = layer; - const s = { - prop1: computed(() => m.prop2()), - prop2: computed(() => m.prop1() - m.prop3()), - prop3: computed(() => m.prop2() + m.prop4()), - prop4: computed(() => m.prop3()), - }; - - computed(() => s.prop1()).subscribe(() => {}); - computed(() => s.prop2()).subscribe(() => {}); - computed(() => s.prop3()).subscribe(() => {}); - computed(() => s.prop4()).subscribe(() => {}); - - s.prop1(); - s.prop2(); - s.prop3(); - s.prop4(); - - layer = s; - } - - const end = layer; - - expect(end.prop1()).toBe(expectedBefore[0]); - expect(end.prop2()).toBe(expectedBefore[1]); - expect(end.prop3()).toBe(expectedBefore[2]); - expect(end.prop4()).toBe(expectedBefore[3]); - - batch(() => { - start.prop1.set(4); - start.prop2.set(3); - start.prop3.set(2); - start.prop4.set(1); - }); - - expect(end.prop1()).toBe(expectedAfter[0]); - expect(end.prop2()).toBe(expectedAfter[1]); - expect(end.prop3()).toBe(expectedAfter[2]); - expect(end.prop4()).toBe(expectedAfter[3]); -}; - -type BenchmarkResults = [ - readonly [number, number, number, number], - readonly [number, number, number, number], -]; - -const expected: Record = { - 1000: [ - [-3, -6, -2, 2], - [-2, -4, 2, 3], - ], - 2500: [ - [-3, -6, -2, 2], - [-2, -4, 2, 3], - ], - 5000: [ - [2, 4, -1, -6], - [-2, 1, -4, -4], - ], -}; - -for (const layers in expected) { - const params = expected[layers]; - bench(`cellx${layers}`, () => cellx(+layers, params[0], params[1]), { throws: true, setup }); -} diff --git a/benchmarks/js-reactivity-benchmarks/dynamic.bench.ts b/benchmarks/js-reactivity-benchmarks/dynamic.bench.ts deleted file mode 100644 index 2bf09b2..0000000 --- a/benchmarks/js-reactivity-benchmarks/dynamic.bench.ts +++ /dev/null @@ -1,335 +0,0 @@ -// adapted from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/dynamicBench.ts - -import { bench, expect } from 'vitest'; -import type { ReadableSignal, WritableSignal } from '../../src'; -import { computed, writable } from '../../src'; -import { setup } from '../gc'; - -// from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/util/pseudoRandom.ts - -export function pseudoRandom(seed = 'seed'): () => number { - const hash = xmur3a(seed); - const rng = sfc32(hash(), hash(), hash(), hash()); - return rng; -} - -/* these are adapted from https://github.com/bryc/code/blob/master/jshash/PRNGs.md - * (License: Public domain) */ - -/** random number generator originally in PractRand */ -function sfc32(a: number, b: number, c: number, d: number): () => number { - return function () { - a >>>= 0; - b >>>= 0; - c >>>= 0; - d >>>= 0; - let t = (a + b) | 0; - a = b ^ (b >>> 9); - b = (c + (c << 3)) | 0; - c = (c << 21) | (c >>> 11); - d = (d + 1) | 0; - t = (t + d) | 0; - c = (c + t) | 0; - return (t >>> 0) / 4294967296; - }; -} - -/** MurmurHash3 */ -export function xmur3a(str: string): () => number { - let h = 2166136261 >>> 0; - for (let k: number, i = 0; i < str.length; i++) { - k = Math.imul(str.charCodeAt(i), 3432918353); - k = (k << 15) | (k >>> 17); - h ^= Math.imul(k, 461845907); - h = (h << 13) | (h >>> 19); - h = (Math.imul(h, 5) + 3864292196) | 0; - } - h ^= str.length; - return function () { - h ^= h >>> 16; - h = Math.imul(h, 2246822507); - h ^= h >>> 13; - h = Math.imul(h, 3266489909); - h ^= h >>> 16; - return h >>> 0; - }; -} - -// from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/util/perfTests.ts - -export interface TestResult { - sum: number; - count: number; -} - -// from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/util/frameworkTypes.ts - -interface TestConfig { - /** friendly name for the test, should be unique */ - name?: string; - - /** width of dependency graph to construct */ - width: number; - - /** depth of dependency graph to construct */ - totalLayers: number; - - /** fraction of nodes that are static */ // TODO change to dynamicFraction - staticFraction: number; - - /** construct a graph with number of sources in each node */ - nSources: number; - - /** fraction of [0, 1] elements in the last layer from which to read values in each test iteration */ - readFraction: number; - - /** number of test iterations */ - iterations: number; - - /** sum and count of all iterations, for verification */ - expected: Partial; -} - -// from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/util/dependencyGraph.ts - -interface Graph { - sources: WritableSignal[]; - layers: ReadableSignal[][]; -} - -interface GraphAndCounter { - graph: Graph; - counter: Counter; -} - -/** - * Make a rectangular dependency graph, with an equal number of source elements - * and computation elements at every layer. - * - * @param width number of source elements and number of computed elements per layer - * @param totalLayers total number of source and computed layers - * @param staticFraction every nth computed node is static (1 = all static, 3 = 2/3rd are dynamic) - * @returns the graph - */ -function makeGraph(config: TestConfig): GraphAndCounter { - const { width, totalLayers, staticFraction, nSources } = config; - - const sources = new Array(width).fill(0).map((_, i) => writable(i)); - const counter = new Counter(); - const rows = makeDependentRows(sources, totalLayers - 1, counter, staticFraction, nSources); - const graph = { sources, layers: rows }; - return { graph, counter }; -} - -/** - * Execute the graph by writing one of the sources and reading some or all of the leaves. - * - * @return the sum of all leaf values - */ -function runGraph(graph: Graph, iterations: number, readFraction: number): number { - const rand = pseudoRandom(); - const { sources, layers } = graph; - const leaves = layers[layers.length - 1]; - const skipCount = Math.round(leaves.length * (1 - readFraction)); - const readLeaves = removeElems(leaves, skipCount, rand); - - for (let i = 0; i < iterations; i++) { - const sourceDex = i % sources.length; - sources[sourceDex].set(i + sourceDex); - for (const leaf of readLeaves) { - leaf(); - } - } - - const sum = readLeaves.reduce((total, leaf) => leaf() + total, 0); - return sum; -} - -function removeElems(src: T[], rmCount: number, rand: () => number): T[] { - const copy = src.slice(); - for (let i = 0; i < rmCount; i++) { - const rmDex = Math.floor(rand() * copy.length); - copy.splice(rmDex, 1); - } - return copy; -} - -class Counter { - count = 0; -} - -function makeDependentRows( - sources: ReadableSignal[], - numRows: number, - counter: Counter, - staticFraction: number, - nSources: number -): ReadableSignal[][] { - let prevRow = sources; - const random = pseudoRandom(); - const rows = []; - for (let l = 0; l < numRows; l++) { - const row = makeRow(prevRow, counter, staticFraction, nSources, l, random); - rows.push(row); - prevRow = row; - } - return rows; -} - -function makeRow( - sources: ReadableSignal[], - counter: Counter, - staticFraction: number, - nSources: number, - layer: number, - random: () => number -): ReadableSignal[] { - return sources.map((_, myDex) => { - const mySources: ReadableSignal[] = []; - for (let sourceDex = 0; sourceDex < nSources; sourceDex++) { - mySources.push(sources[(myDex + sourceDex) % sources.length]); - } - - const staticNode = random() < staticFraction; - if (staticNode) { - // static node, always reference sources - return computed(() => { - counter.count++; - - let sum = 0; - for (const src of mySources) { - sum += src(); - } - return sum; - }); - } else { - // dynamic node, drops one of the sources depending on the value of the first element - const first = mySources[0]; - const tail = mySources.slice(1); - const node = computed(() => { - counter.count++; - let sum = first(); - const shouldDrop = sum & 0x1; - const dropDex = sum % tail.length; - - for (let i = 0; i < tail.length; i++) { - if (shouldDrop && i === dropDex) continue; - sum += tail[i](); - } - - return sum; - }); - return node; - } - }); -} - -// cf https://github.com/milomg/js-reactivity-benchmark/blob/main/src/config.ts -const perfTests = [ - { - name: 'simple component', - width: 10, // can't change for decorator tests - staticFraction: 1, // can't change for decorator tests - nSources: 2, // can't change for decorator tests - totalLayers: 5, - readFraction: 0.2, - iterations: 600000, - expected: { - sum: 19199968, - count: 3480000, - }, - }, - { - name: 'dynamic component', - width: 10, - totalLayers: 10, - staticFraction: 3 / 4, - nSources: 6, - readFraction: 0.2, - iterations: 15000, - expected: { - sum: 302310782860, - count: 1155000, - }, - }, - { - name: 'large web app', - width: 1000, - totalLayers: 12, - staticFraction: 0.95, - nSources: 4, - readFraction: 1, - iterations: 7000, - expected: { - sum: 29355933696000, - count: 1463000, - }, - }, - { - name: 'wide dense', - width: 1000, - totalLayers: 5, - staticFraction: 1, - nSources: 25, - readFraction: 1, - iterations: 3000, - expected: { - sum: 1171484375000, - count: 732000, - }, - }, - { - name: 'deep', - width: 5, - totalLayers: 500, - staticFraction: 1, - nSources: 3, - readFraction: 1, - iterations: 500, - expected: { - sum: 3.0239642676898464e241, - count: 1246500, - }, - }, - { - name: 'very dynamic', - width: 100, - totalLayers: 15, - staticFraction: 0.5, - nSources: 6, - readFraction: 1, - iterations: 2000, - expected: { - sum: 15664996402790400, - count: 1078000, - }, - }, -]; - -for (const config of perfTests) { - let graphAndCounter: GraphAndCounter; - - bench( - `dynamic ${config.name}`, - () => { - const { graph, counter } = graphAndCounter; - counter.count = 0; - const sum = runGraph(graph, config.iterations, config.readFraction); - - if (config.expected.sum) { - expect(sum).toBe(config.expected.sum); - } - if (config.expected.count) { - expect(counter.count).toBe(config.expected.count); - } - }, - { - throws: true, - time: 5000, - setup() { - graphAndCounter = makeGraph(config); - setup(); - }, - } - ); -} diff --git a/benchmarks/js-reactivity-benchmarks/kairo/avoidable.bench.ts b/benchmarks/js-reactivity-benchmarks/kairo/avoidable.bench.ts deleted file mode 100644 index 5597567..0000000 --- a/benchmarks/js-reactivity-benchmarks/kairo/avoidable.bench.ts +++ /dev/null @@ -1,37 +0,0 @@ -// adapted from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/kairo/avoidable.ts - -import { bench, expect } from 'vitest'; -import { computed, writable } from '../../../src'; -import { setup } from '../../gc'; - -function busy() { - let a = 0; - for (let i = 0; i < 1_00; i++) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - a++; - } -} - -const head = writable(0); -const computed1 = computed(() => head()); -const computed2 = computed(() => (computed1(), 0)); -const computed3 = computed(() => (busy(), computed2() + 1)); // heavy computation -const computed4 = computed(() => computed3() + 2); -const computed5 = computed(() => computed4() + 3); -computed(() => { - computed5(); - busy(); // heavy side effect -}).subscribe(() => {}); - -bench( - 'avoidablePropagation', - () => { - head.set(1); - expect(computed5()).toBe(6); - for (let i = 0; i < 1000; i++) { - head.set(i); - expect(computed5()).toBe(6); - } - }, - { throws: true, setup } -); diff --git a/benchmarks/js-reactivity-benchmarks/kairo/broad.bench.ts b/benchmarks/js-reactivity-benchmarks/kairo/broad.bench.ts deleted file mode 100644 index 9beb5cb..0000000 --- a/benchmarks/js-reactivity-benchmarks/kairo/broad.bench.ts +++ /dev/null @@ -1,40 +0,0 @@ -// adapted from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/kairo/broad.ts - -import { bench, expect } from 'vitest'; -import type { ReadableSignal } from '../../../src'; -import { computed, writable } from '../../../src'; -import { setup } from '../../gc'; - -const loopCount = 50; - -const head = writable(0); -let last: ReadableSignal = head; -let callCounter = 0; -for (let i = 0; i < loopCount; i++) { - const current = computed(() => { - return head() + i; - }); - const current2 = computed(() => { - return current() + 1; - }); - computed(() => { - current2(); - callCounter++; - }).subscribe(() => {}); - last = current2; -} - -bench( - 'broad', - () => { - head.set(1); - const atleast = loopCount * loopCount; - callCounter = 0; - for (let i = 0; i < loopCount; i++) { - head.set(i); - expect(last()).toBe(i + loopCount); - } - expect(callCounter).toBe(atleast); - }, - { throws: true, setup } -); diff --git a/benchmarks/js-reactivity-benchmarks/kairo/deep.bench.ts b/benchmarks/js-reactivity-benchmarks/kairo/deep.bench.ts deleted file mode 100644 index 180cc1b..0000000 --- a/benchmarks/js-reactivity-benchmarks/kairo/deep.bench.ts +++ /dev/null @@ -1,40 +0,0 @@ -// adapted from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/kairo/deep.ts - -import { bench, expect } from 'vitest'; -import type { ReadableSignal } from '../../../src'; -import { computed, writable } from '../../../src'; -import { setup } from '../../gc'; - -const len = 50; - -const head = writable(0); -let current = head as ReadableSignal; -for (let i = 0; i < len; i++) { - const c = current; - current = computed(() => { - return c() + 1; - }); -} -let callCounter = 0; - -computed(() => { - current(); - callCounter++; -}).subscribe(() => {}); - -const iter = 50; - -bench( - 'deep', - () => { - head.set(1); - const atleast = iter; - callCounter = 0; - for (let i = 0; i < iter; i++) { - head.set(i); - expect(current()).toBe(len + i); - } - expect(callCounter).toBe(atleast); - }, - { throws: true, setup } -); diff --git a/benchmarks/js-reactivity-benchmarks/kairo/diamond.bench.ts b/benchmarks/js-reactivity-benchmarks/kairo/diamond.bench.ts deleted file mode 100644 index c1c0758..0000000 --- a/benchmarks/js-reactivity-benchmarks/kairo/diamond.bench.ts +++ /dev/null @@ -1,36 +0,0 @@ -// adapted from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/kairo/diamond.ts - -import { bench, expect } from 'vitest'; -import type { ReadableSignal } from '../../../src'; -import { computed, writable } from '../../../src'; -import { setup } from '../../gc'; - -const width = 5; - -const head = writable(0); -const current: ReadableSignal[] = []; -for (let i = 0; i < width; i++) { - current.push(computed(() => head() + 1)); -} -const sum = computed(() => current.map((x) => x()).reduce((a, b) => a + b, 0)); -let callCounter = 0; -computed(() => { - sum(); - callCounter++; -}).subscribe(() => {}); - -bench( - 'diamond', - () => { - head.set(1); - expect(sum()).toBe(2 * width); - const atleast = 500; - callCounter = 0; - for (let i = 0; i < 500; i++) { - head.set(i); - expect(sum()).toBe((i + 1) * width); - } - expect(callCounter).toBe(atleast); - }, - { throws: true, setup } -); diff --git a/benchmarks/js-reactivity-benchmarks/kairo/mux.bench.ts b/benchmarks/js-reactivity-benchmarks/kairo/mux.bench.ts deleted file mode 100644 index 94b7e35..0000000 --- a/benchmarks/js-reactivity-benchmarks/kairo/mux.bench.ts +++ /dev/null @@ -1,32 +0,0 @@ -// adapted from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/kairo/mux.ts - -import { bench, expect } from 'vitest'; -import { computed, writable } from '../../../src'; -import { setup } from '../../gc'; - -const heads = new Array(100).fill(null).map(() => writable(0)); -const mux = computed(() => { - return Object.fromEntries(heads.map((h) => h()).entries()); -}); -const splited = heads - .map((_, index) => computed(() => mux()[index])) - .map((x) => computed(() => x() + 1)); - -splited.forEach((x) => { - computed(() => x()).subscribe(() => {}); -}); - -bench( - 'mux', - () => { - for (let i = 0; i < 10; i++) { - heads[i].set(i); - expect(splited[i]()).toBe(i + 1); - } - for (let i = 0; i < 10; i++) { - heads[i].set(i * 2); - expect(splited[i]()).toBe(i * 2 + 1); - } - }, - { throws: true, setup } -); diff --git a/benchmarks/js-reactivity-benchmarks/kairo/repeated.bench.ts b/benchmarks/js-reactivity-benchmarks/kairo/repeated.bench.ts deleted file mode 100644 index b6a2b35..0000000 --- a/benchmarks/js-reactivity-benchmarks/kairo/repeated.bench.ts +++ /dev/null @@ -1,39 +0,0 @@ -// adapted from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/kairo/repeated.ts - -import { bench, expect } from 'vitest'; -import { computed, writable } from '../../../src'; -import { setup } from '../../gc'; - -const size = 30; - -const head = writable(0); -const current = computed(() => { - let result = 0; - for (let i = 0; i < size; i++) { - // tbh I think it's meanigless to be this big... - result += head(); - } - return result; -}); - -let callCounter = 0; -computed(() => { - current(); - callCounter++; -}).subscribe(() => {}); - -bench( - 'repeated', - () => { - head.set(1); - expect(current()).toBe(size); - const atleast = 100; - callCounter = 0; - for (let i = 0; i < 100; i++) { - head.set(i); - expect(current()).toBe(i * size); - } - expect(callCounter).toBe(atleast); - }, - { throws: true, setup } -); diff --git a/benchmarks/js-reactivity-benchmarks/kairo/triangle.bench.ts b/benchmarks/js-reactivity-benchmarks/kairo/triangle.bench.ts deleted file mode 100644 index 27d23ce..0000000 --- a/benchmarks/js-reactivity-benchmarks/kairo/triangle.bench.ts +++ /dev/null @@ -1,53 +0,0 @@ -// adapted from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/kairo/triangle.ts - -import { bench, expect } from 'vitest'; -import type { ReadableSignal } from '../../../src'; -import { computed, writable } from '../../../src'; -import { setup } from '../../gc'; - -const width = 10; - -const head = writable(0); -let current = head as ReadableSignal; -const list: ReadableSignal[] = []; -for (let i = 0; i < width; i++) { - const c = current; - list.push(current); - current = computed(() => { - return c() + 1; - }); -} -const sum = computed(() => { - return list.map((x) => x()).reduce((a, b) => a + b, 0); -}); - -let callCounter = 0; - -computed(() => { - sum(); - callCounter++; -}).subscribe(() => {}); - -bench( - 'triangle', - () => { - const constant = count(width); - head.set(1); - expect(sum()).toBe(constant); - const atleast = 100; - callCounter = 0; - for (let i = 0; i < 100; i++) { - head.set(i); - expect(sum()).toBe(constant - width + i * width); - } - expect(callCounter).toBe(atleast); - }, - { throws: true, setup } -); - -function count(number: number) { - return new Array(number) - .fill(0) - .map((_, i) => i + 1) - .reduce((x, y) => x + y, 0); -} diff --git a/benchmarks/js-reactivity-benchmarks/kairo/unstable.bench.ts b/benchmarks/js-reactivity-benchmarks/kairo/unstable.bench.ts deleted file mode 100644 index 744f3d5..0000000 --- a/benchmarks/js-reactivity-benchmarks/kairo/unstable.bench.ts +++ /dev/null @@ -1,38 +0,0 @@ -// adapted from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/kairo/unstable.ts - -import { bench, expect } from 'vitest'; -import { computed, writable } from '../../../src'; -import { setup } from '../../gc'; - -const head = writable(0); -const double = computed(() => head() * 2); -const inverse = computed(() => -head()); -const current = computed(() => { - let result = 0; - for (let i = 0; i < 20; i++) { - result += head() % 2 ? double() : inverse(); - } - return result; -}); - -let callCounter = 0; -computed(() => { - current(); - callCounter++; -}).subscribe(() => {}); - -bench( - 'unstable', - () => { - head.set(1); - expect(current()).toBe(40); - const atleast = 100; - callCounter = 0; - for (let i = 0; i < 100; i++) { - head.set(i); - // expect(current()).toBe(i % 2 ? i * 2 * 10 : i * -10); - } - expect(callCounter).toBe(atleast); - }, - { throws: true, setup } -); diff --git a/benchmarks/js-reactivity-benchmarks/molBench.bench.ts b/benchmarks/js-reactivity-benchmarks/molBench.bench.ts deleted file mode 100644 index 91dbfb8..0000000 --- a/benchmarks/js-reactivity-benchmarks/molBench.bench.ts +++ /dev/null @@ -1,47 +0,0 @@ -// adapted from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/molBench.ts - -import { bench } from 'vitest'; -import { batch, computed, writable } from '../../src'; -import { setup } from '../gc'; - -function fib(n: number): number { - if (n < 2) return 1; - return fib(n - 1) + fib(n - 2); -} - -function hard(n: number) { - return n + fib(16); -} - -const numbers = Array.from({ length: 5 }, (_, i) => i); - -const res = []; - -const A = writable(0); -const B = writable(0); -const C = computed(() => (A() % 2) + (B() % 2)); -const D = computed(() => numbers.map((i) => ({ x: i + (A() % 2) - (B() % 2) }))); -const E = computed(() => hard(C() + A() + D()[0].x /*, 'E'*/)); -const F = computed(() => hard(D()[2].x || B() /*, 'F'*/)); -const G = computed(() => C() + (C() || E() % 2) + D()[4].x + F()); -computed(() => res.push(hard(G() /*, 'H'*/))).subscribe(() => {}); -computed(() => res.push(G())).subscribe(() => {}); -computed(() => res.push(hard(F() /*, 'J'*/))).subscribe(() => {}); - -bench( - 'molBench', - () => { - for (let i = 0; i < 1e4; i++) { - res.length = 0; - batch(() => { - B.set(1); - A.set(1 + i * 2); - }); - batch(() => { - A.set(2 + i * 2); - B.set(2); - }); - } - }, - { throws: true, setup } -); diff --git a/benchmarks/js-reactivity-benchmarks/sBench.bench.ts b/benchmarks/js-reactivity-benchmarks/sBench.bench.ts deleted file mode 100644 index a331f22..0000000 --- a/benchmarks/js-reactivity-benchmarks/sBench.bench.ts +++ /dev/null @@ -1,252 +0,0 @@ -// adapted from https://github.com/milomg/js-reactivity-benchmark/blob/main/src/sBench.ts - -import { bench } from 'vitest'; -import type { ReadableSignal, WritableSignal } from '../../src'; -import { computed, writable } from '../../src'; -import { setup } from '../gc'; - -// Inspired by https://github.com/solidjs/solid/blob/main/packages/solid/bench/bench.cjs - -const COUNT = 1e4; - -type Reader = () => number; - -defineBench(onlyCreateDataSignals, COUNT, COUNT); -defineBench(createComputations0to1, COUNT, 0); -defineBench(createComputations1to1, COUNT, COUNT); -defineBench(createComputations2to1, COUNT / 2, COUNT); -defineBench(createComputations4to1, COUNT / 4, COUNT); -defineBench(createComputations1000to1, COUNT / 1000, COUNT); -// createTotal += bench(createComputations8to1, COUNT, 8 * COUNT); -defineBench(createComputations1to2, COUNT, COUNT / 2); -defineBench(createComputations1to4, COUNT, COUNT / 4); -defineBench(createComputations1to8, COUNT, COUNT / 8); -defineBench(createComputations1to1000, COUNT, COUNT / 1000); -defineBench(updateComputations1to1, COUNT * 4, 1); -defineBench(updateComputations2to1, COUNT * 2, 2); -defineBench(updateComputations4to1, COUNT, 4); -defineBench(updateComputations1000to1, COUNT / 100, 1000); -defineBench(updateComputations1to2, COUNT * 4, 1); -defineBench(updateComputations1to4, COUNT * 4, 1); -defineBench(updateComputations1to1000, COUNT * 4, 1); - -function defineBench(fn: (n: number, sources: any[]) => void, n: number, scount: number) { - bench(fn.name, () => fn(n, createDataSignals(scount, [])), { throws: true, setup }); -} - -function onlyCreateDataSignals() { - // createDataSignals is already called before -} - -function createDataSignals(n: number, sources: ReadableSignal[]) { - for (let i = 0; i < n; i++) { - sources[i] = writable(i); - } - return sources; -} - -function createComputations0to1(n: number /*, _sources: ReadableSignal[]*/) { - for (let i = 0; i < n; i++) { - createComputation0(i); - } -} - -function createComputations1to1000(n: number, sources: ReadableSignal[]) { - for (let i = 0; i < n / 1000; i++) { - const get = sources[i]; - for (let j = 0; j < 1000; j++) { - createComputation1(get); - } - } -} - -function createComputations1to8(n: number, sources: ReadableSignal[]) { - for (let i = 0; i < n / 8; i++) { - const get = sources[i]; - createComputation1(get); - createComputation1(get); - createComputation1(get); - createComputation1(get); - createComputation1(get); - createComputation1(get); - createComputation1(get); - createComputation1(get); - } -} - -function createComputations1to4(n: number, sources: ReadableSignal[]) { - for (let i = 0; i < n / 4; i++) { - const get = sources[i]; - createComputation1(get); - createComputation1(get); - createComputation1(get); - createComputation1(get); - } -} - -function createComputations1to2(n: number, sources: ReadableSignal[]) { - for (let i = 0; i < n / 2; i++) { - const get = sources[i]; - createComputation1(get); - createComputation1(get); - } -} - -function createComputations1to1(n: number, sources: ReadableSignal[]) { - for (let i = 0; i < n; i++) { - const get = sources[i]; - createComputation1(get); - } -} - -function createComputations2to1(n: number, sources: ReadableSignal[]) { - for (let i = 0; i < n; i++) { - createComputation2(sources[i * 2], sources[i * 2 + 1]); - } -} - -function createComputations4to1(n: number, sources: ReadableSignal[]) { - for (let i = 0; i < n; i++) { - createComputation4(sources[i * 4], sources[i * 4 + 1], sources[i * 4 + 2], sources[i * 4 + 3]); - } -} - -// function createComputations8to1(n: number, sources: ReadableSignal[]) { -// for (let i = 0; i < n; i++) { -// createComputation8( -// sources[i * 8], -// sources[i * 8 + 1], -// sources[i * 8 + 2], -// sources[i * 8 + 3], -// sources[i * 8 + 4], -// sources[i * 8 + 5], -// sources[i * 8 + 6], -// sources[i * 8 + 7] -// ); -// } -// } - -// only create n / 100 computations, as otherwise takes too long -function createComputations1000to1(n: number, sources: ReadableSignal[]) { - for (let i = 0; i < n; i++) { - createComputation1000(sources, i * 1000); - } -} - -function createComputation0(i: number) { - computed(() => i).subscribe(() => {}); -} - -function createComputation1(s1: Reader) { - computed(() => s1()).subscribe(() => {}); -} -function createComputation2(s1: Reader, s2: Reader) { - computed(() => s1() + s2()).subscribe(() => {}); -} - -function createComputation4(s1: Reader, s2: Reader, s3: Reader, s4: Reader) { - computed(() => s1() + s2() + s3() + s4()).subscribe(() => {}); -} - -// function createComputation8( -// s1: Reader, -// s2: Reader, -// s3: Reader, -// s4: Reader, -// s5: Reader, -// s6: Reader, -// s7: Reader, -// s8: Reader -// ) { -// computed( -// () => s1() + s2() + s3() + s4() + s5() + s6() + s7() + s8() -// ); -// } - -function createComputation1000(ss: ReadableSignal[], offset: number) { - computed(() => { - let sum = 0; - for (let i = 0; i < 1000; i++) { - sum += ss[offset + i](); - } - return sum; - }).subscribe(() => {}); -} - -function updateComputations1to1(n: number, sources: WritableSignal[]) { - const get1 = sources[0]; - const set1 = get1.set; - computed(() => get1()).subscribe(() => {}); - for (let i = 0; i < n; i++) { - set1(i); - } -} - -function updateComputations2to1(n: number, sources: WritableSignal[]) { - const get1 = sources[0], - set1 = get1.set, - get2 = sources[1]; - computed(() => get1() + get2()).subscribe(() => {}); - for (let i = 0; i < n; i++) { - set1(i); - } -} - -function updateComputations4to1(n: number, sources: WritableSignal[]) { - const get1 = sources[0], - set1 = get1.set, - get2 = sources[1], - get3 = sources[2], - get4 = sources[3]; - computed(() => get1() + get2() + get3() + get4()).subscribe(() => {}); - for (let i = 0; i < n; i++) { - set1(i); - } -} - -function updateComputations1000to1(n: number, sources: WritableSignal[]) { - const { set: set1 } = sources[0]; - computed(() => { - let sum = 0; - for (let i = 0; i < 1000; i++) { - sum += sources[i](); - } - return sum; - }).subscribe(() => {}); - for (let i = 0; i < n; i++) { - set1(i); - } -} - -function updateComputations1to2(n: number, sources: WritableSignal[]) { - const get1 = sources[0]; - const set1 = get1.set; - computed(() => get1()).subscribe(() => {}); - computed(() => get1()).subscribe(() => {}); - for (let i = 0; i < n / 2; i++) { - set1(i); - } -} - -function updateComputations1to4(n: number, sources: WritableSignal[]) { - const get1 = sources[0]; - const set1 = get1.set; - computed(() => get1()).subscribe(() => {}); - computed(() => get1()).subscribe(() => {}); - computed(() => get1()).subscribe(() => {}); - computed(() => get1()).subscribe(() => {}); - for (let i = 0; i < n / 4; i++) { - set1(i); - } -} - -function updateComputations1to1000(n: number, sources: WritableSignal[]) { - const get1 = sources[0]; - const set1 = get1.set; - for (let i = 0; i < 1000; i++) { - computed(() => get1()).subscribe(() => {}); - } - for (let i = 0; i < n / 1000; i++) { - set1(i); - } -} diff --git a/benchmarks/jsonArrayReporter.ts b/benchmarks/jsonArrayReporter.ts index 045901a..ace3415 100644 --- a/benchmarks/jsonArrayReporter.ts +++ b/benchmarks/jsonArrayReporter.ts @@ -19,8 +19,8 @@ class JsonArrayReporter implements Reporter { if (value) { results.push({ name: `${name} > ${task.name}`, - unit: 'Hz', - value, + unit: 'ns', + value: 1000000000 / value, }); } } @@ -31,7 +31,7 @@ class JsonArrayReporter implements Reporter { processTasks(file.tasks, file.name); } - await writeFile('benchmarks.json', JSON.stringify(results, null, ' ')); + await writeFile('vitest-benchmarks.json', JSON.stringify(results, null, ' ')); } } diff --git a/benchmarks/mergeBenchmarkResults.js b/benchmarks/mergeBenchmarkResults.js new file mode 100644 index 0000000..93b1392 --- /dev/null +++ b/benchmarks/mergeBenchmarkResults.js @@ -0,0 +1,8 @@ +import { readFileSync, writeFileSync } from 'fs'; + +const jsReactivityBenchmarks = JSON.parse(readFileSync('js-reactivity-benchmarks.json')); +const vitestBenchmarks = JSON.parse(readFileSync('vitest-benchmarks.json')); +writeFileSync( + 'benchmarks.json', + JSON.stringify([...jsReactivityBenchmarks, ...vitestBenchmarks], null, 2) +); diff --git a/package-lock.json b/package-lock.json index 59b04eb..2644368 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "eslint-config-prettier": "^10.0.2", "happy-dom": "^17.1.9", "husky": "^9.1.7", + "js-reactivity-benchmark": "divdavem/js-reactivity-benchmark#48848a7345da142f838a6332f67360f134cdd44b", "prettier": "^3.5.3", "pretty-quick": "^4.0.0", "rollup": "^4.34.9", @@ -4106,6 +4107,13 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-reactivity-benchmark": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/divdavem/js-reactivity-benchmark.git#48848a7345da142f838a6332f67360f134cdd44b", + "integrity": "sha512-CwqakUbUrmzIlcG/6hDWW5oXQIbOVBpqoqP95zmzktcTeOKPjy8BHB2+kOaswel6pFGa1hMI21p+wQDEp6Hrpw==", + "dev": true, + "license": "ISC" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index bfe19aa..7796f9b 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "eslint-config-prettier": "^10.0.2", "happy-dom": "^17.1.9", "husky": "^9.1.7", + "js-reactivity-benchmark": "divdavem/js-reactivity-benchmark#48848a7345da142f838a6332f67360f134cdd44b", "prettier": "^3.5.3", "pretty-quick": "^4.0.0", "rollup": "^4.34.9", @@ -73,7 +74,9 @@ "test": "vitest run", "tdd": "vitest", "tdd:ui": "vitest --ui", - "benchmark": "vitest bench --run", + "vitest-benchmark": "vitest bench --run", + "js-reactivity-benchmark": "esbuild benchmarks/js-reactivity-benchmark/js-reactivity-benchmark.ts --bundle --format=esm --platform=node --outdir=dist --sourcemap=external && node --expose-gc ./dist/js-reactivity-benchmark.js", + "benchmark": "npm run js-reactivity-benchmark && npm run vitest-benchmark && node benchmarks/mergeBenchmarkResults.js", "clean": "rm -rf dist temp", "lint": "eslint {src,benchmarks}/{,**/}*.ts", "build:rollup": "rollup --failAfterWarnings -c",