PocketMine-MP 5.28.3 git-94fb5d95b92604840dabb719f04327efa559cf94
Loading...
Searching...
No Matches
MemoryManager.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine;
25
33use function gc_collect_cycles;
34use function gc_mem_caches;
35use function ini_set;
36use function intdiv;
37use function mb_strtoupper;
38use function min;
39use function preg_match;
40use function round;
41use function sprintf;
42
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;
47
48 private GarbageCollectorManager $cycleGcManager;
49
50 private int $memoryLimit;
51 private int $globalMemoryLimit;
52 private int $checkRate;
53 private int $checkTicker = 0;
54 private bool $lowMemory = false;
55
56 private bool $continuousTrigger = true;
57 private int $continuousTriggerRate;
58 private int $continuousTriggerCount = 0;
59 private int $continuousTriggerTicker = 0;
60
61 private int $garbageCollectionPeriod;
62 private int $garbageCollectionTicker = 0;
63
64 private int $lowMemChunkRadiusOverride;
65
66 private bool $dumpWorkers = true;
67
68 private \Logger $logger;
69
70 public function __construct(
71 private Server $server
72 ){
73 $this->logger = new \PrefixedLogger($server->getLogger(), "Memory Manager");
74 $this->cycleGcManager = new GarbageCollectorManager($this->logger, Timings::$memoryManager);
75
76 $this->init($server->getConfigGroup());
77 }
78
79 private function init(ServerConfigGroup $config) : void{
80 $this->memoryLimit = $config->getPropertyInt(Yml::MEMORY_MAIN_LIMIT, 0) * 1024 * 1024;
81
82 $defaultMemory = 1024;
83
84 if(preg_match("/([0-9]+)([KMGkmg])/", $config->getConfigString("memory-limit", ""), $matches) > 0){
85 $m = (int) $matches[1];
86 if($m <= 0){
87 $defaultMemory = 0;
88 }else{
89 $defaultMemory = match(mb_strtoupper($matches[2])){
90 "K" => intdiv($m, 1024),
91 "M" => $m,
92 "G" => $m * 1024,
93 default => $m,
94 };
95 }
96 }
97
98 $hardLimit = $config->getPropertyInt(Yml::MEMORY_MAIN_HARD_LIMIT, $defaultMemory);
99
100 if($hardLimit <= 0){
101 ini_set("memory_limit", '-1');
102 }else{
103 ini_set("memory_limit", $hardLimit . "M");
104 }
105
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);
110
111 $this->garbageCollectionPeriod = $config->getPropertyInt(Yml::MEMORY_GARBAGE_COLLECTION_PERIOD, self::DEFAULT_TICKS_PER_GC);
112
113 $this->lowMemChunkRadiusOverride = $config->getPropertyInt(Yml::MEMORY_MAX_CHUNKS_CHUNK_RADIUS, 4);
114
115 $this->dumpWorkers = $config->getPropertyBool(Yml::MEMORY_MEMORY_DUMP_DUMP_ASYNC_WORKER, true);
116 }
117
118 public function isLowMemory() : bool{
119 return $this->lowMemory;
120 }
121
122 public function getGlobalMemoryLimit() : int{
123 return $this->globalMemoryLimit;
124 }
125
129 public function canUseChunkCache() : bool{
130 return !$this->lowMemory;
131 }
132
136 public function getViewDistance(int $distance) : int{
137 return ($this->lowMemory && $this->lowMemChunkRadiusOverride > 0) ? min($this->lowMemChunkRadiusOverride, $distance) : $distance;
138 }
139
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);
148 }
149 ChunkCache::pruneCaches();
150
151 foreach($this->server->getWorldManager()->getWorlds() as $world){
152 $world->doChunkGarbageCollection();
153 }
154
155 $ev = new LowMemoryEvent($memory, $limit, $global, $triggerCount);
156 $ev->call();
157
158 $cycles = $this->triggerGarbageCollector();
159
160 $this->logger->debug(sprintf("Freed %gMB, $cycles cycles", round(($ev->getMemoryFreed() / 1024) / 1024, 2)));
161 }
162
166 public function check() : void{
167 Timings::$memoryManager->startTiming();
168
169 if(($this->memoryLimit > 0 || $this->globalMemoryLimit > 0) && ++$this->checkTicker >= $this->checkRate){
170 $this->checkTicker = 0;
171 $memory = Process::getAdvancedMemoryUsage();
172 $trigger = false;
173 if($this->memoryLimit > 0 && $memory[0] > $this->memoryLimit){
174 $trigger = 0;
175 }elseif($this->globalMemoryLimit > 0 && $memory[1] > $this->globalMemoryLimit){
176 $trigger = 1;
177 }
178
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);
184 }
185 }else{
186 $this->lowMemory = true;
187 $this->continuousTriggerCount = 0;
188 $this->trigger($memory[$trigger], $this->memoryLimit, $trigger > 0);
189 }
190 }else{
191 $this->lowMemory = false;
192 }
193 }
194
195 if($this->garbageCollectionPeriod > 0 && ++$this->garbageCollectionTicker >= $this->garbageCollectionPeriod){
196 $this->garbageCollectionTicker = 0;
197 $this->triggerGarbageCollector();
198 }else{
199 $this->cycleGcManager->maybeCollectCycles();
200 }
201
202 Timings::$memoryManager->stopTiming();
203 }
204
205 public function triggerGarbageCollector() : int{
206 Timings::$garbageCollector->startTiming();
207
208 $pool = $this->server->getAsyncPool();
209 if(($w = $pool->shutdownUnusedWorkers()) > 0){
210 $this->logger->debug("Shut down $w idle async pool workers");
211 }
212 foreach($pool->getRunningWorkers() as $i){
213 $pool->submitTaskToWorker(new GarbageCollectionTask(), $i);
214 }
215
216 $cycles = gc_collect_cycles();
217 gc_mem_caches();
218
219 Timings::$garbageCollector->stopTiming();
220
221 return $cycles;
222 }
223
227 public function dumpServerMemory(string $outputFolder, int $maxNesting, int $maxStringSize) : void{
228 $logger = new \PrefixedLogger($this->server->getLogger(), "Memory Dump");
229 $logger->notice("After the memory dump is done, the server might crash");
230 MemoryDump::dumpMemory($this->server, $outputFolder, $maxNesting, $maxStringSize, $logger);
231
232 if($this->dumpWorkers){
233 $pool = $this->server->getAsyncPool();
234 foreach($pool->getRunningWorkers() as $i){
235 $pool->submitTaskToWorker(new DumpWorkerMemoryTask($outputFolder, $maxNesting, $maxStringSize), $i);
236 }
237 }
238 }
239
245 public static function dumpMemory(mixed $startingObject, string $outputFolder, int $maxNesting, int $maxStringSize, \Logger $logger) : void{
246 MemoryDump::dumpMemory($startingObject, $outputFolder, $maxNesting, $maxStringSize, $logger);
247 }
248}
notice($message)
dumpServerMemory(string $outputFolder, int $maxNesting, int $maxStringSize)
static dumpMemory(mixed $startingObject, string $outputFolder, int $maxNesting, int $maxStringSize, \Logger $logger)
getViewDistance(int $distance)
trigger(int $memory, int $limit, bool $global=false, int $triggerCount=0)