From 15b08c1417a496c5d987c28eb7b4e3eb04f3250a Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Thu, 14 Sep 2017 16:45:48 +0100 Subject: [PATCH] Added capability to dump AsyncWorkers' memory (#1379) This now actually works with PHP 7.2 + latest pthreads, before it was too unstable. --- src/pocketmine/MemoryManager.php | 84 +++++++++++++++---- src/pocketmine/resources/pocketmine.yml | 6 ++ src/pocketmine/scheduler/AsyncWorker.php | 4 + .../scheduler/DumpWorkerMemoryTask.php | 61 ++++++++++++++ 4 files changed, 141 insertions(+), 14 deletions(-) create mode 100644 src/pocketmine/scheduler/DumpWorkerMemoryTask.php diff --git a/src/pocketmine/MemoryManager.php b/src/pocketmine/MemoryManager.php index 355bcf38f..204e24986 100644 --- a/src/pocketmine/MemoryManager.php +++ b/src/pocketmine/MemoryManager.php @@ -25,7 +25,9 @@ namespace pocketmine; use pocketmine\event\server\LowMemoryEvent; use pocketmine\event\Timings; +use pocketmine\scheduler\DumpWorkerMemoryTask; use pocketmine\scheduler\GarbageCollectionTask; +use pocketmine\utils\MainLogger; use pocketmine\utils\Utils; class MemoryManager{ @@ -74,6 +76,9 @@ class MemoryManager{ /** @var bool */ private $cacheTrigger; + /** @var bool */ + private $dumpWorkers = true; + public function __construct(Server $server){ $this->server = $server; @@ -131,6 +136,7 @@ class MemoryManager{ $this->chunkCache = (bool) $this->server->getProperty("memory.world-caches.disable-chunk-cache", true); $this->cacheTrigger = (bool) $this->server->getProperty("memory.world-caches.low-memory-trigger", true); + $this->dumpWorkers = (bool) $this->server->getProperty("memory.memory-dump.dump-async-worker", true); gc_enable(); } @@ -261,6 +267,27 @@ class MemoryManager{ * @param int $maxStringSize */ public function dumpServerMemory(string $outputFolder, int $maxNesting, int $maxStringSize){ + MainLogger::getLogger()->notice("[Dump] After the memory dump is done, the server might crash"); + self::dumpMemory($this->server, $this->server->getLoader(), $outputFolder, $maxNesting, $maxStringSize); + + if($this->dumpWorkers){ + $scheduler = $this->server->getScheduler(); + for($i = 0, $size = $scheduler->getAsyncTaskPoolSize(); $i < $size; ++$i){ + $scheduler->scheduleAsyncTaskToWorker(new DumpWorkerMemoryTask($outputFolder, $maxNesting, $maxStringSize), $i); + } + } + } + + /** + * Static memory dumper accessible from any thread. + * + * @param mixed $startingObject + * @param \ClassLoader $loader + * @param string $outputFolder + * @param int $maxNesting + * @param int $maxStringSize + */ + public static function dumpMemory($startingObject, \ClassLoader $loader, string $outputFolder, int $maxNesting, int $maxStringSize){ $hardLimit = ini_get('memory_limit'); ini_set('memory_limit', '-1'); gc_disable(); @@ -269,12 +296,8 @@ class MemoryManager{ mkdir($outputFolder, 0777, true); } - $this->server->getLogger()->notice("[Dump] After the memory dump is done, the server might crash"); - $obData = fopen($outputFolder . "/objects.js", "wb+"); - $staticProperties = []; - $data = []; $objects = []; @@ -283,8 +306,10 @@ class MemoryManager{ $instanceCounts = []; + $staticProperties = []; $staticCount = 0; - foreach($this->server->getLoader()->getClasses() as $className){ + + foreach($loader->getClasses() as $className){ $reflection = new \ReflectionClass($className); $staticProperties[$className] = []; foreach($reflection->getProperties() as $property){ @@ -297,7 +322,7 @@ class MemoryManager{ } $staticCount++; - $this->continueDump($property->getValue(), $staticProperties[$className][$property->getName()], $objects, $refCounts, 0, $maxNesting, $maxStringSize); + self::continueDump($property->getValue(), $staticProperties[$className][$property->getName()], $objects, $refCounts, 0, $maxNesting, $maxStringSize); } if(count($staticProperties[$className]) === 0){ @@ -305,9 +330,39 @@ class MemoryManager{ } } - echo "[Dump] Wrote $staticCount static properties\n"; + file_put_contents($outputFolder . "/staticProperties.js", json_encode($staticProperties, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); + MainLogger::getLogger()->info("[Dump] Wrote $staticCount static properties"); - $this->continueDump($this->server, $data, $objects, $refCounts, 0, $maxNesting, $maxStringSize); + if($GLOBALS !== null){ //This might be null if we're on a different thread + $globalVariables = []; + $globalCount = 0; + + $ignoredGlobals = [ + 'GLOBALS' => true, + '_SERVER' => true, + '_REQUEST' => true, + '_POST' => true, + '_GET' => true, + '_FILES' => true, + '_ENV' => true, + '_COOKIE' => true, + '_SESSION' => true + ]; + + foreach($GLOBALS as $varName => $value){ + if(isset($ignoredGlobals[$varName])){ + continue; + } + + $globalCount++; + self::continueDump($value, $globalVariables[$varName], $objects, $refCounts, 0, $maxNesting, $maxStringSize); + } + + file_put_contents($outputFolder . "/globalVariables.js", json_encode($globalVariables, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); + MainLogger::getLogger()->info("[Dump] Wrote $globalCount global variables"); + } + + self::continueDump($startingObject, $data, $objects, $refCounts, 0, $maxNesting, $maxStringSize); do{ $continue = false; @@ -349,25 +404,26 @@ class MemoryManager{ if(!$property->isPublic()){ $property->setAccessible(true); } - $this->continueDump($property->getValue($object), $info["properties"][$property->getName()], $objects, $refCounts, 0, $maxNesting, $maxStringSize); + self::continueDump($property->getValue($object), $info["properties"][$property->getName()], $objects, $refCounts, 0, $maxNesting, $maxStringSize); } fwrite($obData, "$hash@$className: " . json_encode($info, JSON_UNESCAPED_SLASHES) . "\n"); } - echo "[Dump] Wrote " . count($objects) . " objects\n"; + }while($continue); + MainLogger::getLogger()->info("[Dump] Wrote " . count($objects) . " objects"); + fclose($obData); - file_put_contents($outputFolder . "/staticProperties.js", json_encode($staticProperties, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); file_put_contents($outputFolder . "/serverEntry.js", json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); file_put_contents($outputFolder . "/referenceCounts.js", json_encode($refCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); arsort($instanceCounts, SORT_NUMERIC); file_put_contents($outputFolder . "/instanceCounts.js", json_encode($instanceCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); - echo "[Dump] Finished!\n"; + MainLogger::getLogger()->info("[Dump] Finished!"); ini_set('memory_limit', $hardLimit); gc_enable(); @@ -382,7 +438,7 @@ class MemoryManager{ * @param int $maxNesting * @param int $maxStringSize */ - private function continueDump($from, &$data, array &$objects, array &$refCounts, int $recursion, int $maxNesting, int $maxStringSize){ + private static function continueDump($from, &$data, array &$objects, array &$refCounts, int $recursion, int $maxNesting, int $maxStringSize){ if($maxNesting <= 0){ $data = "(error) NESTING LIMIT REACHED"; return; @@ -406,7 +462,7 @@ class MemoryManager{ } $data = []; foreach($from as $key => $value){ - $this->continueDump($value, $data[$key], $objects, $refCounts, $recursion + 1, $maxNesting, $maxStringSize); + self::continueDump($value, $data[$key], $objects, $refCounts, $recursion + 1, $maxNesting, $maxStringSize); } }elseif(is_string($from)){ $data = "(string) len(". strlen($from) .") " . substr(Utils::printable($from), 0, $maxStringSize); diff --git a/src/pocketmine/resources/pocketmine.yml b/src/pocketmine/resources/pocketmine.yml index bde0ec7ef..2295ec732 100644 --- a/src/pocketmine/resources/pocketmine.yml +++ b/src/pocketmine/resources/pocketmine.yml @@ -62,6 +62,12 @@ memory: #Trigger on low memory low-memory-trigger: true + #Settings controlling memory dump handling. + memory-dump: + #Dump memory from async workers as well as the main thread. If you have issues with segfaults when dumping memory, disable this setting. + dump-async-worker: true + + max-chunks: #Maximum render distance per player when low memory is triggered chunk-radius: 4 diff --git a/src/pocketmine/scheduler/AsyncWorker.php b/src/pocketmine/scheduler/AsyncWorker.php index 80608acda..57d41d5c1 100644 --- a/src/pocketmine/scheduler/AsyncWorker.php +++ b/src/pocketmine/scheduler/AsyncWorker.php @@ -64,4 +64,8 @@ class AsyncWorker extends Worker{ public function getThreadName() : string{ return "Asynchronous Worker #" . $this->id; } + + public function getAsyncWorkerId() : int{ + return $this->id; + } } diff --git a/src/pocketmine/scheduler/DumpWorkerMemoryTask.php b/src/pocketmine/scheduler/DumpWorkerMemoryTask.php new file mode 100644 index 000000000..17cd5fc8b --- /dev/null +++ b/src/pocketmine/scheduler/DumpWorkerMemoryTask.php @@ -0,0 +1,61 @@ +outputFolder = $outputFolder; + $this->maxNesting = $maxNesting; + $this->maxStringSize = $maxStringSize; + } + + public function onRun(){ + global $store; + + MemoryManager::dumpMemory( + ["worker" => $this->worker, "store" => $store], + $this->worker->getClassLoader(), + $this->outputFolder . DIRECTORY_SEPARATOR . "AsyncWorker#" . $this->worker->getAsyncWorkerId(), + $this->maxNesting, + $this->maxStringSize + ); + } +} \ No newline at end of file