Created
November 23, 2025 22:56
-
-
Save DavyCraft648/5ed413f726d86714e117cd8daa2af6e8 to your computer and use it in GitHub Desktop.
Theis script parses the JSON dump files generated by src/MemoryDump.php and produces a command-line summary of reference counts, instance counts, static/function statics, and top objects. It can also build an object reference graph to show reference chains, detect reference cycles, report string-size statistics, and print detailed information fo…
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
| <?php | |
| $top = 15; | |
| $folder = null; | |
| $script = basename($argv[0]); | |
| $doDetails = false; | |
| $detailHash = null; | |
| $doCycles = false; | |
| $doSizes = false; | |
| for ($i = 1; $i < $argc; $i++) { | |
| $arg = $argv[$i]; | |
| if ($arg === '--') { | |
| if (isset($argv[$i + 1])) { | |
| $folder = $argv[$i + 1]; | |
| } | |
| break; | |
| } | |
| if (str_starts_with($arg, '--top=')) { | |
| $topVal = substr($arg, strlen('--top=')); | |
| if (is_numeric($topVal) && (int)$topVal >= 0) { | |
| $top = (int)$topVal; | |
| } else { | |
| fwrite(STDERR, "Invalid value for --top: $topVal. Must be an integer >= 0. Using default $top.\n"); | |
| } | |
| continue; | |
| } | |
| if (str_starts_with($arg, '--details=')) { | |
| $doDetails = true; | |
| $detailHash = substr($arg, strlen('--details=')); | |
| continue; | |
| } | |
| if ($arg === '--details') { | |
| $doDetails = true; | |
| if (isset($argv[$i + 1])) { | |
| $i++; | |
| $detailHash = $argv[$i]; | |
| } else { | |
| fwrite(STDERR, "Missing value for --details\n"); | |
| exit(1); | |
| } | |
| continue; | |
| } | |
| if ($arg === '--cycles') { | |
| $doCycles = true; | |
| continue; | |
| } | |
| if ($arg === '--sizes') { | |
| $doSizes = true; | |
| continue; | |
| } | |
| if ($arg === '--top' || $arg === '-t') { | |
| if (isset($argv[$i + 1])) { | |
| $i++; | |
| $topVal = $argv[$i]; | |
| if (is_numeric($topVal) && (int)$topVal >= 0) { | |
| $top = (int)$topVal; | |
| } else { | |
| fwrite(STDERR, "Invalid value for $arg: $topVal. Must be an integer >= 0. Using default $top.\n"); | |
| } | |
| } else { | |
| fwrite(STDERR, "Missing value for $arg.\n"); | |
| exit(1); | |
| } | |
| continue; | |
| } | |
| if (strlen($arg) > 0 && $arg[0] === '-') { | |
| fwrite(STDERR, "Unknown option: $arg\n"); | |
| continue; | |
| } | |
| if ($folder === null) { | |
| $folder = $arg; | |
| } | |
| } | |
| if ($folder === null) { | |
| fwrite(STDERR, "Usage: php $script path\\to\\dump\\folder [--top N] [--details HASH] [--cycles] [--sizes]\n"); | |
| exit(1); | |
| } | |
| if (!is_dir($folder) || !is_readable($folder)) { | |
| fwrite(STDERR, "Folder does not exist or is not readable: $folder\n"); | |
| exit(1); | |
| } | |
| $files = [ | |
| 'ref' => $folder . DIRECTORY_SEPARATOR . 'referenceCounts.js', | |
| 'inst' => $folder . DIRECTORY_SEPARATOR . 'instanceCounts.js', | |
| 'static' => $folder . DIRECTORY_SEPARATOR . 'staticProperties.js', | |
| 'funcstatic' => $folder . DIRECTORY_SEPARATOR . 'functionStaticVars.js', | |
| 'server' => $folder . DIRECTORY_SEPARATOR . 'serverEntry.js', | |
| 'objects' => $folder . DIRECTORY_SEPARATOR . 'objects.js', | |
| 'globals' => $folder . DIRECTORY_SEPARATOR . 'globalVariables.js', | |
| ]; | |
| foreach ($files as $k => $p) { | |
| if (!file_exists($p)) { | |
| fwrite(STDERR, "Missing file: $p\n"); | |
| } | |
| } | |
| function loadJsonFile($path) { | |
| if (!file_exists($path)) return null; | |
| $s = file_get_contents($path); | |
| if ($s === false) return null; | |
| $data = json_decode($s, true); | |
| if (json_last_error() !== JSON_ERROR_NONE) { | |
| fwrite(STDERR, "JSON decode error for $path: " . json_last_error_msg() . "\n"); | |
| return null; | |
| } | |
| return $data; | |
| } | |
| $refCounts = loadJsonFile($files['ref']) ?? []; | |
| $instanceCounts = loadJsonFile($files['inst']) ?? []; | |
| $staticProps = loadJsonFile($files['static']) ?? []; | |
| $functionStatics = loadJsonFile($files['funcstatic']) ?? []; | |
| $serverEntry = loadJsonFile($files['server']) ?? null; | |
| $globalVariables = loadJsonFile($files['globals']) ?? null; | |
| $objectsIndex = []; // hash => info array | |
| $mostProps = []; | |
| $closures = []; | |
| $lineNo = 0; | |
| if (file_exists($files['objects'])) { | |
| $fh = fopen($files['objects'], 'rb'); | |
| if ($fh) { | |
| while (($line = fgets($fh)) !== false) { | |
| $lineNo++; | |
| $o = json_decode($line, true); | |
| if ($o === null) continue; | |
| if (!isset($o['information'])) continue; | |
| // hash@className | |
| $parts = explode('@', $o['information'], 2); | |
| $hash = $parts[0]; | |
| $class = $parts[1] ?? '(unknown)'; | |
| $objectsIndex[$hash] = $o; | |
| $propCount = 0; | |
| if (isset($o['properties']) && is_array($o['properties'])) { | |
| $propCount = count($o['properties']); | |
| } | |
| $mostProps[$hash] = ['class' => $class, 'props' => $propCount]; | |
| if (isset($o['definition']) || isset($o['referencedVars'])) { | |
| $closures[$hash] = [ | |
| 'class' => $class, | |
| 'definition' => $o['definition'] ?? null, | |
| 'refs' => isset($o['referencedVars']) ? count($o['referencedVars']) : 0, | |
| 'has_this' => isset($o['this']), | |
| ]; | |
| } | |
| } | |
| fclose($fh); | |
| } | |
| } | |
| function findReferencedObjectHashes(mixed $data, array &$found) : void{ | |
| if (is_string($data)){ | |
| if (preg_match('/^\(object\)\s*([0-9a-fA-F]+)$/', $data, $m)){ | |
| $found[$m[1]] = true; | |
| } | |
| return; | |
| } | |
| if (is_array($data)){ | |
| foreach ($data as $v){ | |
| findReferencedObjectHashes($v, $found); | |
| } | |
| } | |
| } | |
| function findStringLens(mixed $data, array &$lens) : void{ | |
| if (is_string($data)){ | |
| if (preg_match('/^\(string\) len\((\d+)\)/', $data, $m)){ | |
| $l = (int)$m[1]; | |
| $lens[] = $l; | |
| } | |
| return; | |
| } | |
| if (is_array($data)){ | |
| foreach ($data as $v){ | |
| findStringLens($v, $lens); | |
| } | |
| } | |
| } | |
| function buildReferenceGraph(array $objectsIndex, array $staticProps, array $functionStatics, mixed $serverEntry, mixed $globalVariables) : array{ | |
| $adj = []; | |
| foreach ($objectsIndex as $owner => $info){ | |
| $found = []; | |
| if (isset($info['properties'])){ | |
| findReferencedObjectHashes($info['properties'], $found); | |
| } | |
| if (isset($info['referencedVars'])){ | |
| findReferencedObjectHashes($info['referencedVars'], $found); | |
| } | |
| if (isset($info['this'])){ | |
| findReferencedObjectHashes($info['this'], $found); | |
| } | |
| if (count($found) > 0){ | |
| $adj[$owner] = array_keys($found); | |
| } | |
| } | |
| foreach ($staticProps as $cls => $props){ | |
| foreach ($props as $name => $val){ | |
| $found = []; | |
| findReferencedObjectHashes($val, $found); | |
| if (count($found) > 0){ | |
| $owner = "static:" . $cls; | |
| if (!isset($adj[$owner])) $adj[$owner] = []; | |
| $adj[$owner] = array_unique(array_merge($adj[$owner], array_keys($found))); | |
| } | |
| } | |
| } | |
| foreach ($functionStatics as $fn => $vars){ | |
| foreach ($vars as $name => $val){ | |
| $found = []; | |
| findReferencedObjectHashes($val, $found); | |
| if (count($found) > 0){ | |
| $owner = "func:" . $fn; | |
| if (!isset($adj[$owner])) $adj[$owner] = []; | |
| $adj[$owner] = array_unique(array_merge($adj[$owner], array_keys($found))); | |
| } | |
| } | |
| } | |
| if ($serverEntry !== null){ | |
| $found = []; | |
| findReferencedObjectHashes($serverEntry, $found); | |
| if (count($found) > 0){ | |
| $adj['serverEntry'] = array_keys($found); | |
| } | |
| } | |
| if ($globalVariables !== null && is_array($globalVariables)){ | |
| foreach ($globalVariables as $gname => $val){ | |
| $found = []; | |
| findReferencedObjectHashes($val, $found); | |
| if (count($found) > 0){ | |
| $owner = "global:" . $gname; | |
| if (!isset($adj[$owner])) $adj[$owner] = []; | |
| $adj[$owner] = array_unique(array_merge($adj[$owner], array_keys($found))); | |
| } | |
| } | |
| } | |
| return $adj; | |
| } | |
| function findPathFromRoots(array $adj, array $roots, string $target) : array{ | |
| $queue = []; | |
| $seen = []; | |
| $parent = []; | |
| foreach ($roots as $r) { | |
| $queue[] = $r; | |
| $seen[$r] = true; | |
| $parent[$r] = null; | |
| } | |
| $found = false; | |
| while (!empty($queue)){ | |
| $node = array_shift($queue); | |
| $neighbors = $adj[$node] ?? []; | |
| foreach ($neighbors as $n){ | |
| if (!isset($seen[$n])){ | |
| $seen[$n] = true; | |
| $parent[$n] = $node; | |
| if ($n === $target){ | |
| $found = true; | |
| break 2; | |
| } | |
| $queue[] = $n; | |
| } | |
| } | |
| } | |
| if (!$found) return []; | |
| // reconstruct path | |
| $path = []; | |
| $cur = $target; | |
| while ($cur !== null){ | |
| $path[] = $cur; | |
| $cur = $parent[$cur] ?? null; | |
| } | |
| return array_reverse($path); | |
| } | |
| function detectCycles(array $adj, int $limit = 20) : array{ | |
| $visited = []; | |
| $onstack = []; | |
| $stack = []; | |
| $cycles = []; | |
| $dfs = function($node) use (&$dfs, &$visited, &$onstack, &$stack, &$adj, &$cycles, $limit){ | |
| if (isset($visited[$node])) return; | |
| $visited[$node] = true; | |
| $onstack[$node] = count($stack); | |
| $stack[] = $node; | |
| foreach ($adj[$node] ?? [] as $n){ | |
| if (!isset($visited[$n])){ | |
| $dfs($n); | |
| if (count($cycles) >= $limit) return; | |
| }elseif (isset($onstack[$n])){ | |
| $start = $onstack[$n]; | |
| $cycle = array_slice($stack, $start); | |
| $cycle[] = $n; | |
| $cycles[] = $cycle; | |
| if (count($cycles) >= $limit) return; | |
| } | |
| } | |
| array_pop($stack); | |
| unset($onstack[$node]); | |
| }; | |
| foreach (array_keys($adj) as $n){ | |
| if (!isset($visited[$n])){ | |
| $dfs($n); | |
| if (count($cycles) >= $limit) break; | |
| } | |
| } | |
| return $cycles; | |
| } | |
| arsort($refCounts, SORT_NUMERIC); | |
| $topRefs = array_slice($refCounts, 0, $top, true); | |
| arsort($instanceCounts, SORT_NUMERIC); | |
| $topClasses = array_slice($instanceCounts, 0, $top, true); | |
| uasort($mostProps, function($a, $b){ return $b['props'] <=> $a['props']; }); | |
| $mostProps = array_slice($mostProps, 0, $top, true); | |
| uasort($closures, function($a, $b){ return $b['refs'] <=> $a['refs']; }); | |
| $topClosures = array_slice($closures, 0, $top, true); | |
| $graphAdj = null; | |
| $graphRev = null; | |
| $roots = []; | |
| if ($doDetails || $doCycles || $doSizes) { | |
| $graphAdj = buildReferenceGraph($objectsIndex, $staticProps, $functionStatics, $serverEntry, $globalVariables); | |
| $graphRev = []; | |
| foreach ($graphAdj as $owner => $targets) { | |
| foreach ($targets as $t) { | |
| $graphRev[$t][] = $owner; | |
| } | |
| } | |
| if ($serverEntry !== null) { | |
| $found = []; | |
| findReferencedObjectHashes($serverEntry, $found); | |
| foreach (array_keys($found) as $h) $roots[] = $h; | |
| } | |
| if ($globalVariables !== null && is_array($globalVariables)){ | |
| foreach ($globalVariables as $g){ | |
| $found = []; | |
| findReferencedObjectHashes($g, $found); | |
| foreach (array_keys($found) as $h) $roots[] = $h; | |
| } | |
| } | |
| $roots = array_values(array_unique($roots)); | |
| } | |
| if ($doSizes) { | |
| $lens = []; | |
| foreach ($objectsIndex as $info){ | |
| if (isset($info['properties'])) findStringLens($info['properties'], $lens); | |
| if (isset($info['referencedVars'])) findStringLens($info['referencedVars'], $lens); | |
| if (isset($info['definition'])) findStringLens($info['definition'], $lens); | |
| } | |
| foreach ($staticProps as $cls => $props) { | |
| foreach ($props as $p) findStringLens($p, $lens); | |
| } | |
| foreach ($functionStatics as $fn => $vars) { | |
| foreach ($vars as $v) findStringLens($v, $lens); | |
| } | |
| if ($serverEntry !== null) findStringLens($serverEntry, $lens); | |
| if ($globalVariables !== null) findStringLens($globalVariables, $lens); | |
| rsort($lens, SORT_NUMERIC); | |
| $totalStrings = count($lens); | |
| $totalBytes = array_sum($lens); | |
| $topStrings = array_slice($lens, 0, $top); | |
| echo "--- String size summary (approx from dump serializer) ---\n"; | |
| echo "total strings counted: $totalStrings\n"; | |
| echo "total reported string bytes: $totalBytes\n"; | |
| echo "top $top longest strings (reported len): " . implode(", ", $topStrings) . "\n\n"; | |
| } | |
| if ($doCycles) { | |
| $cycles = detectCycles($graphAdj ?? [], 50); | |
| echo "--- Detected reference cycles (showing up to 50) ---\n"; | |
| if (count($cycles) === 0) { | |
| echo "(none)\n\n"; | |
| } else { | |
| foreach ($cycles as $idx => $c) { | |
| echo sprintf("%2d) %s\n", $idx + 1, implode(' -> ', $c)); | |
| } | |
| echo "\n"; | |
| } | |
| } | |
| if ($doDetails) { | |
| if ($detailHash === null) { | |
| fwrite(STDERR, "--details requires an object hash\n"); | |
| exit(1); | |
| } | |
| echo "--- Details for object: $detailHash ---\n"; | |
| if (!isset($objectsIndex[$detailHash])) { | |
| echo "Object not found in objects.js\n\n"; | |
| } else { | |
| $info = $objectsIndex[$detailHash]; | |
| $parts = explode('@', $info['information'] ?? "$detailHash@(unknown)", 2); | |
| $cls = $parts[1] ?? '(unknown)'; | |
| echo "class: $cls\n"; | |
| echo "refCount: " . ($refCounts[$detailHash] ?? 0) . "\n"; | |
| $props = $info['properties'] ?? []; | |
| echo "properties (" . count($props) . "):\n"; | |
| foreach ($props as $name => $val) { | |
| echo " - $name => "; | |
| if (is_string($val)) echo $val . "\n"; else echo json_encode($val) . "\n"; | |
| } | |
| $refsBy = $graphRev[$detailHash] ?? []; | |
| echo "referenced-by (" . count($refsBy) . "):\n"; | |
| foreach ($refsBy as $r) { | |
| echo " - $r\n"; | |
| } | |
| if (!empty($roots)){ | |
| $path = findPathFromRoots($graphAdj ?? [], $roots, $detailHash); | |
| if (!empty($path)){ | |
| echo "\npath from root: " . implode(' -> ', $path) . "\n"; | |
| } else { | |
| echo "\nNo path from serverEntry/global roots found.\n"; | |
| } | |
| } | |
| echo "\n"; | |
| } | |
| } | |
| echo "=== Memory Dump Analysis for: {$folder} ===\n\n"; | |
| echo "(showing top $top entries where applicable)\n\n"; | |
| echo "--- Top referenced objects (by reference count) ---\n"; | |
| $i = 0; | |
| foreach ($topRefs as $hash => $count) { | |
| $i++; | |
| $class = isset($objectsIndex[$hash]) ? (explode('@', $objectsIndex[$hash]['information'], 2)[1] ?? '(unknown)') : '(unknown)'; | |
| echo sprintf("%2d) %s - refs=%d class=%s\n", $i, $hash, $count, $class); | |
| } | |
| if ($i === 0) echo "(none)\n"; | |
| echo "\n"; | |
| echo "--- Top instance counts (classes) ---\n"; | |
| $i = 0; | |
| foreach ($topClasses as $cls => $count) { | |
| $i++; | |
| echo sprintf("%2d) %s => %d\n", $i, $cls, $count); | |
| } | |
| if ($i === 0) echo "(none)\n"; | |
| echo "\n"; | |
| echo "--- Objects with most properties ---\n"; | |
| $i = 0; | |
| foreach ($mostProps as $hash => $info) { | |
| $i++; | |
| echo sprintf("%2d) %s - props=%d class=%s\n", $i, $hash, $info['props'], $info['class']); | |
| } | |
| if ($i === 0) echo "(none)\n"; | |
| echo "\n"; | |
| echo "--- Closures with most referenced statics ---\n"; | |
| $i = 0; | |
| foreach ($topClosures as $hash => $info) { | |
| $i++; | |
| $def = $info['definition'] ?? '(anonymous)'; | |
| echo sprintf("%2d) %s class=%s refs=%d has_this=%s def=%s\n", $i, $hash, $info['class'], $info['refs'], $info['has_this'] ? 'yes' : 'no', $def); | |
| } | |
| if ($i === 0) echo "(none)\n"; | |
| echo "\n"; | |
| echo "--- Static properties summary ---\n"; | |
| $totalStaticClasses = count($staticProps); | |
| $totalStaticProps = 0; | |
| foreach ($staticProps as $cls => $props) $totalStaticProps += count($props); | |
| echo "classes with statics: $totalStaticClasses\n"; | |
| echo "total static properties: $totalStaticProps\n\n"; | |
| echo "--- Function static vars summary ---\n"; | |
| $totalFunc = 0; | |
| foreach ($functionStatics as $fn => $vars) $totalFunc += count($vars); | |
| echo "functions/methods with statics: " . count($functionStatics) . "\n"; | |
| echo "total function static vars: $totalFunc\n\n"; | |
| echo "--- Server entry root type ---\n"; | |
| if ($serverEntry !== null) { | |
| $found = []; | |
| findReferencedObjectHashes($serverEntry, $found); | |
| if (count($found) > 0) { | |
| echo "serverEntry references the following object hashes:\n"; | |
| foreach ($found as $hash => $_) { | |
| $cls = isset($objectsIndex[$hash]) ? (explode('@', $objectsIndex[$hash]['information'], 2)[1] ?? '(unknown)') : '(not in objects.js)'; | |
| echo " - $hash => $cls\n"; | |
| } | |
| } else { | |
| if (is_string($serverEntry) && preg_match('/^\(object\)\s*([0-9a-fA-F]+)$/', $serverEntry, $m)){ | |
| $h = $m[1]; | |
| $cls = isset($objectsIndex[$h]) ? (explode('@', $objectsIndex[$h]['information'], 2)[1] ?? '(unknown)') : '(not in objects.js)'; | |
| echo "serverEntry appears to be an object reference: $h => $cls\n"; | |
| } | |
| } | |
| } else { | |
| echo "serverEntry.js not found or failed to parse\n"; | |
| } | |
| echo "\n"; | |
| echo "Finished.\n"; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment