├── replit.nix ├── .gitpod.yml ├── index.html ├── index.ts ├── .replit ├── LICENSE ├── README.md ├── package.json ├── utils.ts ├── chart_template.svg ├── .gitignore ├── tsconfig.json ├── popular_chart_AMD_EPYC_7B13.svg ├── popular_chart_Apple_M1.svg ├── chart_gen.ts ├── all_chart_AMD_EPYC_7B13.svg ├── all_chart_Apple_M1.svg ├── dynamic_deps_bench.ts └── computers_bench.ts /replit.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: { 2 | deps = [ 3 | pkgs.yarn 4 | pkgs.esbuild 5 | pkgs.nodejs-16_x 6 | 7 | pkgs.nodePackages.typescript 8 | pkgs.nodePackages.typescript-language-server 9 | ]; 10 | } -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was automatically generated by Gitpod. 2 | # Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file) 3 | # and commit this file to your remote git repository to share the goodness with others. 4 | 5 | tasks: 6 | - init: npm install 7 | command: npm run start 8 | 9 | 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Bench 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | // import BenchWorker from './computers_bench?worker' 2 | // const worker = new BenchWorker() 3 | 4 | import { test } from './computers_bench' 5 | 6 | const { table } = console 7 | console.table = function (data) { 8 | const pre = document.createElement('pre') 9 | pre.innerHTML = JSON.stringify(data, null, 2) 10 | document.body.appendChild(pre) 11 | table.call(this, data) 12 | } 13 | 14 | test() 15 | -------------------------------------------------------------------------------- /.replit: -------------------------------------------------------------------------------- 1 | run = "npm run start" 2 | 3 | [languages.typescript] 4 | pattern = "**/{*.ts,*.js,*.tsx,*.jsx}" 5 | syntax = "typescript" 6 | 7 | [languages.typescript.languageServer] 8 | start = [ "typescript-language-server", "--stdio" ] 9 | 10 | [packager] 11 | language = "nodejs" 12 | 13 | [packager.features] 14 | enabledForHosting = false 15 | packageSearch = true 16 | guessImports = true 17 | 18 | [env] 19 | XDG_CONFIG_HOME = "/home/runner/.config" 20 | 21 | [nix] 22 | channel = "stable-21_11" 23 | 24 | [gitHubImport] 25 | requiredFiles = [".replit", "replit.nix", ".config"] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Arutyunyan Artyom 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This benchmark measured computation of complex computed reactive unit when it deep children change. 2 | 3 | The rules: 4 | 5 | - A user space computations should be dumb as possible, to prevent it impact to the measurement. Also, it should return a new value each time to force all dependencies recomputation. For these purposes we use integer summarization, as it light, pure and plain for JIT. 6 | - Each update for each library should **not** go one after the other to prevent unexpected JIT overoptimizations. It is not how real applications work. In the iteration loop we apply the update (iteration counter) only once to each library. To prevent performance influence between two nearby tests we shuffle the list of tests before each iteration. 7 | - To prevent GC influence there is a minimum timeout after each iteration. It is not very honest, but there is no clear way to measure the library overhead and it GC usage. 8 | 9 | The charts shows median value. If you will start the bench by yourself you could see average, minimum and maximum (5%) values in your console. 10 | 11 | The results above is a median value in percent from a fastest library for each iteration loop. 12 | 13 | > [Notes about Reatom performance](https://www.reatom.dev/#how-performant-reatom-is) 🤗 14 | 15 | > Run it localy to see detailed numbers (node 18 required). 16 | 17 | ## Results 18 | 19 | ### AMD_EPYC_7B13 20 | 21 | ![](./popular_chart_AMD_EPYC_7B13.svg) 22 | 23 |
24 | all results 25 | 26 | ![](./all_chart_AMD_EPYC_7B13.svg) 27 | 28 |
29 | 30 | 31 | 32 | ### Apple_M1 33 | 34 | ![](./popular_chart_Apple_M1.svg) 35 | 36 |
37 | all results 38 | 39 | ![](./all_chart_Apple_M1.svg) 40 | 41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactive-computed-bench", 3 | "private": true, 4 | "description": "Benchmarks of complex computed update performance", 5 | "scripts": { 6 | "start": "tsx computers_bench.ts", 7 | "start:mem": "node --expose-gc --import=tsx computers_bench.ts", 8 | "site:dev": "vite dev", 9 | "site:build": "vite build", 10 | "update": "npx npm-check-updates -u" 11 | }, 12 | "author": "artalar", 13 | "license": "MIT", 14 | "readme": "README.md", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+ssh://git@github.com/artalar/reactive-computed-bench.git" 18 | }, 19 | "engines": { 20 | "node": ">=18.0.0" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/artalar/reactive-computed-bench/issues" 24 | }, 25 | "homepage": "https://github.com/artalar/reactive-computed-bench/tree/main", 26 | "devDependencies": { 27 | "@artalar/act": "^3.2.1", 28 | "@frp-ts/core": "^1.0.0-beta.6", 29 | "@ngrx/store": "^17.1.1", 30 | "@preact/signals-core": "^1.6.0", 31 | "@reatom/core": "^3.7.0", 32 | "@types/node": "latest", 33 | "@webreflection/signal": "^2.1.1", 34 | "cellx": "^1.10.30", 35 | "effector": "^23.2.0", 36 | "jotai": "^2.7.1", 37 | "mobx": "^6.12.1", 38 | "mol_wire_lib": "^1.0.978", 39 | "nanostores": "^0.10.0", 40 | "react": "^18.2.0", 41 | "redux": "^5.0.1", 42 | "reselect": "^5.1.0", 43 | "s-js": "^0.4.9", 44 | "solid-js": "^1.8.16", 45 | "spred": "^0.34.0", 46 | "tsx": "^4.7.1", 47 | "typescript": "^5.4.3", 48 | "usignal": "^0.9.0", 49 | "uvu": "^0.5.6", 50 | "vite": "^5.2.5", 51 | "whatsup": "^2.6.0", 52 | "wonka": "^6.3.4" 53 | }, 54 | "keywords": [ 55 | "reactive", 56 | "reactivity", 57 | "computed", 58 | "bench", 59 | "perf" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | export type Rec = Record 2 | 3 | export const POSITION_KEY = 'pos %' 4 | 5 | export function printLogs(results: Rec>) { 6 | const medFastest = Math.min(...Object.values(results).map(({ med }) => med)) 7 | 8 | const tabledData = Object.entries(results) 9 | .sort(([, { med: a }], [, { med: b }]) => a - b) 10 | .reduce((acc, [name, { min, med, max }]) => { 11 | acc[name] = { 12 | [POSITION_KEY]: ((medFastest / med) * 100).toFixed(0), 13 | 'avg ms': med.toFixed(3), 14 | 'min ms': min.toFixed(5), 15 | 'med ms': med.toFixed(5), 16 | 'max ms': max.toFixed(5), 17 | } 18 | return acc 19 | }, {} as Rec) 20 | 21 | console.table(tabledData) 22 | 23 | return tabledData 24 | } 25 | 26 | export function formatPercent(n = 0) { 27 | return `${n < 1 ? ` ` : ``}${(n * 100).toFixed(0)}%` 28 | } 29 | 30 | export function formatLog(values: Array) { 31 | return { 32 | min: min(values), 33 | med: med(values), 34 | max: max(values), 35 | } 36 | } 37 | 38 | export function med(values: Array) { 39 | if (values.length === 0) return 0 40 | 41 | values = values.map((v) => +v) 42 | 43 | values.sort((a, b) => (a - b < 0 ? 1 : -1)) 44 | 45 | var half = Math.floor(values.length / 2) 46 | 47 | if (values.length % 2) return values[half]! 48 | 49 | return (values[half - 1]! + values[half]!) / 2.0 50 | } 51 | 52 | export function min(values: Array) { 53 | if (values.length === 0) return 0 54 | 55 | values = values.map((v) => +v) 56 | 57 | values.sort((a, b) => (a - b < 0 ? -1 : 1)) 58 | 59 | const limit = Math.floor(values.length / 20) 60 | 61 | return values[limit]! 62 | } 63 | 64 | export function max(values: Array) { 65 | if (values.length === 0) return 0 66 | 67 | values = values.map((v) => +v) 68 | 69 | values.sort((a, b) => (a - b < 0 ? -1 : 1)) 70 | 71 | const limit = values.length - 1 - Math.floor(values.length / 20) 72 | 73 | return values[limit]! 74 | } 75 | -------------------------------------------------------------------------------- /chart_template.svg: -------------------------------------------------------------------------------- 1 | 2 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 10 59 | 100 60 | 1000 61 | 10000 62 | ITERATIONS 63 | 64 | 65 | 66 | 100% 67 | 75% 68 | 50% 69 | 25% 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Basic Options */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | "target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ 7 | "module": "ESNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 8 | // "lib": [], /* Specify library files to be included in the compilation. */ 9 | // "allowJs": true, /* Allow javascript files to be compiled. */ 10 | // "checkJs": true, /* Report errors in .js files. */ 11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 12 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | // "outDir": "./", /* Redirect output structure to the directory. */ 17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | // "composite": true, /* Enable project compilation */ 19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | /* Additional Checks */ 35 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 37 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 39 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 40 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 41 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 42 | /* Module Resolution Options */ 43 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 44 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 47 | "typeRoots": [ 48 | "./node_modules/@types", 49 | ], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | /* Experimental Options */ 56 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 57 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 58 | /* Advanced Options */ 59 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 60 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 61 | }, 62 | "exclude": [ 63 | "node_modules", 64 | ".build" 65 | ] 66 | } -------------------------------------------------------------------------------- /popular_chart_AMD_EPYC_7B13.svg: -------------------------------------------------------------------------------- 1 | 2 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 10 59 | 100 60 | 1000 61 | 10000 62 | ITERATIONS 63 | 64 | 65 | 66 | 100% 67 | 75% 68 | 50% 69 | 25% 70 | 71 | 72 | 73 | nanostores 0.7.4 79 | 85 | 86 | preact 1.2.3 92 | 98 | 99 | solid 1.7.3 105 | 111 | 112 | reatom 3.2.0 118 | 124 | 125 | mol 1.0.548 131 | 137 | 138 | mobx 6.9.0 144 | 150 | 151 | jotai 2.0.3 157 | 163 | 164 | wonka 6.3.1 170 | 176 | 177 | effector 22.8.0 183 | 189 | 190 | effector.fork 22.8.0 196 | 202 | 203 | -------------------------------------------------------------------------------- /popular_chart_Apple_M1.svg: -------------------------------------------------------------------------------- 1 | 2 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 10 59 | 100 60 | 1000 61 | 10000 62 | ITERATIONS 63 | 64 | 65 | 66 | 100% 67 | 75% 68 | 50% 69 | 25% 70 | 71 | 72 | 73 | nanostores 0.10.0 79 | 85 | 86 | usignal 0.9.0 92 | 98 | 99 | preact 1.6.0 105 | 111 | 112 | solid 1.8.16 118 | 124 | 125 | mol 1.0.978 131 | 137 | 138 | v4 0.0.0 144 | 150 | 151 | cellx 1.10.30 157 | 163 | 164 | reatom 3.7.0 170 | 176 | 177 | mobx 6.12.1 183 | 189 | 190 | wonka 6.3.4 196 | 202 | 203 | jotai 2.7.1 209 | 215 | 216 | effector 23.2.0 222 | 228 | 229 | effector.fork 23.2.0 235 | 241 | 242 | -------------------------------------------------------------------------------- /chart_gen.ts: -------------------------------------------------------------------------------- 1 | import { writeFile, readFile } from 'fs/promises' 2 | import { cpus } from 'os' 3 | import path from 'path' 4 | import { formatLog, Rec } from './utils' 5 | 6 | type LibName = string 7 | type LibValues = ReturnType 8 | type IterationName = number 9 | type IterationValues = Record 10 | type BenchResults = Record 11 | type Iterations = Array['length'] 12 | type Point = [number, number] 13 | type ChartData = Array<{ 14 | name: string 15 | version: string 16 | color: string 17 | min: Point[] 18 | med: Point[] 19 | max: Point[] 20 | medValue: number 21 | labelY: number 22 | }> 23 | 24 | const DOWNLOAD_LIMIT = 500 25 | 26 | const CPU = cpus()[0]?.model?.replace(/ /g, '_') ?? 'unknown_cpu' 27 | const POPULAR_CHART_PATH = `./popular_chart_${CPU}.svg` 28 | const ALL_CHART_PATH = `./all_chart_${CPU}.svg` 29 | const CHART_TEMPLATE = './chart_template.svg' 30 | const START_MARK = '' 31 | const END_MARK = '' 32 | const REGEX = new RegExp(`${START_MARK}(.*)${END_MARK}`, 'gms') 33 | 34 | const X_START = 100 35 | const X_STEP = 230 36 | const Y_START = 20 37 | const Y_RANGE = 500 38 | const LABEL_HEIGHT = 16 39 | 40 | const hsl = (i: number, length: number) => { 41 | const h = Math.round((i * 360) / length) % 360 42 | return `hsl(${h}, 90%, 45%)` 43 | } 44 | 45 | const PACKAGE_NAMES: Rec = { 46 | 'effector.fork': 'effector', 47 | frpts: '@frp-ts/core', 48 | mol: 'mol_wire_lib', 49 | preact: '@preact/signals-core', 50 | reatom: '@reatom/core', 51 | solid: 'solid-js', 52 | v4: 'v4', 53 | } 54 | 55 | export async function genChart(allResults: BenchResults) { 56 | const popularLibs = new Array() 57 | for (const libName of Object.keys(Object.values(allResults)[0]!)) { 58 | const moduleName = PACKAGE_NAMES[libName] ?? libName 59 | const moduleUrlName = moduleName.replace('/', '%2F') 60 | const downloadsUrl = `https://api.npmjs.org/versions/${moduleUrlName}/last-week` 61 | const downloads = await fetch(downloadsUrl) 62 | .then((r) => r.json()) 63 | .then(({ downloads }: { downloads: Rec }) => 64 | Object.values(downloads).reduce((acc, v) => acc + v, 0), 65 | ) 66 | .catch((error) => { 67 | console.error(`Failed to fetch downloads for ${name}`) 68 | console.log(error) 69 | return 0 70 | }) 71 | if (downloads > DOWNLOAD_LIMIT) popularLibs.push(libName) 72 | } 73 | const popularResults = Object.fromEntries( 74 | Object.entries(allResults).map(([iteration, iterationValues]) => [ 75 | iteration, 76 | Object.fromEntries( 77 | Object.entries(iterationValues).filter(([libName]) => 78 | popularLibs.includes(libName), 79 | ), 80 | ), 81 | ]), 82 | ) 83 | 84 | const template = await readFile(CHART_TEMPLATE, 'utf8') 85 | const allData = await getChartData(allResults) 86 | const popularData = await getChartData(popularResults) 87 | const svgAll = allData.map(getLibSVG).join('') 88 | const svgPopular = popularData.map(getLibSVG).join('') 89 | 90 | await writeFile( 91 | ALL_CHART_PATH, 92 | template.replace(REGEX, START_MARK + svgAll + END_MARK), 93 | ) 94 | await writeFile( 95 | POPULAR_CHART_PATH, 96 | template.replace(REGEX, START_MARK + svgPopular + END_MARK), 97 | ) 98 | 99 | let readme = await readFile('./README.md', 'utf8') 100 | if (readme.includes(CPU)) { 101 | readme = readme.replace( 102 | new RegExp(`### ${CPU}(.|\n)*`), 103 | `### ${CPU} 104 | 105 | ![](${POPULAR_CHART_PATH}) 106 | 107 |
108 | all results 109 | 110 | ![](${ALL_CHART_PATH}) 111 | 112 |
113 | 114 | `, 115 | ) 116 | } else { 117 | readme = readme.replace( 118 | '## Results', 119 | `## Results 120 | 121 | ### ${CPU} 122 | 123 | ![](${POPULAR_CHART_PATH}) 124 | 125 |
126 | all results 127 | 128 | ![](${ALL_CHART_PATH}) 129 | 130 |
131 | 132 | `, 133 | ) 134 | } 135 | 136 | await writeFile('./README.md', readme) 137 | } 138 | 139 | async function getChartData(results: BenchResults): Promise { 140 | const fastest = { 141 | min: [] as Array, 142 | med: [] as Array, 143 | max: [] as Array, 144 | } 145 | const libsResults: Rec = {} 146 | const data: ChartData = [] 147 | 148 | let i = -1 149 | for (const [iteration, iterationValues] of Object.entries(results)) { 150 | i++ 151 | 152 | if (!fastest.min[i]) fastest.min[i] = Infinity 153 | if (!fastest.med[i]) fastest.med[i] = Infinity 154 | if (!fastest.max[i]) fastest.max[i] = Infinity 155 | 156 | for (const [lib, libValues] of Object.entries(iterationValues)) { 157 | const group = (libsResults[lib] ??= { 158 | min: [], 159 | med: [], 160 | max: [], 161 | }) 162 | 163 | group.min.push(libValues.min) 164 | group.med.push(libValues.med) 165 | group.max.push(libValues.max) 166 | 167 | if (libValues.min < fastest.min[i]!) { 168 | fastest.min[i] = libValues.min 169 | } 170 | 171 | if (libValues.med < fastest.med[i]!) { 172 | fastest.med[i] = libValues.med 173 | } 174 | 175 | if (libValues.max < fastest.max[i]!) { 176 | fastest.max[i] = libValues.max 177 | } 178 | } 179 | } 180 | 181 | const libsNames = Object.keys(libsResults).sort() 182 | 183 | const toPoint = (v: number, i: number) => getPoint(v, fastest.med[i]!, i) 184 | for (const [lib, libResults] of Object.entries(libsResults)) { 185 | const moduleName = PACKAGE_NAMES[lib] || lib 186 | const color = hsl(libsNames.indexOf(lib), libsNames.length) 187 | const min = libResults.min.map(toPoint) 188 | const med = libResults.med.map(toPoint) 189 | const max = libResults.max.map(toPoint) 190 | const medValue = libResults.med[0] 191 | const modulePath = path.join( 192 | __dirname, 193 | 'node_modules', 194 | moduleName, 195 | 'package.json', 196 | ) 197 | const file = await readFile(modulePath, 'utf8').catch( 198 | () => '{"version": "0.0.0"}', 199 | ) 200 | const version = JSON.parse(file).version 201 | 202 | data.push({ 203 | name: lib, 204 | version, 205 | color, 206 | min, 207 | med, 208 | max, 209 | medValue, 210 | labelY: 0, 211 | }) 212 | } 213 | 214 | data.sort((a, b) => a.medValue - b.medValue) 215 | 216 | data.forEach((el, i, arr) => { 217 | el.labelY = el.med[0]![1]! + 3 218 | 219 | if (i && el.labelY - arr[i - 1]!.labelY < LABEL_HEIGHT) { 220 | el.labelY = arr[i - 1]!.labelY + LABEL_HEIGHT 221 | } 222 | }) 223 | 224 | return data 225 | } 226 | 227 | function getPoint(value: number, minValue: number, step: number): Point { 228 | const x = X_START + X_STEP * step 229 | const y = Y_START + Y_RANGE * (1 - minValue / value) 230 | 231 | return [x, y] 232 | } 233 | 234 | function getLibSVG({ 235 | name: lib, 236 | color, 237 | med, 238 | labelY, 239 | version, 240 | }: ChartData[number]) { 241 | return ` 242 | ${lib} ${version} 248 | 254 | ` 255 | } 256 | -------------------------------------------------------------------------------- /all_chart_AMD_EPYC_7B13.svg: -------------------------------------------------------------------------------- 1 | 2 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 10 59 | 100 60 | 1000 61 | 10000 62 | ITERATIONS 63 | 64 | 65 | 66 | 100% 67 | 75% 68 | 50% 69 | 25% 70 | 71 | 72 | 73 | nanostores 0.7.4 79 | 85 | 86 | @artalar/act 3.2.1 92 | 98 | 99 | s-js 0.4.9 105 | 111 | 112 | spred 0.32.2 118 | 124 | 125 | frpts 1.0.0-beta.3 131 | 137 | 138 | preact 1.2.3 144 | 150 | 151 | usignal 0.9.0 157 | 163 | 164 | solid 1.7.3 170 | 176 | 177 | cellx 1.10.30 183 | 189 | 190 | @webreflection/signal 2.0.0 196 | 202 | 203 | reatom 3.2.0 209 | 215 | 216 | mol 1.0.548 222 | 228 | 229 | whatsup 2.5.0 235 | 241 | 242 | mobx 6.9.0 248 | 254 | 255 | jotai 2.0.3 261 | 267 | 268 | wonka 6.3.1 274 | 280 | 281 | effector 22.8.0 287 | 293 | 294 | effector.fork 22.8.0 300 | 306 | 307 | -------------------------------------------------------------------------------- /all_chart_Apple_M1.svg: -------------------------------------------------------------------------------- 1 | 2 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 10 59 | 100 60 | 1000 61 | 10000 62 | ITERATIONS 63 | 64 | 65 | 66 | 100% 67 | 75% 68 | 50% 69 | 25% 70 | 71 | 72 | 73 | frpts 1.0.0-beta.6 79 | 85 | 86 | nanostores 0.10.0 92 | 98 | 99 | @artalar/act 3.2.1 105 | 111 | 112 | s-js 0.4.9 118 | 124 | 125 | whatsup 2.6.0 131 | 137 | 138 | usignal 0.9.0 144 | 150 | 151 | preact 1.6.0 157 | 163 | 164 | spred 0.34.0 170 | 176 | 177 | @webreflection/signal 2.1.1 183 | 189 | 190 | solid 1.8.16 196 | 202 | 203 | mol 1.0.978 209 | 215 | 216 | v4 0.0.0 222 | 228 | 229 | cellx 1.10.30 235 | 241 | 242 | reatom 3.7.0 248 | 254 | 255 | mobx 6.12.1 261 | 267 | 268 | wonka 6.3.4 274 | 280 | 281 | jotai 2.7.1 287 | 293 | 294 | effector 23.2.0 300 | 306 | 307 | effector.fork 23.2.0 313 | 319 | 320 | -------------------------------------------------------------------------------- /dynamic_deps_bench.ts: -------------------------------------------------------------------------------- 1 | import { forEach } from 'wonka' 2 | import { printLogs, formatLog, POSITION_KEY } from './utils' 3 | 4 | async function testAggregateGrowing(count: number, method: 'push' | 'unshift') { 5 | const mol_wire_lib = await import('mol_wire_lib') 6 | const { $mol_wire_atom } = mol_wire_lib.default 7 | 8 | const { atom, createCtx } = await import('@reatom/core') 9 | 10 | const V4 = await import('../../reatom4/packages/core/build') 11 | 12 | const { observable, computed, autorun, configure } = await import('mobx') 13 | configure({ enforceActions: 'never' }) 14 | 15 | const { act } = await import('@artalar/act') 16 | 17 | const molAtoms = [new $mol_wire_atom(`0`, (next: number = 0) => next)] 18 | const reAtoms = [atom(0, `${0}`)] 19 | const V4Atoms = [V4.atom(0, `${0}`)] 20 | const mobxAtoms = [observable.box(0, { name: `${0}` })] 21 | const actAtoms = [act(0)] 22 | 23 | const molAtom = new $mol_wire_atom(`sum`, () => 24 | molAtoms.reduce((sum, atom) => sum + atom.sync(), 0), 25 | ) 26 | const reAtom = atom( 27 | (ctx) => reAtoms.reduce((sum, atom) => sum + ctx.spy(atom), 0), 28 | `sum`, 29 | ) 30 | const V4Atom = V4.atom( 31 | () => V4Atoms.reduce((sum, atom) => sum + atom(), 0), 32 | `sum`, 33 | ) 34 | const mobxAtom = computed( 35 | () => mobxAtoms.reduce((sum, atom) => sum + atom.get(), 0), 36 | { name: `sum` }, 37 | ) 38 | const actAtom = act(() => actAtoms.reduce((sum, a) => sum + a(), 0)) 39 | 40 | const ctx = createCtx() 41 | const V4Root = V4.AsyncContext.Snapshot.createRoot() 42 | 43 | ctx.subscribe(reAtom, () => {}) 44 | V4.effect(() => V4Atom()).run(V4Root.frame) 45 | molAtom.sync() 46 | autorun(() => mobxAtom.get()) 47 | actAtom.subscribe(() => {}) 48 | 49 | const reatomLogs = new Array() 50 | const V4Logs = new Array() 51 | const molLogs = new Array() 52 | const mobxLogs = new Array() 53 | const actLogs = new Array() 54 | let i = 1 55 | while (i++ < count) { 56 | const startReatom = performance.now() 57 | reAtoms[method](atom(i, `${i}`)) 58 | reAtoms.at(-2)!(ctx, i) 59 | reatomLogs.push(performance.now() - startReatom) 60 | 61 | const startMol = performance.now() 62 | molAtoms[method](new $mol_wire_atom(`${i}`, (next: number = i) => next)) 63 | molAtoms.at(-2)!.put(i) 64 | molAtom.sync() 65 | molLogs.push(performance.now() - startMol) 66 | 67 | const startMobx = performance.now() 68 | mobxAtoms[method](observable.box(i, { name: `${i}` })) 69 | mobxAtoms.at(-2)!.set(i) 70 | mobxLogs.push(performance.now() - startMobx) 71 | 72 | const startV4 = performance.now() 73 | V4Root.run(() => { 74 | V4Atoms[method](V4.atom(i)) 75 | V4Atoms.at(-2)!(i) 76 | V4.notify() 77 | }) 78 | V4Logs.push(performance.now() - startV4) 79 | 80 | const startAct = performance.now() 81 | actAtoms[method](act(i)) 82 | actAtoms.at(-2)!(i) 83 | act.notify() 84 | actLogs.push(performance.now() - startAct) 85 | 86 | await new Promise((resolve) => setTimeout(resolve, 0)) 87 | } 88 | 89 | if ( 90 | new Set([ 91 | molAtom.sync(), 92 | ctx.get(reAtom), 93 | mobxAtom.get(), 94 | actAtom(), 95 | V4Root.run(V4Atom), 96 | ]).size > 1 97 | ) { 98 | throw new Error( 99 | 'Mismatch: ' + 100 | JSON.stringify({ 101 | mol: molAtom.sync(), 102 | reatom: ctx.get(reAtom), 103 | mobx: mobxAtom.get(), 104 | act: actAtom(), 105 | V4: V4Root.run(V4Atom), 106 | }), 107 | ) 108 | } 109 | 110 | console.log( 111 | `Median of sum calc of reactive nodes in list from 1 to ${count} (with "${method}")`, 112 | ) 113 | 114 | return printLogs({ 115 | reatom: formatLog(reatomLogs), 116 | $mol_wire: formatLog(molLogs), 117 | mobx: formatLog(mobxLogs), 118 | // act: formatLog(actLogs), 119 | V4: formatLog(V4Logs), 120 | }) 121 | } 122 | 123 | async function testAggregateShrinking(count: number, method: 'pop' | 'shift') { 124 | const mol_wire_lib = await import('mol_wire_lib') 125 | const { $mol_wire_atom } = mol_wire_lib.default 126 | 127 | const { atom, createCtx } = await import('@reatom/core') 128 | 129 | const V4 = await import('../../reatom4/packages/core/build') 130 | 131 | const { observable, computed, autorun, configure } = await import('mobx') 132 | configure({ enforceActions: 'never' }) 133 | 134 | const molAtoms = Array.from( 135 | { length: count }, 136 | (_, i) => new $mol_wire_atom(`${i}`, (next: number = 1) => next), 137 | ) 138 | const reAtoms = Array.from({ length: count }, (_, i) => atom(1, `${i}`)) 139 | const V4Atoms = Array.from({ length: count }, (_, i) => V4.atom(1, `${i}`)) 140 | const mobxAtoms = Array.from({ length: count }, (_, i) => 141 | observable.box(1, { name: `${i}` }), 142 | ) 143 | 144 | const molAtom = new $mol_wire_atom(`sum`, () => 145 | molAtoms.reduce((sum, atom) => sum + atom.sync(), 0), 146 | ) 147 | const reAtom = atom( 148 | (ctx) => reAtoms.reduce((sum, atom) => sum + ctx.spy(atom), 0), 149 | `sum`, 150 | ) 151 | const V4Atom = V4.atom( 152 | () => V4Atoms.reduce((sum, atom) => sum + atom(), 0), 153 | `sum`, 154 | ) 155 | const mobxAtom = computed( 156 | () => mobxAtoms.reduce((sum, atom) => sum + atom.get(), 0), 157 | { name: `sum` }, 158 | ) 159 | 160 | const ctx = createCtx() 161 | const V4Root = V4.AsyncContext.Snapshot.createRoot() 162 | 163 | ctx.subscribe(reAtom, () => {}) 164 | V4.effect(() => V4Atom()).run(V4Root.frame) 165 | molAtom.sync() 166 | autorun(() => mobxAtom.get()) 167 | 168 | const reatomLogs = new Array() 169 | const V4Logs = new Array() 170 | const molLogs = new Array() 171 | const mobxLogs = new Array() 172 | let i = 1 173 | while (i++ < count) { 174 | const startReatom = performance.now() 175 | reAtoms[method]()!(ctx, i) 176 | reatomLogs.push(performance.now() - startReatom) 177 | 178 | const startV4 = performance.now() 179 | V4Root.run(() => { 180 | V4Atoms[method]()!(i) 181 | V4.notify() 182 | }) 183 | V4Logs.push(performance.now() - startV4) 184 | 185 | const startMol = performance.now() 186 | molAtoms[method]()!.put(i) 187 | molAtom.sync() 188 | molLogs.push(performance.now() - startMol) 189 | 190 | const startMobx = performance.now() 191 | mobxAtoms[method]()!.set(i) 192 | mobxLogs.push(performance.now() - startMobx) 193 | 194 | await new Promise((resolve) => setTimeout(resolve, 0)) 195 | } 196 | 197 | if ( 198 | new Set([ 199 | molAtom.sync(), 200 | ctx.get(reAtom), 201 | V4Root.run(V4Atom), 202 | mobxAtom.get(), 203 | ]).size > 1 204 | ) { 205 | throw new Error( 206 | 'Mismatch: ' + 207 | JSON.stringify({ 208 | mol: molAtom.sync(), 209 | reatom: ctx.get(reAtom), 210 | V4: V4Root.run(V4Atom), 211 | mobx: mobxAtom.get(), 212 | }), 213 | ) 214 | } 215 | 216 | console.log( 217 | `Median of sum calc of reactive nodes in list from ${count} to 1 (with "${method}")`, 218 | ) 219 | 220 | return printLogs({ 221 | reatom: formatLog(reatomLogs), 222 | $mol_wire: formatLog(molLogs), 223 | mobx: formatLog(mobxLogs), 224 | // act: formatLog(actLogs), 225 | V4: formatLog(V4Logs), 226 | }) 227 | } 228 | 229 | async function testParent(count: number) { 230 | const mol_wire_lib = await import('mol_wire_lib') 231 | const { $mol_wire_atom } = mol_wire_lib.default 232 | 233 | const { atom, createCtx } = await import('@reatom/core') 234 | 235 | const V4 = await import('../../reatom4/packages/core/build') 236 | 237 | const { observable, computed, autorun, configure } = await import('mobx') 238 | configure({ enforceActions: 'never' }) 239 | 240 | const molAtom = new $mol_wire_atom(`0`, (next: number = 0) => next) 241 | const molAtoms = [] 242 | const reAtom = atom(0, `${0}`) 243 | const V4Atom = V4.atom(0, `${0}`) 244 | const mobxAtom = observable.box(0, { name: `${0}` }) 245 | 246 | const ctx = createCtx() 247 | const V4Root = V4.AsyncContext.Snapshot.createRoot() 248 | 249 | { 250 | let i = count 251 | while (i--) { 252 | const molPubAtom = new $mol_wire_atom(`${i}`, () => molAtom.sync()) 253 | molPubAtom.sync() 254 | molAtoms.push(molPubAtom) 255 | 256 | ctx.subscribe( 257 | atom((ctx) => ctx.spy(reAtom)), 258 | () => {}, 259 | ) 260 | 261 | const V4DepAtom = V4.atom(() => V4Atom()) 262 | V4.effect(() => V4DepAtom()).run(V4Root.frame) 263 | 264 | const mobxDepAtom = computed(() => mobxAtom.get()) 265 | autorun(() => mobxDepAtom.get()) 266 | } 267 | } 268 | 269 | const reatomLogs = new Array() 270 | const V4Logs = new Array() 271 | const molLogs = new Array() 272 | const mobxLogs = new Array() 273 | let i = count 274 | while (i--) { 275 | const startReatom = performance.now() 276 | reAtom(ctx, i) 277 | reatomLogs.push(performance.now() - startReatom) 278 | 279 | const startV4 = performance.now() 280 | V4Root.run(() => { 281 | V4Atom(i) 282 | V4.notify() 283 | }) 284 | V4Logs.push(performance.now() - startV4) 285 | 286 | const startMol = performance.now() 287 | molAtom.put(i) 288 | molAtoms.forEach((atom) => atom.sync()) 289 | molLogs.push(performance.now() - startMol) 290 | 291 | const startMobx = performance.now() 292 | mobxAtom.set(i) 293 | mobxLogs.push(performance.now() - startMobx) 294 | 295 | await new Promise((resolve) => setTimeout(resolve, 0)) 296 | } 297 | 298 | if ( 299 | new Set([ 300 | molAtom.sync(), 301 | ctx.get(reAtom), 302 | V4Root.run(V4Atom), 303 | mobxAtom.get(), 304 | ]).size > 1 305 | ) { 306 | throw new Error( 307 | 'Mismatch: ' + 308 | JSON.stringify({ 309 | mol: molAtom.sync(), 310 | reatom: ctx.get(reAtom), 311 | V4: V4Root.run(V4Atom), 312 | mobx: mobxAtom.get(), 313 | }), 314 | ) 315 | } 316 | 317 | console.log(`Median of update 1 node with ${count} reactive children`) 318 | 319 | return printLogs({ 320 | reatom: formatLog(reatomLogs), 321 | $mol_wire: formatLog(molLogs), 322 | mobx: formatLog(mobxLogs), 323 | // act: formatLog(actLogs), 324 | V4: formatLog(V4Logs), 325 | }) 326 | } 327 | 328 | ;(async () => { 329 | // const subscribers = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024] 330 | const subscribers = [1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 4, 4, 4, 10] 331 | 332 | for (const i of subscribers) { 333 | var results = [ 334 | await testAggregateGrowing(i, 'push'), 335 | await testAggregateGrowing(i, 'unshift'), 336 | await testAggregateShrinking(i, 'pop'), 337 | await testAggregateShrinking(i, 'shift'), 338 | // await testParent(i), 339 | ] 340 | } 341 | 342 | console.log('\nAVERAGE for', subscribers.join(','), 'subscribers') 343 | 344 | Object.keys(results![0]) 345 | .map((name) => ({ 346 | name, 347 | pos: 348 | results!.reduce((acc, log) => +log[name][POSITION_KEY] + acc, 0) / 349 | results!.length, 350 | })) 351 | .sort((a, b) => a.pos - b.pos) 352 | .forEach(({ name, pos }) => console.log(pos, name)) 353 | 354 | process.exit() 355 | })() 356 | -------------------------------------------------------------------------------- /computers_bench.ts: -------------------------------------------------------------------------------- 1 | // TODO move to test source 2 | import type { Source as WSource } from 'wonka' 3 | import { genChart } from './chart_gen' 4 | 5 | import { Rec, formatLog, printLogs } from './utils' 6 | 7 | type UpdateLeaf = (value: number) => void 8 | 9 | type Setup = (hooks: { 10 | listener: (computedValue: number) => void 11 | startCreation: () => void 12 | endCreation: () => void 13 | }) => Promise 14 | 15 | // There is a few tests skipped 16 | // coz I don't know how to turn off their batching 17 | // which delays computations 18 | 19 | const testComputers = setupComputersTest({ 20 | // async native({ listener, startCreation, endCreation }) { 21 | // startCreation() 22 | 23 | // const entry = (i = 0) => i 24 | // const a = (i: number) => entry(i) 25 | // const b = (i: number) => a(i) + 1 26 | // const c = (i: number) => a(i) + 1 27 | // const d = (i: number) => b(i) + c(i) 28 | // const e = (i: number) => d(i) + 1 29 | // const f = (i: number) => d(i) + e(i) 30 | // const g = (i: number) => d(i) + e(i) 31 | // const h = (i: number) => f(i) + g(i) 32 | 33 | // endCreation() 34 | 35 | // return (i) => listener(h(i)) 36 | // }, 37 | // async selector({ listener, startCreation, endCreation }) { 38 | // const createSelector = (cb: (input: any) => any) => { 39 | // let input: any, res: any 40 | // return (i: any) => (i === input ? res : (res = cb((input = i)))) 41 | // } 42 | 43 | // startCreation() 44 | 45 | // const entry = createSelector((i = 0) => i) 46 | // const a = createSelector((i: number) => entry(i)) 47 | // const b = createSelector((i: number) => a(i) + 1) 48 | // const c = createSelector((i: number) => a(i) + 1) 49 | // const d = createSelector((i: number) => b(i) + c(i)) 50 | // const e = createSelector((i: number) => d(i) + 1) 51 | // const f = createSelector((i: number) => d(i) + e(i)) 52 | // const g = createSelector((i: number) => d(i) + e(i)) 53 | // const h = createSelector((i: number) => f(i) + g(i)) 54 | 55 | // endCreation() 56 | 57 | // return (i) => listener(h(i)) 58 | // }, 59 | async spred({ listener, startCreation, endCreation }) { 60 | const { writable, computed } = await import('spred') 61 | 62 | startCreation() 63 | 64 | const entry = writable(0) 65 | const a = computed(() => entry.get()) 66 | const b = computed(() => a.get() + 1) 67 | const c = computed(() => a.get() + 1) 68 | const d = computed(() => b.get() + c.get()) 69 | const e = computed(() => d.get() + 1) 70 | const f = computed(() => d.get() + e.get()) 71 | const g = computed(() => d.get() + e.get()) 72 | const h = computed(() => f.get() + g.get()) 73 | 74 | h.subscribe(listener) 75 | 76 | endCreation() 77 | 78 | return (i) => entry.set(i) 79 | }, 80 | async cellx({ listener, startCreation, endCreation }) { 81 | const { cellx, Cell } = await import('cellx') 82 | 83 | startCreation() 84 | 85 | const entry = cellx(0) 86 | const a = cellx(() => entry()) 87 | const b = cellx(() => a() + 1) 88 | const c = cellx(() => a() + 1) 89 | const d = cellx(() => b() + c()) 90 | const e = cellx(() => d() + 1) 91 | const f = cellx(() => d() + e()) 92 | const g = cellx(() => d() + e()) 93 | const h = cellx(() => f() + g()) 94 | 95 | h.subscribe((err, v) => listener(h())) 96 | 97 | endCreation() 98 | 99 | return (i) => { 100 | entry(i) 101 | Cell.release() 102 | } 103 | }, 104 | async effector({ listener, startCreation, endCreation }) { 105 | const { createEvent, createStore, combine } = await import('effector') 106 | 107 | startCreation() 108 | 109 | const entry = createEvent() 110 | const a = createStore(0).on(entry, (state, v) => v) 111 | const b = a.map((a) => a + 1) 112 | const c = a.map((a) => a + 1) 113 | const d = combine(b, c, (b, c) => b + c) 114 | const e = d.map((d) => d + 1) 115 | const f = combine(d, e, (d, e) => d + e) 116 | const g = combine(d, e, (d, e) => d + e) 117 | const h = combine(f, g, (h1, h2) => h1 + h2) 118 | 119 | h.subscribe(listener) 120 | 121 | endCreation() 122 | 123 | return (i) => entry(i) 124 | }, 125 | async 'effector.fork'({ listener, startCreation, endCreation }) { 126 | const { createEvent, createStore, combine, allSettled, fork } = 127 | await import('effector') 128 | 129 | startCreation() 130 | 131 | const entry = createEvent() 132 | const a = createStore(0).on(entry, (state, v) => v) 133 | const b = a.map((a) => a + 1) 134 | const c = a.map((a) => a + 1) 135 | const d = combine(b, c, (b, c) => b + c) 136 | const e = d.map((d) => d + 1) 137 | const f = combine(d, e, (d, e) => d + e) 138 | const g = combine(d, e, (d, e) => d + e) 139 | const h = combine(f, g, (h1, h2) => h1 + h2) 140 | 141 | const scope = fork() 142 | 143 | endCreation() 144 | 145 | return (i) => { 146 | allSettled(entry, { scope, params: i }) 147 | // this is not wrong 148 | // coz effector graph a hot always 149 | // and `getState` is doing nothing here 150 | // only internal state reading 151 | listener(scope.getState(h)) 152 | } 153 | }, 154 | async frpts({ listener, startCreation, endCreation }) { 155 | const { newAtom, combine } = await import('@frp-ts/core') 156 | 157 | startCreation() 158 | 159 | const entry = newAtom(0) 160 | const a = combine(entry, (v) => v) 161 | const b = combine(a, (a) => a + 1) 162 | const c = combine(a, (a) => a + 1) 163 | const d = combine(b, c, (b, c) => b + c) 164 | const e = combine(d, (d) => d + 1) 165 | const f = combine(d, e, (d, e) => d + e) 166 | const g = combine(d, e, (d, e) => d + e) 167 | const h = combine(f, g, (f, g) => f + g) 168 | 169 | h.subscribe({ next: () => listener(h.get()) }) 170 | 171 | endCreation() 172 | 173 | return (i) => entry.set(i) 174 | }, 175 | async jotai({ listener, startCreation, endCreation }) { 176 | const { atom, createStore } = await import('jotai') 177 | 178 | startCreation() 179 | 180 | const entry = atom(0) 181 | const a = atom((get) => get(entry)) 182 | const b = atom((get) => get(a) + 1) 183 | const c = atom((get) => get(a) + 1) 184 | const d = atom((get) => get(b) + get(c)) 185 | const e = atom((get) => get(d) + 1) 186 | const f = atom((get) => get(d) + get(e)) 187 | const g = atom((get) => get(d) + get(e)) 188 | const h = atom((get) => get(f) + get(g)) 189 | 190 | const store = createStore() 191 | store.sub(h, () => listener(store.get(h))) 192 | 193 | endCreation() 194 | 195 | return (i) => store.set(entry, i) 196 | }, 197 | async mobx({ listener, startCreation, endCreation }) { 198 | const { makeAutoObservable, autorun, configure } = await import('mobx') 199 | 200 | configure({ enforceActions: 'never' }) 201 | 202 | startCreation() 203 | 204 | const proxy = makeAutoObservable({ 205 | entry: 0, 206 | get a() { 207 | return this.entry 208 | }, 209 | get b() { 210 | return this.a + 1 211 | }, 212 | get c() { 213 | return this.a + 1 214 | }, 215 | get d() { 216 | return this.b + this.c 217 | }, 218 | get e() { 219 | return this.d + 1 220 | }, 221 | get f() { 222 | return this.d + this.e 223 | }, 224 | get g() { 225 | return this.d + this.e 226 | }, 227 | get h() { 228 | return this.f + this.g 229 | }, 230 | }) 231 | 232 | autorun(() => listener(proxy.h)) 233 | 234 | endCreation() 235 | 236 | return (i) => (proxy.entry = i) 237 | }, 238 | async mol({ listener, startCreation, endCreation }) { 239 | const { 240 | default: { $mol_wire_atom: Atom }, 241 | } = await import('mol_wire_lib') 242 | 243 | startCreation() 244 | 245 | const entry = new Atom('entry', (next: number = 0) => next) 246 | const a = new Atom('mA', () => entry.sync()) 247 | const b = new Atom('mB', () => a.sync() + 1) 248 | const c = new Atom('mC', () => a.sync() + 1) 249 | const d = new Atom('mD', () => b.sync() + c.sync()) 250 | const e = new Atom('mE', () => d.sync() + 1) 251 | const f = new Atom('mF', () => d.sync() + e.sync()) 252 | const g = new Atom('mG', () => d.sync() + e.sync()) 253 | const h = new Atom('mH', () => f.sync() + g.sync()) 254 | 255 | listener(h.sync()) 256 | 257 | endCreation() 258 | 259 | return (i) => { 260 | entry.put(i) 261 | // the batch doing the same https://github.com/hyoo-ru/mam_mol/blob/c9cf0faf966c8bb3d0e76339527ef03e03d273e8/wire/fiber/fiber.ts#L31 262 | listener(h.sync()) 263 | } 264 | }, 265 | // async '@krulod/wire'({ listener, startCreation, endCreation }) { 266 | // const { Atom } = await import('@krulod/wire') 267 | 268 | // startCreation() 269 | 270 | // const entry = new Atom('entry', () => 0) 271 | // const a = new Atom('mA', () => entry.pull()) 272 | // const b = new Atom('mB', () => a.pull() + 1) 273 | // const c = new Atom('mC', () => a.pull() + 1) 274 | // const d = new Atom('mD', () => b.pull() + c.pull()) 275 | // const e = new Atom('mE', () => d.pull() + 1) 276 | // const f = new Atom('mF', () => d.pull() + e.pull()) 277 | // const g = new Atom('mG', () => d.pull() + e.pull()) 278 | // const h = new Atom('mH', () => f.pull() + g.pull()) 279 | 280 | // listener(h.pull()) 281 | 282 | // endCreation() 283 | 284 | // return (i) => { 285 | // entry.put(i) 286 | // listener(h.pull()) 287 | // } 288 | // }, 289 | async nanostores({ listener, startCreation, endCreation }) { 290 | const { atom, computed } = await import('nanostores') 291 | 292 | startCreation() 293 | 294 | const entry = atom(0) 295 | const a = computed(entry, (entry) => entry) 296 | const b = computed(a, (a) => a + 1) 297 | const c = computed(a, (a) => a + 1) 298 | const d = computed([b, c], (b, c) => b + c) 299 | const e = computed(d, (d) => d + 1) 300 | const f = computed([d, e], (d, e) => d + e) 301 | const g = computed([d, e], (d, e) => d + e) 302 | const h = computed([f, g], (h1, h2) => h1 + h2) 303 | 304 | h.subscribe(listener) 305 | 306 | endCreation() 307 | 308 | return (i) => entry.set(i) 309 | }, 310 | async preact({ listener, startCreation, endCreation }) { 311 | const { signal, computed, effect } = await import('@preact/signals-core') 312 | 313 | startCreation() 314 | 315 | const entry = signal(0) 316 | const a = computed(() => entry.value) 317 | const b = computed(() => a.value + 1) 318 | const c = computed(() => a.value + 1) 319 | const d = computed(() => b.value + c.value) 320 | const e = computed(() => d.value + 1) 321 | const f = computed(() => d.value + e.value) 322 | const g = computed(() => d.value + e.value) 323 | const h = computed(() => f.value + g.value) 324 | 325 | effect(() => listener(h.value)) 326 | 327 | endCreation() 328 | 329 | return (i) => (entry.value = i) 330 | }, 331 | // async 'reatom-v1'({ listener, startCreation, endCreation }) { 332 | // const { declareAction, declareAtom, map, combine, createStore } = 333 | // await import('reatom-v1') 334 | 335 | // startCreation() 336 | 337 | // const entry = declareAction() 338 | // const a = declareAtom(0, (on) => [on(entry, (state, v) => v)]) 339 | // const b = map(a, (v) => v + 1) 340 | // const c = map(a, (v) => v + 1) 341 | // const d = map(combine([b, c]), ([v1, v2]) => v1 + v2) 342 | // const e = map(d, (v) => v + 1) 343 | // const f = map(combine([d, e]), ([v1, v2]) => v1 + v2) 344 | // const g = map(combine([d, e]), ([v1, v2]) => v1 + v2) 345 | // const h = map(combine([f, g]), ([v1, v2]) => v1 + v2) 346 | 347 | // const store = createStore() 348 | // store.subscribe(h, listener) 349 | 350 | // endCreation() 351 | 352 | // return (i) => store.dispatch(entry(i)) 353 | // }, 354 | // async 'reatom-v2'({ listener, startCreation, endCreation }) { 355 | // const { createAtom, createStore } = await import('reatom-v2') 356 | 357 | // startCreation() 358 | 359 | // const a = createAtom({ entry: (v: number) => v }, (track, state = 0) => { 360 | // track.onAction('entry', (v) => (state = v)) 361 | // return state 362 | // }) 363 | // const b = createAtom({ a }, (track) => track.get('a') + 1) 364 | // const c = createAtom({ a }, (track) => track.get('a') + 1) 365 | // const d = createAtom({ b, c }, (track) => track.get('b') + track.get('c')) 366 | // const e = createAtom({ d }, (track) => track.get('d') + 1) 367 | // const f = createAtom({ d, e }, (track) => track.get('d') + track.get('e')) 368 | // const g = createAtom({ d, e }, (track) => track.get('d') + track.get('e')) 369 | // const h = createAtom({ f, g }, (track) => track.get('f') + track.get('g')) 370 | 371 | // const store = createStore() 372 | // store.subscribe(h, listener) 373 | 374 | // endCreation() 375 | 376 | // return (i) => store.dispatch(a.entry(i)) 377 | // }, 378 | async reatom({ listener, startCreation, endCreation }) { 379 | const { atom, createCtx } = await import('@reatom/core') 380 | 381 | startCreation() 382 | 383 | const entry = atom(0) 384 | const a = atom((ctx) => ctx.spy(entry)) 385 | const b = atom((ctx) => ctx.spy(a) + 1) 386 | const c = atom((ctx) => ctx.spy(a) + 1) 387 | const d = atom((ctx) => ctx.spy(b) + ctx.spy(c)) 388 | const e = atom((ctx) => ctx.spy(d) + 1) 389 | const f = atom((ctx) => ctx.spy(d) + ctx.spy(e)) 390 | const g = atom((ctx) => ctx.spy(d) + ctx.spy(e)) 391 | const h = atom((ctx) => ctx.spy(f) + ctx.spy(g)) 392 | 393 | const ctx = createCtx() 394 | ctx.subscribe(h, listener) 395 | 396 | endCreation() 397 | 398 | return (i) => entry(ctx, i) 399 | }, 400 | // async v4({ listener, startCreation, endCreation }) { 401 | // const { atom, effect, AsyncContext, wrap, notify, clearDefaults } = 402 | // await import('../../reatom4/packages/core/build') 403 | 404 | // startCreation() 405 | 406 | // clearDefaults() 407 | 408 | // const entry = atom(0, 'entry') 409 | // const a = atom(() => entry(), 'a') 410 | // const b = atom(() => a() + 1, 'b') 411 | // const c = atom(() => a() + 1, 'c') 412 | // const d = atom(() => b() + c(), 'd') 413 | // const e = atom(() => d() + 1, 'e') 414 | // const f = atom(() => d() + e(), 'f') 415 | // const g = atom(() => d() + e(), 'g') 416 | // const h = atom(() => f() + g(), 'h') 417 | 418 | // const root = AsyncContext.Snapshot.createRoot().frame 419 | // effect(() => listener(h())).run(root) 420 | 421 | // endCreation() 422 | 423 | // return wrap((v) => { 424 | // entry(v) 425 | // notify() 426 | // }, root) 427 | // }, 428 | async solid({ listener, startCreation, endCreation }) { 429 | const { createSignal, createMemo, createEffect } = await import( 430 | // FIXME 431 | // @ts-ignore 432 | 'solid-js/dist/solid.cjs' 433 | ) 434 | 435 | startCreation() 436 | 437 | const [entry, update] = createSignal(0) 438 | const a = createMemo(() => entry()) 439 | const b = createMemo(() => a() + 1) 440 | const c = createMemo(() => a() + 1) 441 | const d = createMemo(() => b() + c()) 442 | const e = createMemo(() => d() + 1) 443 | const f = createMemo(() => d() + e()) 444 | const g = createMemo(() => d() + e()) 445 | const h = createMemo(() => f() + g()) 446 | 447 | createEffect(() => listener(h())) 448 | 449 | endCreation() 450 | 451 | return (i) => update(i) 452 | }, 453 | async 's-js'({ listener, startCreation, endCreation }) { 454 | const { default: S } = await import('s-js') 455 | 456 | startCreation() 457 | 458 | const entry = S.root(() => { 459 | const entry = S.data(0) 460 | const a = S(() => entry()) 461 | const b = S(() => a() + 1) 462 | const c = S(() => a() + 1) 463 | const d = S(() => b() + c()) 464 | const e = S(() => d() + 1) 465 | const f = S(() => d() + e()) 466 | const g = S(() => d() + e()) 467 | const h = S(() => f() + g()) 468 | 469 | S(() => listener(h())) 470 | 471 | return entry 472 | }) 473 | 474 | endCreation() 475 | 476 | return (i) => entry(i) 477 | }, 478 | async usignal({ listener, startCreation, endCreation }) { 479 | const { signal, computed, effect } = await import('usignal') 480 | 481 | startCreation() 482 | 483 | const entry = signal(0) 484 | const a = computed(() => entry.value) 485 | const b = computed(() => a.value + 1) 486 | const c = computed(() => a.value + 1) 487 | const d = computed(() => b.value + c.value) 488 | const e = computed(() => d.value + 1) 489 | const f = computed(() => d.value + e.value) 490 | const g = computed(() => d.value + e.value) 491 | const h = computed(() => f.value + g.value) 492 | 493 | effect(() => listener(h.value)) 494 | 495 | endCreation() 496 | 497 | return (i) => (entry.value = i) 498 | }, 499 | async '@webreflection/signal'({ listener, startCreation, endCreation }) { 500 | const { signal, computed, effect } = await import('@webreflection/signal') 501 | 502 | startCreation() 503 | 504 | const entry = signal(0) 505 | const a = computed(() => entry.value) 506 | const b = computed(() => a.value + 1) 507 | const c = computed(() => a.value + 1) 508 | const d = computed(() => b.value + c.value) 509 | const e = computed(() => d.value + 1) 510 | const f = computed(() => d.value + e.value) 511 | const g = computed(() => d.value + e.value) 512 | const h = computed(() => f.value + g.value) 513 | 514 | effect(() => listener(h.value)) 515 | 516 | endCreation() 517 | 518 | return (i) => (entry.value = i) 519 | }, 520 | async whatsup({ listener, startCreation, endCreation }) { 521 | const { observable, computed, autorun } = await import('whatsup') 522 | 523 | startCreation() 524 | 525 | const entry = observable(0) 526 | const a = computed(() => entry()) 527 | const b = computed(() => a() + 1) 528 | const c = computed(() => a() + 1) 529 | const d = computed(() => b() + c()) 530 | const e = computed(() => d() + 1) 531 | const f = computed(() => d() + e()) 532 | const g = computed(() => d() + e()) 533 | const h = computed(() => f() + g()) 534 | 535 | autorun(() => listener(h())) 536 | 537 | endCreation() 538 | 539 | return (i) => entry(i) 540 | }, 541 | async wonka({ listener, startCreation, endCreation }) { 542 | const { makeSubject, pipe, map, subscribe, combine, sample } = await import( 543 | 'wonka' 544 | ) 545 | 546 | const ccombine = ( 547 | sourceA: WSource, 548 | sourceB: WSource, 549 | ): WSource<[A, B]> => { 550 | const source = combine(sourceA, sourceB) 551 | // return source 552 | return pipe(source, sample(source)) 553 | } 554 | 555 | startCreation() 556 | 557 | const entry = makeSubject() 558 | const a = pipe( 559 | entry.source, 560 | map((v) => v), 561 | ) 562 | const b = pipe( 563 | a, 564 | map((v) => v + 1), 565 | ) 566 | const c = pipe( 567 | a, 568 | map((v) => v + 1), 569 | ) 570 | const d = pipe( 571 | ccombine(b, c), 572 | map(([b, c]) => b + c), 573 | ) 574 | const e = pipe( 575 | d, 576 | map((v) => v + 1), 577 | ) 578 | const f = pipe( 579 | ccombine(d, e), 580 | map(([d, e]) => d + e), 581 | ) 582 | const g = pipe( 583 | ccombine(d, e), 584 | map(([d, e]) => d + e), 585 | ) 586 | const h = pipe( 587 | ccombine(f, g), 588 | map(([h1, h2]) => h1 + h2), 589 | ) 590 | pipe(h, subscribe(listener)) 591 | 592 | endCreation() 593 | 594 | return (i) => entry.next(i) 595 | }, 596 | async '@artalar/act'({ listener, startCreation, endCreation }) { 597 | const { act } = await import('@artalar/act') 598 | 599 | startCreation() 600 | 601 | const entry = act(0) 602 | const a = act(() => entry()) 603 | const b = act(() => a() + 1) 604 | const c = act(() => a() + 1) 605 | const d = act(() => b() + c()) 606 | const e = act(() => d() + 1) 607 | const f = act(() => d() + e()) 608 | const g = act(() => d() + e()) 609 | const h = act(() => f() + g()) 610 | 611 | h.subscribe(listener) 612 | 613 | endCreation() 614 | 615 | return (i) => { 616 | entry(i) 617 | act.notify() 618 | } 619 | }, 620 | }) 621 | 622 | function setupComputersTest(tests: Rec) { 623 | return async (iterations: number, creationTries: number) => { 624 | const testsList: Array<{ 625 | ref: { value: number } 626 | update: UpdateLeaf 627 | name: string 628 | creationLogs: Array 629 | updateLogs: Array 630 | memLogs: Array 631 | }> = [] 632 | 633 | for (const name in tests) { 634 | const ref = { value: 0 } 635 | const creationLogs: Array = [] 636 | let update: UpdateLeaf 637 | let start = 0 638 | let end = 0 639 | let i = creationTries || 1 640 | 641 | while (i--) { 642 | update = await tests[name]!({ 643 | listener: (value) => (ref.value = value), 644 | startCreation: () => (start = performance.now()), 645 | endCreation: () => (end = performance.now()), 646 | }) 647 | 648 | creationLogs.push(end - start) 649 | 650 | // try to prevent optimization of code meaning throwing 651 | update(-1) 652 | } 653 | 654 | testsList.push({ 655 | ref, 656 | update: update!, 657 | name, 658 | creationLogs, 659 | updateLogs: [], 660 | memLogs: [], 661 | }) 662 | } 663 | 664 | if (creationTries > 0) { 665 | console.log( 666 | `Median of computers creation and linking from ${creationTries} iterations\n(UNSTABLE)`, 667 | ) 668 | 669 | printLogs( 670 | Object.fromEntries( 671 | testsList.map(({ name, creationLogs }) => [ 672 | name, 673 | formatLog(creationLogs), 674 | ]), 675 | ), 676 | ) 677 | } 678 | 679 | let i = 0 680 | while (i++ < iterations) { 681 | testsList.sort(() => Math.random() - 0.5) 682 | for (const test of testsList) { 683 | globalThis.gc?.() 684 | globalThis.gc?.() 685 | let mem = globalThis.process?.memoryUsage?.().heapUsed 686 | 687 | const start = performance.now() 688 | test.update(i % 2) 689 | test.updateLogs.push(performance.now() - start) 690 | 691 | if (mem) test.memLogs.push(process.memoryUsage().heapUsed - mem) 692 | } 693 | 694 | if (new Set(testsList.map((test) => test.ref.value)).size !== 1) { 695 | console.log(`ERROR!`) 696 | console.error(`Results is not equal (iteration №${i})`) 697 | console.log( 698 | Object.fromEntries( 699 | testsList.map(({ name, ref }) => [name, ref.value]), 700 | ), 701 | ) 702 | process.exit(1) 703 | } 704 | 705 | await new Promise((resolve) => setTimeout(resolve, 0)) 706 | } 707 | 708 | console.log(`Median of update duration from ${iterations} iterations`) 709 | 710 | const results = testsList.reduce( 711 | (acc, { name, updateLogs }) => ((acc[name] = formatLog(updateLogs)), acc), 712 | {} as Rec, 713 | ) 714 | 715 | printLogs(results) 716 | 717 | if (globalThis.gc) { 718 | console.log(`Median of "heapUsed" from ${iterations} iterations`) 719 | printLogs( 720 | testsList.reduce( 721 | (acc, { name, memLogs }) => ((acc[name] = formatLog(memLogs)), acc), 722 | {} as Rec, 723 | ), 724 | ) 725 | } 726 | 727 | return results 728 | } 729 | } 730 | 731 | export async function test() { 732 | const results = { 733 | 10: await testComputers(10, 5), 734 | 100: await testComputers(100, 0), 735 | 1_000: await testComputers(1_000, 0), 736 | 10_000: await testComputers(10_000, 0), 737 | } 738 | 739 | await genChart(results) 740 | } 741 | 742 | if (globalThis.process) { 743 | import('perf_hooks') 744 | // @ts-expect-error 745 | .then(({ performance }) => (globalThis.performance = performance)) 746 | .then((): any => (globalThis.gc ? testComputers(300, 0) : test())) 747 | .then(() => process.exit()) 748 | } 749 | --------------------------------------------------------------------------------