diff --git a/package.json b/package.json index 7efa2b8..88a08c3 100644 --- a/package.json +++ b/package.json @@ -2,18 +2,21 @@ "name": "js-reactivity-benchmark", "version": "1.0.0", "description": "", - "main": "index.js", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { "tsc": "tsc", "test": "vitest", - "build": "esbuild src/index.ts --bundle --format=iife --target=esnext --outfile=public/dist/index.js --sourcemap=inline", + "prepare": "esbuild src/index.ts --bundle --format=esm --target=esnext --outdir=dist --sourcemap=external && tsc -p tsconfig.d.json", + "build": "esbuild src/main.ts --bundle --format=iife --target=esnext --outfile=public/dist/index.js --sourcemap=inline", "start": "serve public", "chrome": "chrome --no-sandbox --js-flags=--log-deopt,--log-ic,--log-maps,--log-maps-details,--log-internal-timer-events,--prof,--expose-gc,--detailed-line-info --enable-precise-memory-info --user-data-dir=$(PWD)/chrome http://localhost:3000" }, "keywords": [], "author": "", "license": "ISC", - "dependencies": { + "devDependencies": { "@angular/core": "19.1.5", "@preact/signals": "^2.0.1", "@reactively/core": "^0.0.8", @@ -29,9 +32,7 @@ "signal-polyfill": "^0.2.2", "solid-js": "^1.9.4", "svelte": "^5.20.0", - "usignal": "^0.9.0" - }, - "devDependencies": { + "usignal": "^0.9.0", "@types/node": "^22.13.1", "esbuild": "^0.25.0", "serve": "^14.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a143c10..753f27a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,7 +7,7 @@ settings: importers: .: - dependencies: + devDependencies: '@angular/core': specifier: 19.1.5 version: 19.1.5(rxjs@7.8.1)(zone.js@0.14.2) @@ -20,6 +20,9 @@ importers: '@solidjs/signals': specifier: ^0.0.9 version: 0.0.9 + '@types/node': + specifier: ^22.13.1 + version: 22.13.1 '@vue/reactivity': specifier: ^3.5.13 version: 3.5.13 @@ -29,6 +32,9 @@ importers: compostate: specifier: ^0.5.1 version: 0.5.1 + esbuild: + specifier: ^0.25.0 + version: 0.25.0 mobx: specifier: ^6.13.6 version: 6.13.6 @@ -44,6 +50,9 @@ importers: s-js: specifier: ^0.4.9 version: 0.4.9 + serve: + specifier: ^14.2.4 + version: 14.2.4 signal-polyfill: specifier: ^0.2.2 version: 0.2.2 @@ -53,22 +62,12 @@ importers: svelte: specifier: ^5.20.0 version: 5.20.0 - usignal: - specifier: ^0.9.0 - version: 0.9.0 - devDependencies: - '@types/node': - specifier: ^22.13.1 - version: 22.13.1 - esbuild: - specifier: ^0.25.0 - version: 0.25.0 - serve: - specifier: ^14.2.4 - version: 14.2.4 typescript: specifier: ^5.7.3 version: 5.7.3 + usignal: + specifier: ^0.9.0 + version: 0.9.0 vitest: specifier: ^3.0.5 version: 3.0.5(@types/node@22.13.1) diff --git a/src/benches/cellxBench.ts b/src/benches/cellxBench.ts index e473117..4394143 100644 --- a/src/benches/cellxBench.ts +++ b/src/benches/cellxBench.ts @@ -1,6 +1,6 @@ // The following is an implementation of the cellx benchmark https://github.com/Riim/cellx/blob/master/perf/perf.html import { nextTick } from "../util/asyncUtil"; -import { logPerfResult } from "../util/perfLogging"; +import { PerfResultCallback } from "../util/perfLogging"; import { Computed, ReactiveFramework } from "../util/reactiveFramework"; const cellx = (framework: ReactiveFramework, layers: number) => { @@ -88,7 +88,7 @@ type BenchmarkResults = [ readonly [number, number, number, number], ]; -export const cellxbench = async (framework: ReactiveFramework) => { +export const cellxbench = async (framework: ReactiveFramework, logPerfResult: PerfResultCallback) => { const expected: Record = { 1000: [ [-3, -6, -2, 2], @@ -121,7 +121,7 @@ export const cellxbench = async (framework: ReactiveFramework) => { logPerfResult({ framework: framework.name, test: `cellx${layers}`, - time: total.toFixed(2), + time: total, }); } diff --git a/src/benches/kairoBench.ts b/src/benches/kairoBench.ts index b58aea4..f9b64c8 100644 --- a/src/benches/kairoBench.ts +++ b/src/benches/kairoBench.ts @@ -8,8 +8,8 @@ import { triangle } from "./kairo/triangle"; import { unstable } from "./kairo/unstable"; import { nextTick } from "../util/asyncUtil"; import { fastestTest } from "../util/benchRepeat"; -import { logPerfResult } from "../util/perfLogging"; import { ReactiveFramework } from "../util/reactiveFramework"; +import { PerfResultCallback } from "../util/perfLogging"; const cases = [ avoidablePropagation, @@ -22,7 +22,7 @@ const cases = [ unstable, ]; -export async function kairoBench(framework: ReactiveFramework) { +export async function kairoBench(framework: ReactiveFramework, logPerfResult: PerfResultCallback) { for (const c of cases) { const iter = framework.withBuild(() => { const iter = c(framework); @@ -44,7 +44,7 @@ export async function kairoBench(framework: ReactiveFramework) { logPerfResult({ framework: framework.name, test: c.name, - time: timing.time.toFixed(2), + time: timing.time, }); } } diff --git a/src/benches/molBench.ts b/src/benches/molBench.ts index f0ff394..dd41102 100644 --- a/src/benches/molBench.ts +++ b/src/benches/molBench.ts @@ -1,6 +1,6 @@ import { nextTick } from "../util/asyncUtil"; import { fastestTest } from "../util/benchRepeat"; -import { logPerfResult } from "../util/perfLogging"; +import { PerfResultCallback } from "../util/perfLogging"; import { ReactiveFramework } from "../util/reactiveFramework"; function fib(n: number): number { @@ -14,7 +14,7 @@ function hard(n: number, _log: string) { const numbers = Array.from({ length: 5 }, (_, i) => i); -export async function molBench(framework: ReactiveFramework) { +export async function molBench(framework: ReactiveFramework, logPerfResult: PerfResultCallback) { let res = []; const iter = framework.withBuild(() => { const A = framework.signal(0); @@ -65,6 +65,6 @@ export async function molBench(framework: ReactiveFramework) { logPerfResult({ framework: framework.name, test: "molBench", - time: timing.time.toFixed(2), + time: timing.time, }); } diff --git a/src/benches/reactively/dynamicBench.ts b/src/benches/reactively/dynamicBench.ts index 6b3f793..ed16396 100644 --- a/src/benches/reactively/dynamicBench.ts +++ b/src/benches/reactively/dynamicBench.ts @@ -1,16 +1,29 @@ import { makeGraph, runGraph } from "./dependencyGraph"; -import { logPerfResult, perfRowStrings } from "../../util/perfLogging"; import { verifyBenchResult } from "../../util/perfTests"; -import { FrameworkInfo } from "../../util/frameworkTypes"; +import { FrameworkInfo, TestConfig } from "../../util/frameworkTypes"; import { perfTests } from "../../config"; import { fastestTest } from "../../util/benchRepeat"; +import { PerfResultCallback } from "../../util/perfLogging"; + +function percent(n: number): string { + return Math.round(n * 100) + "%"; +} + +export function makeTitle(config: TestConfig): string { + const { width, totalLayers, staticFraction, nSources, readFraction } = config; + const dyn = staticFraction < 1 ? " - dynamic" : ""; + const read = readFraction < 1 ? ` - read ${percent(readFraction)}` : ""; + const sources = ` - ${nSources} sources`; + return `${width}x${totalLayers}${sources}${dyn}${read}`; +} /** benchmark a single test under single framework. * The test is run multiple times and the fastest result is logged to the console. */ export async function dynamicBench( frameworkTest: FrameworkInfo, - testRepeats = 5, + logPerfResult: PerfResultCallback, + testRepeats = 5 ): Promise { const { framework } = frameworkTest; for (const config of perfTests) { @@ -33,7 +46,11 @@ export async function dynamicBench( return { sum, count: counter.count }; }); - logPerfResult(perfRowStrings(framework.name, config, timedResult)); + logPerfResult({ + framework: framework.name, + test: `${makeTitle(config)} (${config.name || ""})`, + time: timedResult.timing.time, + }); verifyBenchResult(frameworkTest, config, timedResult); } } diff --git a/src/benches/sBench.ts b/src/benches/sBench.ts index d5811c2..255644f 100644 --- a/src/benches/sBench.ts +++ b/src/benches/sBench.ts @@ -1,13 +1,13 @@ // Inspired by https://github.com/solidjs/solid/blob/main/packages/solid/bench/bench.cjs -import { logPerfResult } from "../util/perfLogging"; +import { PerfResultCallback } from "../util/perfLogging"; import { Computed, ReactiveFramework, Signal } from "../util/reactiveFramework"; const COUNT = 1e5; type Reader = () => number; -export function sbench(framework: ReactiveFramework) { +export function sbench(framework: ReactiveFramework, logPerfResult: PerfResultCallback) { bench(createDataSignals, COUNT, COUNT); bench(createComputations0to1, COUNT, 0); bench(createComputations1to1, COUNT, COUNT); @@ -36,7 +36,7 @@ export function sbench(framework: ReactiveFramework) { logPerfResult({ framework: framework.name, test: fn.name, - time: time.toFixed(2), + time: time, }); } @@ -50,7 +50,7 @@ export function sbench(framework: ReactiveFramework) { let end = 0; framework.withBuild(() => { - if (window.gc) gc!(), gc!(); + if (globalThis.gc) gc!(), gc!(); // run 3 times to warm up let sources: Computed[] | null = createDataSignals(scount, []); @@ -67,7 +67,7 @@ export function sbench(framework: ReactiveFramework) { } // start GC clean - if (window.gc) gc!(), gc!(); + if (globalThis.gc) gc!(), gc!(); start = performance.now(); @@ -75,7 +75,7 @@ export function sbench(framework: ReactiveFramework) { // end GC clean sources = null; - if (window.gc) gc!(), gc!(); + if (globalThis.gc) gc!(), gc!(); end = performance.now(); }); diff --git a/src/config.ts b/src/config.ts index a26bafe..c8b92b0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,41 +1,4 @@ -import { TestConfig, FrameworkInfo } from "./util/frameworkTypes"; - -import { angularFramework as angularFramework2 } from "./frameworks/angularSignals2"; -// import { compostateFramework } from "./frameworks/inactive/compostate"; -// import { kairoFramework } from "./frameworks/inactive/kairo"; -// import { mobxFramework } from "./frameworks/inactive/mobx"; -import { molWireFramework } from "./frameworks/molWire"; -// import { obyFramework } from "./frameworks/inactive/oby"; -import { preactSignalFramework } from "./frameworks/preactSignals"; -import { reactivelyFramework } from "./frameworks/reactively"; -// import { sFramework } from "./frameworks/inactive/s"; -import { solidFramework } from "./frameworks/solid"; -// import { usignalFramework } from "./frameworks/inactive/uSignal"; -// import { vueReactivityFramework } from "./frameworks/inactive/vueReactivity"; -// import { xReactivityFramework } from "./frameworks/inactive/xReactivity"; -import { alienFramework } from "./frameworks/alienSignals"; -import { svelteFramework } from "./frameworks/svelte"; -// import { tc39SignalsFramework } from "./frameworks/tc39signals"; - -export const frameworkInfo: FrameworkInfo[] = [ - { framework: alienFramework, testPullCounts: true }, - { framework: angularFramework2, testPullCounts: true }, - // { framework: angularFramework, testPullCounts: true }, - // { framework: compostateFramework, testPullCounts: true }, - // { framework: kairoFramework, testPullCounts: true }, - // { framework: mobxFramework, testPullCounts: true }, - { framework: molWireFramework, testPullCounts: true }, - // { framework: obyFramework, testPullCounts: true }, - { framework: preactSignalFramework, testPullCounts: true }, - { framework: reactivelyFramework, testPullCounts: true }, - // { framework: sFramework }, - { framework: solidFramework }, // solid can't testPullCounts because batch executes all leaf nodes even if unread - { framework: svelteFramework, testPullCounts: true }, - // { framework: tc39SignalsFramework, testPullCounts: true }, - // { framework: usignalFramework, testPullCounts: true }, - // { framework: vueReactivityFramework, testPullCounts: true }, - // { framework: xReactivityFramework, testPullCounts: true }, -]; +import { TestConfig } from "./util/frameworkTypes"; export const perfTests: TestConfig[] = [ { diff --git a/src/frameworks.test.ts b/src/frameworks.test.ts index 526eba9..c0cca4e 100644 --- a/src/frameworks.test.ts +++ b/src/frameworks.test.ts @@ -1,7 +1,7 @@ import { makeGraph, runGraph } from "./benches/reactively/dependencyGraph"; import { expect, test } from "vitest"; import { FrameworkInfo, TestConfig } from "./util/frameworkTypes"; -import { frameworkInfo } from "./config"; +import { frameworkInfo } from "./frameworksList"; frameworkInfo.forEach((frameworkInfo) => frameworkTests(frameworkInfo)); diff --git a/src/frameworksList.ts b/src/frameworksList.ts new file mode 100644 index 0000000..01a9973 --- /dev/null +++ b/src/frameworksList.ts @@ -0,0 +1,37 @@ +import { FrameworkInfo } from "./util/frameworkTypes"; +import { angularFramework as angularFramework2 } from "./frameworks/angularSignals2"; +// import { compostateFramework } from "./frameworks/inactive/compostate"; +// import { kairoFramework } from "./frameworks/inactive/kairo"; +// import { mobxFramework } from "./frameworks/inactive/mobx"; +import { molWireFramework } from "./frameworks/molWire"; +// import { obyFramework } from "./frameworks/inactive/oby"; +import { preactSignalFramework } from "./frameworks/preactSignals"; +import { reactivelyFramework } from "./frameworks/reactively"; +// import { sFramework } from "./frameworks/inactive/s"; +import { solidFramework } from "./frameworks/solid"; +// import { usignalFramework } from "./frameworks/inactive/uSignal"; +// import { vueReactivityFramework } from "./frameworks/inactive/vueReactivity"; +// import { xReactivityFramework } from "./frameworks/inactive/xReactivity"; +import { alienFramework } from "./frameworks/alienSignals"; +import { svelteFramework } from "./frameworks/svelte"; +// import { tc39SignalsFramework } from "./frameworks/tc39signals"; + +export const frameworkInfo: FrameworkInfo[] = [ + { framework: alienFramework, testPullCounts: true }, + { framework: angularFramework2, testPullCounts: true }, + // { framework: angularFramework, testPullCounts: true }, + // { framework: compostateFramework, testPullCounts: true }, + // { framework: kairoFramework, testPullCounts: true }, + // { framework: mobxFramework, testPullCounts: true }, + { framework: molWireFramework, testPullCounts: true }, + // { framework: obyFramework, testPullCounts: true }, + { framework: preactSignalFramework, testPullCounts: true }, + { framework: reactivelyFramework, testPullCounts: true }, + // { framework: sFramework }, + { framework: solidFramework }, // solid can't testPullCounts because batch executes all leaf nodes even if unread + { framework: svelteFramework, testPullCounts: true }, + // { framework: tc39SignalsFramework, testPullCounts: true }, + // { framework: usignalFramework, testPullCounts: true }, + // { framework: vueReactivityFramework, testPullCounts: true }, + // { framework: xReactivityFramework, testPullCounts: true }, +]; diff --git a/src/index.ts b/src/index.ts index b351d3e..bc314be 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,41 +1,51 @@ import { dynamicBench } from "./benches/reactively/dynamicBench"; import { cellxbench } from "./benches/cellxBench"; import { sbench } from "./benches/sBench"; -import { frameworkInfo } from "./config"; -import { logPerfResult, perfReportHeaders } from "./util/perfLogging"; import { molBench } from "./benches/molBench"; import { kairoBench } from "./benches/kairoBench"; import { promiseDelay } from "./util/asyncUtil"; - -async function main() { - logPerfResult(perfReportHeaders()); - +import { FrameworkInfo } from "./util/frameworkTypes"; +import { PerfResultCallback } from "./util/perfLogging"; + +export { ReactiveFramework } from "./util/reactiveFramework"; +export { + perfResultHeaders, + formatPerfResult, + formatPerfResultStrings, + PerfResult, + PerfResultStrings, + PerfResultCallback, +} from "./util/perfLogging"; +export { FrameworkInfo }; + +export async function runTests( + frameworkInfo: FrameworkInfo[], + logPerfResult: PerfResultCallback +) { await promiseDelay(0); for (const { framework } of frameworkInfo) { - await kairoBench(framework); + await kairoBench(framework, logPerfResult); await promiseDelay(2000); } for (const { framework } of frameworkInfo) { - await molBench(framework); + await molBench(framework, logPerfResult); await promiseDelay(2000); } for (const { framework } of frameworkInfo) { - sbench(framework); + sbench(framework, logPerfResult); await promiseDelay(2000); } for (const { framework } of frameworkInfo) { - cellxbench(framework); + cellxbench(framework, logPerfResult); await promiseDelay(2000); } for (const frameworkTest of frameworkInfo) { - await dynamicBench(frameworkTest); + await dynamicBench(frameworkTest, logPerfResult); await promiseDelay(2000); } } - -(window as any).main = main; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..10b20b0 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,24 @@ +import { frameworkInfo } from "./frameworksList"; +import { + formatPerfResult, + formatPerfResultStrings, + PerfResult, + perfResultHeaders, + runTests, +} from "./index"; + +const pre = document.querySelector("pre")!; +function logLine(line: string): void { + pre.innerText += line + "\n"; +} + +function logPerfResult(result: PerfResult): void { + logLine(formatPerfResult(result)); +} + +async function main() { + logLine(formatPerfResultStrings(perfResultHeaders())); + await runTests(frameworkInfo, logPerfResult); +} + +(window as any).main = main; diff --git a/src/util/benchRepeat.ts b/src/util/benchRepeat.ts index 793f2ae..a30011e 100644 --- a/src/util/benchRepeat.ts +++ b/src/util/benchRepeat.ts @@ -34,7 +34,7 @@ export function runTimed(fn: () => T): TimedResult { /** run a function, reporting the wall clock time and garbage collection time. */ async function runTracked(fn: () => T): Promise> { - if (window.gc) gc!(), gc!(); + if (globalThis.gc) gc!(), gc!(); let out = runTimed(fn); const { result, time } = out; return { result, timing: { time } }; diff --git a/src/util/perfLogging.ts b/src/util/perfLogging.ts index 1ab7edc..d24bf8e 100644 --- a/src/util/perfLogging.ts +++ b/src/util/perfLogging.ts @@ -1,59 +1,32 @@ -import { TestConfig } from "./frameworkTypes"; -import { TestResult, TimingResult } from "./perfTests"; - -const pre = document.querySelector("pre")!; -export function logPerfResult(row: PerfRowStrings): void { - const line = Object.values(trimColumns(row)).join(" , "); - pre.innerText += line + "\n"; +export interface PerfResult { + framework: string; + test: string; + time: number; } -export interface PerfRowStrings { +export interface PerfResultStrings { framework: string; test: string; time: string; } +export type PerfResultCallback = (result: PerfResult) => void; + const columnWidth = { - framework: 16, + framework: 32, test: 60, time: 8, }; -export function perfReportHeaders(): PerfRowStrings { - const keys: (keyof PerfRowStrings)[] = Object.keys(columnWidth) as any; +export function perfResultHeaders(): PerfResultStrings { + const keys: (keyof PerfResultStrings)[] = Object.keys(columnWidth) as any; const kv = keys.map((key) => [key, key]); const untrimmed = Object.fromEntries(kv); return trimColumns(untrimmed); } -export function perfRowStrings( - frameworkName: string, - config: TestConfig, - timed: TimingResult, -): PerfRowStrings { - const { timing } = timed; - - return { - framework: frameworkName, - test: `${makeTitle(config)} (${config.name || ""})`, - time: timing.time.toFixed(2), - }; -} - -export function makeTitle(config: TestConfig): string { - const { width, totalLayers, staticFraction, nSources, readFraction } = config; - const dyn = staticFraction < 1 ? " - dynamic" : ""; - const read = readFraction < 1 ? ` - read ${percent(readFraction)}` : ""; - const sources = ` - ${nSources} sources`; - return `${width}x${totalLayers}${sources}${dyn}${read}`; -} - -function percent(n: number): string { - return Math.round(n * 100) + "%"; -} - -function trimColumns(row: PerfRowStrings): PerfRowStrings { - const keys: (keyof PerfRowStrings)[] = Object.keys(columnWidth) as any; +function trimColumns(row: PerfResultStrings): PerfResultStrings { + const keys: (keyof PerfResultStrings)[] = Object.keys(columnWidth) as any; const trimmed = { ...row }; for (const key of keys) { const length = columnWidth[key]; @@ -62,3 +35,15 @@ function trimColumns(row: PerfRowStrings): PerfRowStrings { } return trimmed; } + +export function formatPerfResultStrings(row: PerfResultStrings): string { + return Object.values(trimColumns(row)).join(" , "); +} + +export function formatPerfResult(row: PerfResult): string { + return formatPerfResultStrings({ + framework: row.framework, + test: row.test, + time: row.time.toFixed(2), + }); +} diff --git a/tsconfig.d.json b/tsconfig.d.json new file mode 100644 index 0000000..bcfdd8a --- /dev/null +++ b/tsconfig.d.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "declaration": true, + "noEmit": false, + "emitDeclarationOnly": true + }, + "files": ["./src/index.ts"] +}