Skip to content

Commit

Permalink
perf: LRU cache strategy.
Browse files Browse the repository at this point in the history
Keep a list of recently accessed keys in `OnyxCache` - `recentKeys`
This serves to clean "Least Recently Used" keys from cache

Keys are added to `recentKeys` for any cache access operation - get/set/merge

Added a configurable limit of 150 keys in cache
This looks like a sane value from observations during chat browsing
  • Loading branch information
kidroca committed Aug 3, 2021
1 parent 4f28ab6 commit f883ad8
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 58 deletions.
70 changes: 12 additions & 58 deletions lib/Onyx.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ const evictionBlocklist = {};
// Optional user-provided key value states set when Onyx initializes or clears
let defaultKeyStates = {};

// Cache cleaning uses this to remove least recently accessed keys
let MAX_CACHED_KEYS = 150;

// Connections can be made before `Onyx.init`. They would wait for this task before resolving
const deferredInitTask = createDeferredTask();

Expand Down Expand Up @@ -102,19 +105,6 @@ function isCollectionKey(key) {
return _.contains(_.values(onyxKeys.COLLECTION), key);
}

/**
* Find the collection a collection item belongs to
* or return null if them item is not a part of a collection
* @param {string} key
* @returns {string|null}
*/
function getCollectionKeyForItem(key) {
return _.chain(onyxKeys.COLLECTION)
.values()
.find(name => key.startsWith(name))
.value();
}

/**
* Checks to see if a given key matches with the
* configured key of our connected subscriber
Expand Down Expand Up @@ -412,48 +402,6 @@ function connect(mapping) {
return connectionID;
}

/**
* Remove cache items that are no longer connected through Onyx
* @param {string} key
*/
function cleanCache(key) {
// Don't remove default keys from cache, they don't take much memory and are accessed frequently
if (_.has(defaultKeyStates, key)) {
return;
}

const hasRemainingConnections = _.some(callbackToStateMapping, {key});

// When the key is still used in other places don't remove it from cache
if (hasRemainingConnections) {
return;
}

// When this is a collection - also recursively remove any unused individual items
if (isCollectionKey(key)) {
cache.drop(key);

getAllKeys().then(cachedKeys => _.chain(cachedKeys)
.filter(name => name.startsWith(key))
.forEach(cleanCache));

return;
}

// When this is a collection item - check if the collection is still used
const collectionKey = getCollectionKeyForItem(key);
if (collectionKey) {
// When there's an active subscription for a collection don't remove the item
const hasRemainingConnectionsForCollection = _.some(callbackToStateMapping, {key: collectionKey});
if (hasRemainingConnectionsForCollection) {
return;
}
}

// Otherwise remove the value from cache
cache.drop(key);
}

/**
* Remove the listener for a react component
*
Expand All @@ -471,11 +419,10 @@ function disconnect(connectionID, keyToRemoveFromEvictionBlocklist) {
removeFromEvictionBlockList(keyToRemoveFromEvictionBlocklist, connectionID);
}

const key = callbackToStateMapping[connectionID].key;
delete callbackToStateMapping[connectionID];

// When the last subscriber disconnects, drop cache as well
cleanCache(key);
// Try to free anything dated from cache
cache.removeLeastRecentUsedKeys(MAX_CACHED_KEYS);
}

/**
Expand Down Expand Up @@ -742,13 +689,16 @@ function mergeCollection(collectionKey, collection) {
* @param {function} registerStorageEventListener a callback when a storage event happens.
* This applies to web platforms where the local storage emits storage events
* across all open tabs and allows Onyx to stay in sync across all open tabs.
* @param {Number} [options.maxCachedKeysCount=150] Sets how many recent keys should we try to keep in cache
* Setting this to 0 would only keep active connections in cache
* @param {Boolean} [options.captureMetrics]
*/
function init({
keys,
initialKeyStates,
safeEvictionKeys,
registerStorageEventListener,
maxCachedKeysCount,
captureMetrics = false,
}) {
if (captureMetrics) {
Expand All @@ -757,6 +707,10 @@ function init({
applyDecorators();
}

if (_.isNumber(maxCachedKeysCount)) {
MAX_CACHED_KEYS = maxCachedKeysCount;
}

// Let Onyx know about all of our keys
onyxKeys = keys;

Expand Down
38 changes: 38 additions & 0 deletions lib/OnyxCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ class OnyxCache {
*/
this.storageKeys = new Set();

/**
* @private
* Unique list of keys maintained in access order (most recent at the end)
* @type {Set<string>}
*/
this.recentKeys = new Set();

/**
* @private
* A map of cached values
Expand Down Expand Up @@ -53,6 +60,7 @@ class OnyxCache {
* @returns {*}
*/
getValue(key) {
this.addToAccessedKeys(key);
return this.storageMap[key];
}

Expand Down Expand Up @@ -83,6 +91,7 @@ class OnyxCache {
*/
set(key, value) {
this.addKey(key);
this.addToAccessedKeys(key);
this.storageMap[key] = value;

return value;
Expand All @@ -106,6 +115,7 @@ class OnyxCache {
const storageKeys = this.getAllKeys();
const mergedKeys = _.keys(data);
this.storageKeys = new Set([...storageKeys, ...mergedKeys]);
_.each(mergedKeys, key => this.addToAccessedKeys(key));
}

/**
Expand Down Expand Up @@ -144,6 +154,34 @@ class OnyxCache {

return this.pendingPromises[taskName];
}

/**
* @private
* Adds a key to the top of the recently accessed keys
* @param {string} key
*/
addToAccessedKeys(key) {
// Removing and re-adding a key ensures it's at the end of the list
this.recentKeys.delete(key);
this.recentKeys.add(key);
}

/**
* Remove keys that don't fall into the range of recently used keys
* @param {number} recentKeysSize - a list of most recent keys from this size will remain in cache.
*/
removeLeastRecentUsedKeys(recentKeysSize) {
// Get the last N keys by doing a negative slice
const recentlyAccessed = new Set([...this.recentKeys].slice(-recentKeysSize));

_.chain(this.storageMap)
.keys()
.each((key) => {
if (!recentlyAccessed.has(key)) {
this.drop(key);
}
});
}
}

const instance = new OnyxCache();
Expand Down

0 comments on commit f883ad8

Please sign in to comment.