@zeix/cause-effect
Version:
Cause & Effect - reactive state management primitives library for TypeScript.
198 lines (178 loc) • 5.33 kB
text/typescript
import { Random } from 'random'
import type { TestConfig } from './framework-types'
import type { Computed, ReactiveFramework, Signal } from './reactive-framework'
export interface Graph {
sources: Signal<number>[]
layers: Computed<number>[][]
}
/**
* 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
*/
export function makeGraph(
framework: ReactiveFramework,
config: TestConfig,
counter: Counter,
): Graph {
const { width, totalLayers, staticFraction, nSources } = config
return framework.withBuild(() => {
const sources = new Array(width)
.fill(0)
.map((_, i) => framework.signal(i))
const rows = makeDependentRows(
sources,
totalLayers - 1,
counter,
staticFraction,
nSources,
framework,
)
const graph = { sources, layers: rows }
return graph
})
}
/**
* Execute the graph by writing one of the sources and reading some or all of the leaves.
*
* @return the sum of all leaf values
*/
export function runGraph(
graph: Graph,
iterations: number,
readFraction: number,
framework: ReactiveFramework,
): number {
const rand = new Random('seed')
const { sources, layers } = graph
// biome-ignore lint/style/noNonNullAssertion: test
const leaves = layers[layers.length - 1]!
const skipCount = Math.round(leaves.length * (1 - readFraction))
const readLeaves = removeElems(leaves, skipCount, rand)
const frameworkName = framework.name.toLowerCase()
// const start = Date.now();
let sum = 0
if (frameworkName === 's-js' || frameworkName === 'solidjs') {
// [S.js freeze](https://github.com/adamhaile/S#sdatavalue) doesn't allow different values to be set during a single batch, so special case it.
for (let i = 0; i < iterations; i++) {
framework.withBatch(() => {
const sourceDex = i % sources.length
// biome-ignore lint/style/noNonNullAssertion: test
sources[sourceDex]!.write(i + sourceDex)
})
for (const leaf of readLeaves) {
leaf.read()
}
}
sum = readLeaves.reduce((total, leaf) => leaf.read() + total, 0)
} else {
framework.withBatch(() => {
for (let i = 0; i < iterations; i++) {
// Useful for debugging edge cases for some frameworks that experience
// dramatic slow downs for certain test configurations. These are generally
// due to `computed` effects not being cached efficiently, and as the number
// of layers increases, the uncached `computed` effects are re-evaluated in
// an `O(n^2)` manner where `n` is the number of layers.
/* if (i % 100 === 0) {
console.log("iteration:", i, "delta:", Date.now() - start);
} */
const sourceDex = i % sources.length
// biome-ignore lint/style/noNonNullAssertion: test
sources[sourceDex]!.write(i + sourceDex)
for (const leaf of readLeaves) {
leaf.read()
}
}
sum = readLeaves.reduce((total, leaf) => leaf.read() + total, 0)
})
}
return sum
}
function removeElems<T>(src: T[], rmCount: number, rand: Random): T[] {
const copy = src.slice()
for (let i = 0; i < rmCount; i++) {
const rmDex = rand.int(0, copy.length - 1)
copy.splice(rmDex, 1)
}
return copy
}
export class Counter {
count = 0
}
function makeDependentRows(
sources: Computed<number>[],
numRows: number,
counter: Counter,
staticFraction: number,
nSources: number,
framework: ReactiveFramework,
): Computed<number>[][] {
let prevRow = sources
const rand = new Random('seed')
const rows: Computed<number>[][] = []
for (let l = 0; l < numRows; l++) {
const row = makeRow(
prevRow,
counter,
staticFraction,
nSources,
framework,
l,
rand,
)
rows.push(row)
prevRow = row
}
return rows
}
function makeRow(
sources: Computed<number>[],
counter: Counter,
staticFraction: number,
nSources: number,
framework: ReactiveFramework,
_layer: number,
random: Random,
): Computed<number>[] {
return sources.map((_, myDex) => {
const mySources: Computed<number>[] = []
for (let sourceDex = 0; sourceDex < nSources; sourceDex++) {
// biome-ignore lint/style/noNonNullAssertion: test
mySources.push(sources[(myDex + sourceDex) % sources.length]!)
}
const staticNode = random.float() < staticFraction
if (staticNode) {
// static node, always reference sources
return framework.computed(() => {
counter.count++
let sum = 0
for (const src of mySources) {
sum += src.read()
}
return sum
})
} else {
// dynamic node, drops one of the sources depending on the value of the first element
// biome-ignore lint/style/noNonNullAssertion: test
const first = mySources[0]!
const tail = mySources.slice(1)
const node = framework.computed(() => {
counter.count++
let sum = first.read()
const shouldDrop = sum & 0x1
const dropDex = sum % tail.length
for (let i = 0; i < tail.length; i++) {
if (shouldDrop && i === dropDex) continue
// biome-ignore lint/style/noNonNullAssertion: test
sum += tail[i]!.read()
}
return sum
})
return node
}
})
}