Skip to content

Conversation

samouri
Copy link
Contributor

@samouri samouri commented Jan 12, 2018

exploring to see if treeSelect can bring a win to the stats modules.

before
screen shot 2018-01-12 at 4 27 44 pm

after
screen shot 2018-01-12 at 5 19 14 pm

@samouri samouri changed the title Stats: use treeSelect for selectors Stats: use treeSelect for stats-list item's selectors Jan 12, 2018
@dmsnell
Copy link
Member

dmsnell commented Jan 12, 2018

Clear win, but we should also track memory usage too and make sure nothing crazy happens with it.

@samouri
Copy link
Contributor Author

samouri commented Jan 12, 2018

yep, this one in particular is worst case actually. many of the selectors are grabbing primitives

const serializedQuery = getSerializedStatsQuery( query );
return get( state.stats.lists.items, [ siteId, statType, serializedQuery ], null );
}

function withSerializedQuery( selector, argNum ) {
return ( ...args ) => {
const newArgs = [ ...args ];
Copy link
Member

Choose a reason for hiding this comment

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

You don't need to clone the args array here. You can directly mutate it.

By the way, what if the selectors required an already serialized query passed as an argument? That would avoid transforming the argument with this somewhat tricky function. And in the case of PostTrends, its mapStateToProps would do the serialization only once, not three times.

Copy link
Contributor Author

@samouri samouri Jan 15, 2018

Choose a reason for hiding this comment

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

what if the selectors required an already serialized query

Definitely what i was thinking too! This PR is not in anything close to a mergeable state -- its full of hacks and lacks any polish. It was just a quick test to see if I could address the rerender issue by modifying the selectors + using treeSelect.

streakData: getSiteStatsForQuery( state, siteId, 'statsStreak', query ),
} ),
( state, siteId, query ) => {
const serializedQuery = getSerializedStatsQuery( query );
Copy link
Member

Choose a reason for hiding this comment

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

Isn't the query already serialized here?

} ),
( state, siteId, query ) => {
const serializedQuery = getSerializedStatsQuery( query );
return [ siteId, 'statsStreak', serializedQuery ].join();
Copy link
Member

Choose a reason for hiding this comment

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

Why the extra 'statsStreak' string here? And does createCachedSelector even have a third getKey arg?

@jsnajdr jsnajdr force-pushed the update/treeSelect/stats branch from e7c726b to 14a76f1 Compare January 17, 2018 23:09
@jsnajdr jsnajdr requested review from dmsnell and lamosty January 17, 2018 23:14
@jsnajdr jsnajdr added [Feature] Stats Everything related to our analytics product at /stats/ [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. and removed [Status] In Progress labels Jan 17, 2018
@jsnajdr
Copy link
Member

jsnajdr commented Jan 17, 2018

This PR is ready for review now. It depends on the treeSelect improvements in #21625 and temporarily includes the commit.

Read the commit messages to learn more about the changes.

How to test:

  1. Have React devtool open and check the "Highlight updates" checkbox.
  2. Go to /stats/day/your.blog and to /stats/insights/your.blog
  3. Trigger some irrelevant Redux state update, e.g., open and close the notifications

Before this patch: The stats chart and the stats module boxes on /stats/day are rerendered on each dispatch, and so is the Github-eque "Post Activity" chart on /stats/insights

After this patch: These rerenders disappeared.

};

export default connect( mapStateToProps, null, null, {
areStatePropsEqual: compareProps( { deep: [ 'query' ] } ),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

very cool -- it keeps the ugliness out of the component

export const getSiteStatsMaxPostsByDay = treeSelect(
( state, siteId, query ) => [ getSiteStatsPostStreakData( state, siteId, query ) ],
( [ postStreakData ] ) =>
reduce( postStreakData, ( max, count ) => ( count > max ? count : max ), 0 ) || null,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

nice

return [ siteId, 'statsStreakMax', serializedQuery ].join();
export const getSiteStatsTotalPostsForStreakQuery = treeSelect(
( state, siteId, query ) => [ getSiteStatsPostStreakData( state, siteId, query ) ],
( [ postStreakData ] ) => reduce( postStreakData, ( sum, posts ) => sum + posts, 0 ),
Copy link
Contributor Author

@samouri samouri Jan 17, 2018

Choose a reason for hiding this comment

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

FFTI: we could even use the sum function here

Copy link
Member

Choose a reason for hiding this comment

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

I'm going to remove the "total posts" and "max posts" selectors in a follow-up PR, as they are not really needed. I just didn't want to make this PR more complicated. And I already finished their treeSelect implementation when I realized they can be removed :(

getSiteStatsMaxPostsByDay.memoizedSelector.cache.clear();
getSiteStatsTotalPostsForStreakQuery.memoizedSelector.cache.clear();
getSiteStatsNormalizedData.memoizedSelector.cache.clear();
getSiteStatsPostStreakData.clearCache();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think you could use jest.resetModules() here instead of clearCache.

Copy link
Member

Choose a reason for hiding this comment

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

This is an awesome idea, but I'm afraid it requires nontrivial effort to make it work. Instead of importing all the tested selectors once at the top of the test file, we'd have to require them in each beforeEach call. Otherwise, the jest.resetModules() call doesn't have the desired effect.

It's definitely worth exploring in a standalone PR -- there is a lot of clearCache-style calls in our tests and they could be all avoided.

The previous implementations rarely memoized anything, because they
busted the `createSelector` cache each time they were called with
different arguments. `treeSelect` can memoize much better in situations
like these.
… components

These components have `query` props that are objects always created from scratch,
and therefore not identical, but they are usually deep-equal. Use `compareProps`
to implement proper comparison and avoid wasted rerenders.
@jsnajdr jsnajdr force-pushed the update/treeSelect/stats branch from 14a76f1 to 464038e Compare January 18, 2018 10:29
@jsnajdr jsnajdr merged commit 20f4df4 into master Jan 18, 2018
@jsnajdr jsnajdr deleted the update/treeSelect/stats branch January 18, 2018 10:37
jsnajdr added a commit that referenced this pull request Jan 18, 2018
It's only used in `PostTrends.shouldComponentUpdate` as an optimization attempt
to reduce rerenders. The changes in #21507 achieve that goal better, though: by
selector memoization and deep comparison of the `query` prop.

The `shouldComponentUpdate` and the associated selector is no longer needed. This
patch even solves a bug where the component wouldn't get rerendered when the
`state.canScrollLeft` and `state.canScrollRight` flags changed, and the scrolling
arrows on the "Posting Activity" chart wouldn't get the `is-active` CSS class.
jsnajdr added a commit that referenced this pull request Jan 18, 2018
It's only used in `PostTrends.shouldComponentUpdate` as an optimization attempt
to reduce rerenders. The changes in #21507 achieve that goal better, though: by
selector memoization and deep comparison of the `query` prop.

The `shouldComponentUpdate` and the associated selector is no longer needed. This
patch even solves a bug where the component wouldn't get rerendered when the
`state.canScrollLeft` and `state.canScrollRight` flags changed, and the scrolling
arrows on the "Posting Activity" chart wouldn't get the `is-active` CSS class.
@sirreal sirreal removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Mar 27, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Stats Everything related to our analytics product at /stats/ [Type] Performance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants