44 private const DEFAULT_CHECK_RATE = Server::TARGET_TICKS_PER_SECOND;
45 private const DEFAULT_CONTINUOUS_TRIGGER_RATE = Server::TARGET_TICKS_PER_SECOND * 2;
46 private const DEFAULT_TICKS_PER_GC = 30 * 60 * Server::TARGET_TICKS_PER_SECOND;
50 private int $memoryLimit;
51 private int $globalMemoryLimit;
52 private int $checkRate;
53 private int $checkTicker = 0;
54 private bool $lowMemory =
false;
56 private bool $continuousTrigger =
true;
57 private int $continuousTriggerRate;
58 private int $continuousTriggerCount = 0;
59 private int $continuousTriggerTicker = 0;
61 private int $garbageCollectionPeriod;
62 private int $garbageCollectionTicker = 0;
64 private int $lowMemChunkRadiusOverride;
66 private bool $dumpWorkers =
true;
68 private \Logger $logger;
70 public function __construct(
73 $this->logger = new \PrefixedLogger($server->getLogger(),
"Memory Manager");
76 $this->init($server->getConfigGroup());
80 $this->memoryLimit = $config->getPropertyInt(Yml::MEMORY_MAIN_LIMIT, 0) * 1024 * 1024;
82 $defaultMemory = 1024;
84 if(preg_match(
"/([0-9]+)([KMGkmg])/", $config->getConfigString(
"memory-limit",
""), $matches) > 0){
85 $m = (int) $matches[1];
89 $defaultMemory = match(mb_strtoupper($matches[2])){
90 "K" => intdiv($m, 1024),
98 $hardLimit = $config->getPropertyInt(Yml::MEMORY_MAIN_HARD_LIMIT, $defaultMemory);
101 ini_set(
"memory_limit",
'-1');
103 ini_set(
"memory_limit", $hardLimit .
"M");
106 $this->globalMemoryLimit = $config->getPropertyInt(Yml::MEMORY_GLOBAL_LIMIT, 0) * 1024 * 1024;
107 $this->checkRate = $config->getPropertyInt(Yml::MEMORY_CHECK_RATE, self::DEFAULT_CHECK_RATE);
108 $this->continuousTrigger = $config->getPropertyBool(Yml::MEMORY_CONTINUOUS_TRIGGER,
true);
109 $this->continuousTriggerRate = $config->getPropertyInt(Yml::MEMORY_CONTINUOUS_TRIGGER_RATE, self::DEFAULT_CONTINUOUS_TRIGGER_RATE);
111 $this->garbageCollectionPeriod = $config->getPropertyInt(Yml::MEMORY_GARBAGE_COLLECTION_PERIOD, self::DEFAULT_TICKS_PER_GC);
113 $this->lowMemChunkRadiusOverride = $config->getPropertyInt(Yml::MEMORY_MAX_CHUNKS_CHUNK_RADIUS, 4);
115 $this->dumpWorkers = $config->getPropertyBool(Yml::MEMORY_MEMORY_DUMP_DUMP_ASYNC_WORKER,
true);
118 public function isLowMemory() :
bool{
119 return $this->lowMemory;
122 public function getGlobalMemoryLimit() :
int{
123 return $this->globalMemoryLimit;
130 return ($this->lowMemory && $this->lowMemChunkRadiusOverride > 0) ? min($this->lowMemChunkRadiusOverride, $distance) : $distance;
136 public function trigger(
int $memory,
int $limit,
bool $global =
false,
int $triggerCount = 0) : void{
137 $this->logger->debug(sprintf(
"%sLow memory triggered, limit %gMB, using %gMB",
138 $global ?
"Global " :
"", round(($limit / 1024) / 1024, 2), round(($memory / 1024) / 1024, 2)));
139 foreach($this->
server->getWorldManager()->getWorlds() as $world){
140 $world->clearCache(true);
142 ChunkCache::pruneCaches();
144 foreach($this->server->getWorldManager()->getWorlds() as $world){
145 $world->doChunkGarbageCollection();
148 $ev =
new LowMemoryEvent($memory, $limit, $global, $triggerCount);
151 $cycles = $this->triggerGarbageCollector();
153 $this->logger->debug(sprintf(
"Freed %gMB, $cycles cycles", round(($ev->getMemoryFreed() / 1024) / 1024, 2)));
160 Timings::$memoryManager->startTiming();
162 if(($this->memoryLimit > 0 || $this->globalMemoryLimit > 0) && ++$this->checkTicker >= $this->checkRate){
163 $this->checkTicker = 0;
164 $memory = Process::getAdvancedMemoryUsage();
166 if($this->memoryLimit > 0 && $memory[0] > $this->memoryLimit){
168 }elseif($this->globalMemoryLimit > 0 && $memory[1] > $this->globalMemoryLimit){
172 if($trigger !==
false){
173 if($this->lowMemory && $this->continuousTrigger){
174 if(++$this->continuousTriggerTicker >= $this->continuousTriggerRate){
175 $this->continuousTriggerTicker = 0;
176 $this->trigger($memory[$trigger], $this->memoryLimit, $trigger > 0, ++$this->continuousTriggerCount);
179 $this->lowMemory =
true;
180 $this->continuousTriggerCount = 0;
181 $this->trigger($memory[$trigger], $this->memoryLimit, $trigger > 0);
184 $this->lowMemory =
false;
188 if($this->garbageCollectionPeriod > 0 && ++$this->garbageCollectionTicker >= $this->garbageCollectionPeriod){
189 $this->garbageCollectionTicker = 0;
190 $this->triggerGarbageCollector();
192 $this->cycleGcManager->maybeCollectCycles();
195 Timings::$memoryManager->stopTiming();
198 public function triggerGarbageCollector() : int{
199 Timings::$garbageCollector->startTiming();
201 $pool = $this->
server->getAsyncPool();
202 if(($w = $pool->shutdownUnusedWorkers()) > 0){
203 $this->logger->debug(
"Shut down $w idle async pool workers");
205 foreach($pool->getRunningWorkers() as $i){
206 $pool->submitTaskToWorker(new GarbageCollectionTask(), $i);
209 $cycles = gc_collect_cycles();
212 Timings::$garbageCollector->stopTiming();
220 public function dumpServerMemory(
string $outputFolder,
int $maxNesting,
int $maxStringSize) : void{
222 $logger->
notice(
"After the memory dump is done, the server might crash");
223 MemoryDump::dumpMemory($this->server, $outputFolder, $maxNesting, $maxStringSize, $logger);
225 if($this->dumpWorkers){
226 $pool = $this->
server->getAsyncPool();
227 foreach($pool->getRunningWorkers() as $i){
228 $pool->submitTaskToWorker(
new DumpWorkerMemoryTask($outputFolder, $maxNesting, $maxStringSize), $i);