logger = new \PrefixedLogger($server->getLogger(), "Memory Manager"); $this->cycleGcManager = new GarbageCollectorManager($this->logger, Timings::$memoryManager); $this->init($server->getConfigGroup()); } private function init(ServerConfigGroup $config) : void{ $this->memoryLimit = $config->getPropertyInt(Yml::MEMORY_MAIN_LIMIT, 0) * 1024 * 1024; $defaultMemory = 1024; if(preg_match("/([0-9]+)([KMGkmg])/", $config->getConfigString("memory-limit", ""), $matches) > 0){ $m = (int) $matches[1]; if($m <= 0){ $defaultMemory = 0; }else{ $defaultMemory = match(mb_strtoupper($matches[2])){ "K" => intdiv($m, 1024), "M" => $m, "G" => $m * 1024, default => $m, }; } } $hardLimit = $config->getPropertyInt(Yml::MEMORY_MAIN_HARD_LIMIT, $defaultMemory); if($hardLimit <= 0){ ini_set("memory_limit", '-1'); }else{ ini_set("memory_limit", $hardLimit . "M"); } $this->globalMemoryLimit = $config->getPropertyInt(Yml::MEMORY_GLOBAL_LIMIT, 0) * 1024 * 1024; $this->checkRate = $config->getPropertyInt(Yml::MEMORY_CHECK_RATE, self::DEFAULT_CHECK_RATE); $this->continuousTrigger = $config->getPropertyBool(Yml::MEMORY_CONTINUOUS_TRIGGER, true); $this->continuousTriggerRate = $config->getPropertyInt(Yml::MEMORY_CONTINUOUS_TRIGGER_RATE, self::DEFAULT_CONTINUOUS_TRIGGER_RATE); $this->garbageCollectionPeriod = $config->getPropertyInt(Yml::MEMORY_GARBAGE_COLLECTION_PERIOD, self::DEFAULT_TICKS_PER_GC); $this->lowMemChunkRadiusOverride = $config->getPropertyInt(Yml::MEMORY_MAX_CHUNKS_CHUNK_RADIUS, 4); $this->dumpWorkers = $config->getPropertyBool(Yml::MEMORY_MEMORY_DUMP_DUMP_ASYNC_WORKER, true); } public function isLowMemory() : bool{ return $this->lowMemory; } public function getGlobalMemoryLimit() : int{ return $this->globalMemoryLimit; } /** * @deprecated */ public function canUseChunkCache() : bool{ return !$this->lowMemory; } /** * Returns the allowed chunk radius based on the current memory usage. */ public function getViewDistance(int $distance) : int{ return ($this->lowMemory && $this->lowMemChunkRadiusOverride > 0) ? min($this->lowMemChunkRadiusOverride, $distance) : $distance; } /** * Triggers garbage collection and cache cleanup to try and free memory. */ public function trigger(int $memory, int $limit, bool $global = false, int $triggerCount = 0) : void{ $this->logger->debug(sprintf("%sLow memory triggered, limit %gMB, using %gMB", $global ? "Global " : "", round(($limit / 1024) / 1024, 2), round(($memory / 1024) / 1024, 2))); foreach($this->server->getWorldManager()->getWorlds() as $world){ $world->clearCache(true); } ChunkCache::pruneCaches(); foreach($this->server->getWorldManager()->getWorlds() as $world){ $world->doChunkGarbageCollection(); } $ev = new LowMemoryEvent($memory, $limit, $global, $triggerCount); $ev->call(); $cycles = $this->triggerGarbageCollector(); $this->logger->debug(sprintf("Freed %gMB, $cycles cycles", round(($ev->getMemoryFreed() / 1024) / 1024, 2))); } /** * Called every tick to update the memory manager state. */ public function check() : void{ Timings::$memoryManager->startTiming(); if(($this->memoryLimit > 0 || $this->globalMemoryLimit > 0) && ++$this->checkTicker >= $this->checkRate){ $this->checkTicker = 0; $memory = Process::getAdvancedMemoryUsage(); $trigger = false; if($this->memoryLimit > 0 && $memory[0] > $this->memoryLimit){ $trigger = 0; }elseif($this->globalMemoryLimit > 0 && $memory[1] > $this->globalMemoryLimit){ $trigger = 1; } if($trigger !== false){ if($this->lowMemory && $this->continuousTrigger){ if(++$this->continuousTriggerTicker >= $this->continuousTriggerRate){ $this->continuousTriggerTicker = 0; $this->trigger($memory[$trigger], $this->memoryLimit, $trigger > 0, ++$this->continuousTriggerCount); } }else{ $this->lowMemory = true; $this->continuousTriggerCount = 0; $this->trigger($memory[$trigger], $this->memoryLimit, $trigger > 0); } }else{ $this->lowMemory = false; } } if($this->garbageCollectionPeriod > 0 && ++$this->garbageCollectionTicker >= $this->garbageCollectionPeriod){ $this->garbageCollectionTicker = 0; $this->triggerGarbageCollector(); }else{ $this->cycleGcManager->maybeCollectCycles(); } Timings::$memoryManager->stopTiming(); } public function triggerGarbageCollector() : int{ Timings::$garbageCollector->startTiming(); $pool = $this->server->getAsyncPool(); if(($w = $pool->shutdownUnusedWorkers()) > 0){ $this->logger->debug("Shut down $w idle async pool workers"); } foreach($pool->getRunningWorkers() as $i){ $pool->submitTaskToWorker(new GarbageCollectionTask(), $i); } $cycles = gc_collect_cycles(); gc_mem_caches(); Timings::$garbageCollector->stopTiming(); return $cycles; } /** * Dumps the server memory into the specified output folder. */ public function dumpServerMemory(string $outputFolder, int $maxNesting, int $maxStringSize) : void{ $logger = new \PrefixedLogger($this->server->getLogger(), "Memory Dump"); $logger->notice("After the memory dump is done, the server might crash"); MemoryDump::dumpMemory($this->server, $outputFolder, $maxNesting, $maxStringSize, $logger); if($this->dumpWorkers){ $pool = $this->server->getAsyncPool(); foreach($pool->getRunningWorkers() as $i){ $pool->submitTaskToWorker(new DumpWorkerMemoryTask($outputFolder, $maxNesting, $maxStringSize), $i); } } } /** * Static memory dumper accessible from any thread. * @deprecated * @see MemoryDump */ public static function dumpMemory(mixed $startingObject, string $outputFolder, int $maxNesting, int $maxStringSize, \Logger $logger) : void{ MemoryDump::dumpMemory($startingObject, $outputFolder, $maxNesting, $maxStringSize, $logger); } }