From 1f781223ff4dae6271c89a1b3f1d64ed55057c5f Mon Sep 17 00:00:00 2001 From: Juanra GM Date: Mon, 1 May 2023 21:02:45 +0200 Subject: [PATCH 1/2] perf: rewrite `splitProps` and `mergeProps` --- packages/solid/src/render/component.ts | 110 ++++++++++++++++--------- 1 file changed, 71 insertions(+), 39 deletions(-) diff --git a/packages/solid/src/render/component.ts b/packages/solid/src/render/component.ts index 57661ca9..8a54cc2a 100644 --- a/packages/solid/src/render/component.ts +++ b/packages/solid/src/render/component.ts @@ -180,7 +180,15 @@ function resolveSource(s: any) { return !(s = typeof s === "function" ? s() : s) ? {} : s; } +function resolveSources(this: (() => any)[]) { + for (let i = 0, length = this.length; i < length; ++i) { + const v = this[i](); + if (v !== undefined) return v; + } +} + export function mergeProps(...sources: T): MergeProps { + if (sources.length === 1) return sources[0] as any; let proxy = false; for (let i = 0; i < sources.length; i++) { const s = sources[i]; @@ -213,25 +221,45 @@ export function mergeProps(...sources: T): MergeProps { propTraps ) as unknown as MergeProps; } - const target = {} as MergeProps; + const target: Record = {}; + const sourcesMap: Record = {}; + let someNonTargetKey = false; + 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; - } + const source = sources[i] as Record; + if (!source) continue; + const sourceKeys = Object.getOwnPropertyNames(source); + someNonTargetKey = someNonTargetKey || (i !== 0 && !!sourceKeys.length); + for (let i = 0, length = sourceKeys.length; i < length; i++) { + const key = sourceKeys[i]; + if (!(key in target)) { + const desc = Object.getOwnPropertyDescriptor(source, key)!; + if (desc.get) { + Object.defineProperty(target, key, { + enumerable: true, + get: resolveSources.bind( + (sourcesMap[key] = [desc.get.bind(source)]) + ), + }); + } else { + target[key] = desc.value; + } + } else { + const sources = sourcesMap[key]; + const desc = Object.getOwnPropertyDescriptor(source, key)!; + if (sources) { + if (desc.get) { + sources.push(desc.get.bind(source)); + } else if (desc.value !== undefined) { + sources.push(() => desc.value); } - }); + } else if (target[key] === undefined) { + target[key] = desc.value; + } } } } - return target; + return (someNonTargetKey ? target : sources[0]) as any; } export type SplitProps = [ @@ -247,8 +275,8 @@ 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 blocked = new Set(keys.length > 1 ? keys.flat() : keys[0]); const res = keys.map(k => { return new Proxy( { @@ -283,31 +311,35 @@ export function splitProps< ); 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 - } - ); + const otherObject: Record = {}; + const objects: Record[] = keys.map(() => ({})); + + for (const propName of Object.getOwnPropertyNames(props)) { + const desc = Object.getOwnPropertyDescriptor(props, propName)!; + const isDefaultDesc = + !desc.get && + !desc.set && + desc.enumerable && + desc.writable && + desc.configurable; + let blocked = false; + let objectIndex = 0; + for (const k of keys) { + if (k.includes(propName)) { + blocked = true; + isDefaultDesc + ? (objects[objectIndex][propName] = desc.value) + : Object.defineProperty(objects[objectIndex], propName, desc) + } + ++objectIndex; } - return clone; - }) as SplitProps; + if (!blocked) { + isDefaultDesc + ? (otherObject[propName] = desc.value) + : Object.defineProperty(otherObject, propName, desc); + } + } + return [...objects, otherObject] as any; } // lazy load a function component asynchronously From 81b53e3e69571a98623252197b234efa54afbf89 Mon Sep 17 00:00:00 2001 From: Juanra GM Date: Mon, 1 May 2023 21:04:09 +0200 Subject: [PATCH 2/2] test: add benchmark tests to `splitProps` and `mergeProps` (WIP) --- packages/solid/test/component.bench.ts | 92 ++++++++++++++ packages/solid/test/component.old.ts | 161 +++++++++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 packages/solid/test/component.bench.ts create mode 100644 packages/solid/test/component.old.ts diff --git a/packages/solid/test/component.bench.ts b/packages/solid/test/component.bench.ts new file mode 100644 index 00000000..87c13b20 --- /dev/null +++ b/packages/solid/test/component.bench.ts @@ -0,0 +1,92 @@ +import { bench } from "vitest"; +import { mergeProps, splitProps } from "../src"; +import * as old from "./component.old" + +const createObject = (prefix: string, amount: number, descriptor: any) => { + const proto: Record = {} + for (let index = 0; index < amount; ++index) + proto[`${prefix}${index}`] = descriptor + return Object.defineProperties({}, proto) +} + +function pickProps(inProps: Record, ...amounts: T[]): { + [K in T]: { names: string[], props: Record } +} { + const result: Record }> = {} + const desc = Object.getOwnPropertyDescriptors(inProps) + const inPropNames = Object.keys(inProps) + for (const amount of amounts) { + const names = inPropNames.slice(0, amount) + const props: Record = {} + for (const name of inPropNames.slice(0, amount)) { + Object.defineProperty(props, name, desc[name]) + } + result[amount] = { names, props } + } + return result +} + +const mergePropsItems: { + name: string + func: Function +}[] = [{ + name: "mergeProps", + func: mergeProps +}, { + name: "oldMergeProps", + func: old.mergeProps +}] + +const splitPropsItems: { + name: string + func: Function +}[] = [{ + name: "splitProps", + func: splitProps +}, { + name: "oldSplitProps", + func: old.splitProps +}] + + +const staticDesc = { value: 1, writable: true, configurable: true, enumerable: true } +const signalDesc = { get() { return 1 }, configurable: true, enumerable: true } +const staticObject = createObject("signal", 100, signalDesc) +const staticProps = pickProps(staticObject, 0, 5, 15, 25, 50, 100) +const iterations = 1000 +const inputs = [ + [0, 15], + [0, 100], + [25, 100], + [50, 100], + [100, 25] +] as const + +for (const input of inputs) { + const [a, b] = input + describe(`splitProps(${a}, ${b})`, () => { + for (const { name, func } of splitPropsItems) + bench(`${name}(${a}, ${b})`, () => { + func(staticProps[a].props, staticProps[b].names) + }, { + iterations + }) + }); + describe(`splitProps(${a}, ${b}, ${b})`, () => { + for (const { name, func } of splitPropsItems) + bench(`${name}(${a}, ${b}, ${b})`, () => { + func(staticProps[a].props, staticProps[b].names, staticProps[b].names) + }, { + iterations + }) + }); + describe(`mergeProps(${a}, ${b})`, () => { + for (const { name, func } of mergePropsItems) + bench(`${name}(${a}, ${b})`, () => { + func(staticProps[a].props, staticProps[b].props) + }, { + iterations: 1000 + }) + }) +} + diff --git a/packages/solid/test/component.old.ts b/packages/solid/test/component.old.ts new file mode 100644 index 00000000..c3896ea9 --- /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; +}