Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

General purpose memoization tool #43868

Merged
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
2e34ee7
install fast-equals
kacper-mikolajczak Jun 16, 2024
0cbbed0
add array cache POC
kacper-mikolajczak Jun 17, 2024
4af2f05
fix comments and types
kacper-mikolajczak Jun 17, 2024
fbc0d00
remove unnecessary variable
kacper-mikolajczak Jun 17, 2024
af24aa6
add MemoizedFn type
kacper-mikolajczak Jun 17, 2024
f049597
fix array cache
kacper-mikolajczak Jun 17, 2024
6a699b8
fix get method and remove has
kacper-mikolajczak Jun 17, 2024
6e4ca00
fix delete method
kacper-mikolajczak Jun 17, 2024
a8a8849
add tests
kacper-mikolajczak Jun 17, 2024
932d26c
fix types
kacper-mikolajczak Jun 18, 2024
6fb59ab
make monitor false on default
kacper-mikolajczak Jun 19, 2024
3212301
add cache statistics module
kacper-mikolajczak Jun 19, 2024
3dfe8d5
fix cache entries
kacper-mikolajczak Jun 19, 2024
8e26459
add entry identity check
kacper-mikolajczak Jun 19, 2024
a368c07
rename registerStat to track
kacper-mikolajczak Jun 19, 2024
3ea3af0
add cache size to cache and stats
kacper-mikolajczak Jun 19, 2024
5695b2d
Global stats api v1
kacper-mikolajczak Jun 20, 2024
7b59ac5
fix cache size & entries api
kacper-mikolajczak Jun 20, 2024
19e59b1
fix function id for stats
kacper-mikolajczak Jun 20, 2024
383488f
change default equality to deep
kacper-mikolajczak Jun 20, 2024
7858c9d
fix cumulative avg calcs
kacper-mikolajczak Jun 20, 2024
4cf90e6
add monitoring to profiling menu
kacper-mikolajczak Jun 20, 2024
35c842f
rename delete variable name
kacper-mikolajczak Jun 20, 2024
dc8ad8b
use memoize for NumberFormatUtils
kacper-mikolajczak Jun 20, 2024
20a9711
replace lodash memoize
kacper-mikolajczak Jun 20, 2024
1b1ff6f
add monitoringName for anonymous funcitons
kacper-mikolajczak Jun 20, 2024
85a0e91
fix tests
kacper-mikolajczak Jun 20, 2024
df7466a
Improve cache array builder description
kacper-mikolajczak Jun 21, 2024
7b03364
add getKeyIndex comment
kacper-mikolajczak Jun 21, 2024
a815e24
arraCacheBuilder get early return
kacper-mikolajczak Jun 21, 2024
bc9b9ce
remove unused APIs
kacper-mikolajczak Jun 21, 2024
67890fc
use default export for const
kacper-mikolajczak Jun 21, 2024
fa904c0
rename monitoringEnable
kacper-mikolajczak Jun 21, 2024
1b0c9e6
Fix typo in memoize desc
kacper-mikolajczak Jun 21, 2024
e90d63e
MemoizeStats enabled -> isEnabled
kacper-mikolajczak Jun 21, 2024
c6910ac
add explanation to cumulativeAvg
kacper-mikolajczak Jun 21, 2024
dc64819
take out the isMemoizeStatsEntry identity function
kacper-mikolajczak Jun 21, 2024
1f3bd24
fix saveEntry
kacper-mikolajczak Jun 21, 2024
9390547
move eslint-disable
kacper-mikolajczak Jun 21, 2024
65139da
fix tests
kacper-mikolajczak Jun 21, 2024
7da3423
merge main
kacper-mikolajczak Jun 21, 2024
0a06149
fix package-lock.json
kacper-mikolajczak Jun 21, 2024
9226c50
add eslint recommendation
kacper-mikolajczak Jul 1, 2024
607db57
add getSet method to cache
kacper-mikolajczak Jul 1, 2024
4ae9a9c
add trackTime method to statsEntry
kacper-mikolajczak Jul 1, 2024
0228144
fix typo
kacper-mikolajczak Jul 1, 2024
e95e208
refactor keyComparator
kacper-mikolajczak Jul 1, 2024
5e13d40
add maxArgs option
kacper-mikolajczak Jul 7, 2024
d8e82a0
fix lodash memoize instance
kacper-mikolajczak Jul 7, 2024
de4a8aa
Merge branch 'main' into feat/memoization-tool-poc2
kacper-mikolajczak Jul 7, 2024
acbe201
supress TS errors
kacper-mikolajczak Jul 7, 2024
8055ec0
fix constructable
kacper-mikolajczak Jul 8, 2024
dc9850e
add description for getSet method
kacper-mikolajczak Jul 9, 2024
79d19c0
lodash memoize named export restriction
kacper-mikolajczak Jul 9, 2024
4ecb327
add fast-equals license
kacper-mikolajczak Jul 9, 2024
ff557b7
merge main
kacper-mikolajczak Jul 9, 2024
baf7a5f
fix iOS Intl.NumberFormat polyfill
kacper-mikolajczak Jul 9, 2024
50b98d8
NumberFormatUtils ios platform variant
kacper-mikolajczak Jul 10, 2024
7f8aea1
restructure ios variant
kacper-mikolajczak Jul 10, 2024
f1aa456
Merge branch 'main' into feat/memoization-tool-poc2
kacper-mikolajczak Jul 12, 2024
62b4684
fix package-lock typo
kacper-mikolajczak Jul 15, 2024
5c173c3
refactor ArrayCache
kacper-mikolajczak Jul 15, 2024
f7b4d9c
add lines between methods
kacper-mikolajczak Jul 15, 2024
462d70e
remove avgKeyLength stat
kacper-mikolajczak Jul 19, 2024
b1e4b02
add transformKey
kacper-mikolajczak Jul 22, 2024
c218004
make Key generic required
kacper-mikolajczak Jul 22, 2024
79328ce
fix Search memoization
kacper-mikolajczak Jul 22, 2024
ec6251e
Merge branch 'main' into feat/memoization-tool-poc2
kacper-mikolajczak Jul 22, 2024
f710dc3
add comment explainers on existing issues
kacper-mikolajczak Jul 22, 2024
cc8b268
replace lodash in freezeScreenWithLazyLoading
kacper-mikolajczak Jul 23, 2024
768bf3a
ArrayCache tweaks
kacper-mikolajczak Jul 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
"focus-trap-react": "^10.2.3",
"htmlparser2": "^7.2.0",
"idb-keyval": "^6.2.1",
Expand Down
74 changes: 74 additions & 0 deletions src/libs/memoize/cache/arrayCacheBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type {Cache, CacheOpts} from '@libs/memoize/types';
import {getEqualityComparator} from '@libs/memoize/utils';

/**
* Builder of the cache using `Array` primitive under the hood.
kacper-mikolajczak marked this conversation as resolved.
Show resolved Hide resolved
* @param opts - Cache options, check `CacheOpts` type for more details.
* @returns
*/
function buildArrayCache<K extends unknown[], V>(opts: CacheOpts): Cache<K, V> {
const cache: Array<[K, V]> = [];

const keyComparator = getEqualityComparator(opts);

function getKeyIndex(key: K) {
for (let i = cache.length - 1; i >= 0; i--) {
kacper-mikolajczak marked this conversation as resolved.
Show resolved Hide resolved
if (keyComparator(cache[i][0], key)) {
return i;
}
}
return -1;
}

return {
get(key) {
const index = getKeyIndex(key);

if (index !== -1) {
kacper-mikolajczak marked this conversation as resolved.
Show resolved Hide resolved
const [entry] = cache.splice(index, 1);
cache.push(entry);
return {value: entry[1]};
}
},
set(key, value) {
const index = getKeyIndex(key);

if (index !== -1) {
cache.splice(index, 1);
}

cache.push([key, value]);

if (cache.length > opts.maxSize) {
cache.shift();
}
},
delete(key) {
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
const index = getKeyIndex(key);
const has = index !== -1;
kacper-mikolajczak marked this conversation as resolved.
Show resolved Hide resolved

if (has) {
cache.splice(index, 1);
}

return has;
},
clear() {
cache.length = 0;
},
snapshot: {
keys() {
return cache.map((entry) => entry[0]);
},
values() {
return cache.map((entry) => entry[1]);
},
cache() {
return [...cache];
},
size: cache.length,
},
};
}

export default buildArrayCache;
11 changes: 11 additions & 0 deletions src/libs/memoize/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type {Options} from './types';

const DEFAULT_OPTIONS = {
maxSize: Infinity,
equality: 'shallow',
monitor: false,
cache: 'array',
} satisfies Options;

// eslint-disable-next-line import/prefer-default-export
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
export {DEFAULT_OPTIONS};
54 changes: 54 additions & 0 deletions src/libs/memoize/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
import buildArrayCache from './cache/arrayCacheBuilder';
import {MemoizeStats} from './stats';
import type {ClientOptions, MemoizeFnPredicate} 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 - Options for the memoization layer, for more details see `ClientOptions` type.
kacper-mikolajczak marked this conversation as resolved.
Show resolved Hide resolved
* @returns Memoized function with a cache API attached to it.
*/
function memoize<Fn extends MemoizeFnPredicate>(fn: Fn, opts?: ClientOptions) {
const options = mergeOptions(opts);

const cache = buildArrayCache<Parameters<Fn>, ReturnType<Fn>>(options);

const stats = new MemoizeStats(options.monitor);

const memoized = function memoized(...key: Parameters<Fn>): ReturnType<Fn> {
const statsEntry = stats.createEntry();
statsEntry.track('keyLength', key.length);

const retrievalTimeStart = performance.now();
let cached = cache.get(key);
statsEntry.track('cacheRetrievalTime', performance.now() - retrievalTimeStart);
statsEntry.track('didHit', !!cached);

if (!cached) {
const fnTimeStart = performance.now();
const result = fn(...key);
statsEntry.track('fnTime', performance.now() - fnTimeStart);

cached = {value: result};
cache.set(key, result as ReturnType<Fn>);
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
}

statsEntry.track('cacheSize', cache.snapshot.size);
statsEntry.save();

return cached.value;
};

memoized.cache = cache;

memoized.stats = {
startMonitoring: () => stats.startMonitoring(),
stopMonitoring: () => stats.stopMonitoring(),
};

return memoized;
}

export default memoize;
104 changes: 104 additions & 0 deletions src/libs/memoize/stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
type MemoizeStatsEntry = {
keyLength: number;
didHit: boolean;
cacheRetrievalTime: number;
fnTime?: number;
cacheSize: number;
};

class MemoizeStats {
private calls = 0;

private hits = 0;

private avgKeyLength = 0;

private avgCacheRetrievalTime = 0;

private avgFnTime = 0;

private cacheSize = 0;

enabled = false;
kacper-mikolajczak marked this conversation as resolved.
Show resolved Hide resolved

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) {
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);

if (entry.fnTime !== undefined) {
this.avgFnTime = this.calculateCumulativeAvg(this.avgFnTime, this.calls - this.hits, entry.fnTime);
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
static isMemoizeStatsEntry(entry: any): entry is MemoizeStatsEntry {
kacper-mikolajczak marked this conversation as resolved.
Show resolved Hide resolved
return entry.keyLength !== undefined && entry.didHit !== undefined && entry.cacheRetrievalTime !== undefined;
}

// eslint-disable-next-line rulesdir/prefer-early-return
kacper-mikolajczak marked this conversation as resolved.
Show resolved Hide resolved
private saveEntry(entry: Partial<MemoizeStatsEntry>) {
if (this.enabled && MemoizeStats.isMemoizeStatsEntry(entry)) {
kacper-mikolajczak marked this conversation as resolved.
Show resolved Hide resolved
this.cumulateEntry(entry);
}
}

createEntry() {
// If monitoring is disabled, return a dummy object that does nothing
if (!this.enabled) {
return {
track: () => {},
save: () => {},
};
}

const entry: Partial<MemoizeStatsEntry> = {};

return {
track: <P extends keyof MemoizeStatsEntry>(cacheProp: P, value: MemoizeStatsEntry[P]) => {
entry[cacheProp] = value;
},
save: () => this.saveEntry(entry),
};
}

startMonitoring() {
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
this.enabled = true;
this.calls = 0;
this.hits = 0;
this.avgKeyLength = 0;
this.avgCacheRetrievalTime = 0;
this.avgFnTime = 0;
this.cacheSize = 0;
}

stopMonitoring() {
this.enabled = false;

return {
calls: this.calls,
hits: this.hits,
avgKeyLength: this.avgKeyLength,
avgCacheRetrievalTime: this.avgCacheRetrievalTime,
avgFnTime: this.avgFnTime,
cacheSize: this.cacheSize,
};
}
}

export type {MemoizeStatsEntry};
export {MemoizeStats};
38 changes: 38 additions & 0 deletions src/libs/memoize/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
type KeyComparator = <K>(key1: K[], key2: K[]) => boolean;

type Cache<K, V> = {
get: (key: K) => {value: V} | undefined;
set: (key: K, value: V) => void;
delete: (key: K) => boolean;
clear: () => void;
snapshot: {
keys: () => K[];
values: () => V[];
cache: () => Array<[K, V]>;
size: number;
};
};

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<Omit<Options, keyof InternalOptions>>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type MemoizeFnPredicate = (...args: any[]) => any;
kacper-mikolajczak marked this conversation as resolved.
Show resolved Hide resolved

type MemoizedFn<Fn extends MemoizeFnPredicate> = Fn & {cache: Cache<Parameters<Fn>, ReturnType<Fn>>};

export type {Cache, CacheOpts, Options, ClientOptions, MemoizedFn, KeyComparator, MemoizeFnPredicate};
23 changes: 23 additions & 0 deletions src/libs/memoize/utils.ts
Original file line number Diff line number Diff line change
@@ -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};
Loading
Loading