Skip to content

Commit

Permalink
[DO NOT MERGE] try Kahn's toposort
Browse files Browse the repository at this point in the history
  • Loading branch information
dmaskasky committed Nov 22, 2024
1 parent 6b406ff commit abbbaab
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 27 deletions.
104 changes: 102 additions & 2 deletions src/vanilla/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,13 +482,114 @@ const buildStore = (
return [sorted, visited]
}

function kahnsToposort(
pending: Pending,
rootAtom: AnyAtom,
rootAtomState: AtomState,
): [[AnyAtom, AtomState, number][], Set<AnyAtom>] {
function createQueue<T>() {
let head = 0
let tail = 0
let items: Record<number, T> = {}

Check failure on line 493 in src/vanilla/store.ts

View workflow job for this annotation

GitHub Actions / lint

'items' is never reassigned. Use 'const' instead
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 for topological sorting
const graph = new Map<
AnyAtom,
[dependents: AnyAtom[], atomState: AtomState]
>()
const inDegree: Map<AnyAtom, number> = new Map([[rootAtom, 0]])
{
// 1: build the dependency graph
// and calculate in-degrees (number of incomming edges aka dependencies)
const queue = createQueue<[a: AnyAtom, aState: AtomState]>()
queue.enqueue([rootAtom, rootAtomState])
while (!queue.isEmpty()) {
const [a, aState] = queue.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)
queue.enqueue([d, ds])
}
graph.set(a, [dependentAtoms, aState])
}
}
const queue = createQueue<AnyAtom>()
{
// 2. Initialize queue with nodes that have no incoming edges
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<AnyAtom>(graph.keys())]
}

const recomputeDependents = <Value>(
pending: Pending,
atom: Atom<Value>,
atomState: AtomState<Value>,
) => {
// 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,
Expand Down Expand Up @@ -685,8 +786,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 () => {
Expand Down
77 changes: 52 additions & 25 deletions tests/vanilla/store.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>, ...Atom<number>[]] = [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<number>, ...Atom<number>[]] = [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<number>[] = [baseAtom]
const maxAtoms = 10_000
let atomCount = 1
function createInvertedAtomsTree(

Check failure on line 968 in tests/vanilla/store.test.tsx

View workflow job for this annotation

GitHub Actions / lint

Move function declaration to function body root
parentAtom: Atom<number>,
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<number>, ...Atom<number>[]] = [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
})

0 comments on commit abbbaab

Please sign in to comment.