server = $server; $this->init(); } private function init(){ $this->memoryLimit = ((int) $this->server->getProperty("memory.main-limit", 320)) * 1024 * 1024; $this->globalMemoryLimit = ((int) $this->server->getProperty("memory.global-limit", 512)) * 1024 * 1024; $this->checkRate = (int) $this->server->getProperty("memory.check-rate", 20); $this->continuousTrigger = (bool) $this->server->getProperty("memory.continuous-trigger", true); $this->continuousTriggerRate = (int) $this->server->getProperty("memory.continuous-trigger-rate", 30); $this->garbageCollectionPeriod = (int) $this->server->getProperty("memory.garbage-collection.period", 36000); $this->garbageCollectionTrigger = (bool) $this->server->getProperty("memory.garbage-collection.low-memory-trigger", true); $this->garbageCollectionAsync = (bool) $this->server->getProperty("memory.garbage-collection.collect-async-worker", true); $this->chunkLimit = (int) $this->server->getProperty("memory.max-chunks.trigger-limit", 96); $this->chunkCollect = (bool) $this->server->getProperty("memory.max-chunks.trigger-chunk-collect", true); $this->chunkTrigger = (bool) $this->server->getProperty("memory.max-chunks.low-memory-trigger", true); $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); gc_enable(); } public function isLowMemory(){ return $this->lowMemory; } public function canUseChunkCache(){ return !($this->lowMemory and $this->chunkTrigger); } public function getViewDistance($distance){ return $this->lowMemory ? min($this->chunkLimit, $distance) : $distance; } public function trigger($memory, $limit, $global = false, $triggerCount = 0){ $this->server->getLogger()->debug("[Memory Manager] ".($global ? "Global " : "") ."Low memory triggered, limit ". round(($limit / 1024) / 1024, 2)."MB, using ". round(($memory / 1024) / 1024, 2)."MB"); if($this->cacheTrigger){ foreach($this->server->getLevels() as $level){ $level->clearCache(true); } } if($this->chunkTrigger and $this->chunkCollect){ foreach($this->server->getLevels() as $level){ $level->doChunkGarbageCollection(); } } $ev = new LowMemoryEvent($memory, $limit, $global, $triggerCount); $this->server->getPluginManager()->callEvent($ev); $cycles = 0; if($this->garbageCollectionTrigger){ $cycles = $this->triggerGarbageCollector(); } $this->server->getLogger()->debug("[Memory Manager] Freed " . round(($ev->getMemoryFreed() / 1024) / 1024, 2)."MB, $cycles cycles"); } public function check(){ Timings::$memoryManagerTimer->startTiming(); if(($this->memoryLimit > 0 or $this->globalMemoryLimit > 0) and ++$this->checkTicker >= $this->checkRate){ $this->checkTicker = 0; $memory = Utils::getMemoryUsage(true); $trigger = false; if($this->memoryLimit > 0 and $memory[0] > $this->memoryLimit){ $trigger = 0; }elseif($this->globalMemoryLimit > 0 and $memory[1] > $this->globalMemoryLimit){ $trigger = 1; } if($trigger !== false){ if($this->lowMemory and $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 and ++$this->garbageCollectionTicker >= $this->garbageCollectionPeriod){ $this->garbageCollectionTicker = 0; $this->triggerGarbageCollector(); } Timings::$memoryManagerTimer->stopTiming(); } public function triggerGarbageCollector(){ Timings::$garbageCollectorTimer->startTiming(); if($this->garbageCollectionAsync){ $size = $this->server->getScheduler()->getAsyncTaskPoolSize(); for($i = 0; $i < $size; ++$i){ $this->server->getScheduler()->scheduleAsyncTaskToWorker(new GarbageCollectionTask(), $i); } } $cycles = gc_collect_cycles(); Timings::$garbageCollectorTimer->stopTiming(); return $cycles; } /** * @param object $object * * @return string Object identifier for future checks */ public function addObjectWatcher($object){ if(!is_object($object)){ throw new \InvalidArgumentException("Not an object!"); } $identifier = spl_object_hash($object) . ":" . get_class($object); if(isset($this->leakInfo[$identifier])){ return $this->leakInfo["id"]; } $this->leakInfo[$identifier] = [ "id" => $id = Utils::dataToUUID($identifier . ":" . $this->leakSeed++), "class" => get_class($object), "hash" => $identifier ]; $this->leakInfo[$id] = $this->leakInfo[$identifier]; $this->leakWatch[$id] = new \WeakRef($object); return $id; } public function isObjectAlive($id){ if(isset($this->leakWatch[$id])){ return $this->leakWatch[$id]->valid(); } return false; } public function removeObjectWatch($id){ if(!isset($this->leakWatch[$id])){ return; } unset($this->leakInfo[$this->leakInfo[$id]["hash"]]); unset($this->leakInfo[$id]); unset($this->leakWatch[$id]); } public function doObjectCleanup(){ foreach($this->leakWatch as $id => $w){ if(!$w->valid()){ $this->removeObjectWatch($id); } } } public function getObjectInformation($id, $includeObject = false){ if(!isset($this->leakWatch[$id])){ return null; } $valid = false; $references = 0; $object = null; if($this->leakWatch[$id]->acquire()){ $object = $this->leakWatch[$id]->get(); $this->leakWatch[$id]->release(); $valid = true; $references = getReferenceCount($object, false); } return [ "id" => $id, "class" => $this->leakInfo[$id]["class"], "hash" => $this->leakInfo[$id]["hash"], "valid" => $valid, "references" => $references, "object" => $includeObject ? $object : null ]; } }