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;
137 return ($this->lowMemory && $this->lowMemChunkRadiusOverride > 0) ? min($this->lowMemChunkRadiusOverride, $distance) : $distance;
143 public function trigger(
int $memory,
int $limit,
bool $global =
false,
int $triggerCount = 0) : void{
144 $this->logger->debug(sprintf(
"%sLow memory triggered, limit %gMB, using %gMB",
145 $global ?
"Global " :
"", round(($limit / 1024) / 1024, 2), round(($memory / 1024) / 1024, 2)));
146 foreach($this->
server->getWorldManager()->getWorlds() as $world){
147 $world->clearCache(true);
149 ChunkCache::pruneCaches();
151 foreach($this->server->getWorldManager()->getWorlds() as $world){
152 $world->doChunkGarbageCollection();
155 $ev =
new LowMemoryEvent($memory, $limit, $global, $triggerCount);
158 $cycles = $this->triggerGarbageCollector();
160 $this->logger->debug(sprintf(
"Freed %gMB, $cycles cycles", round(($ev->getMemoryFreed() / 1024) / 1024, 2)));
167 Timings::$memoryManager->startTiming();
169 if(($this->memoryLimit > 0 || $this->globalMemoryLimit > 0) && ++$this->checkTicker >= $this->checkRate){
170 $this->checkTicker = 0;
171 $memory = Process::getAdvancedMemoryUsage();
173 if($this->memoryLimit > 0 && $memory[0] > $this->memoryLimit){
175 }elseif($this->globalMemoryLimit > 0 && $memory[1] > $this->globalMemoryLimit){
179 if($trigger !==
false){
180 if($this->lowMemory && $this->continuousTrigger){
181 if(++$this->continuousTriggerTicker >= $this->continuousTriggerRate){
182 $this->continuousTriggerTicker = 0;
183 $this->trigger($memory[$trigger], $this->memoryLimit, $trigger > 0, ++$this->continuousTriggerCount);
186 $this->lowMemory =
true;
187 $this->continuousTriggerCount = 0;
188 $this->trigger($memory[$trigger], $this->memoryLimit, $trigger > 0);
191 $this->lowMemory =
false;
195 if($this->garbageCollectionPeriod > 0 && ++$this->garbageCollectionTicker >= $this->garbageCollectionPeriod){
196 $this->garbageCollectionTicker = 0;
197 $this->triggerGarbageCollector();
199 $this->cycleGcManager->maybeCollectCycles();
202 Timings::$memoryManager->stopTiming();
205 public function triggerGarbageCollector() : int{
206 Timings::$garbageCollector->startTiming();
208 $pool = $this->
server->getAsyncPool();
209 if(($w = $pool->shutdownUnusedWorkers()) > 0){
210 $this->logger->debug(
"Shut down $w idle async pool workers");
212 foreach($pool->getRunningWorkers() as $i){
213 $pool->submitTaskToWorker(new GarbageCollectionTask(), $i);
216 $cycles = gc_collect_cycles();
219 Timings::$garbageCollector->stopTiming();
227 public function dumpServerMemory(
string $outputFolder,
int $maxNesting,
int $maxStringSize) : void{
229 $logger->
notice(
"After the memory dump is done, the server might crash");
230 MemoryDump::dumpMemory($this->server, $outputFolder, $maxNesting, $maxStringSize, $logger);
232 if($this->dumpWorkers){
233 $pool = $this->
server->getAsyncPool();
234 foreach($pool->getRunningWorkers() as $i){
235 $pool->submitTaskToWorker(
new DumpWorkerMemoryTask($outputFolder, $maxNesting, $maxStringSize), $i);
245 public static function dumpMemory(mixed $startingObject,
string $outputFolder,
int $maxNesting,
int $maxStringSize, \
Logger $logger) : void{
246 MemoryDump::dumpMemory($startingObject, $outputFolder, $maxNesting, $maxStringSize, $logger);