test: reactive benchmark init

This commit is contained in:
haiqin 2024-01-27 09:48:00 +08:00
parent 4365018274
commit bc0857255e
14 changed files with 743 additions and 17 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@
package-lock.json
pnpm-lock.yaml
/packages/**/node_modules
dist

View File

@ -0,0 +1,319 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
/**
* Extracted from: https://github.com/Riim/cellx#benchmark
*/
import chalk from 'chalk';
import * as solid from './solid-reactive.js';
import * as reactively from '@reactively/core';
import Table from 'cli-table';
import { reactive, computed } from '../dist/index.js';
const BATCHED = true;
const RUNS_PER_TIER = 150;
const LAYER_TIERS = [10, 100, 500, 1000, 2000];
const med = array => array.sort((a, b) => (a - b < 0 ? 1 : -1))[Math.floor(array.length / 2)] || 0;
const SOLUTIONS = {
10: [2, 4, -2, -3],
100: [-2, -4, 2, 3],
500: [-2, 1, -4, -4],
1000: [-2, -4, 2, 3],
2000: [-2, 1, -4, -4],
// 2500: [-2, -4, 2, 3],
};
/**
* @param {number} layers
* @param {number[]} answer
*/
const isSolution = (layers, answer) => answer.every((_, i) => SOLUTIONS[layers][i] === _);
async function main() {
debugger
const report = {};
// report.solid = { fn: runSolid, runs: [] };
// report.reactively = { fn: runReactively, runs: [], avg: [] };
report.inluaReactive = { fn: runInulaReactive, runs: [], avg: [] };
// report.inluaDeepReactive = { fn: runInulaDeepReactive, runs: [], avg: [] };
for (const lib of Object.keys(report)) {
const current = report[lib];
for (let i = 0; i < LAYER_TIERS.length; i += 1) {
let layers = LAYER_TIERS[i];
const runs = [];
for (let j = 0; j < RUNS_PER_TIER; j += 1) {
runs.push(await start(current.fn, layers));
}
// Give cellx time to release its global pendingCells array
await new Promise(resolve => setTimeout(resolve, 0));
current.runs[i] = med(runs) * 1000;
}
}
const table = new Table({
head: ['', ...LAYER_TIERS.map(n => chalk.bold(chalk.cyan(n)))],
});
for (let i = 0; i < LAYER_TIERS.length; i += 1) {
let min = Infinity,
max = -1,
fastestLib,
slowestLib;
for (const lib of Object.keys(report)) {
const time = report[lib].runs[i];
if (time < min) {
min = time;
fastestLib = lib;
}
if (time > max) {
max = time;
slowestLib = lib;
}
}
report[fastestLib].runs[i] = chalk.green(report[fastestLib].runs[i].toFixed(2));
report[slowestLib].runs[i] = chalk.red(report[slowestLib].runs[i].toFixed(2));
}
for (const lib of Object.keys(report)) {
table.push([chalk.magenta(lib), ...report[lib].runs.map(n => (typeof n === 'number' ? n.toFixed(2) : n))]);
}
console.log(table.toString());
}
async function start(runner, layers) {
return new Promise(done => {
runner(layers, done);
}).catch(e => {
console.error(e.message);
});
}
function runInulaReactive(layers, done) {
const start = {
a: new reactive(1),
b: new reactive(2),
c: new reactive(3),
d: new reactive(4),
};
let layer = start;
for (let i = layers; i--; ) {
layer = (m => {
return {
a: new computed(() => m.b.get()),
b: new computed(() => m.a.get() - m.c.get()),
c: new computed(() => m.b.get() + m.d.get()),
d: new computed(() => m.c.get()),
};
})(layer);
}
const startTime = performance.now();
start.a.set(4), start.b.set(3), start.c.set(2), start.d.set(1);
const end = layer;
const solution = [end.a.get(), end.b.get(), end.c.get(), end.d.get()];
const endTime = performance.now() - startTime;
done(isSolution(layers, solution) ? endTime : -1);
}
function runInulaDeepReactive(layers, done) {
const start = {
a: new reactive({ v: 1 }),
b: new reactive({ v: 2 }),
c: new reactive({ v: 3 }),
d: new reactive({ v: 4 }),
};
let layer = start;
for (let i = layers; i--; ) {
layer = (l => {
return {
a: new computed(() => ({
v: l.b.v.get(),
})),
b: new computed(() => ({
v: l.a.v.get() - l.c.v.get(),
})),
c: new computed(() => ({
v: l.b.v.get() + l.d.v.get(),
})),
d: new computed(() => ({
v: l.c.v.get(),
})),
};
})(layer);
}
const startTime = performance.now();
start.a.v.set(4), start.b.v.set(3), start.c.v.set(2), start.d.v.set(1);
const end = layer;
const solution = [end.a.v.get(), end.b.v.get(), end.c.v.get(), end.d.v.get()];
const endTime = performance.now() - startTime;
done(isSolution(layers, solution) ? endTime : -1);
}
function runInulaDeepReactive3Layers(layers, done) {
const start = {
a: new reactive({ v: { v: { v: 1 } } }),
b: new reactive({ v: { v: { v: 2 } } }),
c: new reactive({ v: { v: { v: 3 } } }),
d: new reactive({ v: { v: { v: 4 } } }),
};
let layer = start;
for (let i = layers; i--; ) {
layer = (l => {
return {
a: new computed(() => ({
v: {
v: {
v: l.b.v.v.v.get(),
},
},
})),
b: new computed(() => ({
v: {
v: {
v: l.a.v.v.v.get() - l.c.v.v.v.get(),
},
},
})),
c: new computed(() => ({
v: {
v: {
v: l.b.v.v.v.get() + l.d.v.v.v.get(),
},
},
})),
d: new computed(() => ({
v: {
v: {
v: l.c.v.v.v.get(),
},
},
})),
};
})(layer);
}
const startTime = performance.now();
start.a.v.v.v.set(4), start.b.v.v.v.set(3), start.c.v.v.v.set(2), start.d.v.v.v.set(1);
const end = layer;
const solution = [end.a.v.v.v.get(), end.b.v.v.v.get(), end.c.v.v.v.get(), end.d.v.v.v.get()];
const endTime = performance.now() - startTime;
done(isSolution(layers, solution) ? endTime : -1);
}
/**
* @see {@link https://github.com/solidjs/solid}
*/
function runSolid(layers, done) {
solid.createRoot(async dispose => {
const [a, setA] = solid.createSignal(1),
[b, setB] = solid.createSignal(2),
[c, setC] = solid.createSignal(3),
[d, setD] = solid.createSignal(4);
const start = { a, b, c, d };
let layer = start;
for (let i = layers; i--; ) {
layer = (m => {
const props = {
a: solid.createMemo(() => m.b()),
b: solid.createMemo(() => m.a() - m.c()),
c: solid.createMemo(() => m.b() + m.d()),
d: solid.createMemo(() => m.c()),
};
return props;
})(layer);
}
const startTime = performance.now();
const run = BATCHED ? solid.batch : fn => fn();
run(() => {
setA(4), setB(3), setC(2), setD(1);
});
const end = layer;
const solution = [end.a(), end.b(), end.c(), end.d()];
const endTime = performance.now() - startTime;
dispose();
done(isSolution(layers, solution) ? endTime : -1);
});
}
/**
* @see {@link https://github.com/modderme123/reactively}
*/
function runReactively(layers, done) {
const start = {
a: new reactively.Reactive(1),
b: new reactively.Reactive(2),
c: new reactively.Reactive(3),
d: new reactively.Reactive(4),
};
let layer = start;
for (let i = layers; i--; ) {
layer = (m => {
return {
a: new reactively.Reactive(() => m.b.get()),
b: new reactively.Reactive(() => m.a.get() - m.c.get()),
c: new reactively.Reactive(() => m.b.get() + m.d.get()),
d: new reactively.Reactive(() => m.c.get()),
};
})(layer);
}
const startTime = performance.now();
start.a.set(4), start.b.set(3), start.c.set(2), start.d.set(1);
const end = layer;
const solution = [end.a.get(), end.b.get(), end.c.get(), end.d.get()];
const endTime = performance.now() - startTime;
done(isSolution(layers, solution) ? endTime : -1);
}
main();

View File

@ -0,0 +1,320 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
// Extract from SolidJS for benchmark
const equalFn = (a, b) => a === b;
const signalOptions = {
equals: equalFn,
};
let ERROR = null;
let runEffects = runQueue;
const NOTPENDING = {};
const STALE = 1;
const PENDING = 2;
const UNOWNED = {
owned: null,
cleanups: null,
context: null,
owner: null,
};
var Owner = null;
let Listener = null;
let Pending = null;
let Updates = null;
let Effects = null;
let ExecCount = 0;
function createRoot(fn, detachedOwner) {
detachedOwner && (Owner = detachedOwner);
const listener = Listener,
owner = Owner,
root =
fn.length === 0 && !false
? UNOWNED
: {
owned: null,
cleanups: null,
context: null,
owner,
};
Owner = root;
Listener = null;
let result;
try {
runUpdates(() => (result = fn(() => cleanNode(root))), true);
} finally {
Listener = listener;
Owner = owner;
}
return result;
}
function createSignal(value, options) {
options = options ? Object.assign({}, signalOptions, options) : signalOptions;
const s = {
value,
observers: null,
observerSlots: null,
pending: NOTPENDING,
comparator: options.equals || undefined,
};
return [
readSignal.bind(s),
(value) => {
if (typeof value === 'function') {
value = value(s.pending !== NOTPENDING ? s.pending : s.value);
}
return writeSignal(s, value);
},
];
}
function createComputed(fn, value) {
updateComputation(createComputation(fn, value, true, STALE));
}
function createMemo(fn, value, options) {
options = options ? Object.assign({}, signalOptions, options) : signalOptions;
const c = createComputation(fn, value, true, 0);
c.pending = NOTPENDING;
c.observers = null;
c.observerSlots = null;
c.comparator = options.equals || undefined;
updateComputation(c);
return readSignal.bind(c);
}
function batch(fn) {
if (Pending) return fn();
let result;
const q = (Pending = []);
try {
result = fn();
} finally {
Pending = null;
}
runUpdates(() => {
for (let i = 0; i < q.length; i += 1) {
const data = q[i];
if (data.pending !== NOTPENDING) {
const pending = data.pending;
data.pending = NOTPENDING;
writeSignal(data, pending);
}
}
}, false);
return result;
}
function untrack(fn) {
let result,
listener = Listener;
Listener = null;
result = fn();
Listener = listener;
return result;
}
function readSignal() {
if (this.state && this.sources) {
const updates = Updates;
Updates = null;
this.state === STALE ? updateComputation(this) : lookDownstream(this);
Updates = updates;
}
if (Listener) {
const sSlot = this.observers ? this.observers.length : 0;
if (!Listener.sources) {
Listener.sources = [this];
Listener.sourceSlots = [sSlot];
} else {
Listener.sources.push(this);
Listener.sourceSlots.push(sSlot);
}
if (!this.observers) {
this.observers = [Listener];
this.observerSlots = [Listener.sources.length - 1];
} else {
this.observers.push(Listener);
this.observerSlots.push(Listener.sources.length - 1);
}
}
return this.value;
}
function writeSignal(node, value, isComp) {
if (node.comparator) {
if (node.comparator(node.value, value)) return value;
}
if (Pending) {
if (node.pending === NOTPENDING) Pending.push(node);
node.pending = value;
return value;
}
node.value = value;
if (node.observers && node.observers.length) {
runUpdates(() => {
for (let i = 0; i < node.observers.length; i += 1) {
const o = node.observers[i];
if (!o.state) {
if (o.pure) Updates.push(o);
else Effects.push(o);
if (o.observers) markUpstream(o);
}
o.state = STALE;
}
if (Updates.length > 10e5) {
Updates = [];
throw new Error();
}
}, false);
}
return value;
}
function updateComputation(node) {
if (!node.fn) return;
cleanNode(node);
const owner = Owner,
listener = Listener,
time = ExecCount;
Listener = Owner = node;
runComputation(node, node.value, time);
Listener = listener;
Owner = owner;
}
function runComputation(node, value, time) {
let nextValue;
nextValue = node.fn(value);
if (!node.updatedAt || node.updatedAt <= time) {
if (node.observers && node.observers.length) {
writeSignal(node, nextValue, true);
} else node.value = nextValue;
node.updatedAt = time;
}
}
function createComputation(fn, init, pure, state = STALE, options) {
const c = {
fn,
state: state,
updatedAt: null,
owned: null,
sources: null,
sourceSlots: null,
cleanups: null,
value: init,
owner: Owner,
context: null,
pure,
};
if (Owner === null);
else if (Owner !== UNOWNED) {
if (!Owner.owned) Owner.owned = [c];
else Owner.owned.push(c);
}
return c;
}
function runTop(node) {
if (node.state !== STALE) return lookDownstream(node);
const ancestors = [node];
while ((node = node.owner) && (!node.updatedAt || node.updatedAt < ExecCount)) {
if (node.state) ancestors.push(node);
}
for (let i = ancestors.length - 1; i >= 0; i--) {
node = ancestors[i];
if (node.state === STALE) {
updateComputation(node);
} else if (node.state === PENDING) {
const updates = Updates;
Updates = null;
lookDownstream(node);
Updates = updates;
}
}
}
function runUpdates(fn, init) {
if (Updates) return fn();
let wait = false;
if (!init) Updates = [];
if (Effects) wait = true;
else Effects = [];
ExecCount++;
try {
fn();
} finally {
completeUpdates(wait);
}
}
function completeUpdates(wait) {
if (Updates) {
runQueue(Updates);
Updates = null;
}
if (wait) return;
if (Effects.length)
batch(() => {
runEffects(Effects);
Effects = null;
});
else {
Effects = null;
}
}
function runQueue(queue) {
for (let i = 0; i < queue.length; i++) runTop(queue[i]);
}
function lookDownstream(node) {
node.state = 0;
for (let i = 0; i < node.sources.length; i += 1) {
const source = node.sources[i];
if (source.sources) {
if (source.state === STALE) runTop(source);
else if (source.state === PENDING) lookDownstream(source);
}
}
}
function markUpstream(node) {
for (let i = 0; i < node.observers.length; i += 1) {
const o = node.observers[i];
if (!o.state) {
o.state = PENDING;
if (o.pure) Updates.push(o);
else Effects.push(o);
o.observers && markUpstream(o);
}
}
}
function cleanNode(node) {
let i;
if (node.sources) {
while (node.sources.length) {
const source = node.sources.pop(),
index = node.sourceSlots.pop(),
obs = source.observers;
if (obs && obs.length) {
const n = obs.pop(),
s = source.observerSlots.pop();
if (index < obs.length) {
n.sourceSlots[s] = index;
obs[index] = n;
source.observerSlots[index] = s;
}
}
}
}
if (node.owned) {
for (i = 0; i < node.owned.length; i++) cleanNode(node.owned[i]);
node.owned = null;
}
if (node.cleanups) {
for (i = 0; i < node.cleanups.length; i++) node.cleanups[i]();
node.cleanups = null;
}
node.state = 0;
node.context = null;
}
export { createComputed, createMemo, createRoot, createSignal, batch };

View File

@ -13,7 +13,7 @@
* See the Mulan PSL v2 for more details.
*/
module.exports = {
export default {
coverageDirectory: 'coverage',
resetModules: true,

View File

@ -2,11 +2,20 @@
"name": "inula-reactive",
"version": "0.0.1",
"description": "reactive core",
"main": "index.ts",
"main": "dist/index.js",
"type": "module",
"scripts": {
"test": "jest --config=jest.config.js"
"build": "rollup --config ./rollup.config.js",
"test": "jest --config=jest.config.js",
"bench": "node --experimental-specifier-resolution=node --inspect ./bench/index.js"
},
"dependencies": {
"@reactively/core": "^0.0.8",
"inula-reactive": "workspace:^0.0.1"
},
"devDependencies": {
"chalk": "^5.3.0",
"cli-table": "^0.3.11",
"tslib": "^2.6.2"
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import nodeResolve from '@rollup/plugin-node-resolve';
import babel from '@rollup/plugin-babel';
import path from 'path';
import replace from '@rollup/plugin-replace';
const extensions = ['.js', '.ts', '.tsx'];
const getBasicPlugins = mode => {
return [
nodeResolve({
extensions,
modulesOnly: true,
}),
babel({
exclude: 'node_modules/**',
configFile: path.join(__dirname, './babel.config.cjs'),
extensions,
}),
replace({
values: {
'process.env.NODE_ENV': `"${mode}"`,
},
preventAssignment: true,
}),
];
};
function genConfig(mode) {
return {
input: path.resolve(__dirname, 'src', 'index.ts'),
output: [
{
file:path.resolve(__dirname, 'dist', 'index.js'),
format: 'esm',
},
],
plugins: [
...getBasicPlugins(mode),
],
};
}
export default [genConfig('production')];

View File

@ -187,7 +187,7 @@ export class RNode<T = any> implements Signal<T> {
}
// 执行 reactive 函数
this.execute();
this._value = this.fn!();
if (calledGets) {
// remove all old sources' .observers links to us
@ -239,11 +239,6 @@ export class RNode<T = any> implements Signal<T> {
this.state = Fresh;
}
execute() {
// 执行 reactive 函数
this._value = this.fn!();
}
/**
* 1this是checkdirty的parent
* 2dirty的parent后

View File

@ -15,7 +15,7 @@
import { isPrimitive } from './Utils';
import { RNode } from './RNode';
import { Fn, NonFunctionType, Signal } from './Types';
import { Fn, NoArgFn, NonFunctionType, Signal } from './Types';
import { DeepReactive, RProxyNode } from './RProxyNode';
import { getRNodeVal } from './RNodeAccessor';
@ -35,7 +35,7 @@ export function createReactive<T extends NonFunctionType>(raw?: T): DeepReactive
}
}
export function createComputed<T extends Fn>(fn: T) {
export function createComputed<T extends NoArgFn>(fn: T) {
const rNode = new RProxyNode<T>(fn, { isComputed: true });
return rNode.proxy;
}

View File

@ -13,9 +13,9 @@
* See the Mulan PSL v2 for more details.
*/
import { createComputed as computed, createReactive as reactive, createWatch as watch} from './src/RNodeCreator';
import { isReactiveObj } from './src/Utils';
import { RNode, untrack } from './src/RNode';
import { createComputed as computed, createReactive as reactive, createWatch as watch} from './RNodeCreator';
import { isReactiveObj } from './Utils';
import { RNode, untrack } from './RNode';
export interface Index {
reactive<T>(initialValue: T): RNode<T>;
@ -30,5 +30,6 @@ export {
watch,
computed,
isReactiveObj,
RNode,
untrack
};

View File

@ -15,7 +15,7 @@
import { RNode } from '../RNode';
export function printNode(signal: RNode) {
export /*# __PURE__*/function printNode(signal: RNode) {
let name: string;
if (signal.fn) {
if (signal.isEffect) {

View File

@ -1,4 +1,4 @@
import { reactive, computed, watch } from '../index';
import { reactive, computed, watch } from '../src';
describe('test reactive', () => {
it('computation should work with two reactive', () => {

View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "esnext",
"outDir": "./dist",
"strict": true,
"skipLibCheck": true,
"declaration": true,
"moduleResolution": "node",
"esModuleInterop": true,
"importHelpers": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts", "./src/template/**/*"],
"ts-node": {
"esm": true,
},
}

View File

@ -50,7 +50,7 @@ const getBasicPlugins = mode => {
}),
babel({
exclude: 'node_modules/**',
configFile: path.join(__dirname, '../../babel.config.js'),
configFile: path.join(__dirname, '../../babel.config.cjs'),
babelHelpers: 'runtime',
extensions,
}),
@ -80,6 +80,11 @@ function genConfig(mode) {
sourcemap,
format: 'cjs',
},
{
file: outputResolve('esm', getOutputName(mode)),
sourcemap,
format: 'esm',
},
{
file: outputResolve('umd', getOutputName(mode)),
sourcemap,