feat: add maxSize option to weakMapMemoize to bound cache growth (#635)#761
Open
veksa wants to merge 4 commits into
Open
feat: add maxSize option to weakMapMemoize to bound cache growth (#635)#761veksa wants to merge 4 commits into
veksa wants to merge 4 commits into
Conversation
✅ Deploy Preview for reselect-docs canceled.
|
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: add
maxSizeoption toweakMapMemoizeto bound cache growthFixes #635
Problem
weakMapMemoizebuilds a tree of cache nodes keyed by the identity of eachargument. Object/function arguments are stored in
WeakMaps and are cleaned upby the garbage collector once the argument becomes unreachable. Primitive
arguments, however, are stored in regular strong
Maps.When a selector is called with a long-lived object as the first argument (for
example the Redux state, or a stable value derived by a sub-selector) followed
by ever-changing primitives, every result that was ever computed is retained for
the entire lifetime of that object. Nothing is evicted, so the strong
Mapsgrow without bound — a de-facto memory leak.
A realistic trigger is an endlessly scrolling list backed by a selector such as
(state, from, to) => state.items.slice(from, to): asfrom/tochange withevery scroll event, every intermediate slice stays in memory for as long as the
state object is alive.
Change
Add an opt-in
maxSizeoption toWeakMapMemoizeOptions:now-empty ancestor
Mapbranches are pruned, so the strong primitiveMapsstay bounded and evicted results become eligible for garbage collection.
WeakMap) branches continue to rely on normal garbage collection andare not pruned.
Design notes
maxSizethe cacheis unbounded, exactly as before. The LRU bookkeeping and the extra node
back-references (
parent/map/key) are only maintained whenmaxSizeisset, so the hot path for existing users is unchanged.
weakMapMemoizebounded by default is a separate, breaking decision and is out of scope here.
maxSizeis validated to be a positive integer.clearCache()resets the LRU state.Bounding a
createSelectorcreateSelectormemoizes on two levels:argsMemoizecaches on the rawselector arguments, and
memoizecaches the result function on the extractedinput values. Both hold results keyed by primitive arguments, so both must be
bounded — otherwise
argsMemoizeshort-circuits and the leak remains:Tests
test/weakMapMemoize.memory.spec.tsadds atoBeGarbageCollectedmatcher(garbage collection is exposed via
node --expose-gc, already used by thetestscript) and covers:cached after many distinct calls and is not collected while the state object
remains reachable.
maxSizevalidation.recently-used result surviving while a staler one is evicted.
WeakMapbranch),clearCache()resetting the bounded cache, and coexistence with
resultEqualityCheck.createSelectorviamaxSizeon bothmemoizeOptionsandargsMemoizeOptions.maxSizeand many distinct primitive combinations, an earlyresult is garbage collected while the state object and the selector stay
alive.