Common - Fix potential leak on cachedCall - #11478
Open
LinkIsGrim wants to merge 2 commits into
Open
Conversation
A cache that clears on an event was added to that event's list every time it recomputed, not once. The list is only emptied when the event fires, so any cache whose expiry is shorter than the gap between firings pushed a duplicate entry per expiry, forever. Worst where a short lived cache has many uids: canTreatCached is keyed per body part and per treatment with a few seconds of expiry, and its event fires on interact menu key up, so a menu held open pushes duplicates for as long as it is open. Erasing the same cache repeatedly is harmless, so this never misbehaved, it just grew. What a cache is listed on is now stored alongside it, so listing happens once per set of events rather than once per recompute. Keeping it with the cache rather than inferring it from whether the cache exists means a cache first created without events and later called with some is still listed, and one erased by its event is listed again when it comes back. Nothing new runs on a cache hit, only on recompute.
Defaulting the events parameter to an empty array removes the nil check, and with it a level of nesting. No events then means an empty list compared against an empty one, so nothing is listed without needing to ask. Normalising a single event to an array stays inside the recompute branch, so a cache hit still does nothing but read its variable.
Contributor
|
ELI5? |
Member
Author
|
cached call recomputed and re-added itself to the list of caches that event clears instead of only doing that once list only gets reset when the event fires, so if the cache expires faster than the event fires, the list just keeps growing with duplicates forever until the event actually fires fix makes it store what the cache was last listed on alongside the cache itself, so it only re-adds itself to an event's list when that set of events actually changes. if it's already listed then recompute skips listing. |
Contributor
|
I now see the issue, but there are more I also see:
The more I look at this code, the more I see issues. I'm looking into rewriting it completely. |
Contributor
params ["_params", "_function", "_namespace", "_uid", "_duration", ["_events", []]];
// GVAR(clearCache) = createHashMap; // would be defined in pre or post init
_uid = QGVAR(uid) + _uid;
(_namespace getVariable [_uid, [-99999]]) params ["_expiry", "", ["_listedOn", []]];
// Using a hashmap here might be more efficient, but wouldn't delete cached result if namespace is deleted (especially relevant for objects)
if (_expiry < diag_tickTime) then {
// Does the cache need to be cleared on an event?
if (_events isNotEqualTo [] && _events isNotEqualTo "") then {
if (_events isEqualType "") then {
_events = [_events];
};
{
private _cacheList = GVAR(clearCache) getOrDefault [toLower _x, []];
if (_cacheList isEqualTo []) then {
continue;
};
_cacheList deleteAt (_cacheList find [_namespace, _uid]);
} forEach (_listedOn - _events);
_listedOn = _events - _listedOn;
{
private _event = toLower _x;
private _cacheList = GVAR(clearCache) get _event;
// If there was no EH to clear these caches, add one
if (isNil "_cacheList") then {
_cacheList = [];
GVAR(clearCache) set [_event, _cacheList];
[_event, {
#ifdef DEBUG_MODE_FULL
INFO_1("Clear cached variables on event: %1",_eventName);
#endif
// Get the list of caches to clear
//IGNORE_PRIVATE_WARNING ["_eventName"];
// _eventName is defined on the function that calls the event
private _event = toLower _eventName;
// Erase all the cached results
{
_x call FUNC(eraseCache);
} forEach (GVAR(clearCache) getOrDefault [_event, []]);
// Empty the list
GVAR(clearCache) set [_event, []];
}] call CBA_fnc_addEventHandler;
};
// Add this cache to the list of the event
_cacheList pushBackUnique [_namespace, _uid];
} forEach _listedOn;
};
_namespace setVariable [_uid, [diag_tickTime + _duration, _params call _function, _listedOn]];
#ifdef DEBUG_MODE_FULL
INFO_2("Calculated result: %1 %2",_namespace,_uid);
} else {
INFO_2("Cached result: %1 %2",_namespace,_uid);
#endif
};
(_namespace getVariable _uid) select 1This handles (at least in theory) different events used for clearing caches - thoughts? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When merged this pull request will:
cachedCalladding a cache to its event's clear list on every recompute rather than once. The list is only emptied when the event fires, so a cache whose expiry is shorter than the gap between firings adds an entry per expiry until it does.Would leak when short expiry combined with rare event. Not the case for existing usages but #11450 may hit it.
IMPORTANT
Component - Add|Fix|Improve|Change|Make|Remove {changes}.