UNPKG

@zeix/cause-effect

Version:

Cause & Effect - reactive state management primitives library for TypeScript.

256 lines (238 loc) 7.2 kB
import { describe, expect, test } from 'bun:test' import * as stableModule from '@zeix/cause-effect-stable' import type { CollectionChanges } from '../index.ts' import * as current from '../index.ts' const stable = stableModule as unknown as typeof current /* === DCE sink — prevents the JIT from eliminating allocations with no other observable effect === */ let _sink = 0 /* === Measurement === */ const PERF_MARGIN = 0.2 const PERF_FLOOR = 1 // minimum absolute tolerance in ms /** * Build both graphs, cross-warm them interleaved (100 pairs), then take 5 * alternating timed passes of `iterations` iterations each. Alternating passes * equalize JIT state between implementations; forcing GC beforehand removes * accumulated cross-scenario heap pressure; median of 5 suppresses OS outliers. */ function measurePair( setupCurrent: () => () => void, setupStable: () => () => void, iterations: number, ): [number, number] { Bun.gc(true) const fnC = setupCurrent() const fnS = setupStable() for (let i = 0; i < 100; i++) { fnC() fnS() } const passesC: number[] = [] const passesS: number[] = [] for (let p = 0; p < 11; p++) { let start = performance.now() for (let i = 0; i < iterations; i++) fnC() passesC.push(performance.now() - start) start = performance.now() for (let i = 0; i < iterations; i++) fnS() passesS.push(performance.now() - start) } passesC.sort((a, b) => a - b) passesS.sort((a, b) => a - b) // biome-ignore lint/style/noNonNullAssertion: arrays always have 11 elements return [passesC[5]!, passesS[5]!] } function check(name: string, currentMs: number, stableMs: number): void { const limit = Math.max(stableMs * (1 + PERF_MARGIN), stableMs + PERF_FLOOR) console.log( ` ${name}: current ${currentMs.toFixed(1)}ms |` + ` stable ${stableMs.toFixed(1)}ms |` + ` limit ${limit.toFixed(1)}ms`, ) expect(currentMs).toBeLessThanOrEqual(limit) } /* === Primitive Scenarios (State / Memo / Effect) === */ describe('Performance — primitive nodes', () => { test('deep propagation (50 layers, 1000 iterations)', () => { const setup = (f: typeof current) => () => { const head = f.createState(0) let cur: { get(): number } = head for (let i = 0; i < 50; i++) { const c = cur cur = f.createMemo(() => c.get() + 1) } f.createEffect(() => { cur.get() }) let i = 0 return () => f.batch(() => head.set(++i)) } const [currentMs, stableMs] = measurePair( setup(current), setup(stable), 1000, ) check('deepPropagation', currentMs, stableMs) }) test('broad propagation (50 effects, 1000 iterations)', () => { const setup = (f: typeof current) => () => { const head = f.createState(0) for (let i = 0; i < 50; i++) { const c = f.createMemo(() => head.get() + i) const c2 = f.createMemo(() => c.get() + 1) f.createEffect(() => { c2.get() }) } let i = 0 return () => f.batch(() => head.set(++i)) } const [currentMs, stableMs] = measurePair( setup(current), setup(stable), 1000, ) check('broadPropagation', currentMs, stableMs) }) test('diamond propagation (width 5, 5000 iterations)', () => { const setup = (f: typeof current) => () => { const head = f.createState(0) const branches = Array.from({ length: 5 }, () => f.createMemo(() => head.get() + 1), ) const sum = f.createMemo(() => branches.reduce((a, b) => a + b.get(), 0), ) f.createEffect(() => { sum.get() }) let i = 0 return () => f.batch(() => head.set(++i)) } const [currentMs, stableMs] = measurePair( setup(current), setup(stable), 5000, ) check('diamondPropagation', currentMs, stableMs) }) test('create 1k signals (500 rounds)', () => { const setup = (f: typeof current) => () => { return () => { for (let i = 0; i < 1000; i++) _sink += f.createState(i).get() } } const [currentMs, stableMs] = measurePair( setup(current), setup(stable), 500, ) check('signalCreation', currentMs, stableMs) }) }) /* === Composite Scenarios (List / Store / Collection) === */ describe('Performance — composite nodes', () => { test('list structural mutations (add+remove, 5000 iterations)', () => { const setup = (f: typeof current) => () => { const list = f.createList<number>([]) f.createEffect(() => { list.get() }) let i = 0 return () => f.batch(() => { const key = list.add(++i) list.remove(key) }) } const [currentMs, stableMs] = measurePair( setup(current), setup(stable), 5000, ) check('listStructural', currentMs, stableMs) }) test('list item replace — item signal to direct subscriber (10000 iterations)', () => { // listStructural covers the list-node → structural-subscriber path. // This scenario isolates the item-signal → direct-subscriber path, // which is the unique contribution of replace(). const setup = (f: typeof current) => () => { const list = f.createList<number>([0]) // biome-ignore lint/style/noNonNullAssertion: list is pre-populated const key = list.keyAt(0)! // biome-ignore lint/style/noNonNullAssertion: list is pre-populated f.createEffect(() => void list.byKey(key)!.get()) let i = 0 return () => f.batch(() => list.replace(key, ++i)) } const [currentMs, stableMs] = measurePair( setup(current), setup(stable), 10000, ) check('listReplace', currentMs, stableMs) }) test('store property update via proxy (5000 iterations)', () => { const setup = (f: typeof current) => () => { const store = f.createStore({ x: 0 }) f.createEffect(() => { store.get() }) let i = 0 return () => f.batch(() => store.x.set(++i)) } const [currentMs, stableMs] = measurePair( setup(current), setup(stable), 5000, ) check('storeUpdate', currentMs, stableMs) }) test('collection structural mutations (add+remove via applyChanges, 5000 iterations)', () => { type Item = { id: string } const setup = (f: typeof current) => () => { let apply!: (changes: CollectionChanges<Item>) => void const col = f.createCollection<Item>( applyChanges => { apply = applyChanges return () => {} }, { keyConfig: (item: Item) => item.id }, ) f.createEffect(() => { col.get() }) return () => f.batch(() => { apply({ add: [{ id: 'k' }] }) apply({ remove: [{ id: 'k' }] }) }) } const [currentMs, stableMs] = measurePair( setup(current), setup(stable), 5000, ) check('collectionMutate', currentMs, stableMs) }) test('derived collection item update (2000 iterations)', () => { const setup = (f: typeof current) => () => { const list = f.createList<number>( Array.from({ length: 5 }, (_, i) => i), ) const derived = list.deriveCollection((v: number) => v * 2) // biome-ignore lint/style/noNonNullAssertion: list is pre-populated const firstKey = list.keyAt(0)! f.createEffect(() => { derived.get() }) let i = 0 return () => f.batch(() => list.replace(firstKey, ++i)) } const [currentMs, stableMs] = measurePair( setup(current), setup(stable), 2000, ) check('derivedCollection', currentMs, stableMs) }) })