From 26997185eea7e5e031da4730b2d8b48347ba7874 Mon Sep 17 00:00:00 2001 From: David Maskasky Date: Thu, 21 Nov 2024 16:30:22 -0800 Subject: [PATCH] [DO NOT MERGE] try Kahn's toposort --- src/vanilla/store.ts | 99 +++++++++++++++++++++++++++++++++++- tests/vanilla/store.test.tsx | 77 +++++++++++++++++++--------- 2 files changed, 149 insertions(+), 27 deletions(-) diff --git a/src/vanilla/store.ts b/src/vanilla/store.ts index 422c4ee99f..61b290f13d 100644 --- a/src/vanilla/store.ts +++ b/src/vanilla/store.ts @@ -482,6 +482,82 @@ const buildStore = ( return [sorted, visited] } + function kahnsToposort( + pending: Pending, + rootAtom: AnyAtom, + rootAtomState: AtomState, + ): [[AnyAtom, AtomState, number][], Set] { + function createQueue() { + let head = 0 + let tail = 0 + let items: Record = {} + const isEmpty = () => head === tail + const enqueue = (item: T) => { + items[tail++] = item + } + const dequeue = () => { + if (head === tail) { + return undefined + } + const item = items[head] + items[head++] = undefined as any + return item + } + return { enqueue, dequeue, isEmpty } + } + // Kahn's algorithm// Kahn's algorithm + // 1: build the dependency graph and calculate in-degrees (number of incomming edges aka dependencies) + const graph = new Map< + AnyAtom, + [dependents: AnyAtom[], atomState: AtomState] + >() + const inDegree: Map = new Map([[rootAtom, 0]]) + const queue1 = createQueue<[a: AnyAtom, aState: AtomState]>() + queue1.enqueue([rootAtom, rootAtomState]) + while (!queue1.isEmpty()) { + const [a, aState] = queue1.dequeue()! + if (graph.has(a)) { + continue + } + const dependentAtoms = [] + for (const [d, ds] of getDependents(pending, a, aState).entries()) { + inDegree.set(d, (inDegree.get(d) || 0) + 1) + dependentAtoms.push(d) + queue1.enqueue([d, ds]) + } + graph.set(a, [dependentAtoms, aState]) + } + + // 2. Initialize queue with nodes that have no incoming edges + const queue = createQueue() + for (const a of graph.keys()) { + if (inDegree.get(a) || 0 !== 0) { + continue + } + queue.enqueue(a) + } + const sorted: [ + atom: AnyAtom, + atomState: AtomState, + epochNumber: number, // + ][] = [] + + // 3. Process each node + while (!queue.isEmpty()) { + const a = queue.dequeue()! + const [dependents, aState] = graph.get(a)! + sorted.push([a, aState, aState.n]) + for (const d of dependents) { + const degree = (inDegree.get(d)! || 0) - 1 + inDegree.set(d, degree) + if (degree === 0) { + queue.enqueue(d) + } + } + } + return [sorted, new Set(graph.keys())] + } + const recomputeDependents = ( pending: Pending, atom: Atom, @@ -489,6 +565,26 @@ const buildStore = ( ) => { // Step 1: traverse the dependency graph to build the topsorted atom list // We don't bother to check for cycles, which simplifies the algorithm. + + const kahnTimes = [] + const toposortTimes = [] + for (let i = 0; i < 100; i++) { + let start = performance.now() + kahnsToposort(pending, atom, atomState) + kahnTimes.push(performance.now() - start) + start = performance.now() + getSortedDependents(pending, atom, atomState) + toposortTimes.push(performance.now() - start) + } + const kahnAverage = kahnTimes.reduce((a, b) => a + b) / 100 + console.log('kahnsToposort:', kahnAverage.toFixed(4)) + const toposortAverage = toposortTimes.reduce((a, b) => a + b) / 100 + console.log('getSortedDependents:', toposortAverage.toFixed(4)) + const fasterBy = ((toposortAverage - kahnAverage) / toposortAverage) * 100 + console.log( + `kahn times are ${Math.abs(fasterBy).toFixed(4)} percent ${fasterBy > 0 ? 'faster' : 'slower'} than toposort`, + ) + const [topsortedAtoms, markedAtoms] = getSortedDependents( pending, atom, @@ -685,8 +781,7 @@ const buildStore = ( const subscribeAtom = (atom: AnyAtom, listener: () => void) => { const pending = createPending() const atomState = getAtomState(atom) - const mounted = mountAtom(pending, atom, atomState) - const listeners = mounted.l + const listeners = mountAtom(pending, atom, atomState).l listeners.add(listener) flushPending(pending) return () => { diff --git a/tests/vanilla/store.test.tsx b/tests/vanilla/store.test.tsx index 2d978ad545..94706adae4 100644 --- a/tests/vanilla/store.test.tsx +++ b/tests/vanilla/store.test.tsx @@ -934,32 +934,59 @@ it('should call subscribers after setAtom updates atom value on mount but not on expect(listener).toHaveBeenCalledTimes(0) }) -it('processes deep atom a graph beyond maxDepth', () => { - function getMaxDepth() { - let depth = 0 - function d(): number { - ++depth - try { - return d() - } catch (error) { - return depth +it.only('processes deep atom a graph beyond maxDepth', () => { + const store = createStore() + { + console.log('LINEAR', '-'.repeat(80)) + const baseAtom = atom(0) + const atoms: [PrimitiveAtom, ...Atom[]] = [baseAtom] + Array.from({ length: 10_000 }, (_, i) => { + const prevAtom = atoms[i]! + const a = atom((get) => get(prevAtom)) + atoms.push(a) + store.sub(a, () => {}) + }) + store.set(baseAtom, 1) + } + { + console.log('STAR', '-'.repeat(80)) + const baseAtom = atom(0) + const atoms: [PrimitiveAtom, ...Atom[]] = [baseAtom] + Array.from({ length: 10_000 }, (_, i) => { + const a = atom((get) => get(baseAtom)) + atoms.push(a) + store.sub(a, () => {}) + }) + store.set(baseAtom, 1) + } + { + console.log('K-ARY', '-'.repeat(80)) + const baseAtom = atom(0) + const atoms: Atom[] = [baseAtom] + const maxAtoms = 10_000 + let atomCount = 1 + function createInvertedAtomsTree( + parentAtom: Atom, + depth: number, + maxDepth: number, + ): void { + if (atomCount >= maxAtoms || depth >= maxDepth) { + return } + const leftAtom = atom((get) => get(parentAtom) + 1) + atoms.push(leftAtom) + atomCount++ + store.sub(leftAtom, () => {}) + + const rightAtom = atom((get) => get(parentAtom) + 1) + atoms.push(rightAtom) + atomCount++ + store.sub(rightAtom, () => {}) + createInvertedAtomsTree(leftAtom, depth + 1, maxDepth) + createInvertedAtomsTree(rightAtom, depth + 1, maxDepth) } - return d() + const maxDepth = Math.ceil(Math.log2(maxAtoms)) + createInvertedAtomsTree(baseAtom, 0, maxDepth) + store.set(baseAtom, 1) } - const maxDepth = getMaxDepth() - const store = createStore() - const baseAtom = atom(0) - const atoms: [PrimitiveAtom, ...Atom[]] = [baseAtom] - Array.from({ length: maxDepth }, (_, i) => { - const prevAtom = atoms[i]! - const a = atom((get) => get(prevAtom)) - atoms.push(a) - }) - const lastAtom = atoms[maxDepth]! - // store.get(lastAtom) // FIXME: This is causing a stack overflow - expect(() => store.sub(lastAtom, () => {})).not.toThrow() - // store.get(lastAtom) // FIXME: This is causing a stack overflow - expect(() => store.set(baseAtom, 1)).not.toThrow() - // store.set(lastAtom) // FIXME: This is causing a stack overflow })