Last active
September 21, 2024 06:38
-
-
Save JanTvrdik/95c6c3eb71cd3c9fc228e62d641e0876 to your computer and use it in GitHub Desktop.
This file contains 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
<?php declare(strict_types = 1); | |
namespace App\Tests; | |
use Closure; | |
use PHPUnit\Framework\Assert; | |
use ReflectionClass; | |
use ReflectionFiber; | |
use ReflectionFunction; | |
use ReflectionGenerator; | |
use ReflectionReference; | |
use SplDoublyLinkedList; | |
use SplObjectStorage; | |
use SplQueue; | |
use WeakMap; | |
use function count; | |
use function debug_backtrace; | |
use function gc_collect_cycles; | |
use function get_declared_classes; | |
use function get_defined_constants; | |
use function get_defined_functions; | |
use function get_resources; | |
use function ini_get_all; | |
use function is_array; | |
use function is_object; | |
use function ob_list_handlers; | |
use function pcntl_signal_get_handler; | |
use function restore_error_handler; | |
use function restore_exception_handler; | |
use function set_error_handler; | |
use function set_exception_handler; | |
use function spl_autoload_functions; | |
use function spl_object_id; | |
use function sprintf; | |
use function str_replace; | |
use function stream_context_get_default; | |
use function stream_context_get_params; | |
class MemoryTracker | |
{ | |
/** | |
* @var WeakMap<object, string>|null | |
*/ | |
private static ?WeakMap $tracked = null; | |
public static function trackObject( | |
object $object, | |
string $label, | |
): void | |
{ | |
$tracked = self::$tracked ?? new WeakMap(); | |
$tracked[$object] ??= $label; | |
self::$tracked = $tracked; | |
} | |
public static function assertAllTrackedObjectsHaveBeenGarbageCollected(): void | |
{ | |
gc_collect_cycles(); | |
$leaked = []; | |
foreach (self::$tracked ?? [] as $humanId) { | |
$leaked[$humanId] = []; | |
} | |
if (count($leaked) > 0) { | |
$global = self::findGlobalValues(); | |
$reachable = self::findReachableObjects(['global' => $global]); | |
foreach ($reachable as $path => $reachableObject) { | |
if (isset(self::$tracked[$reachableObject])) { | |
$humanId = self::$tracked[$reachableObject]; | |
$leaked[$humanId][] = $path; | |
} | |
} | |
self::$tracked = null; | |
Assert::assertSame([], $leaked, sprintf('%d tracked objects have not been garbage collected', count($leaked))); | |
} | |
} | |
/** | |
* @return array<string, object> | |
*/ | |
private static function findReachableObjects(mixed $entrypoint): array | |
{ | |
$objects = []; | |
$visitedObjects = []; | |
$visitedReferences = []; | |
/** @var SplQueue<array{mixed, string}> $queue */ | |
$queue = new SplQueue(); | |
$queue->enqueue([$entrypoint, '']); | |
while (!$queue->isEmpty()) { | |
[$item, $path] = $queue->dequeue(); | |
if (is_object($item)) { | |
$objects[$path] = $item; | |
$objectId = spl_object_id($item); | |
if (isset($visitedObjects[$objectId])) { | |
continue; | |
} | |
$visitedObjects[$objectId] = true; | |
if (isset(self::$tracked[$item])) { | |
$path = '/' . self::$tracked[$item]; | |
} | |
foreach ((array) ($item) as $key => $value) { | |
$key = str_replace("\x00", '.', (string) $key); | |
$queue->enqueue([$value, "$path/$key"]); | |
} | |
if ($item instanceof Closure) { | |
$reflection = new ReflectionFunction($item); | |
$closureThis = $reflection->getClosureThis(); | |
$queue->enqueue([$closureThis, "$path/this"]); | |
foreach ($reflection->getClosureUsedVariables() as $key => $value) { | |
$queue->enqueue([$value, "$path/use/$key"]); | |
} | |
foreach ($reflection->getStaticVariables() as $key => $value) { | |
$queue->enqueue([$value, "$path/static/$key"]); | |
} | |
} elseif ($item instanceof ReflectionFiber) { | |
$queue->enqueue([$item->getCallable(), "$path/callable"]); | |
$queue->enqueue([$item->getTrace(), "$path/trace"]); | |
} elseif ($item instanceof ReflectionFunction) { | |
$queue->enqueue([$item->getClosure(), "$path/closure"]); | |
} elseif ($item instanceof ReflectionGenerator) { | |
$queue->enqueue([$item->getFunction(), "$path/function"]); | |
$queue->enqueue([$item->getThis(), "$path/this"]); | |
$queue->enqueue([$item->getTrace(), "$path/trace"]); | |
} elseif ($item instanceof SplDoublyLinkedList || $item instanceof SplObjectStorage) { | |
$idx = 0; | |
foreach ($item as $key => $value) { | |
$queue->enqueue([$value, "$path/$idx/value"]); | |
$queue->enqueue([$key, "$path/$idx/key"]); | |
$idx++; | |
} | |
} | |
} elseif (is_array($item)) { | |
foreach ($item as $key => $value) { | |
$refId = ReflectionReference::fromArrayElement($item, $key)?->getId(); | |
if ($refId === null) { | |
$queue->enqueue([$value, "$path/$key"]); | |
} elseif (!isset($visitedReferences[$refId])) { | |
$visitedReferences[$refId] = true; | |
$queue->enqueue([$value, "$path/$key"]); | |
} | |
} | |
} | |
} | |
return $objects; | |
} | |
/** | |
* Returns all values that are reachable from the global scope. | |
* | |
* Known limitations: | |
* - does not track callback registered with register_shutdown_function() | |
* | |
* @return array<string, mixed> | |
*/ | |
private static function findGlobalValues(): array | |
{ | |
$global = [ | |
'globals' => $GLOBALS, | |
'post' => $_POST, | |
'get' => $_GET, | |
'server' => $_SERVER, | |
'session' => $_SESSION ?? [], | |
'env' => $_ENV, | |
'cookie' => $_COOKIE, | |
'files' => $_FILES, | |
'request' => $_REQUEST, | |
]; | |
foreach (get_declared_classes() as $class) { | |
$classReflection = new ReflectionClass($class); | |
foreach ($classReflection->getProperties() as $property) { | |
if ($property->isStatic() && $property->isInitialized()) { | |
$propertyValue = $property->getValue(); | |
$propertyName = $property->getName(); | |
$global['class'][$class]['prop'][$propertyName] = $propertyValue; | |
} | |
} | |
foreach ($classReflection->getMethods() as $method) { | |
$methodName = $method->getName(); | |
foreach ($method->getStaticVariables() as $key => $value) { | |
$global['class'][$class]['method'][$methodName][$key] = $value; | |
} | |
} | |
} | |
foreach (get_defined_functions()['user'] as $function) { | |
$functionReflection = new ReflectionFunction($function); | |
foreach ($functionReflection->getStaticVariables() as $key => $value) { | |
$global['function'][$function][$key] = $value; | |
} | |
} | |
$global['trace'] = debug_backtrace(); | |
$global['constant'] = get_defined_constants(true); | |
$global['stream_context_params'] = stream_context_get_params(stream_context_get_default()); | |
$global['resource'] = get_resources(); | |
$global['autoload'] = spl_autoload_functions(); | |
$global['ob_handler'] = ob_list_handlers(); | |
$global['ini'] = ini_get_all(); | |
$global['error_handler'] = set_error_handler(static fn () => false); | |
$global['exception_handler'] = set_exception_handler(static fn () => null); | |
restore_error_handler(); | |
restore_exception_handler(); | |
for ($signalNumber = 1; $signalNumber < 32; $signalNumber++) { | |
$signalHandler = pcntl_signal_get_handler($signalNumber); | |
$global['signal'][$signalNumber] = $signalHandler; | |
} | |
return $global; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment