-
Notifications
You must be signed in to change notification settings - Fork 2k
Sites: improve memoization of the getSites selector #19760
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
Conversation
|
|
nope! I have the issue documented pretty well here: Also relevant is @edo-ran's attempt to invalidate the cache on a per-key basis: #12712. This solution makes a lot of sense to me. Would be nice if we could generalize it too |
|
Wooooooooooo |
client/state/sites/selectors.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shoud update the type too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
client/state/sites/selectors.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be above the docblock?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can also give it its own JSDOC block comment indicating its use and contents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docblock rearranged, added own docblock for getSiteCache
That was actually the first thing I tried to do 🙂 But as @samouri said -- the memoization didn't work, because the cache is cleared every time the |
|
I haven't been able to repro the slow I placed a console.time('sync') before and a console.timeEnd('sync'); and the times are relatively similar. Could this be due to that I only have 18 sites in my sidebar and the issue only crops up with more? |
|
For context I have 345 sites, with 315 of them hidden. |
In the case of I have 54 sites and the number of rerenders is ten times smaller after patching. If Andy's profile shows 16 seconds spent in Given the quadratic growth, it sounds plausible that 18 sites are not enough to show a big difference. |
I've taken a stab at a generalized solution by adding more fine grained cache-invalidation capabilities to |
client/state/sites/selectors.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we still need the comment about support for non-id site retrieval? Or should we rename the param siteId to siteIdOrSiteSlug
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renaming the param to siteIdOrSlug
client/state/sites/selectors.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like the usage was there before, but do these selectors work properly if we pass through a siteSlug vs a siteId?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, that's a pre-existing bug in the selector. I'll fix it before merging.
client/state/sites/selectors.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So tracing this down the state.sites.items array holds a hard reference to our rawSites. So in theory we should be able to GC weak refs in the map after state.sites.items is updated again, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the rawSite will be collected but the cache of it won't. as long as the actual rawSite reference is still in memory (such as in state.sites.items) then we'll be able to retrieve the value here. once it's no longer in use outside of this place it can be freed. at that point, we'll love our ability to retrieve the cached value; it's not clear to me if that value will leak or if the engine will free it as well
clearly I don't know what I'm doing here, but I tried to see if the weak map freed its local objects. in this ill-performed experiment i'm not sure what it indicates. but, the data is there for any more informed eyes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The WeakMap entry holds a weak reference to the key and a strong reference to the value. If anyone is holding a strong reference to the key, they can use it to access the value in the map.
If a reference to the key no longer exists, the entry becomes inaccessible and is removed from the map during the GC. During that GC, the strong reference to the value is removed. If it was the last reference to it, the value also becomes eligible for GC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
During that GC, the strong reference to the value is removed. If it was the last reference to it, the value also becomes eligible for GC.
This is what seems most expected to me, but I couldn't find the documentation for it; seems like it should be obvious.
gwwar
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
client/state/sites/selectors.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how odd, WeakMap has no clear…
client/state/sites/selectors.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe it's worth mentioning here at the top in the comment that this function depends on state in the selector module
samouri
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't able to repro the slow case... but i can confirm it works and didn't get slower!
😆 @samouri just test more nux flows! |
dmsnell
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wish I had more cycles to dedicate to reviewing this but I don't ¯\_(ツ)_/¯ - don't let me hold you back.
A selector implemented using `createSelector` that has a `state.sites.items` dependenant will clear the memoization cache on every update of `state.sites.items`, even if only one site out of hundreds got updated and the rest are identical objects. That's inefficient. On a repeated `getSite( state, id )`, new site object will be computed, not `===`-identical to the old one, and many components that depend on it will do an unneeded rerender. This patch rewrites the `getSite` selector to memoize a site object in a `WeakMap` cache until the source `rawSite` object really changes. The selector no longer depends on `state.currentUser.capabilities`, as that is a structure that fully depends on `state.sites.items` and is populated and changed only by actions that change `state.sites.items` at the same time. There are awesome performance benefits when doing `SitesList.sync` during initial load. Executing the function is 3 times faster (1100ms to 350ms) and the number of rerenders is more than 10 times smaller (4700 to 380). Fixes a big part of #19446.
2d4c534 to
6d299e3
Compare
|
I rebased the PR and fixed a few documentation and naming nits reported by reviewers. I also fixed the bug reported by @gwwar where the computed attributes wouldn't be computed when getting the site by slug -- the slug would be passed to the Going to merge now. |
|
@jsnajdr I'm not sure if it's the same issue, or a different one causing a similar symptom, but I'm still seeing a massive slowdown/unresponsiveness in the UI. I've found a way to make it easily reproducible. A caveat here is that I can only spot it on an account with a lot of sites (over 400). I think on accounts with less sites it's maybe such a brief hang that I don't notice it while clicking around:
I guess switching environment is clearing something in localstorage(?) that's requiring whatever is causing the slowdown to happen again. Interestingly, trying to reproduce this by just clearing localstorage manually was not successful. |
If it's a storage issue, you'd need to clear both localStorage and indexed DB. However, there's a decent chance the problem is in how we merge the sites-list we get from storage and the one we get from the api... |
|
#19735 might be related |

A selector implemented using
createSelectorthat has astate.sites.itemsdependenant will clear the memoization cache on every update ofstate.sites.items, even if only one site out of hundreds got updated and the rest are identical objects. That's inefficient. On a repeatedgetSite( state, id ), new site object will be computed, not===-identical to the old one, and many components that depend on it will do an unneeded rerender.This patch rewrites the
getSiteselector to memoize a site object in aWeakMapcache until the sourcerawSiteobject really changes.The selector no longer depends on
state.currentUser.capabilities, as that is a structure that fully depends onstate.sites.itemsand is populated and changed only by actions that changestate.sites.itemsat the same time.There are awesome performance benefits when doing
SitesList.syncduring initial load. Executing the function is 3 times faster (1100ms to 350ms) and the number of rerenders is more than 10 times smaller (4700 to 380).Fixes a big part of #19446.
How to test:
console.logstatements with timings around thethis.sync( data );in thefetchmethod inclient/lib/sites-list/list.js.