UNPKG

@efflore/ui-element

Version:

UIElement - minimal reactive framework based on Web Components

521 lines (472 loc) 16.5 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Performance Test</title> </head> <body> <script type="module"> import { runTests } from '@web/test-runner-mocha'; import { assert } from '@esm-bundle/chai'; import { cause, derive, effect } from '../cause-effect.js'; import { makeGraph, runGraph, Counter } from "./util/dependency-graph.js"; const busy = () => { let a = 0; for (let i = 0; i < 1_00; i++) { a++; } }; const framework = { name: "Cause & Effect", signal: initialValue => { const s = cause(initialValue); return { write: v => s.set(v), read: () => s(), }; }, computed: fn => { const c = derive(fn, true); return { read: () => c() }; }, effect: fn => effect(fn), withBatch: fn => fn(), withBuild: fn => fn(), }; const testPullCounts = true; function makeConfig() { return { width: 3, totalLayers: 3, staticFraction: 1, nSources: 2, readFraction: 1, expected: {}, iterations: 1, }; } /** * Make sure all tests from JS Reactivity Benchmark pass, so we can run their performance tests * * @see https://github.com/modderme123/js-reactivity-benchmark */ runTests(() => { /** some basic tests to validate the reactive framework * wrapper works and can run performance tests. */ describe('Basic test', function () { const name = framework.name; it(`${name} | simple dependency executes`, () => { const s = framework.signal(2); const c = framework.computed(() => s.read() * 2); assert.equal(c.read(), 4); }); it(`${name} | static graph`, () => { const config = makeConfig(); const { graph, counter } = makeGraph(framework, config); const sum = runGraph(graph, 2, 1, framework); assert.equal(sum, 16); if (testPullCounts) { assert.equal(counter.count, 11); } }); it(`${name} | static graph, read 2/3 of leaves`, () => { const config = makeConfig(); config.readFraction = 2 / 3; config.iterations = 10; const { counter, graph } = makeGraph(framework, config); const sum = runGraph(graph, 10, 2 / 3, framework); assert.equal(sum, 72); if (testPullCounts) { assert.equal(counter.count, 41); } }); it(`${name} | dynamic graph`, () => { const config = makeConfig(); config.staticFraction = 0.5; config.width = 4; config.totalLayers = 2; const { graph, counter } = makeGraph(framework, config); const sum = runGraph(graph, 10, 1, framework); assert.equal(sum, 72); if (testPullCounts) { assert.equal(counter.count, 22); } }); }); describe('Kairo tests', function () { const name = framework.name; it(`${name} | avoidable propagation`, () => { const head = framework.signal(0); const computed1 = framework.computed(() => head.read()); const computed2 = framework.computed(() => (computed1.read(), 0)); const computed3 = framework.computed(() => (busy(), computed2.read() + 1)); // heavy computation const computed4 = framework.computed(() => computed3.read() + 2); const computed5 = framework.computed(() => computed4.read() + 3); framework.effect(() => { computed5.read(); busy(); // heavy side effect }); return () => { framework.withBatch(() => { head.write(1); }); assert.equal(computed5.read(), 6); for (let i = 0; i < 10; i++) { framework.withBatch(() => { head.write(i); }); assert.equal(computed5.read(), 6); } }; }); it(`${name} | broad propagation`, () => { const head = framework.signal(0); let last = head; const callCounter = new Counter(); for (let i = 0; i < 50; i++) { let current = framework.computed(() => { return head.read() + i; }); let current2 = framework.computed(() => { return current.read() + 1; }); framework.effect(() => { current2.read(); callCounter.count++; }); last = current2; } return () => { framework.withBatch(() => { head.write(1); }); const atleast = 50 * 50; callCounter.count = 0; for (let i = 0; i < 50; i++) { framework.withBatch(() => { head.write(i); }); assert.equal(last.read(), i + 50); } assert.equal(callCounter.count, atleast, callCounter.count); }; }); it(`${name} | deep propagation`, () => { let len = 50; const head = framework.signal(0); let current = head; for (let i = 0; i < len; i++) { let c = current; current = framework.computed(() => { return c.read() + 1; }); } let callCounter = new Counter(); framework.effect(() => { current.read(); callCounter.count++; }); const iter = 50; return () => { framework.withBatch(() => { head.write(1); }); const atleast = iter; callCounter.count = 0; for (let i = 0; i < iter; i++) { framework.withBatch(() => { head.write(i); }); assert.equal(current.read(), len + i); } assert.equal(callCounter.count, atleast); }; }); it(`${name} | diamond`, function () { let width = 5; const head = framework.signal(0); let current = []; for (let i = 0; i < width; i++) { current.push( framework.computed(() => { return head.read() + 1; }) ); } let sum = framework.computed(() => { return current.map((x) => x.read()).reduce((a, b) => a + b, 0); }); let callCounter = new Counter(); framework.effect(() => { sum.read(); callCounter.count++; }); return () => { framework.withBatch(() => { head.write(1); }); assert.equal(sum.read(), 2 * width); const atleast = 500; callCounter.count = 0; for (let i = 0; i < 500; i++) { framework.withBatch(() => { head.write(i); }); assert.equal(sum.read(), (i + 1) * width); } assert.equal(callCounter.count, atleast); }; }); it(`${name} | mux`, function () { let heads = new Array(100).fill(null).map((_) => framework.signal(0)); const mux = framework.computed(() => { return Object.fromEntries(heads.map((h) => h.read()).entries()); }); const splited = heads .map((_, index) => framework.computed(() => mux.read()[index])) .map((x) => framework.computed(() => x.read() + 1)); splited.forEach((x) => { framework.effect(() => x.read()); }); return () => { for (let i = 0; i < 10; i++) { framework.withBatch(() => { heads[i].write(i); }); assert.equal(splited[i].read(), i + 1); } for (let i = 0; i < 10; i++) { framework.withBatch(() => { heads[i].write(i * 2); }); assert.equal(splited[i].read(), i * 2 + 1); } }; }); it(`${name} | repeated observers`, function () { const size = 30; let head = framework.signal(0); let current = framework.computed(() => { let result = 0; for (let i = 0; i < size; i++) { // tbh I think it's meanigless to be this big... result += head.read(); } return result; }); let callCounter = new Counter(); framework.effect(() => { current.read(); callCounter.count++; }); return () => { framework.withBatch(() => { head.write(1); }); assert.equal(current.read(), size); const atleast = 100; callCounter.count = 0; for (let i = 0; i < 100; i++) { framework.withBatch(() => { head.write(i); }); assert.equal(current.read(), i * size); } assert.equal(callCounter.count, atleast); }; }); it(`${name} | triangle`, function () { let width = 10; let head = framework.signal(0); let current = head; let list = []; for (let i = 0; i < width; i++) { let c = current; list.push(current); current = framework.computed(() => { return c.read() + 1; }); } let sum = framework.computed(() => { return list.map((x) => x.read()).reduce((a, b) => a + b, 0); }); let callCounter = new Counter(); framework.effect(() => { sum.read(); callCounter.count++; }); return () => { const constant = count(width); framework.withBatch(() => { head.write(1); }); assert.equal(sum.read(), constant); const atleast = 100; callCounter.count = 0; for (let i = 0; i < 100; i++) { framework.withBatch(() => { head.write(i); }); assert.equal(sum.read(), constant - width + i * width); } assert.equal(callCounter.count, atleast); }; }); it(`${name} | unstable`, function () { let head = framework.signal(0); const double = framework.computed(() => head.read() * 2); const inverse = framework.computed(() => -head.read()); let current = framework.computed(() => { let result = 0; for (let i = 0; i < 20; i++) { result += head.read() % 2 ? double.read() : inverse.read(); } return result; }); let callCounter = new Counter(); framework.effect(() => { current.read(); callCounter.count++; }); return () => { framework.withBatch(() => { head.write(1); }); assert.equal(current.read(), 40); const atleast = 100; callCounter.count = 0; for (let i = 0; i < 100; i++) { framework.withBatch(() => { head.write(i); }); assert.equal(current.read(), i % 2 ? i * 2 * 10 : i * -10); } assert.equal(callCounter.count, atleast); }; }); }); describe('$mol_wire tests', function () { const name = framework.name; it(`${name} | $mol_wire benchmark`, function() { const fib = (n) => { if (n < 2) return 1; return fib(n - 1) + fib(n - 2); }; const hard = (n) => { return n + fib(16); }; const numbers = Array.from({ length: 5 }, (_, i) => i); let res = []; const iter = framework.withBuild(() => { const A = framework.signal(0); const B = framework.signal(0); const C = framework.computed(() => (A.read() % 2) + (B.read() % 2)); const D = framework.computed(() => numbers.map((i) => ({ x: i + (A.read() % 2) - (B.read() % 2) })) ); const E = framework.computed(() => hard(C.read() + A.read() + D.read()[0].x, "E") ); const F = framework.computed(() => hard(D.read()[2].x || B.read(), "F")); const G = framework.computed( () => C.read() + (C.read() || E.read() % 2) + D.read()[4].x + F.read() ); framework.effect(() => res.push(hard(G.read(), "H"))); framework.effect(() => res.push(G.read()), "I"); framework.effect(() => res.push(hard(F.read(), "J"))); framework.effect(() => res[0] = hard(G.read(), "H")); framework.effect(() => res[1] = G.read(), "I"); framework.effect(() => res[2] = hard(F.read(), "J")); return (i) => { res.length = 0; framework.withBatch(() => { B.write(1); A.write(1 + i * 2); }); framework.withBatch(() => { A.write(2 + i * 2); B.write(2); }); }; }); assert.equal(res.toString(), [3201, 1604, 3196].toString()); }); }); /* describe('CellX tests', function () { const name = framework.name; it(`${name} | CellX benchmark`, function() { const expected = { 1000: [ [-3, -6, -2, 2], [-2, -4, 2, 3], ], 2500: [ [-3, -6, -2, 2], [-2, -4, 2, 3], ], 5000: [ [2, 4, -1, -6], [-2, 1, -4, -4], ] }; const results = {}; const cellx = (framework, layers) => { const start = { prop1: framework.signal(1), prop2: framework.signal(2), prop3: framework.signal(3), prop4: framework.signal(4), }; let layer = start; for (let i = layers; i > 0; i--) { const m = layer; const s = { prop1: framework.computed(() => m.prop2.read()), prop2: framework.computed(() => m.prop1.read() - m.prop3.read()), prop3: framework.computed(() => m.prop2.read() + m.prop4.read()), prop4: framework.computed(() => m.prop3.read()), }; framework.effect(() => s.prop1.read()); framework.effect(() => s.prop2.read()); framework.effect(() => s.prop3.read()); framework.effect(() => s.prop4.read()); s.prop1.read(); s.prop2.read(); s.prop3.read(); s.prop4.read(); layer = s; } const end = layer; const before = [ end.prop1.read(), end.prop2.read(), end.prop3.read(), end.prop4.read(), ]; framework.withBatch(() => { start.prop1.write(4); start.prop2.write(3); start.prop3.write(2); start.prop4.write(1); }); const after = [ end.prop1.read(), end.prop2.read(), end.prop3.read(), end.prop4.read(), ]; return [before, after]; }; for (const layers in expected) { const [before, after] = cellx(framework, layers); const [expectedBefore, expectedAfter] = expected[layers]; assert.equal(before.toString(), expectedBefore.toString(), `Expected first layer ${expectedBefore}, found first layer ${before}`); assert.equal(after.toString(), expectedAfter.toString(), `Expected first layer ${expectedAfter}, found first layer ${after}`); } }); }); */ }); </script> </body> </html>