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

CPLAT-9720 Connect perf optimizations #462

Merged
merged 26 commits into from
Jul 15, 2020
Merged

CPLAT-9720 Connect perf optimizations #462

merged 26 commits into from
Jul 15, 2020

Conversation

greglittlefield-wf
Copy link
Contributor

@greglittlefield-wf greglittlefield-wf commented Feb 17, 2020

Motivation

The current implementations of connect / connectFlux by default call into Dart code for almost all of the supported callbacks, and perform shallow operator== equality checks whereas the JS defaults to ===, which is equivalent to Dart's identical operator.

There's not really a good reason for this default, and aligning it to be closer with the JS behavior would reduce performance hits from:

  • Calling into Dart code
  • Wrapping JS maps in JsBackedMaps and typed props classes
  • Calling potentially inefficient operator== implementations on prop values and state objects

Changes

Instead of implementing defaults for these callbacks with Dart wrappers, use the JS defaults for:

  • (changed from Dart operator== to JS ===)
    • areStatesEqual
  • (changed from shallow props map equality using Dart operator== for values, to shallow props map equality using JS ===)
    • areOwnPropsEqual
    • areStatePropsEqual
    • areMergedPropsEqual

Other changes I made to aid in debugging test failures:

  • Eliminate most store global variables
  • Update some test expectations to have better failures (mostly around callback calls)
  • Remote expect calls from lifecycle callbacks, since they'd fail silently due to not being run in the test zone

Release Notes

Review

See CONTRIBUTING.md for more details on review types (+1 / QA +1 / +10) and code review process.

Please review:

QA Checklist

  • Tests were updated and provide good coverage of the changeset and other affected code
  • Manual testing was performed if needed

Merge Checklist

While we perform many automated checks before auto-merging, some manual checks are needed:

  • A Client Platform member has reviewed these changes
  • There are no unaddressed comments - this check can be automated if reviewers use the "Request Changes" feature
  • For release PRs - Version metadata in Rosie comment is correct

@aviary-wf
Copy link

Security Insights

No security relevant content was detected by automated scans.

Action Items

  • Review PR for security impact; comment "security review required" if needed or unsure
  • Verify aviary.yaml coverage of security relevant code

Questions or Comments? Reach out on Slack: #support-infosec.

@greglittlefield-wf greglittlefield-wf marked this pull request as ready for review February 18, 2020 00:02
@rmconsole2-wf rmconsole2-wf changed the title Connect perf optimizations CPLAT-9720 Connect perf optimizations Feb 18, 2020
Comment on lines 377 to 393
const updateMethodCalls = [
'mapStateToProps',
'areStatePropsEqual',
'mapStateToProps',
'areStatePropsEqual'
];
Copy link
Contributor

Choose a reason for hiding this comment

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

It's dope that the update cycle of connectFlux is more efficient now! However, now connectFlux components and connect components (with areStatesEqual set to false) don't behave the same way. Is that expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good question. I meant to ask you: why, in this case, did we previously expect double the calls?

However, now connectFlux components and connect components (with areStatesEqual set to false) don't behave the same way.

I thought I understood what you were talking about at first but now I'm having trouble wrapping my head around it haha. Can we talk through this offline?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also just following up on this - if it hasn't been a priority yet and you haven't had a chance to dig in, I'd be happy to as well, just curious if you already did any discovery there :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I have no idea haha

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Omg I finally figured it out; it's because of the memoizedWrapInteropValue.

In Redux, store.state is updated, resulting in a new object every time, which busts some internal memoization caches, causing extra calls. In Flux, store.state is always the same object.

See more info here: c58c3dc

Copy link
Contributor

Choose a reason for hiding this comment

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

Niiice, that does make sense! Thanks for following up on that!!

test/over_react_redux/connect_test.dart Show resolved Hide resolved
@greglittlefield-wf greglittlefield-wf changed the base branch from CPLAT-8693-connect-for-w_flux to master February 25, 2020 16:11
Copy link
Contributor

@joebingham-wk joebingham-wk left a comment

Choose a reason for hiding this comment

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

Just made another pass at the new comments - they all looked good! Just bumping a couple original points

Comment on lines 377 to 393
const updateMethodCalls = [
'mapStateToProps',
'areStatePropsEqual',
'mapStateToProps',
'areStatePropsEqual'
];
Copy link
Contributor

Choose a reason for hiding this comment

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

Also just following up on this - if it hasn't been a priority yet and you haven't had a chance to dig in, I'd be happy to as well, just curious if you already did any discovery there :)

@greglittlefield-wf
Copy link
Contributor Author

I can't believe I forgot about this... @joebingham-wk would you mind making another pass when you have a sec?

@joebingham-wk
Copy link
Contributor

Yee @greglittlefield-wf! There's some stuff on my plate this morning but I'll definitely make another pass today

@greglittlefield-wf
Copy link
Contributor Author

Thanks! No rush btw. This PR has been hanging out for four months; it can wait a couple days 😉 .

const propHasher = CollectionLengthHasher();
bool areStatePropsEqualWrapper(TProps nextProps, TProps prevProps) {
final result = defaultAreStatePropsEqual(nextProps, prevProps);
// In dev mode, if areStatePropsEqual is not specified, pass in a version
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This block of changes are best reviewed with whitespace-agnostic diff

Copy link
Contributor

@joebingham-wk joebingham-wk left a comment

Choose a reason for hiding this comment

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

Looks great to me! It'll be awesome to have that memoization in there. I had one tiny real question and then another just curiosity question.

I looked at the failing test - it's a ReduxMultiProvider test that passes locally. I'm wondering if I didn't await correctly and the browser events aren't happening fast enough? Regardless, they do pass locally so it definitely seems like a lame timing thing and not actually related to your changes

lib/src/over_react_redux/over_react_flux.dart Outdated Show resolved Hide resolved
lib/src/over_react_redux/over_react_redux.dart Outdated Show resolved Hide resolved
Comment on lines -449 to +462
expect(methodsCalled.map((methodObj) => methodObj['called']),
updateMethodCalls);
expect(
methodsCalled,
expectedUpdateMethodCalls
.map((expected) => containsPair('called', expected))
.toList());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This pattern results in the whole calls being included in the test failure messages, which provides a lot of helpful contextual info.

Before:

  Expected: ['mapStateToProps',  'areStatePropsEqual']
    Actual: ['mapStateToProps', 'areStatePropsEqual', 'mapStateToProps', 'areStatePropsEqual']

With these changes:

  Expected: [
              <contains pair 'called' => 'mapStateToProps'>,
              <contains pair 'called' => 'areStatePropsEqual'>
            ]
    Actual: [
              {
                'called': 'mapStateToProps',
                'state': CounterState:CounterState:{count: 1, name: Counter}
              },
              {
                'called': 'areStatePropsEqual',
                'prev': {'ConnectFluxCounterProps.currentCount': 0},
                'next': {'ConnectFluxCounterProps.currentCount': 1}
              },
              {
                'called': 'mapStateToProps',
                'state': CounterState:CounterState:{count: 1, name: Counter}
              },
              {
                'called': 'areStatePropsEqual',
                'prev': {'ConnectFluxCounterProps.currentCount': 1},
                'next': {'ConnectFluxCounterProps.currentCount': 1}
              }
            ]

@@ -691,7 +722,53 @@ main() {
},
};

testParameterCases(testCases);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Inlined this so it could access closure variables; it was only being called on this line, anyways.

Copy link
Contributor

@aaronlademann-wf aaronlademann-wf left a comment

Choose a reason for hiding this comment

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

Mostly minor stuff. This is gonna be awesome!

// Use a const list so that empty children prop values are always identical
// in the JS props, resulting in JS libraries (e.g., react-redux) and Dart code alike
// not marking props as having changed as a result of rerendering the ReactElement with a new list.
childArguments = const [];
Copy link
Contributor

Choose a reason for hiding this comment

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

Boom

test/over_react/util/equality_test.dart Outdated Show resolved Hide resolved
test/over_react/util/equality_test.dart Outdated Show resolved Hide resolved
test/over_react_redux/connect_flux_test.dart Outdated Show resolved Hide resolved
lib/src/util/equality.dart Show resolved Hide resolved
Copy link
Contributor

@joebingham-wk joebingham-wk left a comment

Choose a reason for hiding this comment

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

Looks great to me! +1

Copy link
Contributor

@aaronlademann-wf aaronlademann-wf left a comment

Choose a reason for hiding this comment

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

+1

@joebingham-wk
Copy link
Contributor

  • Dope test coverage
  • Consumer tests pass

QA +1

@joebingham-wk
Copy link
Contributor

@Workiva/release-management-p

Copy link

@rmconsole-wf rmconsole-wf left a comment

Choose a reason for hiding this comment

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

+1 from RM

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

Successfully merging this pull request may close these issues.

8 participants