diff --git a/packages/solid/test/component.bench.ts b/packages/solid/test/component.bench.ts new file mode 100644 index 000000000..b389d0997 --- /dev/null +++ b/packages/solid/test/component.bench.ts @@ -0,0 +1,144 @@ +import { mergeProps, splitProps } from "../src"; +import * as old from "./component.old"; +import { bench } from "vitest"; + +const staticDesc = { + value: 1, + writable: true, + configurable: true, + enumerable: true, +}; +const signalDesc = { + get() { + return 1; + }, + configurable: true, + enumerable: true, +}; +const createObject = ( + prefix: string, + amount: number, + desc: (index: number) => PropertyDescriptor +) => { + const proto: Record = {}; + for (let index = 0; index < amount; ++index) + proto[`${prefix}${index}`] = desc(index); + return Object.defineProperties({}, proto) as Record; +}; + +const keys = (o: Record) => Object.keys(o); + +type Test = { + title: string; + benchs: { title: string; func: any }[]; +}; + +function createTest< + T extends (...args: any[]) => any, + G extends (...args: any[]) => any +>(options: { + subjects: { + name: string; + func: T; + }[]; + generator: Record; + inputs: (generator: G) => Record>; +}) { + const tests: Test[] = []; + for (const generatorName in options.generator) { + const generator = options.generator[generatorName]; + const inputs = options.inputs(generator); + for (const title in inputs) { + const args = inputs[title]; + const test: Test = { title: `${title} (${generatorName})`, benchs: [] }; + for (const subject of options.subjects) { + test.benchs.push({ + title: subject.name, + func: () => subject.func(...args), + }); + } + tests.push(test); + } + } + return tests; +} + +type SplitProps = (...args: any[]) => Record[] + +const generator = { + static: (amount: number) => + createObject("static", amount, () => staticDesc), + dynamic: (amount: number) => + createObject("dynamic", amount, () => signalDesc), + mixed: (amount: number) => + createObject("mixed", amount, (v) => (v % 2 ? staticDesc : signalDesc)), +} as const + +const splitPropsTests = createTest({ + subjects: [ + { + name: "splitProps", + func: splitProps as SplitProps, + }, + { + name: "oldSplitProps", + func: old.splitProps as SplitProps, + }, + ], + generator, + inputs: (g) => ({ + "splitProps(5, 1)": [g(5), keys(g(1))], + "splitProps(5, 1, 2)": [g(5), keys(g(1)), keys(g(2))], + "splitProps(0, 15)": [g(0), keys(g(15))], + "splitProps(0, 3, 2)": [g(0), keys(g(3)), keys(g(2))], + "splitProps(0, 100)": [g(0), keys(g(100))], + "splitProps(0, 100, 3, 2)": [g(0), keys(g(100)), keys(g(3)), keys(g(2))], + "splitProps(25, 100)": [g(25), keys(g(100))], + "splitProps(50, 100)": [g(50), keys(g(100))], + "splitProps(100, 25)": [g(100), keys(g(25))], + }), +}); + +const mergePropsTest = createTest({ + subjects: [ + { + name: "mergeProps", + func: mergeProps, + }, + { + name: "oldMergeProps", + func: old.mergeProps , + }, + ], + generator, + inputs: (g) => ({ + "mergeProps(5, 1)": [g(5), (g(1))], + "mergeProps(5, 1, 2)": [g(5), (g(1)), (g(2))], + "mergeProps(0, 15)": [g(0), (g(15))], + "mergeProps(0, 3, 2)": [g(0), (g(3)), (g(2))], + "mergeProps(0, 100)": [g(0), (g(100))], + "mergeProps(0, 100, 3, 2)": [g(0), (g(100)), (g(3)), (g(2))], + "mergeProps(25, 100)": [g(25), (g(100))], + "mergeProps(50, 100)": [g(50), (g(100))], + "mergeProps(100, 25)": [g(100), (g(25))], + }), +}); + +const iterations = 100 + +for (const test of splitPropsTests) { + describe(test.title, () => { + for (const { title, func } of test.benchs) bench(title, func, { + iterations + }); + }); +} + + +for (const test of mergePropsTest) { + describe(test.title, () => { + for (const { title, func } of test.benchs) bench(title, func, { + iterations + }); + }); +} diff --git a/packages/solid/test/component.old.ts b/packages/solid/test/component.old.ts new file mode 100644 index 000000000..c3896ea90 --- /dev/null +++ b/packages/solid/test/component.old.ts @@ -0,0 +1,161 @@ +import { $PROXY, MergeProps, SplitProps, createMemo } from "../src"; +import { EffectFunction } from "../types"; + +function trueFn() { + return true; +} + +function resolveSource(s: any) { + return !(s = typeof s === "function" ? s() : s) ? {} : s; +} + +const propTraps: ProxyHandler<{ + get: (k: string | number | symbol) => any; + has: (k: string | number | symbol) => boolean; + keys: () => string[]; +}> = { + get(_, property, receiver) { + if (property === $PROXY) return receiver; + return _.get(property); + }, + has(_, property) { + if (property === $PROXY) return true; + return _.has(property); + }, + set: trueFn, + deleteProperty: trueFn, + getOwnPropertyDescriptor(_, property) { + return { + configurable: true, + enumerable: true, + get() { + return _.get(property); + }, + set: trueFn, + deleteProperty: trueFn + }; + }, + ownKeys(_) { + return _.keys(); + } +}; +export function splitProps< + T extends Record, + K extends [readonly (keyof T)[], ...(readonly (keyof T)[])[]] +>(props: T, ...keys: K): SplitProps { + const blocked = new Set(keys.length > 1 ? keys.flat() : keys[0]); + if ($PROXY in props) { + const res = keys.map(k => { + return new Proxy( + { + get(property) { + return k.includes(property) ? props[property as any] : undefined; + }, + has(property) { + return k.includes(property) && property in props; + }, + keys() { + return k.filter(property => property in props); + } + }, + propTraps + ); + }); + res.push( + new Proxy( + { + get(property) { + return blocked.has(property) ? undefined : props[property as any]; + }, + has(property) { + return blocked.has(property) ? false : property in props; + }, + keys() { + return Object.keys(props).filter(k => !blocked.has(k)); + } + }, + propTraps + ) + ); + return res as SplitProps; + } + const descriptors = Object.getOwnPropertyDescriptors(props); + keys.push(Object.keys(descriptors).filter(k => !blocked.has(k as keyof T)) as (keyof T)[]); + return keys.map(k => { + const clone = {}; + for (let i = 0; i < k.length; i++) { + const key = k[i]; + if (!(key in props)) continue; // skip defining keys that don't exist + Object.defineProperty( + clone, + key, + descriptors[key] + ? descriptors[key] + : { + get() { + return props[key]; + }, + set() { + return true; + }, + enumerable: true + } + ); + } + return clone; + }) as SplitProps; +} + +export function mergeProps(...sources: T): MergeProps { + let proxy = false; + for (let i = 0; i < sources.length; i++) { + const s = sources[i]; + proxy = proxy || (!!s && $PROXY in (s as object)); + sources[i] = + typeof s === "function" ? ((proxy = true), createMemo(s as EffectFunction)) : s; + } + if (proxy) { + return new Proxy( + { + get(property: string | number | symbol) { + for (let i = sources.length - 1; i >= 0; i--) { + const v = resolveSource(sources[i])[property]; + if (v !== undefined) return v; + } + }, + has(property: string | number | symbol) { + for (let i = sources.length - 1; i >= 0; i--) { + if (property in resolveSource(sources[i])) return true; + } + return false; + }, + keys() { + const keys = []; + for (let i = 0; i < sources.length; i++) + keys.push(...Object.keys(resolveSource(sources[i]))); + return [...new Set(keys)]; + } + }, + propTraps + ) as unknown as MergeProps; + } + const target = {} as MergeProps; + for (let i = sources.length - 1; i >= 0; i--) { + if (sources[i]) { + const descriptors = Object.getOwnPropertyDescriptors(sources[i]); + for (const key in descriptors) { + if (key in target) continue; + Object.defineProperty(target, key, { + enumerable: true, + get() { + for (let i = sources.length - 1; i >= 0; i--) { + const v = ((sources[i] as any) || {})[key]; + if (v !== undefined) return v; + } + } + }); + } + } + } + return target; +}