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/PocketMine.php b/src/pocketmine/PocketMine.php index a0a959c68..fe3e639cb 100644 --- a/src/pocketmine/PocketMine.php +++ b/src/pocketmine/PocketMine.php @@ -80,7 +80,7 @@ namespace pocketmine { use raklib\RakLib; const VERSION = "1.7dev"; - const API_VERSION = "3.0.0-ALPHA7"; + const API_VERSION = "3.0.0-ALPHA8"; const CODENAME = "[REDACTED]"; /* @@ -558,19 +558,7 @@ namespace pocketmine { $killer->start(); usleep(10000); //Fixes ServerKiller not being able to start on single-core machines - $erroredThreads = 0; - foreach(ThreadManager::getInstance()->getAll() as $id => $thread){ - $logger->debug("Stopping " . $thread->getThreadName() . " thread"); - try{ - $thread->quit(); - $logger->debug($thread->getThreadName() . " thread stopped successfully."); - }catch(\ThreadException $e){ - ++$erroredThreads; - $logger->debug("Could not stop " . $thread->getThreadName() . " thread: " . $e->getMessage()); - } - } - - if($erroredThreads > 0){ + if(ThreadManager::getInstance()->stopAll() > 0){ if(\pocketmine\DEBUG > 1){ echo "Some threads could not be stopped, performing a force-kill" . PHP_EOL . PHP_EOL; } diff --git a/src/pocketmine/ThreadManager.php b/src/pocketmine/ThreadManager.php index 195a7123e..1d615e6da 100644 --- a/src/pocketmine/ThreadManager.php +++ b/src/pocketmine/ThreadManager.php @@ -23,6 +23,8 @@ declare(strict_types=1); namespace pocketmine; +use pocketmine\utils\MainLogger; + class ThreadManager extends \Volatile{ /** @var ThreadManager */ @@ -68,4 +70,23 @@ class ThreadManager extends \Volatile{ return $array; } + + public function stopAll() : int{ + $logger = MainLogger::getLogger(); + + $erroredThreads = 0; + + foreach($this->getAll() as $thread){ + $logger->debug("Stopping " . $thread->getThreadName() . " thread"); + try{ + $thread->quit(); + $logger->debug($thread->getThreadName() . " thread stopped successfully."); + }catch(\ThreadException $e){ + ++$erroredThreads; + $logger->debug("Could not stop " . $thread->getThreadName() . " thread: " . $e->getMessage()); + } + } + + return $erroredThreads; + } } \ No newline at end of file diff --git a/src/pocketmine/block/BlockFactory.php b/src/pocketmine/block/BlockFactory.php index 2891407d2..dbd65d811 100644 --- a/src/pocketmine/block/BlockFactory.php +++ b/src/pocketmine/block/BlockFactory.php @@ -341,7 +341,7 @@ class BlockFactory{ public static function registerBlock(Block $block, bool $override = false){ $id = $block->getId(); - if(self::$list[$id] !== null and !(self::$list[$id] instanceof UnknownBlock) and !$override){ + if(!$override and self::isRegistered($id)){ throw new \RuntimeException("Trying to overwrite an already registered block"); } @@ -403,4 +403,15 @@ class BlockFactory{ public static function getBlockStatesArray() : \SplFixedArray{ return self::$fullList; } + + /** + * Returns whether a specified block ID is already registered in the block factory. + * + * @param int $id + * @return bool + */ + public static function isRegistered(int $id) : bool{ + $b = self::$list[$id]; + return $b !== null and !($b instanceof UnknownBlock); + } } \ No newline at end of file diff --git a/src/pocketmine/item/ItemFactory.php b/src/pocketmine/item/ItemFactory.php index f08561ff2..3802855e0 100644 --- a/src/pocketmine/item/ItemFactory.php +++ b/src/pocketmine/item/ItemFactory.php @@ -256,7 +256,7 @@ class ItemFactory{ */ public static function registerItem(Item $item, bool $override = false){ $id = $item->getId(); - if(!$override and self::$list[$id] !== null){ + if(!$override and self::isRegistered($id)){ throw new \RuntimeException("Trying to overwrite an already registered item"); } @@ -350,4 +350,17 @@ class ItemFactory{ return $item; } } + + /** + * Returns whether the specified item ID is already registered in the item factory. + * + * @param int $id + * @return bool + */ + public static function isRegistered(int $id) : bool{ + if($id < 256){ + return BlockFactory::isRegistered($id); + } + return self::$list[$id] !== null; + } } \ No newline at end of file diff --git a/src/pocketmine/level/Location.php b/src/pocketmine/level/Location.php index 25de1f346..d29f8b26c 100644 --- a/src/pocketmine/level/Location.php +++ b/src/pocketmine/level/Location.php @@ -41,12 +41,9 @@ class Location extends Position{ * @param Level $level */ public function __construct($x = 0, $y = 0, $z = 0, $yaw = 0.0, $pitch = 0.0, Level $level = null){ - $this->x = $x; - $this->y = $y; - $this->z = $z; $this->yaw = $yaw; $this->pitch = $pitch; - $this->level = $level; + parent::__construct($x, $y, $z, $level); } /** 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 diff --git a/src/pocketmine/updater/AutoUpdater.php b/src/pocketmine/updater/AutoUpdater.php index 50208de1c..d611f1852 100644 --- a/src/pocketmine/updater/AutoUpdater.php +++ b/src/pocketmine/updater/AutoUpdater.php @@ -89,15 +89,17 @@ class AutoUpdater{ * Posts a warning to the console to tell the user there is an update available */ public function showConsoleUpdate(){ - $logger = $this->server->getLogger(); $newVersion = new VersionString($this->updateInfo["version"]); - $logger->warning("----- PocketMine-MP Auto Updater -----"); - $logger->warning("Your version of PocketMine-MP is out of date. Version " . $newVersion->get(false) . " (build #" . $newVersion->getBuild() . ") was released on " . date("D M j h:i:s Y", $this->updateInfo["date"])); + + $messages = [ + "Your version of " . $this->server->getName() . " is out of date. Version " . $newVersion->get(false) . " (build #" . $newVersion->getBuild() . ") was released on " . date("D M j h:i:s Y", $this->updateInfo["date"]) + ]; if($this->updateInfo["details_url"] !== null){ - $logger->warning("Details: " . $this->updateInfo["details_url"]); + $messages[] = "Details: " . $this->updateInfo["details_url"]; } - $logger->warning("Download: " . $this->updateInfo["download_url"]); - $logger->warning("----- -------------------------- -----"); + $messages[] = "Download: " . $this->updateInfo["download_url"]; + + $this->printConsoleMessage($messages, \LogLevel::WARNING); } /** @@ -105,24 +107,33 @@ class AutoUpdater{ * @param Player $player */ public function showPlayerUpdate(Player $player){ - $player->sendMessage(TextFormat::DARK_PURPLE . "The version of PocketMine-MP that this server is running is out of date. Please consider updating to the latest version."); + $player->sendMessage(TextFormat::DARK_PURPLE . "The version of " . $this->server->getName() . " that this server is running is out of date. Please consider updating to the latest version."); $player->sendMessage(TextFormat::DARK_PURPLE . "Check the console for more details."); } protected function showChannelSuggestionStable(){ - $logger = $this->server->getLogger(); - $logger->info("----- PocketMine-MP Auto Updater -----"); - $logger->info("It appears you're running a Stable build, when you've specified that you prefer to run " . ucfirst($this->getChannel()) . " builds."); - $logger->info("If you would like to be kept informed about new Stable builds only, it is recommended that you change 'preferred-channel' in your pocketmine.yml to 'stable'."); - $logger->info("----- -------------------------- -----"); + $this->printConsoleMessage([ + "It appears you're running a Stable build, when you've specified that you prefer to run " . ucfirst($this->getChannel()) . " builds.", + "If you would like to be kept informed about new Stable builds only, it is recommended that you change 'preferred-channel' in your pocketmine.yml to 'stable'." + ]); } protected function showChannelSuggestionBeta(){ + $this->printConsoleMessage([ + "It appears you're running a Beta build, when you've specified that you prefer to run Stable builds.", + "If you would like to be kept informed about new Beta or Development builds, it is recommended that you change 'preferred-channel' in your pocketmine.yml to 'beta' or 'development'." + ]); + } + + protected function printConsoleMessage(array $lines, string $logLevel = \LogLevel::INFO){ $logger = $this->server->getLogger(); - $logger->info("----- PocketMine-MP Auto Updater -----"); - $logger->info("It appears you're running a Beta build, when you've specified that you prefer to run Stable builds."); - $logger->info("If you would like to be kept informed about new Beta or Development builds, it is recommended that you change 'preferred-channel' in your pocketmine.yml to 'beta' or 'development'."); - $logger->info("----- -------------------------- -----"); + + $title = $this->server->getName() . ' Auto Updater'; + $logger->log($logLevel, sprintf('----- %s -----', $title)); + foreach($lines as $line){ + $logger->log($logLevel, $line); + } + $logger->log($logLevel, sprintf('----- %s -----', str_repeat('-', strlen($title)))); } /** diff --git a/tests/plugins/PocketMine-DevTools b/tests/plugins/PocketMine-DevTools index bfae42ce8..eec5da244 160000 --- a/tests/plugins/PocketMine-DevTools +++ b/tests/plugins/PocketMine-DevTools @@ -1 +1 @@ -Subproject commit bfae42ce8985204b0db276d0b8daed7050567323 +Subproject commit eec5da2443b84821bd7e7224adea4921c81dc674 diff --git a/tests/plugins/PocketMine-TesterPlugin b/tests/plugins/PocketMine-TesterPlugin index c568b5ec9..87d6b8198 160000 --- a/tests/plugins/PocketMine-TesterPlugin +++ b/tests/plugins/PocketMine-TesterPlugin @@ -1 +1 @@ -Subproject commit c568b5ec9bd0606f0334d28ba60b0fc6c624a8f9 +Subproject commit 87d6b81989af33bfffea030e0015c3b6d43dc8e0