UNPKG

@zeix/cause-effect

Version:

Cause & Effect - reactive state management with signals.

190 lines (170 loc) 5.33 kB
import { TestConfig } from "./framework-types"; import { Computed, ReactiveFramework, Signal } from "./reactive-framework"; import { Random } from "random"; 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; 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; 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; 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 = []; for (let l = 0; l < numRows; l++) { const row = makeRow( prevRow, counter, staticFraction, nSources, framework, l, rand ); rows.push(row as never); 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++) { 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 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; sum += tail[i].read(); } return sum; }); return node; } }); }