From 2e34ee742b9596b007dc1b26d085ff6855a4145a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Sun, 16 Jun 2024 13:21:30 +0200 Subject: [PATCH 01/66] install fast-equals --- package-lock.json | 14 ++++++++++++-- package.json | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ac22ef3fc64d..db48c56aa07b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,7 @@ "expo-av": "~13.10.4", "expo-image": "1.11.0", "expo-image-manipulator": "11.8.0", + "fast-equals": "^5.0.1", "focus-trap-react": "^10.2.3", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", @@ -21328,8 +21329,12 @@ "license": "Apache-2.0" }, "node_modules/fast-equals": { - "version": "4.0.3", - "license": "MIT" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "engines": { + "node": ">=6.0.0" + } }, "node_modules/fast-glob": { "version": "3.3.1", @@ -31974,6 +31979,11 @@ } } }, + "node_modules/react-native-onyx/node_modules/fast-equals": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==" + }, "node_modules/react-native-pager-view": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.2.3.tgz", diff --git a/package.json b/package.json index 7b2109305a56..32248734920f 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "expo-av": "~13.10.4", "expo-image": "1.11.0", "expo-image-manipulator": "11.8.0", + "fast-equals": "^5.0.1", "focus-trap-react": "^10.2.3", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", From 0cbbed08a7feb2ba90bee825ab8ec498135b0c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 17 Jun 2024 14:07:12 +0200 Subject: [PATCH 02/66] add array cache POC --- src/libs/memoize/cache/arrayCacheBuilder.ts | 60 +++++++++++++++++++++ src/libs/memoize/const.ts | 11 ++++ src/libs/memoize/index.ts | 36 +++++++++++++ src/libs/memoize/types.ts | 48 +++++++++++++++++ src/libs/memoize/utils.ts | 23 ++++++++ 5 files changed, 178 insertions(+) create mode 100644 src/libs/memoize/cache/arrayCacheBuilder.ts create mode 100644 src/libs/memoize/const.ts create mode 100644 src/libs/memoize/index.ts create mode 100644 src/libs/memoize/types.ts create mode 100644 src/libs/memoize/utils.ts diff --git a/src/libs/memoize/cache/arrayCacheBuilder.ts b/src/libs/memoize/cache/arrayCacheBuilder.ts new file mode 100644 index 000000000000..16d1fedce8b4 --- /dev/null +++ b/src/libs/memoize/cache/arrayCacheBuilder.ts @@ -0,0 +1,60 @@ +import type {Cache, CacheOpts} from '@libs/memoize/types'; +import {getEqualityComparator} from '@libs/memoize/utils'; + +function buildArrayCache(opts: CacheOpts): Cache { + const cache: Array<[K, V]> = []; + + const keyComparator = getEqualityComparator(opts); + + return { + // FIXME - Assumption is hot parts of the cache should have quicker access, so let's start our loops from the end + has(key) { + return cache.some((entry) => keyComparator(entry[0], key)); + }, + get(key) { + return cache.find((entry) => keyComparator(entry[0], key))?.[1]; + }, + set(key, value) { + // Set consists of those steps: + // 1. Find index of the key + // 2. If key in cache, delete old entry + // 3. Add new entry to the end + // 4. If cache is too big, remove first entry + // FIXME They are pretty slow, be mindful about it and improve - find better data structure + const index = cache.findIndex((entry) => keyComparator(entry[0], key)); + + if (index !== -1) { + cache.splice(index, 1); + } + + cache.push([key, value]); + + if (cache.length > opts.maxSize) { + cache.shift(); + } + }, + delete(key) { + const index = cache.findIndex((entry) => keyComparator(entry[0], key)); + + if (index !== -1) { + cache.splice(index, 1); + } + }, + clear() { + cache.length = 0; + }, + snapshot: { + keys() { + return cache.map((entry) => entry[0]); + }, + values() { + return cache.map((entry) => entry[1]); + }, + cache() { + return [...cache]; + }, + }, + }; +} + +export default buildArrayCache; diff --git a/src/libs/memoize/const.ts b/src/libs/memoize/const.ts new file mode 100644 index 000000000000..1eb6a15830a5 --- /dev/null +++ b/src/libs/memoize/const.ts @@ -0,0 +1,11 @@ +import type {Options} from './types'; + +const DEFAULT_OPTIONS = { + maxSize: Infinity, + equality: 'shallow', + monitor: true, + cache: 'array', +} satisfies Options; + +// eslint-disable-next-line import/prefer-default-export +export {DEFAULT_OPTIONS}; diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts new file mode 100644 index 000000000000..3d3a851dc8be --- /dev/null +++ b/src/libs/memoize/index.ts @@ -0,0 +1,36 @@ +import buildArrayCache from './cache/arrayCacheBuilder'; +import type {ClientOptions, GenericFn} from './types'; +import {mergeOptions} from './utils'; + +/** + * + * @param fn - Function to memoize + * @param options - + * @returns + */ +function memoize(fn: Fn, opts?: ClientOptions) { + const options = mergeOptions(opts); + + const cache = buildArrayCache, ReturnType>(options); + + const memoized = function memoized(...args: Parameters): ReturnType { + const key = args; + const cached = cache.get(key); + + if (cached) { + return cached; + } + + const result = fn(...args); + + cache.set(key, result); + + return result; + }; + + memoized.cache = cache; + + return memoized; +} + +export default memoize; diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts new file mode 100644 index 000000000000..3e4ea9910d99 --- /dev/null +++ b/src/libs/memoize/types.ts @@ -0,0 +1,48 @@ +/** + * Key is equal to list of arguments passed to memoized function + */ +type Key = K[]; + +type KeyComparator = (key1: Key, key2: Key) => boolean; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type GenericFn = (...args: any[]) => any; + +/** + * Checks wheter keys are equal + * @param key1 + * @param key2 + */ +// declare function isEqual(key1: Key, key2: Key); + +type Cache = { + has: (key: K) => boolean; + get: (key: K) => V | undefined; + set: (key: K, value: V) => void; + delete: (key: K) => void; + clear: () => void; + snapshot: { + keys: () => K[]; + values: () => V[]; + cache: () => Array<[K, V]>; + }; +}; + +type CacheOpts = { + maxSize: number; + equality: 'deep' | 'shallow' | KeyComparator; +}; + +type InternalOptions = { + cache: 'array'; +}; + +type Options = { + maxSize: number; + equality: 'deep' | 'shallow' | KeyComparator; + monitor: boolean; +} & InternalOptions; + +type ClientOptions = Partial>; + +export type {Cache, CacheOpts, Key, KeyComparator, GenericFn, Options, ClientOptions}; diff --git a/src/libs/memoize/utils.ts b/src/libs/memoize/utils.ts new file mode 100644 index 000000000000..3f3ce620026a --- /dev/null +++ b/src/libs/memoize/utils.ts @@ -0,0 +1,23 @@ +import {deepEqual, shallowEqual} from 'fast-equals'; +import {DEFAULT_OPTIONS} from './const'; +import type {CacheOpts, ClientOptions, Options} from './types'; + +function getEqualityComparator(opts: CacheOpts) { + switch (opts.equality) { + case 'deep': + return deepEqual; + case 'shallow': + return shallowEqual; + default: + return opts.equality; + } +} + +function mergeOptions(options?: ClientOptions): Options { + if (!options) { + return DEFAULT_OPTIONS; + } + return {...DEFAULT_OPTIONS, ...options}; +} + +export {mergeOptions, getEqualityComparator}; From 4af2f05ae59aa1277ab6e9089d20856a79a439ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 17 Jun 2024 21:27:37 +0200 Subject: [PATCH 03/66] fix comments and types --- src/libs/memoize/cache/arrayCacheBuilder.ts | 12 ++++++------ src/libs/memoize/index.ts | 11 ++++++----- src/libs/memoize/types.ts | 19 ++----------------- 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/src/libs/memoize/cache/arrayCacheBuilder.ts b/src/libs/memoize/cache/arrayCacheBuilder.ts index 16d1fedce8b4..9c8e867a1bb6 100644 --- a/src/libs/memoize/cache/arrayCacheBuilder.ts +++ b/src/libs/memoize/cache/arrayCacheBuilder.ts @@ -1,7 +1,12 @@ import type {Cache, CacheOpts} from '@libs/memoize/types'; import {getEqualityComparator} from '@libs/memoize/utils'; -function buildArrayCache(opts: CacheOpts): Cache { +/** + * Builder of the cache using `Array` primitive under the hood. + * @param opts - Cache options, check `CacheOpts` type for more details. + * @returns + */ +function buildArrayCache(opts: CacheOpts): Cache { const cache: Array<[K, V]> = []; const keyComparator = getEqualityComparator(opts); @@ -15,11 +20,6 @@ function buildArrayCache(opts: CacheOpts): Cache { return cache.find((entry) => keyComparator(entry[0], key))?.[1]; }, set(key, value) { - // Set consists of those steps: - // 1. Find index of the key - // 2. If key in cache, delete old entry - // 3. Add new entry to the end - // 4. If cache is too big, remove first entry // FIXME They are pretty slow, be mindful about it and improve - find better data structure const index = cache.findIndex((entry) => keyComparator(entry[0], key)); diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 3d3a851dc8be..0982aaa218f6 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -1,14 +1,15 @@ import buildArrayCache from './cache/arrayCacheBuilder'; -import type {ClientOptions, GenericFn} from './types'; +import type {Cache, ClientOptions} from './types'; import {mergeOptions} from './utils'; /** - * + * Wraps a function with a memoization layer. Useful for caching expensive calculations. * @param fn - Function to memoize - * @param options - - * @returns + * @param options - Options for the memoization layer, for more details see `ClientOptions` type. + * @returns Memoized function with a cache API attached to it. */ -function memoize(fn: Fn, opts?: ClientOptions) { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function memoize any>(fn: Fn, opts?: ClientOptions): Fn & {cache: Cache, ReturnType>} { const options = mergeOptions(opts); const cache = buildArrayCache, ReturnType>(options); diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index 3e4ea9910d99..47282a4c224b 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -1,19 +1,4 @@ -/** - * Key is equal to list of arguments passed to memoized function - */ -type Key = K[]; - -type KeyComparator = (key1: Key, key2: Key) => boolean; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type GenericFn = (...args: any[]) => any; - -/** - * Checks wheter keys are equal - * @param key1 - * @param key2 - */ -// declare function isEqual(key1: Key, key2: Key); +type KeyComparator = (key1: K[], key2: K[]) => boolean; type Cache = { has: (key: K) => boolean; @@ -45,4 +30,4 @@ type Options = { type ClientOptions = Partial>; -export type {Cache, CacheOpts, Key, KeyComparator, GenericFn, Options, ClientOptions}; +export type {Cache, CacheOpts, Options, ClientOptions}; From fbc0d00d4134c1839599b69273506c4b3a5bab4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 17 Jun 2024 21:28:19 +0200 Subject: [PATCH 04/66] remove unnecessary variable --- src/libs/memoize/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 0982aaa218f6..a9fc69b28de8 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -14,15 +14,14 @@ function memoize any>(fn: Fn, opts?: ClientOption const cache = buildArrayCache, ReturnType>(options); - const memoized = function memoized(...args: Parameters): ReturnType { - const key = args; + const memoized = function memoized(...key: Parameters): ReturnType { const cached = cache.get(key); if (cached) { return cached; } - const result = fn(...args); + const result = fn(...key); cache.set(key, result); From af24aa6cebd1e72cacf3e528466661cf6fa63e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 17 Jun 2024 21:32:53 +0200 Subject: [PATCH 05/66] add MemoizedFn type --- src/libs/memoize/index.ts | 4 ++-- src/libs/memoize/types.ts | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index a9fc69b28de8..8ff27681e9dc 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -1,5 +1,5 @@ import buildArrayCache from './cache/arrayCacheBuilder'; -import type {Cache, ClientOptions} from './types'; +import type {ClientOptions, MemoizedFn, MemoizeFnPredicate} from './types'; import {mergeOptions} from './utils'; /** @@ -9,7 +9,7 @@ import {mergeOptions} from './utils'; * @returns Memoized function with a cache API attached to it. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -function memoize any>(fn: Fn, opts?: ClientOptions): Fn & {cache: Cache, ReturnType>} { +function memoize(fn: Fn, opts?: ClientOptions): MemoizedFn { const options = mergeOptions(opts); const cache = buildArrayCache, ReturnType>(options); diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index 47282a4c224b..054a55048b1f 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -30,4 +30,9 @@ type Options = { type ClientOptions = Partial>; -export type {Cache, CacheOpts, Options, ClientOptions}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type MemoizeFnPredicate = (...args: any[]) => any; + +type MemoizedFn = Fn & {cache: Cache, ReturnType>}; + +export type {Cache, CacheOpts, Options, ClientOptions, MemoizedFn, KeyComparator, MemoizeFnPredicate}; From f0495975ca7f72b11be2894a5a5078acbe6e3cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 17 Jun 2024 22:31:43 +0200 Subject: [PATCH 06/66] fix array cache --- src/libs/memoize/cache/arrayCacheBuilder.ts | 25 ++++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/libs/memoize/cache/arrayCacheBuilder.ts b/src/libs/memoize/cache/arrayCacheBuilder.ts index 9c8e867a1bb6..a3f081176672 100644 --- a/src/libs/memoize/cache/arrayCacheBuilder.ts +++ b/src/libs/memoize/cache/arrayCacheBuilder.ts @@ -11,17 +11,30 @@ function buildArrayCache(opts: CacheOpts): Cache { const keyComparator = getEqualityComparator(opts); + function getKeyIndex(key: K) { + for (let i = cache.length - 1; i >= 0; i--) { + if (keyComparator(cache[i][0], key)) { + return i; + } + } + return -1; + } + return { - // FIXME - Assumption is hot parts of the cache should have quicker access, so let's start our loops from the end has(key) { - return cache.some((entry) => keyComparator(entry[0], key)); + return getKeyIndex(key) !== -1; }, get(key) { - return cache.find((entry) => keyComparator(entry[0], key))?.[1]; + const index = getKeyIndex(key); + + if (index !== -1) { + const [entry] = cache.splice(index, 1); + cache.push(entry); + return entry[1]; + } }, set(key, value) { - // FIXME They are pretty slow, be mindful about it and improve - find better data structure - const index = cache.findIndex((entry) => keyComparator(entry[0], key)); + const index = getKeyIndex(key); if (index !== -1) { cache.splice(index, 1); @@ -34,7 +47,7 @@ function buildArrayCache(opts: CacheOpts): Cache { } }, delete(key) { - const index = cache.findIndex((entry) => keyComparator(entry[0], key)); + const index = getKeyIndex(key); if (index !== -1) { cache.splice(index, 1); From 6a699b8e8ad0b1c0a1536a7010f3d47e8ed31099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 17 Jun 2024 22:42:03 +0200 Subject: [PATCH 07/66] fix get method and remove has --- src/libs/memoize/cache/arrayCacheBuilder.ts | 5 +---- src/libs/memoize/index.ts | 2 +- src/libs/memoize/types.ts | 3 +-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/libs/memoize/cache/arrayCacheBuilder.ts b/src/libs/memoize/cache/arrayCacheBuilder.ts index a3f081176672..f9b2a2bbcba9 100644 --- a/src/libs/memoize/cache/arrayCacheBuilder.ts +++ b/src/libs/memoize/cache/arrayCacheBuilder.ts @@ -21,16 +21,13 @@ function buildArrayCache(opts: CacheOpts): Cache { } return { - has(key) { - return getKeyIndex(key) !== -1; - }, get(key) { const index = getKeyIndex(key); if (index !== -1) { const [entry] = cache.splice(index, 1); cache.push(entry); - return entry[1]; + return {value: entry[1]}; } }, set(key, value) { diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 8ff27681e9dc..fa6eadafd136 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -18,7 +18,7 @@ function memoize(fn: Fn, opts?: ClientOptions): M const cached = cache.get(key); if (cached) { - return cached; + return cached.value; } const result = fn(...key); diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index 054a55048b1f..6ed977f92bbf 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -1,8 +1,7 @@ type KeyComparator = (key1: K[], key2: K[]) => boolean; type Cache = { - has: (key: K) => boolean; - get: (key: K) => V | undefined; + get: (key: K) => {value: V} | undefined; set: (key: K, value: V) => void; delete: (key: K) => void; clear: () => void; From 6e4ca00e2a3e53cf1c5f038bfd04c0bbfb72a8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 17 Jun 2024 22:45:04 +0200 Subject: [PATCH 08/66] fix delete method --- src/libs/memoize/cache/arrayCacheBuilder.ts | 5 ++++- src/libs/memoize/types.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libs/memoize/cache/arrayCacheBuilder.ts b/src/libs/memoize/cache/arrayCacheBuilder.ts index f9b2a2bbcba9..b4c8ac63d99e 100644 --- a/src/libs/memoize/cache/arrayCacheBuilder.ts +++ b/src/libs/memoize/cache/arrayCacheBuilder.ts @@ -45,10 +45,13 @@ function buildArrayCache(opts: CacheOpts): Cache { }, delete(key) { const index = getKeyIndex(key); + const has = index !== -1; - if (index !== -1) { + if (has) { cache.splice(index, 1); } + + return has; }, clear() { cache.length = 0; diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index 6ed977f92bbf..6f74505b7029 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -3,7 +3,7 @@ type KeyComparator = (key1: K[], key2: K[]) => boolean; type Cache = { get: (key: K) => {value: V} | undefined; set: (key: K, value: V) => void; - delete: (key: K) => void; + delete: (key: K) => boolean; clear: () => void; snapshot: { keys: () => K[]; From a8a88499b2e866ab4fb5a5acb978d6418cef533e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 17 Jun 2024 22:48:19 +0200 Subject: [PATCH 09/66] add tests --- tests/unit/memoizeTest.ts | 111 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 tests/unit/memoizeTest.ts diff --git a/tests/unit/memoizeTest.ts b/tests/unit/memoizeTest.ts new file mode 100644 index 000000000000..17fbf7dffc91 --- /dev/null +++ b/tests/unit/memoizeTest.ts @@ -0,0 +1,111 @@ +import memoize from '../../src/libs/memoize'; + +describe('memoize test', () => { + it('should return the memoized result', () => { + const add = (a: number, b: number) => a + b; + const memoizedAdd = memoize(add); + + const result1 = memoizedAdd(2, 3); + const result2 = memoizedAdd(2, 3); + + expect(result1).toBe(5); + expect(result2).toBe(5); + }); + + it('should not call original function if the same arguments are passed', () => { + const fn = jest.fn(); + const memoizedFn = memoize(fn); + + memoizedFn(2, 3); + memoizedFn(2, 3); + memoizedFn(2, 3); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('should recompute the result if different arguments are passed', () => { + const fn = jest.fn(); + const memoizedFn = memoize(fn); + + memoizedFn(4, 20); + memoizedFn('r2d2'); + + expect(fn).toHaveBeenCalledTimes(2); + }); + + it('should handle deep comparison', () => { + const fn = jest.fn(); + const memoizedFn = memoize(fn, {equality: 'deep'}); + + memoizedFn({a: 1, b: 'test'}, {c: 3, d: 'test'}); + memoizedFn({a: 1, b: 'test'}, {c: 3, d: 'test'}); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('should handle custom key comparator', () => { + const fn = jest.fn(); + const memoizedFn = memoize(fn, {equality: () => true}); + + memoizedFn(1, 2); + memoizedFn(1, 3); + memoizedFn(1, 4); + memoizedFn(1, 5); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('should handle cache eviction', () => { + const fn = jest.fn(); + const memoizedFn = memoize(fn, {maxSize: 1}); + + memoizedFn(1, 2); + memoizedFn(1, 3); + memoizedFn(1, 2); + memoizedFn(1, 3); + + expect(fn).toHaveBeenCalledTimes(4); + }); + + it('should delete cache entry', () => { + const fn = jest.fn(); + const memoizedFn = memoize(fn); + + memoizedFn(1, 2); + memoizedFn.cache.delete([1, 2]); + memoizedFn(1, 2); + + expect(fn).toHaveBeenCalledTimes(2); + }); + + it('should clear cache', () => { + const fn = jest.fn(); + const memoizedFn = memoize(fn); + + memoizedFn(1, 2); + memoizedFn(2, 3); + memoizedFn.cache.clear(); + memoizedFn(1, 2); + memoizedFn(2, 3); + + expect(fn).toHaveBeenCalledTimes(4); + }); + + it('should return cache snapshot', () => { + const fn = (a: number, b: number) => a + b; + const memoizedFn = memoize(fn); + + memoizedFn(1, 2); + memoizedFn(2, 3); + + expect(memoizedFn.cache.snapshot.keys()).toEqual([ + [1, 2], + [2, 3], + ]); + expect(memoizedFn.cache.snapshot.values()).toEqual([3, 5]); + expect(memoizedFn.cache.snapshot.cache()).toEqual([ + [[1, 2], 3], + [[2, 3], 5], + ]); + }); +}); From 932d26c6439c65fad033098cf955eb28f58a6a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 18 Jun 2024 20:59:14 +0200 Subject: [PATCH 10/66] fix types --- src/libs/memoize/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index fa6eadafd136..5f652d345e5f 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -1,5 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import buildArrayCache from './cache/arrayCacheBuilder'; -import type {ClientOptions, MemoizedFn, MemoizeFnPredicate} from './types'; +import type {ClientOptions, MemoizeFnPredicate} from './types'; import {mergeOptions} from './utils'; /** @@ -8,8 +9,7 @@ import {mergeOptions} from './utils'; * @param options - Options for the memoization layer, for more details see `ClientOptions` type. * @returns Memoized function with a cache API attached to it. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function memoize(fn: Fn, opts?: ClientOptions): MemoizedFn { +function memoize(fn: Fn, opts?: ClientOptions) { const options = mergeOptions(opts); const cache = buildArrayCache, ReturnType>(options); @@ -23,7 +23,7 @@ function memoize(fn: Fn, opts?: ClientOptions): M const result = fn(...key); - cache.set(key, result); + cache.set(key, result as ReturnType); return result; }; From 6fb59abaec560b51ce60953385c43aa72dc5b7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 19 Jun 2024 14:04:28 +0200 Subject: [PATCH 11/66] make monitor false on default --- src/libs/memoize/const.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/memoize/const.ts b/src/libs/memoize/const.ts index 1eb6a15830a5..b79772f8a2c8 100644 --- a/src/libs/memoize/const.ts +++ b/src/libs/memoize/const.ts @@ -3,7 +3,7 @@ import type {Options} from './types'; const DEFAULT_OPTIONS = { maxSize: Infinity, equality: 'shallow', - monitor: true, + monitor: false, cache: 'array', } satisfies Options; From 321230115287366277fef82514aa385dc38d3632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 19 Jun 2024 16:47:39 +0200 Subject: [PATCH 12/66] add cache statistics module --- src/libs/memoize/index.ts | 24 ++++++++++ src/libs/memoize/stats.ts | 96 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 src/libs/memoize/stats.ts diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 5f652d345e5f..023e09f02413 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ import buildArrayCache from './cache/arrayCacheBuilder'; +import {MemoizeStats} from './stats'; import type {ClientOptions, MemoizeFnPredicate} from './types'; import {mergeOptions} from './utils'; @@ -14,22 +15,45 @@ function memoize(fn: Fn, opts?: ClientOptions) { const cache = buildArrayCache, ReturnType>(options); + const stats = new MemoizeStats(options.monitor); + const memoized = function memoized(...key: Parameters): ReturnType { + const statsEntry = stats.createEntry(); + statsEntry.registerStat('keyLength', key.length); + + const retrievalTimeStart = performance.now(); const cached = cache.get(key); + // If cached value is there, return it if (cached) { + statsEntry.registerStat('didHit', true); + statsEntry.registerStat('processingTime', performance.now() - retrievalTimeStart); + + statsEntry.save(); + return cached.value; } + // If no cached value, calculate it and store it + statsEntry.registerStat('didHit', false); + const fnTimeStart = performance.now(); const result = fn(...key); + statsEntry.registerStat('processingTime', performance.now() - fnTimeStart); cache.set(key, result as ReturnType); + statsEntry.save(); + return result; }; memoized.cache = cache; + memoized.stats = { + startMonitoring: () => stats.startMonitoring(), + stopMonitoring: () => stats.stopMonitoring(), + }; + return memoized; } diff --git a/src/libs/memoize/stats.ts b/src/libs/memoize/stats.ts new file mode 100644 index 000000000000..4921f95449da --- /dev/null +++ b/src/libs/memoize/stats.ts @@ -0,0 +1,96 @@ +type MemoizeStatsEntry = { + keyLength: number; + didHit: boolean; + processingTime: number; +}; + +class MemoizeStats { + private calls = 0; + + private hits = 0; + + private avgKeyLength = 0; + + private avgCacheRetrievalTime = 0; + + private avgFnTime = 0; + + enabled = false; + + constructor(enabled: boolean) { + this.enabled = enabled; + } + + // See https://en.wikipedia.org/wiki/Moving_average#Cumulative_average + private calculateCumulativeAvg(avg: number, length: number, value: number) { + return (avg * (length - 1) + value) / length; + } + + private cumulateEntry(entry: MemoizeStatsEntry) { + if (!this.enabled) { + return; + } + + this.calls++; + this.hits += entry.didHit ? 1 : 0; + + this.avgKeyLength = this.calculateCumulativeAvg(this.avgKeyLength, this.calls, entry.keyLength); + + if (entry.didHit) { + this.avgCacheRetrievalTime = this.calculateCumulativeAvg(this.avgCacheRetrievalTime, this.hits, entry.processingTime); + } else { + this.avgFnTime = this.calculateCumulativeAvg(this.avgFnTime, this.calls - this.hits, entry.processingTime); + } + } + + createEntry() { + // If monitoring is disabled, return a dummy object that does nothing + if (!this.enabled) { + return { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + registerStat:

(cacheProp: P, value: MemoizeStatsEntry[P]) => {}, + save: () => {}, + }; + } + + const entry: Partial = {}; + + return { + registerStat:

(cacheProp: P, value: MemoizeStatsEntry[P]) => { + entry[cacheProp] = value; + }, + save: () => { + // Check if all required stats are present + if (entry.keyLength === undefined || entry.didHit === undefined || entry.processingTime === undefined) { + return; + } + + this.cumulateEntry(entry as MemoizeStatsEntry); + }, + }; + } + + startMonitoring() { + this.enabled = true; + this.calls = 0; + this.hits = 0; + this.avgKeyLength = 0; + this.avgCacheRetrievalTime = 0; + this.avgFnTime = 0; + } + + stopMonitoring() { + this.enabled = false; + + return { + calls: this.calls, + hits: this.hits, + avgKeyLength: this.avgKeyLength, + avgCacheRetrievalTime: this.avgCacheRetrievalTime, + avgFnTime: this.avgFnTime, + }; + } +} + +export type {MemoizeStatsEntry}; +export {MemoizeStats}; From 3dfe8d50d0e13fae63335509c672462055ac5374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 19 Jun 2024 17:07:05 +0200 Subject: [PATCH 13/66] fix cache entries --- src/libs/memoize/index.ts | 27 ++++++++++----------------- src/libs/memoize/stats.ts | 13 +++++++------ 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 023e09f02413..cee6ed80ac5f 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -22,29 +22,22 @@ function memoize(fn: Fn, opts?: ClientOptions) { statsEntry.registerStat('keyLength', key.length); const retrievalTimeStart = performance.now(); - const cached = cache.get(key); + let cached = cache.get(key); + statsEntry.registerStat('cacheRetrievalTime', performance.now() - retrievalTimeStart); + statsEntry.registerStat('didHit', !!cached); - // If cached value is there, return it - if (cached) { - statsEntry.registerStat('didHit', true); - statsEntry.registerStat('processingTime', performance.now() - retrievalTimeStart); + if (!cached) { + const fnTimeStart = performance.now(); + const result = fn(...key); + statsEntry.registerStat('fnTime', performance.now() - fnTimeStart); - statsEntry.save(); - - return cached.value; + cached = {value: result}; + cache.set(key, result as ReturnType); } - // If no cached value, calculate it and store it - statsEntry.registerStat('didHit', false); - const fnTimeStart = performance.now(); - const result = fn(...key); - statsEntry.registerStat('processingTime', performance.now() - fnTimeStart); - - cache.set(key, result as ReturnType); - statsEntry.save(); - return result; + return cached.value; }; memoized.cache = cache; diff --git a/src/libs/memoize/stats.ts b/src/libs/memoize/stats.ts index 4921f95449da..84cf9cf6470c 100644 --- a/src/libs/memoize/stats.ts +++ b/src/libs/memoize/stats.ts @@ -1,7 +1,8 @@ type MemoizeStatsEntry = { keyLength: number; didHit: boolean; - processingTime: number; + cacheRetrievalTime: number; + fnTime?: number; }; class MemoizeStats { @@ -36,10 +37,10 @@ class MemoizeStats { this.avgKeyLength = this.calculateCumulativeAvg(this.avgKeyLength, this.calls, entry.keyLength); - if (entry.didHit) { - this.avgCacheRetrievalTime = this.calculateCumulativeAvg(this.avgCacheRetrievalTime, this.hits, entry.processingTime); - } else { - this.avgFnTime = this.calculateCumulativeAvg(this.avgFnTime, this.calls - this.hits, entry.processingTime); + this.avgCacheRetrievalTime = this.calculateCumulativeAvg(this.avgCacheRetrievalTime, this.hits, entry.cacheRetrievalTime); + + if (entry.fnTime !== undefined) { + this.avgFnTime = this.calculateCumulativeAvg(this.avgFnTime, this.calls - this.hits, entry.fnTime); } } @@ -61,7 +62,7 @@ class MemoizeStats { }, save: () => { // Check if all required stats are present - if (entry.keyLength === undefined || entry.didHit === undefined || entry.processingTime === undefined) { + if (entry.keyLength === undefined || entry.didHit === undefined || entry.cacheRetrievalTime === undefined) { return; } From 8e2645902e109b132f1dfc4b7d22b2a0d87b74db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 19 Jun 2024 17:24:47 +0200 Subject: [PATCH 14/66] add entry identity check --- src/libs/memoize/stats.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/libs/memoize/stats.ts b/src/libs/memoize/stats.ts index 84cf9cf6470c..65e8e61720b1 100644 --- a/src/libs/memoize/stats.ts +++ b/src/libs/memoize/stats.ts @@ -28,10 +28,6 @@ class MemoizeStats { } private cumulateEntry(entry: MemoizeStatsEntry) { - if (!this.enabled) { - return; - } - this.calls++; this.hits += entry.didHit ? 1 : 0; @@ -44,12 +40,23 @@ class MemoizeStats { } } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static isMemoizeStatsEntry(entry: any): entry is MemoizeStatsEntry { + return entry.keyLength !== undefined && entry.didHit !== undefined && entry.cacheRetrievalTime !== undefined; + } + + // eslint-disable-next-line rulesdir/prefer-early-return + private saveEntry(entry: Partial) { + if (this.enabled && MemoizeStats.isMemoizeStatsEntry(entry)) { + this.cumulateEntry(entry); + } + } + createEntry() { // If monitoring is disabled, return a dummy object that does nothing if (!this.enabled) { return { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - registerStat:

(cacheProp: P, value: MemoizeStatsEntry[P]) => {}, + registerStat: () => {}, save: () => {}, }; } @@ -60,14 +67,7 @@ class MemoizeStats { registerStat:

(cacheProp: P, value: MemoizeStatsEntry[P]) => { entry[cacheProp] = value; }, - save: () => { - // Check if all required stats are present - if (entry.keyLength === undefined || entry.didHit === undefined || entry.cacheRetrievalTime === undefined) { - return; - } - - this.cumulateEntry(entry as MemoizeStatsEntry); - }, + save: () => this.saveEntry(entry), }; } From a368c07c5f9be0f8e9c59083c6da95865598ad5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 19 Jun 2024 17:25:59 +0200 Subject: [PATCH 15/66] rename registerStat to track --- src/libs/memoize/index.ts | 8 ++++---- src/libs/memoize/stats.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index cee6ed80ac5f..ba1b3ab1b155 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -19,17 +19,17 @@ function memoize(fn: Fn, opts?: ClientOptions) { const memoized = function memoized(...key: Parameters): ReturnType { const statsEntry = stats.createEntry(); - statsEntry.registerStat('keyLength', key.length); + statsEntry.track('keyLength', key.length); const retrievalTimeStart = performance.now(); let cached = cache.get(key); - statsEntry.registerStat('cacheRetrievalTime', performance.now() - retrievalTimeStart); - statsEntry.registerStat('didHit', !!cached); + statsEntry.track('cacheRetrievalTime', performance.now() - retrievalTimeStart); + statsEntry.track('didHit', !!cached); if (!cached) { const fnTimeStart = performance.now(); const result = fn(...key); - statsEntry.registerStat('fnTime', performance.now() - fnTimeStart); + statsEntry.track('fnTime', performance.now() - fnTimeStart); cached = {value: result}; cache.set(key, result as ReturnType); diff --git a/src/libs/memoize/stats.ts b/src/libs/memoize/stats.ts index 65e8e61720b1..d04cc2edc26b 100644 --- a/src/libs/memoize/stats.ts +++ b/src/libs/memoize/stats.ts @@ -56,7 +56,7 @@ class MemoizeStats { // If monitoring is disabled, return a dummy object that does nothing if (!this.enabled) { return { - registerStat: () => {}, + track: () => {}, save: () => {}, }; } @@ -64,7 +64,7 @@ class MemoizeStats { const entry: Partial = {}; return { - registerStat:

(cacheProp: P, value: MemoizeStatsEntry[P]) => { + track:

(cacheProp: P, value: MemoizeStatsEntry[P]) => { entry[cacheProp] = value; }, save: () => this.saveEntry(entry), From 3ea3af0783893d620c2e873a09ee0f206c912926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 19 Jun 2024 17:31:45 +0200 Subject: [PATCH 16/66] add cache size to cache and stats --- src/libs/memoize/cache/arrayCacheBuilder.ts | 1 + src/libs/memoize/index.ts | 1 + src/libs/memoize/stats.ts | 7 +++++++ src/libs/memoize/types.ts | 1 + 4 files changed, 10 insertions(+) diff --git a/src/libs/memoize/cache/arrayCacheBuilder.ts b/src/libs/memoize/cache/arrayCacheBuilder.ts index b4c8ac63d99e..42ea65f238bf 100644 --- a/src/libs/memoize/cache/arrayCacheBuilder.ts +++ b/src/libs/memoize/cache/arrayCacheBuilder.ts @@ -66,6 +66,7 @@ function buildArrayCache(opts: CacheOpts): Cache { cache() { return [...cache]; }, + size: cache.length, }, }; } diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index ba1b3ab1b155..1b7e5a9efa6b 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -35,6 +35,7 @@ function memoize(fn: Fn, opts?: ClientOptions) { cache.set(key, result as ReturnType); } + statsEntry.track('cacheSize', cache.snapshot.size); statsEntry.save(); return cached.value; diff --git a/src/libs/memoize/stats.ts b/src/libs/memoize/stats.ts index d04cc2edc26b..c402025872e5 100644 --- a/src/libs/memoize/stats.ts +++ b/src/libs/memoize/stats.ts @@ -3,6 +3,7 @@ type MemoizeStatsEntry = { didHit: boolean; cacheRetrievalTime: number; fnTime?: number; + cacheSize: number; }; class MemoizeStats { @@ -16,6 +17,8 @@ class MemoizeStats { private avgFnTime = 0; + private cacheSize = 0; + enabled = false; constructor(enabled: boolean) { @@ -31,6 +34,8 @@ class MemoizeStats { this.calls++; this.hits += entry.didHit ? 1 : 0; + this.cacheSize = entry.cacheSize; + this.avgKeyLength = this.calculateCumulativeAvg(this.avgKeyLength, this.calls, entry.keyLength); this.avgCacheRetrievalTime = this.calculateCumulativeAvg(this.avgCacheRetrievalTime, this.hits, entry.cacheRetrievalTime); @@ -78,6 +83,7 @@ class MemoizeStats { this.avgKeyLength = 0; this.avgCacheRetrievalTime = 0; this.avgFnTime = 0; + this.cacheSize = 0; } stopMonitoring() { @@ -89,6 +95,7 @@ class MemoizeStats { avgKeyLength: this.avgKeyLength, avgCacheRetrievalTime: this.avgCacheRetrievalTime, avgFnTime: this.avgFnTime, + cacheSize: this.cacheSize, }; } } diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index 6f74505b7029..39331ce47ecc 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -9,6 +9,7 @@ type Cache = { keys: () => K[]; values: () => V[]; cache: () => Array<[K, V]>; + size: number; }; }; From 5695b2dae89e719532f9d3bd56257b52c9ceccb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 20 Jun 2024 12:39:53 +0200 Subject: [PATCH 17/66] Global stats api v1 --- src/libs/memoize/index.ts | 49 ++++++++++++++++++++++++++++++++------- src/libs/memoize/types.ts | 9 +++++-- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 1b7e5a9efa6b..08161c2570e4 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -1,21 +1,52 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ import buildArrayCache from './cache/arrayCacheBuilder'; import {MemoizeStats} from './stats'; -import type {ClientOptions, MemoizeFnPredicate} from './types'; +import type {ClientOptions, MemoizedFn, MemoizeFnPredicate, Stats} from './types'; import {mergeOptions} from './utils'; +/** + * Global memoization class. Use it to orchestrate memoization (e.g. start/stop global monitoring). + */ +class Memoize { + static monitoringEnabled = false; + + private static memoizedList: Array<{id: string; memoized: Stats}> = []; + + static registerMemoized(id: string, memoized: Stats) { + this.memoizedList.push({id, memoized}); + } + + static startMonitoring() { + if (this.monitoringEnabled) { + return; + } + this.monitoringEnabled = true; + Memoize.memoizedList.forEach(({memoized}) => { + memoized.startMonitoring(); + }); + } + + static stopMonitoring() { + if (!this.monitoringEnabled) { + return; + } + this.monitoringEnabled = false; + return Memoize.memoizedList.map(({memoized}) => memoized.stopMonitoring()); + } +} + /** * Wraps a function with a memoization layer. Useful for caching expensive calculations. * @param fn - Function to memoize * @param options - Options for the memoization layer, for more details see `ClientOptions` type. * @returns Memoized function with a cache API attached to it. */ -function memoize(fn: Fn, opts?: ClientOptions) { +function memoize(fn: Fn, opts?: ClientOptions): MemoizedFn { const options = mergeOptions(opts); const cache = buildArrayCache, ReturnType>(options); - const stats = new MemoizeStats(options.monitor); + const stats = new MemoizeStats(options.monitor || Memoize.monitoringEnabled); const memoized = function memoized(...key: Parameters): ReturnType { const statsEntry = stats.createEntry(); @@ -43,12 +74,14 @@ function memoize(fn: Fn, opts?: ClientOptions) { memoized.cache = cache; - memoized.stats = { - startMonitoring: () => stats.startMonitoring(), - stopMonitoring: () => stats.stopMonitoring(), - }; + memoized.startMonitoring = () => stats.startMonitoring(); + memoized.stopMonitoring = () => stats.stopMonitoring(); + + Memoize.registerMemoized(options.monitoringName, memoized); - return memoized; + return memoized as MemoizedFn; } export default memoize; + +export {Memoize}; diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index 39331ce47ecc..1194eef209d5 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -1,3 +1,5 @@ +import type {MemoizeStats} from './stats'; + type KeyComparator = (key1: K[], key2: K[]) => boolean; type Cache = { @@ -26,13 +28,16 @@ type Options = { maxSize: number; equality: 'deep' | 'shallow' | KeyComparator; monitor: boolean; + monitoringName: string; } & InternalOptions; type ClientOptions = Partial>; +type Stats = Pick; + // eslint-disable-next-line @typescript-eslint/no-explicit-any type MemoizeFnPredicate = (...args: any[]) => any; -type MemoizedFn = Fn & {cache: Cache, ReturnType>}; +type MemoizedFn = Fn & {cache: Cache, ReturnType>} & Stats; -export type {Cache, CacheOpts, Options, ClientOptions, MemoizedFn, KeyComparator, MemoizeFnPredicate}; +export type {Cache, CacheOpts, Options, ClientOptions, MemoizedFn, KeyComparator, MemoizeFnPredicate, Stats}; From 7b59ac5b0582dc588d4a097dbd84d570c3b89091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 20 Jun 2024 13:03:23 +0200 Subject: [PATCH 18/66] fix cache size & entries api --- src/libs/memoize/cache/arrayCacheBuilder.ts | 6 ++++-- src/libs/memoize/index.ts | 2 +- src/libs/memoize/types.ts | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/libs/memoize/cache/arrayCacheBuilder.ts b/src/libs/memoize/cache/arrayCacheBuilder.ts index 42ea65f238bf..5ad05773d77b 100644 --- a/src/libs/memoize/cache/arrayCacheBuilder.ts +++ b/src/libs/memoize/cache/arrayCacheBuilder.ts @@ -63,10 +63,12 @@ function buildArrayCache(opts: CacheOpts): Cache { values() { return cache.map((entry) => entry[1]); }, - cache() { + entries() { return [...cache]; }, - size: cache.length, + }, + get size() { + return cache.length; }, }; } diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 08161c2570e4..ea5d68deebbf 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -66,7 +66,7 @@ function memoize(fn: Fn, opts?: ClientOptions): M cache.set(key, result as ReturnType); } - statsEntry.track('cacheSize', cache.snapshot.size); + statsEntry.track('cacheSize', cache.size); statsEntry.save(); return cached.value; diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index 1194eef209d5..96be38503011 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -10,9 +10,9 @@ type Cache = { snapshot: { keys: () => K[]; values: () => V[]; - cache: () => Array<[K, V]>; - size: number; + entries: () => Array<[K, V]>; }; + size: number; }; type CacheOpts = { From 19e59b130eb4967f3a742f688b0c69674f38b3a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 20 Jun 2024 13:10:33 +0200 Subject: [PATCH 19/66] fix function id for stats --- src/libs/memoize/index.ts | 4 ++-- src/libs/memoize/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index ea5d68deebbf..9be892214461 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -31,7 +31,7 @@ class Memoize { return; } this.monitoringEnabled = false; - return Memoize.memoizedList.map(({memoized}) => memoized.stopMonitoring()); + return Memoize.memoizedList.map(({id, memoized}) => ({id, stats: memoized.stopMonitoring()})); } } @@ -77,7 +77,7 @@ function memoize(fn: Fn, opts?: ClientOptions): M memoized.startMonitoring = () => stats.startMonitoring(); memoized.stopMonitoring = () => stats.stopMonitoring(); - Memoize.registerMemoized(options.monitoringName, memoized); + Memoize.registerMemoized(options.monitoringName ?? fn.name, memoized); return memoized as MemoizedFn; } diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index 96be38503011..7cd64fc07bb1 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -28,7 +28,7 @@ type Options = { maxSize: number; equality: 'deep' | 'shallow' | KeyComparator; monitor: boolean; - monitoringName: string; + monitoringName?: string; } & InternalOptions; type ClientOptions = Partial>; From 383488f9a7fc3f57796cb0c3ea6832e7c13fbc57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 20 Jun 2024 13:18:57 +0200 Subject: [PATCH 20/66] change default equality to deep --- src/libs/memoize/const.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/memoize/const.ts b/src/libs/memoize/const.ts index b79772f8a2c8..7517236940e1 100644 --- a/src/libs/memoize/const.ts +++ b/src/libs/memoize/const.ts @@ -2,7 +2,7 @@ import type {Options} from './types'; const DEFAULT_OPTIONS = { maxSize: Infinity, - equality: 'shallow', + equality: 'deep', monitor: false, cache: 'array', } satisfies Options; From 7858c9da7125440bb4a4fa05d0c8aa388b9e058c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 20 Jun 2024 13:26:59 +0200 Subject: [PATCH 21/66] fix cumulative avg calcs --- src/libs/memoize/stats.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/memoize/stats.ts b/src/libs/memoize/stats.ts index c402025872e5..6ea8840f54c9 100644 --- a/src/libs/memoize/stats.ts +++ b/src/libs/memoize/stats.ts @@ -27,7 +27,8 @@ class MemoizeStats { // See https://en.wikipedia.org/wiki/Moving_average#Cumulative_average private calculateCumulativeAvg(avg: number, length: number, value: number) { - return (avg * (length - 1) + value) / length; + const result = (avg * (length - 1) + value) / length; + return Number.isFinite(result) ? result : avg; } private cumulateEntry(entry: MemoizeStatsEntry) { From 4cf90e608028ab59adc6dc165d167167d138f333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 20 Jun 2024 15:46:44 +0200 Subject: [PATCH 22/66] add monitoring to profiling menu --- src/components/ProfilingToolMenu/BaseProfilingToolMenu.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/ProfilingToolMenu/BaseProfilingToolMenu.tsx b/src/components/ProfilingToolMenu/BaseProfilingToolMenu.tsx index 6ab1761fda62..1336d1ea5dc0 100644 --- a/src/components/ProfilingToolMenu/BaseProfilingToolMenu.tsx +++ b/src/components/ProfilingToolMenu/BaseProfilingToolMenu.tsx @@ -14,6 +14,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import toggleProfileTool from '@libs/actions/ProfilingTool'; import getPlatform from '@libs/getPlatform'; import Log from '@libs/Log'; +import {Memoize} from '@libs/memoize'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -52,6 +53,7 @@ function BaseProfilingToolMenu({isProfilingInProgress = false, pathToBeUsed, dis const [sharePath, setSharePath] = useState(''); const [totalMemory, setTotalMemory] = useState(0); const [usedMemory, setUsedMemory] = useState(0); + const [memoizeStats, setMemoizeStats] = useState>(); const {translate} = useLocalize(); // eslint-disable-next-line @lwc/lwc/no-async-await @@ -63,11 +65,13 @@ function BaseProfilingToolMenu({isProfilingInProgress = false, pathToBeUsed, dis const amountOfUsedMemory = await DeviceInfo.getUsedMemory(); setTotalMemory(amountOfTotalMemory); setUsedMemory(amountOfUsedMemory); + setMemoizeStats(Memoize.stopMonitoring()); }, []); const onToggleProfiling = useCallback(() => { const shouldProfiling = !isProfilingInProgress; if (shouldProfiling) { + Memoize.startMonitoring(); startProfiling(); } else { stop(); @@ -86,8 +90,9 @@ function BaseProfilingToolMenu({isProfilingInProgress = false, pathToBeUsed, dis platform: getPlatform(), totalMemory: formatBytes(totalMemory, 2), usedMemory: formatBytes(usedMemory, 2), + memoizeStats, }), - [totalMemory, usedMemory], + [memoizeStats, totalMemory, usedMemory], ); useEffect(() => { From 35c842f28821d2f83e23095ba1b964536ed513eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 20 Jun 2024 15:48:52 +0200 Subject: [PATCH 23/66] rename delete variable name --- src/libs/memoize/cache/arrayCacheBuilder.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/memoize/cache/arrayCacheBuilder.ts b/src/libs/memoize/cache/arrayCacheBuilder.ts index 5ad05773d77b..85a79d215d4b 100644 --- a/src/libs/memoize/cache/arrayCacheBuilder.ts +++ b/src/libs/memoize/cache/arrayCacheBuilder.ts @@ -45,13 +45,13 @@ function buildArrayCache(opts: CacheOpts): Cache { }, delete(key) { const index = getKeyIndex(key); - const has = index !== -1; + const entryExists = index !== -1; - if (has) { + if (entryExists) { cache.splice(index, 1); } - return has; + return entryExists; }, clear() { cache.length = 0; From dc8ad8b4c72b551bc123f73707c03435b25a9987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 20 Jun 2024 15:54:43 +0200 Subject: [PATCH 24/66] use memoize for NumberFormatUtils --- src/libs/NumberFormatUtils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libs/NumberFormatUtils.ts b/src/libs/NumberFormatUtils.ts index bf42e25cbb48..920dcc815bbc 100644 --- a/src/libs/NumberFormatUtils.ts +++ b/src/libs/NumberFormatUtils.ts @@ -1,12 +1,15 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; +import memoize from './memoize'; + +const numberFormatter = memoize(Intl.NumberFormat, {maxSize: 10}); function format(locale: ValueOf, number: number, options?: Intl.NumberFormatOptions): string { - return new Intl.NumberFormat(locale, options).format(number); + return numberFormatter(locale, options).format(number); } function formatToParts(locale: ValueOf, number: number, options?: Intl.NumberFormatOptions): Intl.NumberFormatPart[] { - return new Intl.NumberFormat(locale, options).formatToParts(number); + return numberFormatter(locale, options).formatToParts(number); } export {format, formatToParts}; From 20a9711181d2aad02b989c6239cf9441fcba3af3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 20 Jun 2024 15:55:44 +0200 Subject: [PATCH 25/66] replace lodash memoize --- src/libs/EmojiUtils.ts | 2 +- src/libs/LocaleDigitUtils.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 3c7a23bf31e4..9241d86fbfac 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -1,6 +1,5 @@ import {getUnixTime} from 'date-fns'; import {Str} from 'expensify-common'; -import memoize from 'lodash/memoize'; import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import * as Emojis from '@assets/emojis'; @@ -12,6 +11,7 @@ import type {ReportActionReaction, UsersReactions} from '@src/types/onyx/ReportA import type IconAsset from '@src/types/utils/IconAsset'; import type EmojiTrie from './EmojiTrie'; import type {SupportedLanguage} from './EmojiTrie'; +import memoize from './memoize'; type HeaderIndice = {code: string; index: number; icon: IconAsset}; type EmojiSpacer = {code: string; spacer: boolean}; diff --git a/src/libs/LocaleDigitUtils.ts b/src/libs/LocaleDigitUtils.ts index 156e58c59033..b14604c35e04 100644 --- a/src/libs/LocaleDigitUtils.ts +++ b/src/libs/LocaleDigitUtils.ts @@ -1,8 +1,8 @@ -import _ from 'lodash'; import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import * as Localize from './Localize'; +import memoize from './memoize'; import * as NumberFormatUtils from './NumberFormatUtils'; type Locale = ValueOf; @@ -13,7 +13,7 @@ const INDEX_DECIMAL = 10; const INDEX_MINUS_SIGN = 11; const INDEX_GROUP = 12; -const getLocaleDigits = _.memoize((locale: Locale): string[] => { +const getLocaleDigits = memoize((locale: Locale): string[] => { const localeDigits = [...STANDARD_DIGITS]; for (let i = 0; i <= 9; i++) { localeDigits[i] = NumberFormatUtils.format(locale, i); From 1b1ff6f5776d5f012e0c9a19b6d7a6b0864901de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 20 Jun 2024 16:11:36 +0200 Subject: [PATCH 26/66] add monitoringName for anonymous funcitons --- src/libs/EmojiUtils.ts | 67 +++++++++++++++++++----------------- src/libs/LocaleDigitUtils.ts | 45 +++++++++++++----------- 2 files changed, 59 insertions(+), 53 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 9241d86fbfac..79feb0e95286 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -66,42 +66,45 @@ const getLocalizedEmojiName = (name: string, lang: OnyxEntry): string => /** * Get the unicode code of an emoji in base 16. */ -const getEmojiUnicode = memoize((input: string) => { - if (input.length === 0) { - return ''; - } +const getEmojiUnicode = memoize( + (input: string) => { + if (input.length === 0) { + return ''; + } - if (input.length === 1) { - return input - .charCodeAt(0) - .toString() - .split(' ') - .map((val) => parseInt(val, 10).toString(16)) - .join(' '); - } + if (input.length === 1) { + return input + .charCodeAt(0) + .toString() + .split(' ') + .map((val) => parseInt(val, 10).toString(16)) + .join(' '); + } - const pairs = []; - - // Some Emojis in UTF-16 are stored as a pair of 2 Unicode characters (e.g. Flags) - // The first char is generally between the range U+D800 to U+DBFF called High surrogate - // & the second char between the range U+DC00 to U+DFFF called low surrogate - // More info in the following links: - // 1. https://docs.microsoft.com/en-us/windows/win32/intl/surrogates-and-supplementary-characters - // 2. https://thekevinscott.com/emojis-in-javascript/ - for (let i = 0; i < input.length; i++) { - if (input.charCodeAt(i) >= 0xd800 && input.charCodeAt(i) <= 0xdbff) { - // high surrogate - if (input.charCodeAt(i + 1) >= 0xdc00 && input.charCodeAt(i + 1) <= 0xdfff) { - // low surrogate - pairs.push((input.charCodeAt(i) - 0xd800) * 0x400 + (input.charCodeAt(i + 1) - 0xdc00) + 0x10000); + const pairs = []; + + // Some Emojis in UTF-16 are stored as a pair of 2 Unicode characters (e.g. Flags) + // The first char is generally between the range U+D800 to U+DBFF called High surrogate + // & the second char between the range U+DC00 to U+DFFF called low surrogate + // More info in the following links: + // 1. https://docs.microsoft.com/en-us/windows/win32/intl/surrogates-and-supplementary-characters + // 2. https://thekevinscott.com/emojis-in-javascript/ + for (let i = 0; i < input.length; i++) { + if (input.charCodeAt(i) >= 0xd800 && input.charCodeAt(i) <= 0xdbff) { + // high surrogate + if (input.charCodeAt(i + 1) >= 0xdc00 && input.charCodeAt(i + 1) <= 0xdfff) { + // low surrogate + pairs.push((input.charCodeAt(i) - 0xd800) * 0x400 + (input.charCodeAt(i + 1) - 0xdc00) + 0x10000); + } + } else if (input.charCodeAt(i) < 0xd800 || input.charCodeAt(i) > 0xdfff) { + // modifiers and joiners + pairs.push(input.charCodeAt(i)); } - } else if (input.charCodeAt(i) < 0xd800 || input.charCodeAt(i) > 0xdfff) { - // modifiers and joiners - pairs.push(input.charCodeAt(i)); } - } - return pairs.map((val) => parseInt(String(val), 10).toString(16)).join(' '); -}); + return pairs.map((val) => parseInt(String(val), 10).toString(16)).join(' '); + }, + {monitoringName: 'getEmojiUnicode'}, +); /** * Function to remove Skin Tone and utf16 surrogates from Emoji diff --git a/src/libs/LocaleDigitUtils.ts b/src/libs/LocaleDigitUtils.ts index b14604c35e04..a024276819c1 100644 --- a/src/libs/LocaleDigitUtils.ts +++ b/src/libs/LocaleDigitUtils.ts @@ -13,28 +13,31 @@ const INDEX_DECIMAL = 10; const INDEX_MINUS_SIGN = 11; const INDEX_GROUP = 12; -const getLocaleDigits = memoize((locale: Locale): string[] => { - const localeDigits = [...STANDARD_DIGITS]; - for (let i = 0; i <= 9; i++) { - localeDigits[i] = NumberFormatUtils.format(locale, i); - } - NumberFormatUtils.formatToParts(locale, 1000000.5).forEach((part) => { - switch (part.type) { - case 'decimal': - localeDigits[INDEX_DECIMAL] = part.value; - break; - case 'minusSign': - localeDigits[INDEX_MINUS_SIGN] = part.value; - break; - case 'group': - localeDigits[INDEX_GROUP] = part.value; - break; - default: - break; +const getLocaleDigits = memoize( + (locale: Locale): string[] => { + const localeDigits = [...STANDARD_DIGITS]; + for (let i = 0; i <= 9; i++) { + localeDigits[i] = NumberFormatUtils.format(locale, i); } - }); - return localeDigits; -}); + NumberFormatUtils.formatToParts(locale, 1000000.5).forEach((part) => { + switch (part.type) { + case 'decimal': + localeDigits[INDEX_DECIMAL] = part.value; + break; + case 'minusSign': + localeDigits[INDEX_MINUS_SIGN] = part.value; + break; + case 'group': + localeDigits[INDEX_GROUP] = part.value; + break; + default: + break; + } + }); + return localeDigits; + }, + {monitoringName: 'getLocaleDigits'}, +); /** * Gets the locale digit corresponding to a standard digit. From 85a0e91ba6586159aa3cc140113595b8e5a5719c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 20 Jun 2024 16:20:54 +0200 Subject: [PATCH 27/66] fix tests --- tests/unit/memoizeTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/memoizeTest.ts b/tests/unit/memoizeTest.ts index 17fbf7dffc91..ddc581ec87fc 100644 --- a/tests/unit/memoizeTest.ts +++ b/tests/unit/memoizeTest.ts @@ -103,7 +103,7 @@ describe('memoize test', () => { [2, 3], ]); expect(memoizedFn.cache.snapshot.values()).toEqual([3, 5]); - expect(memoizedFn.cache.snapshot.cache()).toEqual([ + expect(memoizedFn.cache.snapshot.entries()).toEqual([ [[1, 2], 3], [[2, 3], 5], ]); From df7466a2fad1e4126d6d2a8a73ef1543a4d192d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= <62747088+kacper-mikolajczak@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:46:55 +0200 Subject: [PATCH 28/66] Improve cache array builder description Co-authored-by: Rory Abraham <47436092+roryabraham@users.noreply.github.com> --- src/libs/memoize/cache/arrayCacheBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/memoize/cache/arrayCacheBuilder.ts b/src/libs/memoize/cache/arrayCacheBuilder.ts index 85a79d215d4b..77ac66f47844 100644 --- a/src/libs/memoize/cache/arrayCacheBuilder.ts +++ b/src/libs/memoize/cache/arrayCacheBuilder.ts @@ -2,7 +2,7 @@ import type {Cache, CacheOpts} from '@libs/memoize/types'; import {getEqualityComparator} from '@libs/memoize/utils'; /** - * Builder of the cache using `Array` primitive under the hood. + * Builder of the cache using `Array` primitive under the hood. It is an LRU cache, where the most recently accessed elements are at the end of the array, and the least recently accessed elements are at the front. * @param opts - Cache options, check `CacheOpts` type for more details. * @returns */ From 7b03364b81ec53ed9c03a3d980961f748cc20d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 21 Jun 2024 11:53:11 +0200 Subject: [PATCH 29/66] add getKeyIndex comment --- src/libs/memoize/cache/arrayCacheBuilder.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/memoize/cache/arrayCacheBuilder.ts b/src/libs/memoize/cache/arrayCacheBuilder.ts index 77ac66f47844..fd36426f00a1 100644 --- a/src/libs/memoize/cache/arrayCacheBuilder.ts +++ b/src/libs/memoize/cache/arrayCacheBuilder.ts @@ -12,6 +12,7 @@ function buildArrayCache(opts: CacheOpts): Cache { const keyComparator = getEqualityComparator(opts); function getKeyIndex(key: K) { + // We search the array backwards because the most recently added entries are at the end, and our heuristic follows the principles of an LRU cache - that the most recently added entries are most likely to be used again. for (let i = cache.length - 1; i >= 0; i--) { if (keyComparator(cache[i][0], key)) { return i; From a815e24d9d1baf76f5207e3bfc330f4e867cebe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 21 Jun 2024 11:55:21 +0200 Subject: [PATCH 30/66] arraCacheBuilder get early return --- src/libs/memoize/cache/arrayCacheBuilder.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libs/memoize/cache/arrayCacheBuilder.ts b/src/libs/memoize/cache/arrayCacheBuilder.ts index fd36426f00a1..8a1a3bb6a7a4 100644 --- a/src/libs/memoize/cache/arrayCacheBuilder.ts +++ b/src/libs/memoize/cache/arrayCacheBuilder.ts @@ -25,11 +25,13 @@ function buildArrayCache(opts: CacheOpts): Cache { get(key) { const index = getKeyIndex(key); - if (index !== -1) { - const [entry] = cache.splice(index, 1); - cache.push(entry); - return {value: entry[1]}; + if (index === -1) { + return; } + + const [entry] = cache.splice(index, 1); + cache.push(entry); + return {value: entry[1]}; }, set(key, value) { const index = getKeyIndex(key); From bc9b9ceb5e029c8bc0ae838ecc4f02e3cfa25632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 21 Jun 2024 12:15:14 +0200 Subject: [PATCH 31/66] remove unused APIs --- src/libs/memoize/cache/arrayCacheBuilder.ts | 13 ------------- src/libs/memoize/types.ts | 2 -- 2 files changed, 15 deletions(-) diff --git a/src/libs/memoize/cache/arrayCacheBuilder.ts b/src/libs/memoize/cache/arrayCacheBuilder.ts index 8a1a3bb6a7a4..ab8709b6bf6f 100644 --- a/src/libs/memoize/cache/arrayCacheBuilder.ts +++ b/src/libs/memoize/cache/arrayCacheBuilder.ts @@ -46,19 +46,6 @@ function buildArrayCache(opts: CacheOpts): Cache { cache.shift(); } }, - delete(key) { - const index = getKeyIndex(key); - const entryExists = index !== -1; - - if (entryExists) { - cache.splice(index, 1); - } - - return entryExists; - }, - clear() { - cache.length = 0; - }, snapshot: { keys() { return cache.map((entry) => entry[0]); diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index 7cd64fc07bb1..17a34a942670 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -5,8 +5,6 @@ type KeyComparator = (key1: K[], key2: K[]) => boolean; type Cache = { get: (key: K) => {value: V} | undefined; set: (key: K, value: V) => void; - delete: (key: K) => boolean; - clear: () => void; snapshot: { keys: () => K[]; values: () => V[]; From 67890fc40645db7915e21f1da1eca2e12f7b1f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 21 Jun 2024 12:20:24 +0200 Subject: [PATCH 32/66] use default export for const --- src/libs/memoize/const.ts | 3 +-- src/libs/memoize/utils.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/memoize/const.ts b/src/libs/memoize/const.ts index 7517236940e1..adc9706db597 100644 --- a/src/libs/memoize/const.ts +++ b/src/libs/memoize/const.ts @@ -7,5 +7,4 @@ const DEFAULT_OPTIONS = { cache: 'array', } satisfies Options; -// eslint-disable-next-line import/prefer-default-export -export {DEFAULT_OPTIONS}; +export default DEFAULT_OPTIONS; diff --git a/src/libs/memoize/utils.ts b/src/libs/memoize/utils.ts index 3f3ce620026a..3942ae8bfaae 100644 --- a/src/libs/memoize/utils.ts +++ b/src/libs/memoize/utils.ts @@ -1,5 +1,5 @@ import {deepEqual, shallowEqual} from 'fast-equals'; -import {DEFAULT_OPTIONS} from './const'; +import DEFAULT_OPTIONS from './const'; import type {CacheOpts, ClientOptions, Options} from './types'; function getEqualityComparator(opts: CacheOpts) { From fa904c0cc4989ea0826678b579805c6fd67fe2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 21 Jun 2024 12:27:42 +0200 Subject: [PATCH 33/66] rename monitoringEnable --- src/libs/memoize/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 9be892214461..9b476b9ca727 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -8,7 +8,7 @@ import {mergeOptions} from './utils'; * Global memoization class. Use it to orchestrate memoization (e.g. start/stop global monitoring). */ class Memoize { - static monitoringEnabled = false; + static isMonitoringEnabled = false; private static memoizedList: Array<{id: string; memoized: Stats}> = []; @@ -17,20 +17,20 @@ class Memoize { } static startMonitoring() { - if (this.monitoringEnabled) { + if (this.isMonitoringEnabled) { return; } - this.monitoringEnabled = true; + this.isMonitoringEnabled = true; Memoize.memoizedList.forEach(({memoized}) => { memoized.startMonitoring(); }); } static stopMonitoring() { - if (!this.monitoringEnabled) { + if (!this.isMonitoringEnabled) { return; } - this.monitoringEnabled = false; + this.isMonitoringEnabled = false; return Memoize.memoizedList.map(({id, memoized}) => ({id, stats: memoized.stopMonitoring()})); } } @@ -46,7 +46,7 @@ function memoize(fn: Fn, opts?: ClientOptions): M const cache = buildArrayCache, ReturnType>(options); - const stats = new MemoizeStats(options.monitor || Memoize.monitoringEnabled); + const stats = new MemoizeStats(options.monitor || Memoize.isMonitoringEnabled); const memoized = function memoized(...key: Parameters): ReturnType { const statsEntry = stats.createEntry(); From 1b0c9e63ee9883ab79333281f2790013d21a279d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= <62747088+kacper-mikolajczak@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:29:56 +0200 Subject: [PATCH 34/66] Fix typo in memoize desc Co-authored-by: Rory Abraham <47436092+roryabraham@users.noreply.github.com> --- src/libs/memoize/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 9b476b9ca727..ee08564407a6 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -38,7 +38,7 @@ class Memoize { /** * Wraps a function with a memoization layer. Useful for caching expensive calculations. * @param fn - Function to memoize - * @param options - Options for the memoization layer, for more details see `ClientOptions` type. + * @param opts - Options for the memoization layer, for more details see `ClientOptions` type. * @returns Memoized function with a cache API attached to it. */ function memoize(fn: Fn, opts?: ClientOptions): MemoizedFn { From e90d63e6026019a53270f2ac7bdd9a419ec0f589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 21 Jun 2024 12:38:13 +0200 Subject: [PATCH 35/66] MemoizeStats enabled -> isEnabled --- src/libs/memoize/stats.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/memoize/stats.ts b/src/libs/memoize/stats.ts index 6ea8840f54c9..f0bd3017943d 100644 --- a/src/libs/memoize/stats.ts +++ b/src/libs/memoize/stats.ts @@ -19,10 +19,10 @@ class MemoizeStats { private cacheSize = 0; - enabled = false; + isEnabled = false; constructor(enabled: boolean) { - this.enabled = enabled; + this.isEnabled = enabled; } // See https://en.wikipedia.org/wiki/Moving_average#Cumulative_average @@ -53,14 +53,14 @@ class MemoizeStats { // eslint-disable-next-line rulesdir/prefer-early-return private saveEntry(entry: Partial) { - if (this.enabled && MemoizeStats.isMemoizeStatsEntry(entry)) { + if (this.isEnabled && MemoizeStats.isMemoizeStatsEntry(entry)) { this.cumulateEntry(entry); } } createEntry() { // If monitoring is disabled, return a dummy object that does nothing - if (!this.enabled) { + if (!this.isEnabled) { return { track: () => {}, save: () => {}, @@ -78,7 +78,7 @@ class MemoizeStats { } startMonitoring() { - this.enabled = true; + this.isEnabled = true; this.calls = 0; this.hits = 0; this.avgKeyLength = 0; @@ -88,7 +88,7 @@ class MemoizeStats { } stopMonitoring() { - this.enabled = false; + this.isEnabled = false; return { calls: this.calls, From c6910ac9ad54de0ab0888e98c471fba52fd179d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 21 Jun 2024 12:39:18 +0200 Subject: [PATCH 36/66] add explanation to cumulativeAvg --- src/libs/memoize/stats.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/memoize/stats.ts b/src/libs/memoize/stats.ts index f0bd3017943d..fedaf5f071eb 100644 --- a/src/libs/memoize/stats.ts +++ b/src/libs/memoize/stats.ts @@ -28,6 +28,7 @@ class MemoizeStats { // See https://en.wikipedia.org/wiki/Moving_average#Cumulative_average private calculateCumulativeAvg(avg: number, length: number, value: number) { const result = (avg * (length - 1) + value) / length; + // If the length is 0, we return the average. For example, when calculating average cache retrieval time, hits may be 0, and in that case we want to return the current average cache retrieval time return Number.isFinite(result) ? result : avg; } From dc648198095c37c69915ec69d83f82c19a647345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 21 Jun 2024 12:41:43 +0200 Subject: [PATCH 37/66] take out the isMemoizeStatsEntry identity function --- src/libs/memoize/stats.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/memoize/stats.ts b/src/libs/memoize/stats.ts index fedaf5f071eb..474ca0e3b1ac 100644 --- a/src/libs/memoize/stats.ts +++ b/src/libs/memoize/stats.ts @@ -6,6 +6,11 @@ type MemoizeStatsEntry = { cacheSize: number; }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isMemoizeStatsEntry(entry: any): entry is MemoizeStatsEntry { + return entry.keyLength !== undefined && entry.didHit !== undefined && entry.cacheRetrievalTime !== undefined; +} + class MemoizeStats { private calls = 0; @@ -47,14 +52,9 @@ class MemoizeStats { } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - static isMemoizeStatsEntry(entry: any): entry is MemoizeStatsEntry { - return entry.keyLength !== undefined && entry.didHit !== undefined && entry.cacheRetrievalTime !== undefined; - } - // eslint-disable-next-line rulesdir/prefer-early-return private saveEntry(entry: Partial) { - if (this.isEnabled && MemoizeStats.isMemoizeStatsEntry(entry)) { + if (this.isEnabled && isMemoizeStatsEntry(entry)) { this.cumulateEntry(entry); } } From 1f3bd24cd4139c7e4d82ab3a8e3ee9a18a44453b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 21 Jun 2024 12:51:45 +0200 Subject: [PATCH 38/66] fix saveEntry --- src/libs/memoize/stats.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/libs/memoize/stats.ts b/src/libs/memoize/stats.ts index 474ca0e3b1ac..3855c3202997 100644 --- a/src/libs/memoize/stats.ts +++ b/src/libs/memoize/stats.ts @@ -1,3 +1,5 @@ +import Log from '@libs/Log'; + type MemoizeStatsEntry = { keyLength: number; didHit: boolean; @@ -52,11 +54,17 @@ class MemoizeStats { } } - // eslint-disable-next-line rulesdir/prefer-early-return private saveEntry(entry: Partial) { - if (this.isEnabled && isMemoizeStatsEntry(entry)) { - this.cumulateEntry(entry); + if (!this.isEnabled) { + return; } + + if (!isMemoizeStatsEntry(entry)) { + Log.warn('MemoizeStats:saveEntry: Invalid entry', entry); + return; + } + + return this.cumulateEntry(entry); } createEntry() { From 9390547ac79e866c36e1679eabd8bf4a3e92974b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 21 Jun 2024 13:04:37 +0200 Subject: [PATCH 39/66] move eslint-disable --- src/libs/memoize/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index ee08564407a6..e146dd6eee60 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ import buildArrayCache from './cache/arrayCacheBuilder'; import {MemoizeStats} from './stats'; import type {ClientOptions, MemoizedFn, MemoizeFnPredicate, Stats} from './types'; @@ -69,6 +68,7 @@ function memoize(fn: Fn, opts?: ClientOptions): M statsEntry.track('cacheSize', cache.size); statsEntry.save(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return cached.value; }; From 65139da99b87653e36130794bccdcb3e7b7269fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Sat, 22 Jun 2024 00:36:18 +0200 Subject: [PATCH 40/66] fix tests --- tests/unit/memoizeTest.ts | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/tests/unit/memoizeTest.ts b/tests/unit/memoizeTest.ts index ddc581ec87fc..b3d3b5306a7e 100644 --- a/tests/unit/memoizeTest.ts +++ b/tests/unit/memoizeTest.ts @@ -67,30 +67,6 @@ describe('memoize test', () => { expect(fn).toHaveBeenCalledTimes(4); }); - it('should delete cache entry', () => { - const fn = jest.fn(); - const memoizedFn = memoize(fn); - - memoizedFn(1, 2); - memoizedFn.cache.delete([1, 2]); - memoizedFn(1, 2); - - expect(fn).toHaveBeenCalledTimes(2); - }); - - it('should clear cache', () => { - const fn = jest.fn(); - const memoizedFn = memoize(fn); - - memoizedFn(1, 2); - memoizedFn(2, 3); - memoizedFn.cache.clear(); - memoizedFn(1, 2); - memoizedFn(2, 3); - - expect(fn).toHaveBeenCalledTimes(4); - }); - it('should return cache snapshot', () => { const fn = (a: number, b: number) => a + b; const memoizedFn = memoize(fn); From 0a0614909facbc1d2df88bf2d943b5a6a1add9f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Sat, 22 Jun 2024 01:18:36 +0200 Subject: [PATCH 41/66] fix package-lock.json --- package-lock.json | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 53ae2d466ec3..75d95a4c30b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20989,12 +20989,9 @@ "license": "Apache-2.0" }, "node_modules/fast-equals": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", - "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", - "engines": { - "node": ">=6.0.0" - } + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==" }, "node_modules/fast-glob": { "version": "3.3.1", @@ -31647,11 +31644,6 @@ } } }, - "node_modules/react-native-onyx/node_modules/fast-equals": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", - "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==" - }, "node_modules/react-native-pager-view": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.2.3.tgz", From 9226c50866b63efa6e20cc5520dc2d4fba814682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 1 Jul 2024 12:53:26 +0200 Subject: [PATCH 42/66] add eslint recommendation --- .eslintrc.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index d8f8c15eb878..46690ee61348 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -62,6 +62,10 @@ const restrictedImportPaths = [ importNames: ['Device'], message: "Do not import Device directly, it's known to make VSCode's IntelliSense crash. Please import the desired module from `expensify-common/dist/Device` instead.", }, + { + name: 'lodash/memoize', + message: "Please use '@src/libs/memoize' instead.", + }, ]; const restrictedImportPatterns = [ From 607db57f0eea459f07bf2fbc6a47d53f844dff9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 1 Jul 2024 14:31:06 +0200 Subject: [PATCH 43/66] add getSet method to cache --- src/libs/memoize/cache/arrayCacheBuilder.ts | 19 +++++++++++++++++++ src/libs/memoize/index.ts | 16 +++++++--------- src/libs/memoize/stats.ts | 2 ++ src/libs/memoize/types.ts | 5 ++++- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/libs/memoize/cache/arrayCacheBuilder.ts b/src/libs/memoize/cache/arrayCacheBuilder.ts index ab8709b6bf6f..fdce37a304ed 100644 --- a/src/libs/memoize/cache/arrayCacheBuilder.ts +++ b/src/libs/memoize/cache/arrayCacheBuilder.ts @@ -46,6 +46,25 @@ function buildArrayCache(opts: CacheOpts): Cache { cache.shift(); } }, + getSet(key: K, valueProducer: () => V) { + const index = getKeyIndex(key); + + if (index !== -1) { + const [entry] = cache.splice(index, 1); + cache.push(entry); + return {value: entry[1]}; + } + + const value = valueProducer(); + + cache.push([key, value]); + + if (cache.length > opts.maxSize) { + cache.shift(); + } + + return {value}; + }, snapshot: { keys() { return cache.map((entry) => entry[0]); diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index e146dd6eee60..327c6a5e17f6 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import buildArrayCache from './cache/arrayCacheBuilder'; import {MemoizeStats} from './stats'; import type {ClientOptions, MemoizedFn, MemoizeFnPredicate, Stats} from './types'; @@ -50,25 +51,22 @@ function memoize(fn: Fn, opts?: ClientOptions): M const memoized = function memoized(...key: Parameters): ReturnType { const statsEntry = stats.createEntry(); statsEntry.track('keyLength', key.length); + statsEntry.track('didHit', true); const retrievalTimeStart = performance.now(); - let cached = cache.get(key); - statsEntry.track('cacheRetrievalTime', performance.now() - retrievalTimeStart); - statsEntry.track('didHit', !!cached); - - if (!cached) { + const cached = cache.getSet(key, () => { const fnTimeStart = performance.now(); const result = fn(...key); statsEntry.track('fnTime', performance.now() - fnTimeStart); + statsEntry.track('didHit', false); - cached = {value: result}; - cache.set(key, result as ReturnType); - } + return result; + }); + statsEntry.track('cacheRetrievalTime', performance.now() - retrievalTimeStart - (statsEntry.get('fnTime') ?? 0)); statsEntry.track('cacheSize', cache.size); statsEntry.save(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return cached.value; }; diff --git a/src/libs/memoize/stats.ts b/src/libs/memoize/stats.ts index 3855c3202997..cf986f8cb698 100644 --- a/src/libs/memoize/stats.ts +++ b/src/libs/memoize/stats.ts @@ -72,6 +72,7 @@ class MemoizeStats { if (!this.isEnabled) { return { track: () => {}, + get: () => {}, save: () => {}, }; } @@ -82,6 +83,7 @@ class MemoizeStats { track:

(cacheProp: P, value: MemoizeStatsEntry[P]) => { entry[cacheProp] = value; }, + get:

(cacheProp: P) => entry[cacheProp], save: () => this.saveEntry(entry), }; } diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index 17a34a942670..7ba06e48dda8 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -2,9 +2,12 @@ import type {MemoizeStats} from './stats'; type KeyComparator = (key1: K[], key2: K[]) => boolean; +type ValueBox = {value: V}; + type Cache = { - get: (key: K) => {value: V} | undefined; + get: (key: K) => ValueBox | undefined; set: (key: K, value: V) => void; + getSet: (key: K, valueProducer: () => V) => ValueBox; snapshot: { keys: () => K[]; values: () => V[]; From 4ae9a9cc996563057323d096cd997f7853379915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 1 Jul 2024 15:02:53 +0200 Subject: [PATCH 44/66] add trackTime method to statsEntry --- src/libs/memoize/index.ts | 5 +++-- src/libs/memoize/stats.ts | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 327c6a5e17f6..29c5f5650265 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -57,12 +57,13 @@ function memoize(fn: Fn, opts?: ClientOptions): M const cached = cache.getSet(key, () => { const fnTimeStart = performance.now(); const result = fn(...key); - statsEntry.track('fnTime', performance.now() - fnTimeStart); + statsEntry.trackTime('fnTime', fnTimeStart); statsEntry.track('didHit', false); return result; }); - statsEntry.track('cacheRetrievalTime', performance.now() - retrievalTimeStart - (statsEntry.get('fnTime') ?? 0)); + // Subtract the time it took to run the function from the total retrieval time + statsEntry.track('cacheRetrievalTime', retrievalTimeStart + (statsEntry.get('fnTime') ?? 0)); statsEntry.track('cacheSize', cache.size); statsEntry.save(); diff --git a/src/libs/memoize/stats.ts b/src/libs/memoize/stats.ts index cf986f8cb698..2ecf3989d25e 100644 --- a/src/libs/memoize/stats.ts +++ b/src/libs/memoize/stats.ts @@ -74,6 +74,7 @@ class MemoizeStats { track: () => {}, get: () => {}, save: () => {}, + trackTime: () => {}, }; } @@ -83,6 +84,9 @@ class MemoizeStats { track:

(cacheProp: P, value: MemoizeStatsEntry[P]) => { entry[cacheProp] = value; }, + trackTime:

>(cacheProp: P, startTime: number, endTime = performance.now()) => { + entry[cacheProp] = endTime - startTime; + }, get:

(cacheProp: P) => entry[cacheProp], save: () => this.saveEntry(entry), }; From 0228144c294a7acf2f8d8a7d7fc20867ddde2731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 1 Jul 2024 15:05:24 +0200 Subject: [PATCH 45/66] fix typo --- src/libs/memoize/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 29c5f5650265..e84c0299c482 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -63,7 +63,7 @@ function memoize(fn: Fn, opts?: ClientOptions): M return result; }); // Subtract the time it took to run the function from the total retrieval time - statsEntry.track('cacheRetrievalTime', retrievalTimeStart + (statsEntry.get('fnTime') ?? 0)); + statsEntry.trackTime('cacheRetrievalTime', retrievalTimeStart + (statsEntry.get('fnTime') ?? 0)); statsEntry.track('cacheSize', cache.size); statsEntry.save(); From e95e208dff18ccb819cd2057c17bf8db90624441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 1 Jul 2024 15:32:42 +0200 Subject: [PATCH 46/66] refactor keyComparator --- src/libs/memoize/cache/arrayCacheBuilder.ts | 7 +++---- src/libs/memoize/index.ts | 4 ++-- src/libs/memoize/types.ts | 2 +- src/libs/memoize/utils.ts | 20 +++++++++++--------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/libs/memoize/cache/arrayCacheBuilder.ts b/src/libs/memoize/cache/arrayCacheBuilder.ts index fdce37a304ed..ff0e360e31ce 100644 --- a/src/libs/memoize/cache/arrayCacheBuilder.ts +++ b/src/libs/memoize/cache/arrayCacheBuilder.ts @@ -1,5 +1,4 @@ import type {Cache, CacheOpts} from '@libs/memoize/types'; -import {getEqualityComparator} from '@libs/memoize/utils'; /** * Builder of the cache using `Array` primitive under the hood. It is an LRU cache, where the most recently accessed elements are at the end of the array, and the least recently accessed elements are at the front. @@ -9,7 +8,7 @@ import {getEqualityComparator} from '@libs/memoize/utils'; function buildArrayCache(opts: CacheOpts): Cache { const cache: Array<[K, V]> = []; - const keyComparator = getEqualityComparator(opts); + const {maxSize, keyComparator} = opts; function getKeyIndex(key: K) { // We search the array backwards because the most recently added entries are at the end, and our heuristic follows the principles of an LRU cache - that the most recently added entries are most likely to be used again. @@ -42,7 +41,7 @@ function buildArrayCache(opts: CacheOpts): Cache { cache.push([key, value]); - if (cache.length > opts.maxSize) { + if (cache.length > maxSize) { cache.shift(); } }, @@ -59,7 +58,7 @@ function buildArrayCache(opts: CacheOpts): Cache { cache.push([key, value]); - if (cache.length > opts.maxSize) { + if (cache.length > maxSize) { cache.shift(); } diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index e84c0299c482..d3400ba90769 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -2,7 +2,7 @@ import buildArrayCache from './cache/arrayCacheBuilder'; import {MemoizeStats} from './stats'; import type {ClientOptions, MemoizedFn, MemoizeFnPredicate, Stats} from './types'; -import {mergeOptions} from './utils'; +import {getEqualityComparator, mergeOptions} from './utils'; /** * Global memoization class. Use it to orchestrate memoization (e.g. start/stop global monitoring). @@ -44,7 +44,7 @@ class Memoize { function memoize(fn: Fn, opts?: ClientOptions): MemoizedFn { const options = mergeOptions(opts); - const cache = buildArrayCache, ReturnType>(options); + const cache = buildArrayCache, ReturnType>({maxSize: options.maxSize, keyComparator: getEqualityComparator(options)}); const stats = new MemoizeStats(options.monitor || Memoize.isMonitoringEnabled); diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index 7ba06e48dda8..c0e2e74b6ac8 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -18,7 +18,7 @@ type Cache = { type CacheOpts = { maxSize: number; - equality: 'deep' | 'shallow' | KeyComparator; + keyComparator: KeyComparator; }; type InternalOptions = { diff --git a/src/libs/memoize/utils.ts b/src/libs/memoize/utils.ts index 3942ae8bfaae..55912a02f159 100644 --- a/src/libs/memoize/utils.ts +++ b/src/libs/memoize/utils.ts @@ -1,16 +1,18 @@ import {deepEqual, shallowEqual} from 'fast-equals'; import DEFAULT_OPTIONS from './const'; -import type {CacheOpts, ClientOptions, Options} from './types'; +import type {ClientOptions, KeyComparator, Options} from './types'; -function getEqualityComparator(opts: CacheOpts) { - switch (opts.equality) { - case 'deep': - return deepEqual; - case 'shallow': - return shallowEqual; - default: - return opts.equality; +function getEqualityComparator(opts: Options): KeyComparator { + // Use the custom equality comparator if it is provided + if (typeof opts.equality === 'function') { + return opts.equality; } + + if (opts.equality === 'shallow') { + return shallowEqual; + } + + return deepEqual; } function mergeOptions(options?: ClientOptions): Options { From 5e13d40f9c408c340160723a17792a10092f9cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Sun, 7 Jul 2024 19:58:42 +0200 Subject: [PATCH 47/66] add maxArgs option --- src/libs/memoize/index.ts | 8 +++++--- src/libs/memoize/types.ts | 1 + src/libs/memoize/utils.ts | 31 ++++++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index d3400ba90769..bd4c8c448bdc 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -2,7 +2,7 @@ import buildArrayCache from './cache/arrayCacheBuilder'; import {MemoizeStats} from './stats'; import type {ClientOptions, MemoizedFn, MemoizeFnPredicate, Stats} from './types'; -import {getEqualityComparator, mergeOptions} from './utils'; +import {getEqualityComparator, mergeOptions, truncateArgs} from './utils'; /** * Global memoization class. Use it to orchestrate memoization (e.g. start/stop global monitoring). @@ -48,7 +48,9 @@ function memoize(fn: Fn, opts?: ClientOptions): M const stats = new MemoizeStats(options.monitor || Memoize.isMonitoringEnabled); - const memoized = function memoized(...key: Parameters): ReturnType { + const memoized = function memoized(...args: Parameters): ReturnType { + const key = truncateArgs(args, options.maxArgs) as Parameters; + const statsEntry = stats.createEntry(); statsEntry.track('keyLength', key.length); statsEntry.track('didHit', true); @@ -56,7 +58,7 @@ function memoize(fn: Fn, opts?: ClientOptions): M const retrievalTimeStart = performance.now(); const cached = cache.getSet(key, () => { const fnTimeStart = performance.now(); - const result = fn(...key); + const result = fn(...args); statsEntry.trackTime('fnTime', fnTimeStart); statsEntry.track('didHit', false); diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index c0e2e74b6ac8..206be3d248bc 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -29,6 +29,7 @@ type Options = { maxSize: number; equality: 'deep' | 'shallow' | KeyComparator; monitor: boolean; + maxArgs?: number; monitoringName?: string; } & InternalOptions; diff --git a/src/libs/memoize/utils.ts b/src/libs/memoize/utils.ts index 55912a02f159..87c6f98eede5 100644 --- a/src/libs/memoize/utils.ts +++ b/src/libs/memoize/utils.ts @@ -22,4 +22,33 @@ function mergeOptions(options?: ClientOptions): Options { return {...DEFAULT_OPTIONS, ...options}; } -export {mergeOptions, getEqualityComparator}; +function truncateArgs(args: T, maxArgs?: number) { + if (maxArgs === undefined) { + return args; + } + + if (maxArgs >= args.length) { + return args; + } + + // Hot paths are declared explicitly to avoid the overhead of the slice method + if (maxArgs === 0) { + return []; + } + + if (maxArgs === 1) { + return [args[0]]; + } + + if (maxArgs === 2) { + return [args[0], args[1]]; + } + + if (maxArgs === 3) { + return [args[0], args[1], args[2]]; + } + + return args.slice(0, maxArgs); +} + +export {mergeOptions, getEqualityComparator, truncateArgs}; From d8e82a0c5171802bdc39a2cdaa2ec6eb470cb2b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Sun, 7 Jul 2024 20:01:06 +0200 Subject: [PATCH 48/66] fix lodash memoize instance --- src/libs/UnreadIndicatorUpdater/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/UnreadIndicatorUpdater/index.ts b/src/libs/UnreadIndicatorUpdater/index.ts index 7698433c33c1..1fe9e31eaa25 100644 --- a/src/libs/UnreadIndicatorUpdater/index.ts +++ b/src/libs/UnreadIndicatorUpdater/index.ts @@ -1,7 +1,7 @@ import debounce from 'lodash/debounce'; -import memoize from 'lodash/memoize'; import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import memoize from '@libs/memoize'; import * as ReportUtils from '@libs/ReportUtils'; import Navigation, {navigationRef} from '@navigation/Navigation'; import CONST from '@src/CONST'; @@ -37,7 +37,7 @@ export default function getUnreadReportsForUnreadIndicator(reports: OnyxCollecti ); } -const memoizedGetUnreadReportsForUnreadIndicator = memoize(getUnreadReportsForUnreadIndicator); +const memoizedGetUnreadReportsForUnreadIndicator = memoize(getUnreadReportsForUnreadIndicator, {maxArgs: 1}); const triggerUnreadUpdate = debounce(() => { const currentReportID = navigationRef.isReady() ? Navigation.getTopmostReportId() ?? '-1' : '-1'; From acbe2012b9103953779fcda7c45287dd38e2fedf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Sun, 7 Jul 2024 20:17:26 +0200 Subject: [PATCH 49/66] supress TS errors --- src/libs/memoize/index.ts | 1 + src/libs/memoize/stats.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index bd4c8c448bdc..6b75d23486bf 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -58,6 +58,7 @@ function memoize(fn: Fn, opts?: ClientOptions): M const retrievalTimeStart = performance.now(); const cached = cache.getSet(key, () => { const fnTimeStart = performance.now(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const result = fn(...args); statsEntry.trackTime('fnTime', fnTimeStart); statsEntry.track('didHit', false); diff --git a/src/libs/memoize/stats.ts b/src/libs/memoize/stats.ts index 2ecf3989d25e..5cb6f3eb7532 100644 --- a/src/libs/memoize/stats.ts +++ b/src/libs/memoize/stats.ts @@ -10,6 +10,7 @@ type MemoizeStatsEntry = { // eslint-disable-next-line @typescript-eslint/no-explicit-any function isMemoizeStatsEntry(entry: any): entry is MemoizeStatsEntry { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return entry.keyLength !== undefined && entry.didHit !== undefined && entry.cacheRetrievalTime !== undefined; } From 8055ec0e68cdbbf35f05a3ced5ccc7094cfeff0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 8 Jul 2024 15:26:29 +0200 Subject: [PATCH 50/66] fix constructable --- src/libs/NumberFormatUtils.ts | 6 +++--- src/libs/memoize/index.ts | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/NumberFormatUtils.ts b/src/libs/NumberFormatUtils.ts index 920dcc815bbc..e65c245a02e1 100644 --- a/src/libs/NumberFormatUtils.ts +++ b/src/libs/NumberFormatUtils.ts @@ -2,14 +2,14 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; import memoize from './memoize'; -const numberFormatter = memoize(Intl.NumberFormat, {maxSize: 10}); +const MemoizedNumberFormat = memoize(Intl.NumberFormat, {maxSize: 10}); function format(locale: ValueOf, number: number, options?: Intl.NumberFormatOptions): string { - return numberFormatter(locale, options).format(number); + return new MemoizedNumberFormat(locale, options).format(number); } function formatToParts(locale: ValueOf, number: number, options?: Intl.NumberFormatOptions): Intl.NumberFormatPart[] { - return numberFormatter(locale, options).formatToParts(number); + return new MemoizedNumberFormat(locale, options).formatToParts(number); } export {format, formatToParts}; diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 6b75d23486bf..05a9e58f74ea 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ +import type {Constructor} from 'type-fest'; import buildArrayCache from './cache/arrayCacheBuilder'; import {MemoizeStats} from './stats'; import type {ClientOptions, MemoizedFn, MemoizeFnPredicate, Stats} from './types'; @@ -49,6 +50,7 @@ function memoize(fn: Fn, opts?: ClientOptions): M const stats = new MemoizeStats(options.monitor || Memoize.isMonitoringEnabled); const memoized = function memoized(...args: Parameters): ReturnType { + const constructable = !!new.target; const key = truncateArgs(args, options.maxArgs) as Parameters; const statsEntry = stats.createEntry(); @@ -58,8 +60,9 @@ function memoize(fn: Fn, opts?: ClientOptions): M const retrievalTimeStart = performance.now(); const cached = cache.getSet(key, () => { const fnTimeStart = performance.now(); + // If the function is constructable, we need to call it with the `new` keyword // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const result = fn(...args); + const result = constructable ? new (fn as unknown as Constructor, Parameters>)(...args) : fn(...args); statsEntry.trackTime('fnTime', fnTimeStart); statsEntry.track('didHit', false); From dc9850ef66ab7c1c0449a6237cfdaebe60e8e8f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 9 Jul 2024 12:44:51 +0200 Subject: [PATCH 51/66] add description for getSet method --- src/libs/memoize/types.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index 206be3d248bc..8172b06e8872 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -7,6 +7,12 @@ type ValueBox = {value: V}; type Cache = { get: (key: K) => ValueBox | undefined; set: (key: K, value: V) => void; + /** + * Get the value for the key if it exists, otherwise set the value to the result of the valueProducer and return it. + * @param key The key to get or set + * @param valueProducer The function to produce the value if the key does not exist + * @returns The value for the key + */ getSet: (key: K, valueProducer: () => V) => ValueBox; snapshot: { keys: () => K[]; From 79d19c008ff9c714075e9cc23d84be71278f73e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 9 Jul 2024 12:48:28 +0200 Subject: [PATCH 52/66] lodash memoize named export restriction --- .eslintrc.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 1806ebef7257..5219c2b7a770 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -66,6 +66,11 @@ const restrictedImportPaths = [ name: 'lodash/memoize', message: "Please use '@src/libs/memoize' instead.", }, + { + name: 'lodash', + importNames: ['memoize'], + message: "Please use '@src/libs/memoize' instead.", + }, ]; const restrictedImportPatterns = [ From 4ecb32782827ee61fb1a16d82420c8a03c435306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 9 Jul 2024 12:50:20 +0200 Subject: [PATCH 53/66] add fast-equals license --- package-lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package-lock.json b/package-lock.json index 224c7c55f6bd..1a589740051d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26532,6 +26532,7 @@ }, "node_modules/fast-equals": { "version": "4.0.3", + "license": "MIT", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==" }, From baf7a5ff6101f62eb7b49aac71f67e6b464c2188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 9 Jul 2024 14:41:52 +0200 Subject: [PATCH 54/66] fix iOS Intl.NumberFormat polyfill --- src/libs/NumberFormatUtils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libs/NumberFormatUtils.ts b/src/libs/NumberFormatUtils.ts index e65c245a02e1..a1992c769593 100644 --- a/src/libs/NumberFormatUtils.ts +++ b/src/libs/NumberFormatUtils.ts @@ -1,7 +1,10 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; +import intlPolyfill from './IntlPolyfill'; import memoize from './memoize'; +intlPolyfill(); + const MemoizedNumberFormat = memoize(Intl.NumberFormat, {maxSize: 10}); function format(locale: ValueOf, number: number, options?: Intl.NumberFormatOptions): string { From 50b98d8987b93e06c2e5346cf7d9675e6c9047a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 10 Jul 2024 10:04:14 +0200 Subject: [PATCH 55/66] NumberFormatUtils ios platform variant --- src/libs/NumberFormatUtils/index.ios.ts | 21 +++++++++++++++++++ .../index.ts} | 5 +---- 2 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 src/libs/NumberFormatUtils/index.ios.ts rename src/libs/{NumberFormatUtils.ts => NumberFormatUtils/index.ts} (86%) diff --git a/src/libs/NumberFormatUtils/index.ios.ts b/src/libs/NumberFormatUtils/index.ios.ts new file mode 100644 index 000000000000..bc4e13935de3 --- /dev/null +++ b/src/libs/NumberFormatUtils/index.ios.ts @@ -0,0 +1,21 @@ +import type {ValueOf} from 'type-fest'; +import intlPolyfill from '@libs/IntlPolyfill'; +import memoize from '@libs/memoize'; +import type CONST from '@src/CONST'; + +// On iOS, polyfills from `additionalSetup` are applied after memoization, which results in incorrect cache entry of `Intl.NumberFormat` (e.g. lacking `formatToParts` method). +// To fix this, we need to apply the polyfill manually before memoization. +// For further information, see: https://github.com/Expensify/App/pull/43868#issuecomment-2217637217 +intlPolyfill(); + +const MemoizedNumberFormat = memoize(Intl.NumberFormat, {maxSize: 10}); + +function format(locale: ValueOf, number: number, options?: Intl.NumberFormatOptions): string { + return new MemoizedNumberFormat(locale, options).format(number); +} + +function formatToParts(locale: ValueOf, number: number, options?: Intl.NumberFormatOptions): Intl.NumberFormatPart[] { + return new MemoizedNumberFormat(locale, options).formatToParts(number); +} + +export {format, formatToParts}; diff --git a/src/libs/NumberFormatUtils.ts b/src/libs/NumberFormatUtils/index.ts similarity index 86% rename from src/libs/NumberFormatUtils.ts rename to src/libs/NumberFormatUtils/index.ts index a1992c769593..1a81491bca89 100644 --- a/src/libs/NumberFormatUtils.ts +++ b/src/libs/NumberFormatUtils/index.ts @@ -1,9 +1,6 @@ import type {ValueOf} from 'type-fest'; +import memoize from '@libs/memoize'; import type CONST from '@src/CONST'; -import intlPolyfill from './IntlPolyfill'; -import memoize from './memoize'; - -intlPolyfill(); const MemoizedNumberFormat = memoize(Intl.NumberFormat, {maxSize: 10}); From 7f8aea1bec5780e318779b9496407d9ddbf8d96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 10 Jul 2024 10:42:27 +0200 Subject: [PATCH 56/66] restructure ios variant --- src/libs/NumberFormatUtils/index.ios.ts | 21 ------------------- src/libs/NumberFormatUtils/index.ts | 3 +++ .../NumberFormatUtils/intlPolyfill.ios.ts | 10 +++++++++ src/libs/NumberFormatUtils/intlPolyfill.ts | 2 ++ 4 files changed, 15 insertions(+), 21 deletions(-) delete mode 100644 src/libs/NumberFormatUtils/index.ios.ts create mode 100644 src/libs/NumberFormatUtils/intlPolyfill.ios.ts create mode 100644 src/libs/NumberFormatUtils/intlPolyfill.ts diff --git a/src/libs/NumberFormatUtils/index.ios.ts b/src/libs/NumberFormatUtils/index.ios.ts deleted file mode 100644 index bc4e13935de3..000000000000 --- a/src/libs/NumberFormatUtils/index.ios.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type {ValueOf} from 'type-fest'; -import intlPolyfill from '@libs/IntlPolyfill'; -import memoize from '@libs/memoize'; -import type CONST from '@src/CONST'; - -// On iOS, polyfills from `additionalSetup` are applied after memoization, which results in incorrect cache entry of `Intl.NumberFormat` (e.g. lacking `formatToParts` method). -// To fix this, we need to apply the polyfill manually before memoization. -// For further information, see: https://github.com/Expensify/App/pull/43868#issuecomment-2217637217 -intlPolyfill(); - -const MemoizedNumberFormat = memoize(Intl.NumberFormat, {maxSize: 10}); - -function format(locale: ValueOf, number: number, options?: Intl.NumberFormatOptions): string { - return new MemoizedNumberFormat(locale, options).format(number); -} - -function formatToParts(locale: ValueOf, number: number, options?: Intl.NumberFormatOptions): Intl.NumberFormatPart[] { - return new MemoizedNumberFormat(locale, options).formatToParts(number); -} - -export {format, formatToParts}; diff --git a/src/libs/NumberFormatUtils/index.ts b/src/libs/NumberFormatUtils/index.ts index 1a81491bca89..57ad6c63039b 100644 --- a/src/libs/NumberFormatUtils/index.ts +++ b/src/libs/NumberFormatUtils/index.ts @@ -1,6 +1,9 @@ import type {ValueOf} from 'type-fest'; import memoize from '@libs/memoize'; import type CONST from '@src/CONST'; +import initPolyfill from './intlPolyfill'; + +initPolyfill(); const MemoizedNumberFormat = memoize(Intl.NumberFormat, {maxSize: 10}); diff --git a/src/libs/NumberFormatUtils/intlPolyfill.ios.ts b/src/libs/NumberFormatUtils/intlPolyfill.ios.ts new file mode 100644 index 000000000000..4745284c0b61 --- /dev/null +++ b/src/libs/NumberFormatUtils/intlPolyfill.ios.ts @@ -0,0 +1,10 @@ +import intlPolyfill from '@libs/IntlPolyfill'; + +// On iOS, polyfills from `additionalSetup` are applied after memoization, which results in incorrect cache entry of `Intl.NumberFormat` (e.g. lacking `formatToParts` method). +// To fix this, we need to apply the polyfill manually before memoization. +// For further information, see: https://github.com/Expensify/App/pull/43868#issuecomment-2217637217 +const initPolyfill = () => { + intlPolyfill(); +}; + +export default initPolyfill; diff --git a/src/libs/NumberFormatUtils/intlPolyfill.ts b/src/libs/NumberFormatUtils/intlPolyfill.ts new file mode 100644 index 000000000000..31fedd6a01b6 --- /dev/null +++ b/src/libs/NumberFormatUtils/intlPolyfill.ts @@ -0,0 +1,2 @@ +const initPolyfill = () => {}; +export default initPolyfill; From 62b4684f48a734f605e6cebc00bc46522dfc66dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 15 Jul 2024 12:12:35 +0200 Subject: [PATCH 57/66] fix package-lock typo --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 923bf821a85c..6874f7c56f48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26561,9 +26561,9 @@ }, "node_modules/fast-equals": { "version": "4.0.3", - "license": "MIT", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", - "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==" + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.2", From 5c173c3aede5416187962d4bcafda52e4a87b52a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 15 Jul 2024 12:18:40 +0200 Subject: [PATCH 58/66] refactor ArrayCache --- .../{arrayCacheBuilder.ts => ArrayCache.ts} | 22 ++++++--------- src/libs/memoize/cache/types.ts | 26 +++++++++++++++++ src/libs/memoize/index.ts | 4 +-- src/libs/memoize/types.ts | 28 ++----------------- 4 files changed, 38 insertions(+), 42 deletions(-) rename src/libs/memoize/cache/{arrayCacheBuilder.ts => ArrayCache.ts} (77%) create mode 100644 src/libs/memoize/cache/types.ts diff --git a/src/libs/memoize/cache/arrayCacheBuilder.ts b/src/libs/memoize/cache/ArrayCache.ts similarity index 77% rename from src/libs/memoize/cache/arrayCacheBuilder.ts rename to src/libs/memoize/cache/ArrayCache.ts index ff0e360e31ce..a655a0cd6f82 100644 --- a/src/libs/memoize/cache/arrayCacheBuilder.ts +++ b/src/libs/memoize/cache/ArrayCache.ts @@ -1,14 +1,14 @@ -import type {Cache, CacheOpts} from '@libs/memoize/types'; +import type {Cache, CacheConfig} from './types'; /** * Builder of the cache using `Array` primitive under the hood. It is an LRU cache, where the most recently accessed elements are at the end of the array, and the least recently accessed elements are at the front. - * @param opts - Cache options, check `CacheOpts` type for more details. + * @param config - Cache configuration, check `CacheConfig` type for more details. * @returns */ -function buildArrayCache(opts: CacheOpts): Cache { +function ArrayCache(config: CacheConfig): Cache { const cache: Array<[K, V]> = []; - const {maxSize, keyComparator} = opts; + const {maxSize, keyComparator} = config; function getKeyIndex(key: K) { // We search the array backwards because the most recently added entries are at the end, and our heuristic follows the principles of an LRU cache - that the most recently added entries are most likely to be used again. @@ -65,15 +65,9 @@ function buildArrayCache(opts: CacheOpts): Cache { return {value}; }, snapshot: { - keys() { - return cache.map((entry) => entry[0]); - }, - values() { - return cache.map((entry) => entry[1]); - }, - entries() { - return [...cache]; - }, + keys: () => cache.map((entry) => entry[0]), + values: () => cache.map((entry) => entry[1]), + entries: () => [...cache], }, get size() { return cache.length; @@ -81,4 +75,4 @@ function buildArrayCache(opts: CacheOpts): Cache { }; } -export default buildArrayCache; +export default ArrayCache; diff --git a/src/libs/memoize/cache/types.ts b/src/libs/memoize/cache/types.ts new file mode 100644 index 000000000000..fef88c7bc36b --- /dev/null +++ b/src/libs/memoize/cache/types.ts @@ -0,0 +1,26 @@ +type CacheConfig = { + maxSize: number; + keyComparator: (k1: K, k2: K) => boolean; +}; + +type BoxedValue = {value: V}; + +type Cache = { + get: (key: K) => BoxedValue | undefined; + set: (key: K, value: V) => void; + /** + * Get the value for the key if it exists, otherwise set the value to the result of the valueProducer and return it. + * @param key The key to get or set + * @param valueProducer The function to produce the value if the key does not exist + * @returns The value for the key + */ + getSet: (key: K, valueProducer: () => V) => BoxedValue; + snapshot: { + keys: () => K[]; + values: () => V[]; + entries: () => Array<[K, V]>; + }; + size: number; +}; + +export type {CacheConfig, Cache, BoxedValue}; diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 05a9e58f74ea..1850a1c7fd0e 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ import type {Constructor} from 'type-fest'; -import buildArrayCache from './cache/arrayCacheBuilder'; +import ArrayCache from './cache/ArrayCache'; import {MemoizeStats} from './stats'; import type {ClientOptions, MemoizedFn, MemoizeFnPredicate, Stats} from './types'; import {getEqualityComparator, mergeOptions, truncateArgs} from './utils'; @@ -45,7 +45,7 @@ class Memoize { function memoize(fn: Fn, opts?: ClientOptions): MemoizedFn { const options = mergeOptions(opts); - const cache = buildArrayCache, ReturnType>({maxSize: options.maxSize, keyComparator: getEqualityComparator(options)}); + const cache = ArrayCache, ReturnType>({maxSize: options.maxSize, keyComparator: getEqualityComparator(options)}); const stats = new MemoizeStats(options.monitor || Memoize.isMonitoringEnabled); diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index 8172b06e8872..ce843253f156 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -1,32 +1,8 @@ +import type {Cache} from './cache/types'; import type {MemoizeStats} from './stats'; type KeyComparator = (key1: K[], key2: K[]) => boolean; -type ValueBox = {value: V}; - -type Cache = { - get: (key: K) => ValueBox | undefined; - set: (key: K, value: V) => void; - /** - * Get the value for the key if it exists, otherwise set the value to the result of the valueProducer and return it. - * @param key The key to get or set - * @param valueProducer The function to produce the value if the key does not exist - * @returns The value for the key - */ - getSet: (key: K, valueProducer: () => V) => ValueBox; - snapshot: { - keys: () => K[]; - values: () => V[]; - entries: () => Array<[K, V]>; - }; - size: number; -}; - -type CacheOpts = { - maxSize: number; - keyComparator: KeyComparator; -}; - type InternalOptions = { cache: 'array'; }; @@ -48,4 +24,4 @@ type MemoizeFnPredicate = (...args: any[]) => any; type MemoizedFn = Fn & {cache: Cache, ReturnType>} & Stats; -export type {Cache, CacheOpts, Options, ClientOptions, MemoizedFn, KeyComparator, MemoizeFnPredicate, Stats}; +export type {Options, ClientOptions, MemoizedFn, KeyComparator, MemoizeFnPredicate, Stats}; From f7b4d9cb4877b731605925077eccf1349edf60dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 15 Jul 2024 15:08:29 +0200 Subject: [PATCH 59/66] add lines between methods --- src/libs/memoize/cache/ArrayCache.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/memoize/cache/ArrayCache.ts b/src/libs/memoize/cache/ArrayCache.ts index a655a0cd6f82..fd21210dea90 100644 --- a/src/libs/memoize/cache/ArrayCache.ts +++ b/src/libs/memoize/cache/ArrayCache.ts @@ -32,6 +32,7 @@ function ArrayCache(config: CacheConfig): Cache { cache.push(entry); return {value: entry[1]}; }, + set(key, value) { const index = getKeyIndex(key); @@ -45,6 +46,7 @@ function ArrayCache(config: CacheConfig): Cache { cache.shift(); } }, + getSet(key: K, valueProducer: () => V) { const index = getKeyIndex(key); @@ -64,11 +66,13 @@ function ArrayCache(config: CacheConfig): Cache { return {value}; }, + snapshot: { keys: () => cache.map((entry) => entry[0]), values: () => cache.map((entry) => entry[1]), entries: () => [...cache], }, + get size() { return cache.length; }, From 462d70e43bd8c9b1bdbbfb5220075deeca946023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 19 Jul 2024 16:32:54 +0200 Subject: [PATCH 60/66] remove avgKeyLength stat --- src/libs/memoize/stats.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/libs/memoize/stats.ts b/src/libs/memoize/stats.ts index 5cb6f3eb7532..043ecc92b216 100644 --- a/src/libs/memoize/stats.ts +++ b/src/libs/memoize/stats.ts @@ -1,7 +1,6 @@ import Log from '@libs/Log'; type MemoizeStatsEntry = { - keyLength: number; didHit: boolean; cacheRetrievalTime: number; fnTime?: number; @@ -11,7 +10,7 @@ type MemoizeStatsEntry = { // eslint-disable-next-line @typescript-eslint/no-explicit-any function isMemoizeStatsEntry(entry: any): entry is MemoizeStatsEntry { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return entry.keyLength !== undefined && entry.didHit !== undefined && entry.cacheRetrievalTime !== undefined; + return entry.didHit !== undefined && entry.cacheRetrievalTime !== undefined; } class MemoizeStats { @@ -19,8 +18,6 @@ class MemoizeStats { private hits = 0; - private avgKeyLength = 0; - private avgCacheRetrievalTime = 0; private avgFnTime = 0; @@ -46,8 +43,6 @@ class MemoizeStats { this.cacheSize = entry.cacheSize; - this.avgKeyLength = this.calculateCumulativeAvg(this.avgKeyLength, this.calls, entry.keyLength); - this.avgCacheRetrievalTime = this.calculateCumulativeAvg(this.avgCacheRetrievalTime, this.hits, entry.cacheRetrievalTime); if (entry.fnTime !== undefined) { @@ -97,7 +92,6 @@ class MemoizeStats { this.isEnabled = true; this.calls = 0; this.hits = 0; - this.avgKeyLength = 0; this.avgCacheRetrievalTime = 0; this.avgFnTime = 0; this.cacheSize = 0; @@ -109,7 +103,6 @@ class MemoizeStats { return { calls: this.calls, hits: this.hits, - avgKeyLength: this.avgKeyLength, avgCacheRetrievalTime: this.avgCacheRetrievalTime, avgFnTime: this.avgFnTime, cacheSize: this.cacheSize, From b1e4b02d87fcce07b7c487f0449dba5a20e9d93f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 22 Jul 2024 10:05:06 +0200 Subject: [PATCH 61/66] add transformKey --- src/libs/memoize/const.ts | 2 +- src/libs/memoize/index.ts | 14 ++++++++------ src/libs/memoize/types.ts | 24 ++++++++++++++---------- src/libs/memoize/utils.ts | 21 +++++++++++---------- src/types/utils/TupleOperations.ts | 12 ++++++++++++ 5 files changed, 46 insertions(+), 27 deletions(-) create mode 100644 src/types/utils/TupleOperations.ts diff --git a/src/libs/memoize/const.ts b/src/libs/memoize/const.ts index adc9706db597..a9e255131db4 100644 --- a/src/libs/memoize/const.ts +++ b/src/libs/memoize/const.ts @@ -5,6 +5,6 @@ const DEFAULT_OPTIONS = { equality: 'deep', monitor: false, cache: 'array', -} satisfies Options; +} satisfies Options<() => unknown, number, unknown>; export default DEFAULT_OPTIONS; diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 1850a1c7fd0e..b96cb821c340 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -42,19 +42,21 @@ class Memoize { * @param opts - Options for the memoization layer, for more details see `ClientOptions` type. * @returns Memoized function with a cache API attached to it. */ -function memoize(fn: Fn, opts?: ClientOptions): MemoizedFn { - const options = mergeOptions(opts); +function memoize>(fn: Fn, opts?: ClientOptions) { + const options = mergeOptions(opts); - const cache = ArrayCache, ReturnType>({maxSize: options.maxSize, keyComparator: getEqualityComparator(options)}); + const cache = ArrayCache>({maxSize: options.maxSize, keyComparator: getEqualityComparator(options)}); const stats = new MemoizeStats(options.monitor || Memoize.isMonitoringEnabled); const memoized = function memoized(...args: Parameters): ReturnType { const constructable = !!new.target; - const key = truncateArgs(args, options.maxArgs) as Parameters; + + const truncatedArgs = truncateArgs(args, options.maxArgs); + + const key = options.transformKey ? options.transformKey(truncatedArgs) : (truncatedArgs as Key); const statsEntry = stats.createEntry(); - statsEntry.track('keyLength', key.length); statsEntry.track('didHit', true); const retrievalTimeStart = performance.now(); @@ -84,7 +86,7 @@ function memoize(fn: Fn, opts?: ClientOptions): M Memoize.registerMemoized(options.monitoringName ?? fn.name, memoized); - return memoized as MemoizedFn; + return memoized as MemoizedFn; } export default memoize; diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index ce843253f156..a80a2fa77ab7 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -1,27 +1,31 @@ +import type {TakeFirst} from '@src/types/utils/TupleOperations'; import type {Cache} from './cache/types'; import type {MemoizeStats} from './stats'; -type KeyComparator = (key1: K[], key2: K[]) => boolean; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type MemoizeFnPredicate = (...args: any[]) => any; + +type KeyComparator = (k1: Key, k2: Key) => boolean; type InternalOptions = { cache: 'array'; }; -type Options = { +type Options = { maxSize: number; - equality: 'deep' | 'shallow' | KeyComparator; + equality: 'deep' | 'shallow' | KeyComparator; monitor: boolean; - maxArgs?: number; + maxArgs?: MaxArgs; monitoringName?: string; + transformKey?: (truncatedArgs: TakeFirst, MaxArgs>) => Key; } & InternalOptions; -type ClientOptions = Partial>; +type ClientOptions = Partial, keyof InternalOptions>>; type Stats = Pick; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type MemoizeFnPredicate = (...args: any[]) => any; - -type MemoizedFn = Fn & {cache: Cache, ReturnType>} & Stats; +type MemoizedFn = Fn & { + cache: Cache>; +} & Stats; -export type {Options, ClientOptions, MemoizedFn, KeyComparator, MemoizeFnPredicate, Stats}; +export type {Options, ClientOptions, MemoizeFnPredicate, Stats, KeyComparator, MemoizedFn}; diff --git a/src/libs/memoize/utils.ts b/src/libs/memoize/utils.ts index 87c6f98eede5..d11a55da7b04 100644 --- a/src/libs/memoize/utils.ts +++ b/src/libs/memoize/utils.ts @@ -1,8 +1,9 @@ import {deepEqual, shallowEqual} from 'fast-equals'; +import type {TakeFirst} from '@src/types/utils/TupleOperations'; import DEFAULT_OPTIONS from './const'; -import type {ClientOptions, KeyComparator, Options} from './types'; +import type {ClientOptions, KeyComparator, MemoizeFnPredicate, Options} from './types'; -function getEqualityComparator(opts: Options): KeyComparator { +function getEqualityComparator(opts: Options): KeyComparator { // Use the custom equality comparator if it is provided if (typeof opts.equality === 'function') { return opts.equality; @@ -15,23 +16,23 @@ function getEqualityComparator(opts: Options): KeyComparator { return deepEqual; } -function mergeOptions(options?: ClientOptions): Options { +function mergeOptions(options?: ClientOptions): Options { if (!options) { return DEFAULT_OPTIONS; } return {...DEFAULT_OPTIONS, ...options}; } +function truncateArgs(args: T, maxArgs?: MaxArgs): TakeFirst { + // Hot paths are declared explicitly to avoid the overhead of the slice method -function truncateArgs(args: T, maxArgs?: number) { if (maxArgs === undefined) { - return args; + return args as unknown as TakeFirst; } if (maxArgs >= args.length) { - return args; + return args as unknown as TakeFirst; } - // Hot paths are declared explicitly to avoid the overhead of the slice method if (maxArgs === 0) { return []; } @@ -41,14 +42,14 @@ function truncateArgs(args: T, maxArgs?: number) { } if (maxArgs === 2) { - return [args[0], args[1]]; + return [args[0], args[1]] as unknown as TakeFirst; } if (maxArgs === 3) { - return [args[0], args[1], args[2]]; + return [args[0], args[1], args[2]] as unknown as TakeFirst; } - return args.slice(0, maxArgs); + return args.slice(0, maxArgs) as unknown as TakeFirst; } export {mergeOptions, getEqualityComparator, truncateArgs}; diff --git a/src/types/utils/TupleOperations.ts b/src/types/utils/TupleOperations.ts new file mode 100644 index 000000000000..1fc755c7e603 --- /dev/null +++ b/src/types/utils/TupleOperations.ts @@ -0,0 +1,12 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +// Based on: https://stackoverflow.com/questions/67605122/obtain-a-slice-of-a-typescript-parameters-tuple +type TupleSplit = O['length'] extends N + ? [O, T] + : T extends readonly [infer F, ...infer R] + ? TupleSplit + : [O, T]; + +type TakeFirst = TupleSplit[0]; + +export type {TupleSplit, TakeFirst}; From c21800436f2c3b896156e19839dadff2999067f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 22 Jul 2024 11:29:13 +0200 Subject: [PATCH 62/66] make Key generic required --- src/libs/memoize/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index b96cb821c340..25acf7cf57cc 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -42,7 +42,7 @@ class Memoize { * @param opts - Options for the memoization layer, for more details see `ClientOptions` type. * @returns Memoized function with a cache API attached to it. */ -function memoize>(fn: Fn, opts?: ClientOptions) { +function memoize(fn: Fn, opts?: ClientOptions) { const options = mergeOptions(opts); const cache = ArrayCache>({maxSize: options.maxSize, keyComparator: getEqualityComparator(options)}); From 79328ceaa3b8630a0d5957136b3bbe03a3a82f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 22 Jul 2024 12:01:09 +0200 Subject: [PATCH 63/66] fix Search memoization --- src/components/Search/index.tsx | 9 ++++----- src/libs/memoize/index.ts | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 78992496f031..60bf43a59965 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1,6 +1,5 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp} from '@react-navigation/stack'; -import lodashMemoize from 'lodash/memoize'; import React, {useCallback, useEffect, useRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; @@ -13,6 +12,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as SearchActions from '@libs/actions/Search'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; +import memoize from '@libs/memoize'; import * as ReportUtils from '@libs/ReportUtils'; import * as SearchUtils from '@libs/SearchUtils'; import Navigation from '@navigation/Navigation'; @@ -74,15 +74,14 @@ function Search({query, policyIDs, sortBy, sortOrder, isMobileSelectionModeActiv [isLargeScreenWidth], ); - const getItemHeightMemoized = lodashMemoize( - (item: TransactionListItemType | ReportListItemType) => getItemHeight(item), - (item) => { + const getItemHeightMemoized = memoize((item: TransactionListItemType | ReportListItemType) => getItemHeight(item), { + transformKey: ([item]) => { // List items are displayed differently on "L"arge and "N"arrow screens so the height will differ // in addition the same items might be displayed as part of different Search screens ("Expenses", "All", "Finished") const screenSizeHash = isLargeScreenWidth ? 'L' : 'N'; return `${hash}-${item.keyForList}-${screenSizeHash}`; }, - ); + }); // save last non-empty search results to avoid ugly flash of loading screen when hash changes and onyx returns empty data if (currentSearchResults?.data && currentSearchResults !== lastSearchResultsRef.current) { diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 25acf7cf57cc..27edf091dd4d 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -42,7 +42,7 @@ class Memoize { * @param opts - Options for the memoization layer, for more details see `ClientOptions` type. * @returns Memoized function with a cache API attached to it. */ -function memoize(fn: Fn, opts?: ClientOptions) { +function memoize['length']>(fn: Fn, opts?: ClientOptions) { const options = mergeOptions(opts); const cache = ArrayCache>({maxSize: options.maxSize, keyComparator: getEqualityComparator(options)}); From f710dc303f86cf5d0e32cb56de41b821bd0b6690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 22 Jul 2024 16:42:55 +0200 Subject: [PATCH 64/66] add comment explainers on existing issues --- src/libs/memoize/index.ts | 3 +++ src/libs/memoize/types.ts | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 27edf091dd4d..4a14296061a2 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -79,6 +79,9 @@ function memoize stats.startMonitoring(); diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index a80a2fa77ab7..c32bcd2bfd9b 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -17,6 +17,13 @@ type Options = { monitor: boolean; maxArgs?: MaxArgs; monitoringName?: string; + /** + * Function to transform the arguments into a key, which is used to reference the result in the cache. + * When called with constructable (e.g. class, `new` keyword) functions, it won't get proper types for `truncatedArgs` + * Any viable fixes are welcome! + * @param truncatedArgs - Tuple of arguments passed to the memoized function (truncated to `maxArgs`). Does not work with constructable (see description). + * @returns - Key to use for caching + */ transformKey?: (truncatedArgs: TakeFirst, MaxArgs>) => Key; } & InternalOptions; From cc8b268d88695e0b199678fd4d8e64c74146cb0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 23 Jul 2024 07:15:55 +0200 Subject: [PATCH 65/66] replace lodash in freezeScreenWithLazyLoading --- src/libs/freezeScreenWithLazyLoading.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/freezeScreenWithLazyLoading.tsx b/src/libs/freezeScreenWithLazyLoading.tsx index 01da7d7fda58..21a2681b08c1 100644 --- a/src/libs/freezeScreenWithLazyLoading.tsx +++ b/src/libs/freezeScreenWithLazyLoading.tsx @@ -1,5 +1,5 @@ -import memoize from 'lodash/memoize'; import React from 'react'; +import memoize from './memoize'; import FreezeWrapper from './Navigation/FreezeWrapper'; function FrozenScreen(WrappedComponent: React.ComponentType) { From 768bf3ae95b9b3ac5b88bfdde759c0b9313d2bab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 23 Jul 2024 07:19:30 +0200 Subject: [PATCH 66/66] ArrayCache tweaks --- src/libs/memoize/cache/ArrayCache.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/memoize/cache/ArrayCache.ts b/src/libs/memoize/cache/ArrayCache.ts index fd21210dea90..058efefdb1aa 100644 --- a/src/libs/memoize/cache/ArrayCache.ts +++ b/src/libs/memoize/cache/ArrayCache.ts @@ -10,8 +10,11 @@ function ArrayCache(config: CacheConfig): Cache { const {maxSize, keyComparator} = config; - function getKeyIndex(key: K) { - // We search the array backwards because the most recently added entries are at the end, and our heuristic follows the principles of an LRU cache - that the most recently added entries are most likely to be used again. + /** + * Returns the index of the key in the cache array. + * We search the array backwards because the most recently added entries are at the end, and our heuristic follows the principles of an LRU cache - that the most recently added entries are most likely to be used again. + */ + function getKeyIndex(key: K): number { for (let i = cache.length - 1; i >= 0; i--) { if (keyComparator(cache[i][0], key)) { return i; @@ -25,7 +28,7 @@ function ArrayCache(config: CacheConfig): Cache { const index = getKeyIndex(key); if (index === -1) { - return; + return undefined; } const [entry] = cache.splice(index, 1); @@ -47,7 +50,7 @@ function ArrayCache(config: CacheConfig): Cache { } }, - getSet(key: K, valueProducer: () => V) { + getSet(key, valueProducer) { const index = getKeyIndex(key); if (index !== -1) {