Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize mergeProps and splitProps #1710

Merged
merged 2 commits into from
May 31, 2023
Merged

Conversation

juanrgm
Copy link
Contributor

@juanrgm juanrgm commented May 1, 2023

Summary

  1. Object.defineProperties is slower than Object.defineProperty.
  2. Object.getOwnPropertyDescriptors is slower than Object.getOwnPropertyDescriptor.
  3. Object.defineProperty is slower than to assign a value.
  4. Obviously, getter is slower than value access.

How did you test this change?

cd packages/solid
npm t
FILTER=mergeProps npx vitest bench --run
report
 RUN  v0.29.3

 ✓ test/component.bench.ts  (54 tests) 31401ms


 BENCH  Summary

  mergeProps - test/component.bench.ts > mergeProps-static(5, 1)
    1.85x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(5, 1, 2)
    1.96x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(0, 15)
    1.58x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(0, 3, 2)
    1.86x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(0, 100)
    1.67x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(0, 100, 3, 2)
    2.03x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(25, 100)
    1.78x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(50, 100)
    1.86x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(100, 25)
    1.85x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(5, 1)
    1.52x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(5, 1, 2)
    1.65x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(0, 15)
    1.51x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(0, 3, 2)
    1.77x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(0, 100)
    1.61x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(0, 100, 3, 2)
    1.65x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(25, 100)
    1.56x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(50, 100)
    1.55x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(100, 25)
    1.70x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(5, 1)
    1.61x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(5, 1, 2)
    1.69x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(0, 15)
    1.60x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(0, 3, 2)
    1.59x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(0, 100)
    1.55x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(0, 100, 3, 2)
    1.58x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(25, 100)
    1.62x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(50, 100)
    1.62x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(100, 25)
    1.63x faster than oldMergeProps

FILTER=splitProps-mixed npx vitest bench --run
report
 RUN  v0.29.3

 ✓ test/component.bench.ts  (18 tests) 13908ms


 BENCH  Summary

  splitProps - test/component.bench.ts > splitProps-mixed(5, 1)
    2.19x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-mixed(5, 1, 2)
    2.19x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-mixed(0, 15)
    4.71x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-mixed(0, 3, 2)
    6.65x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-mixed(0, 100)
    24.16x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-mixed(0, 100, 3, 2)
    77.38x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-mixed(25, 100)
    2.98x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-mixed(50, 100)
    2.84x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-mixed(100, 25)
    2.92x faster than oldSplitProps
FILTER=splitProps-signal npx vitest bench --run
report

 RUN  v0.29.3

 ✓ test/component.bench.ts  (18 tests) 13717ms


 BENCH  Summary

  splitProps - test/component.bench.ts > splitProps-signal(5, 1)
    1.70x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-signal(5, 1, 2)
    1.87x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-signal(0, 15)
    4.83x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-signal(0, 3, 2)
    6.20x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-signal(0, 100)
    24.08x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-signal(0, 100, 3, 2)
    72.55x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-signal(25, 100)
    2.18x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-signal(50, 100)
    2.04x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-signal(100, 25)
    2.37x faster than oldSplitProps
FILTER=splitProps-static npx vitest bench --run
report

 RUN  v0.29.3

 ✓ test/component.bench.ts  (18 tests) 14521ms


 BENCH  Summary

  splitProps - test/component.bench.ts > splitProps-static(5, 1)
    6.35x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-static(5, 1, 2)
    7.86x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-static(0, 15)
    4.96x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-static(0, 3, 2)
    6.82x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-static(0, 100)
    25.39x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-static(0, 100, 3, 2)
    77.97x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-static(25, 100)
    10.81x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-static(50, 100)
    8.31x faster than oldSplitProps

  splitProps - test/component.bench.ts > splitProps-static(100, 25)
    7.25x faster than oldSplitProps

@changeset-bot
Copy link

changeset-bot bot commented May 1, 2023

🦋 Changeset detected

Latest commit: 6f3c5b3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
solid-js Patch
test-integration Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coveralls
Copy link

coveralls commented May 1, 2023

Pull Request Test Coverage Report for Build 4944594073

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 93 of 93 (100.0%) changed or added relevant lines in 1 file are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.2%) to 94.297%

Totals Coverage Status
Change from base Build 4900547912: 0.2%
Covered Lines: 4142
Relevant Lines: 4323

💛 - Coveralls

@ryansolid
Copy link
Member

Interesting. I didn't realize that, so getting property descriptor n times in a loop is faster than that getting all them all at once in the plural form. That's good to know.

@juanrgm
Copy link
Contributor Author

juanrgm commented May 11, 2023

With the latest changes the tests are successful (I still need to add more tests for splitProps).

Not generating breaking changes means a loss of performance, even so the improvement is noticeable (x2).

In the next merge request you can discuss which improvements to unlock (commented out in the code with // [breaking && performance]), that's when performance can reach x8.

report

 RUN  v0.29.3

 ✓ test/component.bench.ts  (54 tests) 32116ms


 BENCH  Summary

  mergeProps - test/component.bench.ts > mergeProps-static(5, 1)
    7.34x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(5, 1, 2)
    8.00x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(0, 15)
    8.78x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(0, 3, 2)
    6.13x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(0, 100)
    6.98x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(0, 100, 3, 2)
    6.48x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(25, 100)
    5.92x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(50, 100)
    4.73x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-static(100, 25)
    6.28x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(5, 1)
    1.59x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(5, 1, 2)
    1.52x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(0, 15)
    1.46x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(0, 3, 2)
    1.53x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(0, 100)
    1.60x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(0, 100, 3, 2)
    1.55x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(25, 100)
    1.51x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(50, 100)
    1.49x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-signal(100, 25)
    1.51x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(5, 1)
    1.88x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(5, 1, 2)
    2.40x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(0, 15)
    2.25x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(0, 3, 2)
    1.75x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(0, 100)
    2.35x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(0, 100, 3, 2)
    2.22x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(25, 100)
    2.20x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(50, 100)
    2.38x faster than oldMergeProps

  mergeProps - test/component.bench.ts > mergeProps-mixed(100, 25)
    2.27x faster than oldMergeProps

Before merging this branch, I'll remove the component.old.ts file.

component.old.ts
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;
}

@juanrgm juanrgm marked this pull request as ready for review May 17, 2023 06:27
@ryansolid
Copy link
Member

This looks good.. can you expand on the breaking/performance comments. I'm gathering you are saying it would be breaking but allow better performance. And breaking in what sense? Almost all the changes also reduce code so I'm definitely interested.

@juanrgm
Copy link
Contributor Author

juanrgm commented May 27, 2023

Uncommenting the [breaking && performance] lines will break the following tests in mergeProps:

  • keeps references
  • with getter keeps references
  • always returns a new reference
  • always returns getters
  • always returns unwritables

But it will also increase performance.

The conservative option it would be to not break the tests in 1.7.4 and wait for 1.8.0.

That tests are touching the limits and breaking them should not cause problems in the ecosystem because are undocumented features, but I don't know. What do you think?

@ryansolid ryansolid merged commit 194f93c into solidjs:main May 31, 2023
@ryansolid
Copy link
Member

ryansolid commented May 31, 2023

Thanks.

Yeah some of these might be fine now. A lot of the tests were written a certain way for convenience not because we cared about these things. But you are right it is safer not to introduce the changes to 1.8. Let me review them.

@ryansolid
Copy link
Member

On review I think the only intended behavior here was that a single argument clones. And possibly that prop objects could be overridden. But all the identity stuff for straight values was not intended. So I think that will be safe for 1.7 release. We can review the others later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants