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

isShallowEqual: Account for implicit undefined of second object #16329

Merged
merged 4 commits into from
Jul 1, 2019

Conversation

aduth
Copy link
Member

@aduth aduth commented Jun 27, 2019

Fixes #15905

This pull request seeks to resolve an issue in the implementation of isShallowEqualObjects, where an incorrect value of true may be returned in cases where an explicit value of undefined exists in the first, but not second object passed for comparison.

isShallowEqualObjects( { a: undefined }, { b: 5 } )
// Before: `true`
// After: `false`

Implementation Notes:

See #15905 (comment) and #15905 (comment) for additional context.

The implementation makes assumptions that if the length of keys is the same, the values of each object can be compared strictly (looping only using the keys of the first object). However, in the above example, the value of the a key in the second object is implicitly undefined, but it should report false since the key does not exist in the second object.

I have found the changes proposed here to be the best balance of resolving the issue with minimal performance impact. It relegates the edge case to prefer a trivial undefined comparison of the value in the first object before proceeding with the (relatively) expensive Object#hasOwnProperty check. Technically the hasOwnProperty alone is sufficient to resolve the issue, but I had found in benchmarking that it sacrificed more performance than as written here.

Testing Instructions:

Ensure unit tests pass:

npm run test-unit packages/is-shallow-equal

cc @matthewvalentine

@aduth aduth added [Type] Bug An existing feature does not function as intended [Type] Performance Related to performance efforts [Package] is-shallow-equal /packages/is-shallow-equal labels Jun 27, 2019
@aduth aduth requested a review from pento June 27, 2019 13:30
@aduth aduth requested review from gziolo and youknowriad as code owners June 27, 2019 13:30
@@ -46,9 +46,9 @@ describe( 'isShallowEqual', () => {
expect( isShallowEqual( b, a ) ).toBe( false );
} );

it( 'returns false if b object has different key than a', () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff makes it look like this was changed, but in fact I had removed this test case altogether as considered redundant with the previous case, which calls isShallowEqual with the objects in both possible arguments order arrangements.

See 100ab09

aduth added 2 commits June 27, 2019 15:10
Previous test will account for this by calling `isShallowEqual` in both arguments arrangements
@aduth aduth force-pushed the fix/15905-is-shallow-equal-undefined branch from 6a63bc5 to 0fe5183 Compare June 27, 2019 19:10
Copy link
Member

@jorgefilipecosta jorgefilipecosta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change makes logical sense and worked well on some manual sanity checks via wp.isShallowEqual invokations on the browser console.

@@ -28,7 +28,12 @@ function isShallowEqualObjects( a, b ) {

while ( i < aKeys.length ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated: I'm curious of why while was used here instead of a for (that would be more natural for this loop), was it because "while" proved to be more performant?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've played around a little with loop variations, while seems to be the most performant (in Node, at least).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably also worth pointing out that these are very much engine-specific optimizations that I tend to be generally okay with, but also noting that:

  • The performance characteristics can (and likely will) change over time
  • Ideally they are still equally as performant (or as close to as possible) in other engines
    • In the context of Node at least, optimizing specifically for V8 is reasonable, but in a browser environment, we should be considerate of SpiderMonkey and friends

Copy link
Member

@pento pento left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for funsies, I tried a few different ways of implementing this.

The loop types were about 97% as fast as the current loop. The best I got out of precompiled functions was about 60% of the current loop.

Interestingly, replacing the loop with aKeys.reduce() gave a 10% improvement for the equal case, but a 10% penalty for the unequal case.

Finally, I did find one thing that seemed to give pretty consistent benefits: swapping i++ for ++i. This is kind of surprising, as V8 should be smart enough to make this optimisation itself, but apparently not in this case. 🤷🏻‍♂️

aValue = a[ key ];

if (
( aValue === undefined && ! b.hasOwnProperty( key ) ) ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be useful to add a comment here explaining why the comparison is being done like this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be useful to add a comment here explaining why the comparison is being done like this.

Agree 👍 Added in 741fc5c.

@aduth
Copy link
Member Author

aduth commented Jul 1, 2019

Just for funsies, I tried a few different ways of implementing this.

I'm happy to hear I'm not the only one who finds joy in micro-optimizations 😄

Several variations of precompiled comparison functions

You might enjoy my project turbo-combine-reducers which leverages this very technique (it's used in @wordpress/data).

@aduth aduth merged commit 672bd9e into master Jul 1, 2019
@aduth aduth deleted the fix/15905-is-shallow-equal-undefined branch July 1, 2019 20:44
@youknowriad youknowriad added this to the Gutenberg 6.1 milestone Jul 5, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] is-shallow-equal /packages/is-shallow-equal [Type] Bug An existing feature does not function as intended [Type] Performance Related to performance efforts
Projects
None yet
Development

Successfully merging this pull request may close these issues.

is-shallow-equal: invalid for objects with undefined values
4 participants