Skip to content

Instantly share code, notes, and snippets.

@JanTvrdik
Last active September 21, 2024 06:38
Show Gist options
  • Save JanTvrdik/95c6c3eb71cd3c9fc228e62d641e0876 to your computer and use it in GitHub Desktop.
Save JanTvrdik/95c6c3eb71cd3c9fc228e62d641e0876 to your computer and use it in GitHub Desktop.
<?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