Skip to content

Commit

Permalink
test: add benchmark tests to splitProps and mergeProps
Browse files Browse the repository at this point in the history
  • Loading branch information
juanrgm committed May 11, 2023
1 parent a621605 commit c900553
Show file tree
Hide file tree
Showing 2 changed files with 320 additions and 0 deletions.
159 changes: 159 additions & 0 deletions packages/solid/test/component.bench.ts
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);
});
}

161 changes: 161 additions & 0 deletions packages/solid/test/component.old.ts
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;
}

0 comments on commit c900553

Please sign in to comment.