-
-
Notifications
You must be signed in to change notification settings - Fork 883
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: add benchmark tests to
splitProps
and mergeProps
- Loading branch information
Showing
2 changed files
with
320 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
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 cache = new Map<string, any>() | ||
|
||
const createObject = ( | ||
name: string, | ||
amount: number, | ||
desc: (index: number) => PropertyDescriptor | ||
) => { | ||
const key = `${name}-${amount}` | ||
const cached = cache.get(key) | ||
if (cached) return cached | ||
const proto: Record<string, any> = {}; | ||
for (let index = 0; index < amount; ++index) | ||
proto[`${name}${index}`] = desc(index); | ||
const result = Object.defineProperties({}, proto) as Record<string, any>; | ||
cache.set(key, result) | ||
return result | ||
}; | ||
|
||
const keys = (o: Record<string, any>) => 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: { | ||
name: string; | ||
/** | ||
* `vitest bench -t "FILTER"` does not work | ||
*/ | ||
filter?: RegExp; | ||
subjects: { | ||
name: string; | ||
func: T; | ||
}[]; | ||
generator: Record<string, G>; | ||
inputs: (generator: G) => Record<string, Parameters<T>>; | ||
}) { | ||
const tests: Test[] = []; | ||
for (const generatorName in options.generator) { | ||
const generator = options.generator[generatorName]; | ||
const inputs = options.inputs(generator); | ||
for (const inputName in inputs) { | ||
const args = inputs[inputName]; | ||
const test: Test = { | ||
title: `${options.name}-${generatorName}${inputName}`, | ||
benchs: [], | ||
}; | ||
if (options.filter && !options.filter.exec(test.title)) continue; | ||
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<string, any>[]; | ||
|
||
const generator = { | ||
static: (amount: number) => createObject("static", amount, () => staticDesc), | ||
signal: (amount: number) => createObject("signal", amount, () => signalDesc), | ||
mixed: (amount: number) => | ||
createObject("mixed", amount, (v) => (v % 2 ? staticDesc : signalDesc)), | ||
} as const; | ||
|
||
const filter = new RegExp(process.env.FILTER || ".+"); | ||
|
||
const splitPropsTests = createTest({ | ||
filter, | ||
name: "splitProps", | ||
subjects: [ | ||
{ | ||
name: "splitProps", | ||
func: splitProps as SplitProps, | ||
}, | ||
{ | ||
name: "oldSplitProps", | ||
func: old.splitProps as SplitProps, | ||
}, | ||
], | ||
generator, | ||
inputs: (g) => ({ | ||
"(5, 1)": [g(5), keys(g(1))], | ||
"(5, 1, 2)": [g(5), keys(g(1)), keys(g(2))], | ||
"(0, 15)": [g(0), keys(g(15))], | ||
"(0, 3, 2)": [g(0), keys(g(3)), keys(g(2))], | ||
"(0, 100)": [g(0), keys(g(100))], | ||
"(0, 100, 3, 2)": [g(0), keys(g(100)), keys(g(3)), keys(g(2))], | ||
"(25, 100)": [g(25), keys(g(100))], | ||
"(50, 100)": [g(50), keys(g(100))], | ||
"(100, 25)": [g(100), keys(g(25))], | ||
}), | ||
}); | ||
|
||
const mergePropsTest = createTest({ | ||
name: "mergeProps", | ||
filter, | ||
subjects: [ | ||
{ | ||
name: "mergeProps", | ||
func: mergeProps, | ||
}, | ||
{ | ||
name: "oldMergeProps", | ||
func: old.mergeProps, | ||
}, | ||
], | ||
generator, | ||
inputs: (g) => ({ | ||
"(5, 1)": [g(5), g(1)], | ||
"(5, 1, 2)": [g(5), g(1), g(2)], | ||
"(0, 15)": [g(0), g(15)], | ||
"(0, 3, 2)": [g(0), g(3), g(2)], | ||
"(0, 100)": [g(0), g(100)], | ||
"(0, 100, 3, 2)": [g(0), g(100), g(3), g(2)], | ||
"(25, 100)": [g(25), g(100)], | ||
"(50, 100)": [g(50), g(100)], | ||
"(100, 25)": [g(100), g(25)], | ||
}), | ||
}); | ||
|
||
for (const test of splitPropsTests) { | ||
describe(test.title, () => { | ||
for (const { title, func } of test.benchs) bench(title, func); | ||
}); | ||
} | ||
|
||
for (const test of mergePropsTest) { | ||
describe(test.title, () => { | ||
for (const { title, func } of test.benchs) bench(title, func); | ||
}); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<any, any>, | ||
K extends [readonly (keyof T)[], ...(readonly (keyof T)[])[]] | ||
>(props: T, ...keys: K): SplitProps<T, K> { | ||
const blocked = new Set<keyof T>(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<T, K>; | ||
} | ||
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<T, K>; | ||
} | ||
|
||
export function mergeProps<T extends unknown[]>(...sources: T): MergeProps<T> { | ||
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<unknown>)) : 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<T>; | ||
} | ||
const target = {} as MergeProps<T>; | ||
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; | ||
} |