-
Notifications
You must be signed in to change notification settings - Fork 3.7k
General purpose memoization tool #43538
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
Closed
kacper-mikolajczak
wants to merge
15
commits into
Expensify:main
from
kacper-mikolajczak:refactor/memoization-api-poc
Closed
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
6842ae5
install moize
kacper-mikolajczak dd8d26e
add types
kacper-mikolajczak fd8cd35
add array cache
kacper-mikolajczak b3c337a
add map cache
kacper-mikolajczak b316a5a
WIP fix stuff and add builders
kacper-mikolajczak f834289
add NumberFormatUtils example
kacper-mikolajczak 6a19495
add memo fn predicate
kacper-mikolajczak e1821cb
swap lodash usages
kacper-mikolajczak e467abc
remove @ts-expect-error
kacper-mikolajczak 8bce219
fix arrayCache snapshot method
kacper-mikolajczak f03810b
fix map snapshot method
kacper-mikolajczak 2e209bf
change default max size to infinity
kacper-mikolajczak f8974e2
fix array Cache snapshot types
kacper-mikolajczak c6c62b9
add CacheMode description
kacper-mikolajczak 6af5d57
memoize description
kacper-mikolajczak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,18 @@ | ||
| import type {ValueOf} from 'type-fest'; | ||
| import type CONST from '@src/CONST'; | ||
| import memoize from './memoize'; | ||
|
|
||
| const numberFormatter = memoize(Intl.NumberFormat, { | ||
| equalityCheck: 'deep', | ||
| maxSize: 10, | ||
| }); | ||
|
|
||
| function format(locale: ValueOf<typeof CONST.LOCALES>, number: number, options?: Intl.NumberFormatOptions): string { | ||
| return new Intl.NumberFormat(locale, options).format(number); | ||
| return numberFormatter(locale, options).format(number); | ||
| } | ||
|
|
||
| function formatToParts(locale: ValueOf<typeof CONST.LOCALES>, number: number, options?: Intl.NumberFormatOptions): Intl.NumberFormatPart[] { | ||
| return new Intl.NumberFormat(locale, options).formatToParts(number); | ||
| return numberFormatter(locale, options).formatToParts(number); | ||
| } | ||
|
|
||
| export {format, formatToParts}; |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import moize from 'moize'; | ||
|
|
||
| type ArrayCacheConfig = { | ||
| cacheMode: 'array'; | ||
| equalityCheck: 'deep' | 'shallow'; | ||
| maxSize: number; | ||
| }; | ||
|
|
||
| function arrayCacheBuilder<Args, RT>(f: (...args: Args[]) => RT, c: ArrayCacheConfig) { | ||
| // If cacheMode is array we proceed with moize | ||
| const moizeConfig = { | ||
| isDeepEqual: c.equalityCheck === 'deep', | ||
| isShallowEqual: c.equalityCheck === 'shallow', | ||
| maxSize: c.maxSize, | ||
| }; | ||
|
|
||
| const moized = moize(f, moizeConfig); | ||
|
|
||
| moized.snapshot = () => { | ||
| const keys = moized.cache.snapshot.keys; | ||
| const values = moized.cache.snapshot.values as Args[]; | ||
|
|
||
| if (!keys?.length) { | ||
| return []; | ||
| } | ||
|
|
||
| return keys.map((k, i) => [k, values[i]]); | ||
| }; | ||
|
|
||
| return moized; | ||
| } | ||
|
|
||
| export default arrayCacheBuilder; |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import ArrayCacheBuilder from './array'; | ||
| import MapCacheBuilder from './map'; | ||
|
|
||
| export {ArrayCacheBuilder, MapCacheBuilder}; |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| type MapCacheConfig = { | ||
| cacheMode: 'map'; | ||
| equalityCheck: 'deep'; | ||
| maxSize: number; | ||
| }; | ||
|
|
||
| function mapCacheBuilder<Fn extends () => unknown, Key = Parameters<Fn>, Val = ReturnType<Fn>>(f: Fn, c: MapCacheConfig) { | ||
| const cache = new Map<string, Val>(); | ||
|
|
||
| // FIXME - how to type this? | ||
| function memoized(...args: Key) { | ||
| // FIXME - this undermines the effect of using map as a cache struct - serialization might be causing perf and stability issues even with fast map lookup: | ||
| // - the added overhead is dependent on size of arguments provided not cache size so that's a plus | ||
| // - is serialization by JSON.stringify safe to use? How about complex objects, not serializable structs and order of parameters? | ||
| const key = JSON.stringify(args); | ||
|
|
||
| if (cache.has(key)) { | ||
| return cache.get(key); | ||
| } | ||
|
|
||
| const result = f(...args) as Val; | ||
| cache.set(key, result); | ||
|
|
||
| if (cache.size > c.maxSize) { | ||
| // Maps should keep insertion order, so we can safely delete the first key | ||
| cache.delete(cache.keys().next().value as string); | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| memoized.set = (args: Key, value: Val) => { | ||
| const key = JSON.stringify(args); | ||
| cache.set(key, value); | ||
| }; | ||
|
|
||
| memoized.get = (args: Key) => { | ||
| const key = JSON.stringify(args); | ||
| cache.get(key); | ||
| }; | ||
|
|
||
| memoized.clear = () => { | ||
| cache.clear(); | ||
| }; | ||
|
|
||
| memoized.snapshot = () => Array.from(cache.entries()); | ||
|
|
||
| return memoized; | ||
| } | ||
|
|
||
| export default mapCacheBuilder; |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import memoize from './memoize'; | ||
|
|
||
| export default memoize; |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import * as cacheBuilders from './cacheBuilder'; | ||
| import type {ExternalMemoizeConfig, MemoizeConfig, MemoizedInterface, MemoizeFnPredicate} from './types'; | ||
|
|
||
| const DEFAULT_CONFIG = { | ||
| cacheMode: 'array', | ||
| equalityCheck: 'shallow', | ||
| maxSize: Infinity, | ||
| } as const; | ||
|
|
||
| /** | ||
| * Utility which returns a memoized version of a function. If same arguments are passed to the memoized function, its return value will be retrieved from cache instead of calculating it from scratch. | ||
| * | ||
| * Possible options for cache structure: | ||
| * - `map` - uses `Map` primitive under the hood (only `deep` comparison of keys possible) | ||
| * - `array` - uses `Array` primitive under the hood (slower lookup but enables `shallow` comparison) | ||
| * @param f - Function you want to memoize | ||
| * @param config - See `MemoizeConfig` type for more details | ||
| * @returns Memoized function. Just use it and your function input should be memoized. Static methods added for direct cache manipulation. See `MemoizedInterface` type for more details. | ||
| */ | ||
| function memoize<Fn extends MemoizeFnPredicate>(f: Fn, config: ExternalMemoizeConfig = DEFAULT_CONFIG): MemoizedInterface<Fn> { | ||
| // Make sure default values are provided | ||
| const preparedConfig = (config !== DEFAULT_CONFIG ? {...DEFAULT_CONFIG, ...config} : config) as MemoizeConfig; | ||
|
|
||
| switch (preparedConfig.cacheMode) { | ||
| case 'map': | ||
| // FIXME - How to properly type this? | ||
| return cacheBuilders.MapCacheBuilder(f, preparedConfig); | ||
|
|
||
| default: | ||
| // FIXME - How to properly type this? | ||
| return cacheBuilders.ArrayCacheBuilder(f, preparedConfig); | ||
| } | ||
| } | ||
|
|
||
| export default memoize; |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| /** | ||
| * Internal cache structure type. | ||
| * - `array` - it operates on an array, which enables both `shallow` and `deep` comparison of keys. Check `ArrayCacheConfig` for more details. | ||
| * - `map` - it operates on a map, which enables faster lookup but requires `deep` comparison of keys only. Check `MapCacheConfig` for more details. | ||
| */ | ||
| type CacheMode = 'array' | 'map'; | ||
|
|
||
| type MapCacheConfig = { | ||
| cacheMode: 'map'; | ||
| equalityCheck: 'deep'; | ||
| }; | ||
|
|
||
| type ArrayCacheConfig = { | ||
| cacheMode: 'array'; | ||
| equalityCheck: 'shallow' | 'deep'; | ||
| }; | ||
|
|
||
| type CacheConfig = MapCacheConfig | ArrayCacheConfig; | ||
|
|
||
| type MemoizeConfig = { | ||
| maxSize: number; | ||
| } & CacheConfig; | ||
|
|
||
| type ExternalMemoizeConfig = Partial<MemoizeConfig>; | ||
|
|
||
| // Anys are needed as this is only a predicate passed to extends clause | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| type MemoizeFnPredicate = (...args: any[]) => any; | ||
|
|
||
| type CacheBuilder = <Fn extends MemoizeFnPredicate, C extends MemoizeConfig>(f: Fn, config: C) => MemoizedInterface<Fn>; | ||
|
|
||
| type MemoizedInterface<Fn extends MemoizeFnPredicate, Key = Parameters<Fn>, Val = ReturnType<Fn>> = Fn & { | ||
| get: (key: Key) => Val | undefined; | ||
| set: (key: Key, value: Val) => void; | ||
| clear: () => void; | ||
| snapshot: () => Array<[Key, Val]>; | ||
| }; | ||
|
|
||
| export type {MemoizeConfig, ExternalMemoizeConfig, CacheMode, MemoizedInterface, CacheBuilder, MemoizeFnPredicate}; | ||
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.
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.
Could you maybe add a comment somewhere about when you should use which mode?
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.
Sure thing! I have added comments, let me know if they are descriptive enough 🙏