fix: prevent unbounded memory growth in Reflection::function() cache#801
fix: prevent unbounded memory growth in Reflection::function() cache#801MaelitoP wants to merge 1 commit intoCuyZ:masterfrom
Conversation
0e49905 to
d4108d9
Compare
d4108d9 to
b60b03b
Compare
|
Hi @MaelitoP, thanks for the PR and the detailed report. Honestly the memoized cache does not seem to be useful anymore, as we instantiate |
|
Hi @romm, thanks for the review! I tested your suggestion. Removing
Removing the cache entirely is a ~50% improvement over the bug, but the The cache still provides value: it prevents redundant The trade-off with the current Let me know how you'd like to proceed! |
|
Hi @romm, Any news ? |
|
Hi @MaelitoP, sorry for not coming back to you, I've been busy lately. 🙂 There's still something that confuses me, with the changes in this PR, the method looks like this: public static function function(callable $function): ReflectionFunction
{
$closure = Closure::fromCallable($function);
$reflection = new ReflectionFunction($closure);
// @infection-ignore-all / We don't need to test the cache key strategy
$fileName = $reflection->getFileName();
// @infection-ignore-all / Built-in functions have no file; use the function name as key instead.
$key = $fileName !== false
? $fileName . ':' . $reflection->getStartLine()
: $reflection->getName();
return self::$functionReflection[$key] ??= $reflection;
}So, everytime we call it, we do instantiate Any chance your benchmark was not running on the correct version? |
I will double check and I come back to you ! |
Fixes #800
Problem
Reflection::function()usesspl_object_hash($closure)as cache key. SinceClosure::fromCallable()creates a new object each time,spl_object_hashreturns a unique hash per call — even when closures reference the same source function.The static
$functionReflectionarray grows unboundedly, leaking perTreeMapper::map()invocation and causing OOM in long-running processes.Fix
Replace the cache key with
fileName:startLinefor user-defined functions and the function name for built-in functions. This is stable across Closure instances and bounded by the number of unique function definitions — matching the strategy already used byInMemoryFunctionDefinitionRepository.Trade-off: A
ReflectionFunctionis now always instantiated to compute the cache key, even on hits. However,ReflectionFunctionconstruction is cheap and the previous implementation had an effective 0% cache hit rate (unique keys on every call), so this is strictly better.