PocketMine-MP 5.15.1 git-fb9a74e8799c71ed8292cfa53abe7a4c9204629d
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
34use Symfony\Component\Filesystem\Path;
35use function arsort;
36use function count;
37use function fclose;
38use function file_exists;
39use function file_put_contents;
40use function fopen;
41use function fwrite;
42use function gc_collect_cycles;
43use function gc_disable;
44use function gc_enable;
45use function gc_mem_caches;
46use function get_class;
47use function get_declared_classes;
48use function get_defined_functions;
49use function ini_get;
50use function ini_set;
51use function intdiv;
52use function is_array;
53use function is_float;
54use function is_object;
55use function is_resource;
56use function is_string;
57use function json_encode;
58use function mb_strtoupper;
59use function min;
60use function mkdir;
61use function preg_match;
62use function print_r;
63use function round;
64use function spl_object_hash;
65use function sprintf;
66use function strlen;
67use function substr;
68use const JSON_PRETTY_PRINT;
69use const JSON_THROW_ON_ERROR;
70use const JSON_UNESCAPED_SLASHES;
71use const SORT_NUMERIC;
72
74 private const DEFAULT_CHECK_RATE = Server::TARGET_TICKS_PER_SECOND;
75 private const DEFAULT_CONTINUOUS_TRIGGER_RATE = Server::TARGET_TICKS_PER_SECOND * 2;
76 private const DEFAULT_TICKS_PER_GC = 30 * 60 * Server::TARGET_TICKS_PER_SECOND;
77
78 private int $memoryLimit;
79 private int $globalMemoryLimit;
80 private int $checkRate;
81 private int $checkTicker = 0;
82 private bool $lowMemory = false;
83
84 private bool $continuousTrigger = true;
85 private int $continuousTriggerRate;
86 private int $continuousTriggerCount = 0;
87 private int $continuousTriggerTicker = 0;
88
89 private int $garbageCollectionPeriod;
90 private int $garbageCollectionTicker = 0;
91 private bool $garbageCollectionTrigger;
92 private bool $garbageCollectionAsync;
93
94 private int $lowMemChunkRadiusOverride;
95 private bool $lowMemChunkGC;
96
97 private bool $lowMemDisableChunkCache;
98 private bool $lowMemClearWorldCache;
99
100 private bool $dumpWorkers = true;
101
102 private \Logger $logger;
103
104 public function __construct(
105 private Server $server
106 ){
107 $this->logger = new \PrefixedLogger($server->getLogger(), "Memory Manager");
108
109 $this->init($server->getConfigGroup());
110 }
111
112 private function init(ServerConfigGroup $config) : void{
113 $this->memoryLimit = $config->getPropertyInt(Yml::MEMORY_MAIN_LIMIT, 0) * 1024 * 1024;
114
115 $defaultMemory = 1024;
116
117 if(preg_match("/([0-9]+)([KMGkmg])/", $config->getConfigString("memory-limit", ""), $matches) > 0){
118 $m = (int) $matches[1];
119 if($m <= 0){
120 $defaultMemory = 0;
121 }else{
122 $defaultMemory = match(mb_strtoupper($matches[2])){
123 "K" => intdiv($m, 1024),
124 "M" => $m,
125 "G" => $m * 1024,
126 default => $m,
127 };
128 }
129 }
130
131 $hardLimit = $config->getPropertyInt(Yml::MEMORY_MAIN_HARD_LIMIT, $defaultMemory);
132
133 if($hardLimit <= 0){
134 ini_set("memory_limit", '-1');
135 }else{
136 ini_set("memory_limit", $hardLimit . "M");
137 }
138
139 $this->globalMemoryLimit = $config->getPropertyInt(Yml::MEMORY_GLOBAL_LIMIT, 0) * 1024 * 1024;
140 $this->checkRate = $config->getPropertyInt(Yml::MEMORY_CHECK_RATE, self::DEFAULT_CHECK_RATE);
141 $this->continuousTrigger = $config->getPropertyBool(Yml::MEMORY_CONTINUOUS_TRIGGER, true);
142 $this->continuousTriggerRate = $config->getPropertyInt(Yml::MEMORY_CONTINUOUS_TRIGGER_RATE, self::DEFAULT_CONTINUOUS_TRIGGER_RATE);
143
144 $this->garbageCollectionPeriod = $config->getPropertyInt(Yml::MEMORY_GARBAGE_COLLECTION_PERIOD, self::DEFAULT_TICKS_PER_GC);
145 $this->garbageCollectionTrigger = $config->getPropertyBool(Yml::MEMORY_GARBAGE_COLLECTION_LOW_MEMORY_TRIGGER, true);
146 $this->garbageCollectionAsync = $config->getPropertyBool(Yml::MEMORY_GARBAGE_COLLECTION_COLLECT_ASYNC_WORKER, true);
147
148 $this->lowMemChunkRadiusOverride = $config->getPropertyInt(Yml::MEMORY_MAX_CHUNKS_CHUNK_RADIUS, 4);
149 $this->lowMemChunkGC = $config->getPropertyBool(Yml::MEMORY_MAX_CHUNKS_TRIGGER_CHUNK_COLLECT, true);
150
151 $this->lowMemDisableChunkCache = $config->getPropertyBool(Yml::MEMORY_WORLD_CACHES_DISABLE_CHUNK_CACHE, true);
152 $this->lowMemClearWorldCache = $config->getPropertyBool(Yml::MEMORY_WORLD_CACHES_LOW_MEMORY_TRIGGER, true);
153
154 $this->dumpWorkers = $config->getPropertyBool(Yml::MEMORY_MEMORY_DUMP_DUMP_ASYNC_WORKER, true);
155 gc_enable();
156 }
157
158 public function isLowMemory() : bool{
159 return $this->lowMemory;
160 }
161
162 public function getGlobalMemoryLimit() : int{
163 return $this->globalMemoryLimit;
164 }
165
166 public function canUseChunkCache() : bool{
167 return !$this->lowMemory || !$this->lowMemDisableChunkCache;
168 }
169
173 public function getViewDistance(int $distance) : int{
174 return ($this->lowMemory && $this->lowMemChunkRadiusOverride > 0) ? min($this->lowMemChunkRadiusOverride, $distance) : $distance;
175 }
176
180 public function trigger(int $memory, int $limit, bool $global = false, int $triggerCount = 0) : void{
181 $this->logger->debug(sprintf("%sLow memory triggered, limit %gMB, using %gMB",
182 $global ? "Global " : "", round(($limit / 1024) / 1024, 2), round(($memory / 1024) / 1024, 2)));
183 if($this->lowMemClearWorldCache){
184 foreach($this->server->getWorldManager()->getWorlds() as $world){
185 $world->clearCache(true);
186 }
187 ChunkCache::pruneCaches();
188 }
189
190 if($this->lowMemChunkGC){
191 foreach($this->server->getWorldManager()->getWorlds() as $world){
192 $world->doChunkGarbageCollection();
193 }
194 }
195
196 $ev = new LowMemoryEvent($memory, $limit, $global, $triggerCount);
197 $ev->call();
198
199 $cycles = 0;
200 if($this->garbageCollectionTrigger){
201 $cycles = $this->triggerGarbageCollector();
202 }
203
204 $this->logger->debug(sprintf("Freed %gMB, $cycles cycles", round(($ev->getMemoryFreed() / 1024) / 1024, 2)));
205 }
206
210 public function check() : void{
211 Timings::$memoryManager->startTiming();
212
213 if(($this->memoryLimit > 0 || $this->globalMemoryLimit > 0) && ++$this->checkTicker >= $this->checkRate){
214 $this->checkTicker = 0;
215 $memory = Process::getAdvancedMemoryUsage();
216 $trigger = false;
217 if($this->memoryLimit > 0 && $memory[0] > $this->memoryLimit){
218 $trigger = 0;
219 }elseif($this->globalMemoryLimit > 0 && $memory[1] > $this->globalMemoryLimit){
220 $trigger = 1;
221 }
222
223 if($trigger !== false){
224 if($this->lowMemory && $this->continuousTrigger){
225 if(++$this->continuousTriggerTicker >= $this->continuousTriggerRate){
226 $this->continuousTriggerTicker = 0;
227 $this->trigger($memory[$trigger], $this->memoryLimit, $trigger > 0, ++$this->continuousTriggerCount);
228 }
229 }else{
230 $this->lowMemory = true;
231 $this->continuousTriggerCount = 0;
232 $this->trigger($memory[$trigger], $this->memoryLimit, $trigger > 0);
233 }
234 }else{
235 $this->lowMemory = false;
236 }
237 }
238
239 if($this->garbageCollectionPeriod > 0 && ++$this->garbageCollectionTicker >= $this->garbageCollectionPeriod){
240 $this->garbageCollectionTicker = 0;
241 $this->triggerGarbageCollector();
242 }
243
244 Timings::$memoryManager->stopTiming();
245 }
246
247 public function triggerGarbageCollector() : int{
248 Timings::$garbageCollector->startTiming();
249
250 if($this->garbageCollectionAsync){
251 $pool = $this->server->getAsyncPool();
252 if(($w = $pool->shutdownUnusedWorkers()) > 0){
253 $this->logger->debug("Shut down $w idle async pool workers");
254 }
255 foreach($pool->getRunningWorkers() as $i){
256 $pool->submitTaskToWorker(new GarbageCollectionTask(), $i);
257 }
258 }
259
260 $cycles = gc_collect_cycles();
261 gc_mem_caches();
262
263 Timings::$garbageCollector->stopTiming();
264
265 return $cycles;
266 }
267
271 public function dumpServerMemory(string $outputFolder, int $maxNesting, int $maxStringSize) : void{
272 $logger = new \PrefixedLogger($this->server->getLogger(), "Memory Dump");
273 $logger->notice("After the memory dump is done, the server might crash");
274 self::dumpMemory($this->server, $outputFolder, $maxNesting, $maxStringSize, $logger);
275
276 if($this->dumpWorkers){
277 $pool = $this->server->getAsyncPool();
278 foreach($pool->getRunningWorkers() as $i){
279 $pool->submitTaskToWorker(new DumpWorkerMemoryTask($outputFolder, $maxNesting, $maxStringSize), $i);
280 }
281 }
282 }
283
287 public static function dumpMemory(mixed $startingObject, string $outputFolder, int $maxNesting, int $maxStringSize, \Logger $logger) : void{
288 $hardLimit = Utils::assumeNotFalse(ini_get('memory_limit'), "memory_limit INI directive should always exist");
289 ini_set('memory_limit', '-1');
290 gc_disable();
291
292 if(!file_exists($outputFolder)){
293 mkdir($outputFolder, 0777, true);
294 }
295
296 $obData = Utils::assumeNotFalse(fopen(Path::join($outputFolder, "objects.js"), "wb+"));
297
298 $objects = [];
299
300 $refCounts = [];
301
302 $instanceCounts = [];
303
304 $staticProperties = [];
305 $staticCount = 0;
306
307 $functionStaticVars = [];
308 $functionStaticVarsCount = 0;
309
310 foreach(get_declared_classes() as $className){
311 $reflection = new \ReflectionClass($className);
312 $staticProperties[$className] = [];
313 foreach($reflection->getProperties() as $property){
314 if(!$property->isStatic() || $property->getDeclaringClass()->getName() !== $className){
315 continue;
316 }
317
318 if(!$property->isInitialized()){
319 continue;
320 }
321
322 $staticCount++;
323 $staticProperties[$className][$property->getName()] = self::continueDump($property->getValue(), $objects, $refCounts, 0, $maxNesting, $maxStringSize);
324 }
325
326 if(count($staticProperties[$className]) === 0){
327 unset($staticProperties[$className]);
328 }
329
330 foreach($reflection->getMethods() as $method){
331 if($method->getDeclaringClass()->getName() !== $reflection->getName()){
332 continue;
333 }
334 $methodStatics = [];
335 foreach($method->getStaticVariables() as $name => $variable){
336 $methodStatics[$name] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
337 }
338 if(count($methodStatics) > 0){
339 $functionStaticVars[$className . "::" . $method->getName()] = $methodStatics;
340 $functionStaticVarsCount += count($functionStaticVars);
341 }
342 }
343 }
344
345 file_put_contents(Path::join($outputFolder, "staticProperties.js"), json_encode($staticProperties, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
346 $logger->info("Wrote $staticCount static properties");
347
348 $globalVariables = [];
349 $globalCount = 0;
350
351 $ignoredGlobals = [
352 'GLOBALS' => true,
353 '_SERVER' => true,
354 '_REQUEST' => true,
355 '_POST' => true,
356 '_GET' => true,
357 '_FILES' => true,
358 '_ENV' => true,
359 '_COOKIE' => true,
360 '_SESSION' => true
361 ];
362
363 foreach($GLOBALS as $varName => $value){
364 if(isset($ignoredGlobals[$varName])){
365 continue;
366 }
367
368 $globalCount++;
369 $globalVariables[$varName] = self::continueDump($value, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
370 }
371
372 file_put_contents(Path::join($outputFolder, "globalVariables.js"), json_encode($globalVariables, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
373 $logger->info("Wrote $globalCount global variables");
374
375 foreach(get_defined_functions()["user"] as $function){
376 $reflect = new \ReflectionFunction($function);
377
378 $vars = [];
379 foreach($reflect->getStaticVariables() as $varName => $variable){
380 $vars[$varName] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
381 }
382 if(count($vars) > 0){
383 $functionStaticVars[$function] = $vars;
384 $functionStaticVarsCount += count($vars);
385 }
386 }
387 file_put_contents(Path::join($outputFolder, 'functionStaticVars.js'), json_encode($functionStaticVars, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
388 $logger->info("Wrote $functionStaticVarsCount function static variables");
389
390 $data = self::continueDump($startingObject, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
391
392 do{
393 $continue = false;
394 foreach(Utils::stringifyKeys($objects) as $hash => $object){
395 if(!is_object($object)){
396 continue;
397 }
398 $continue = true;
399
400 $className = get_class($object);
401 if(!isset($instanceCounts[$className])){
402 $instanceCounts[$className] = 1;
403 }else{
404 $instanceCounts[$className]++;
405 }
406
407 $objects[$hash] = true;
408 $info = [
409 "information" => "$hash@$className",
410 ];
411 if($object instanceof \Closure){
412 $info["definition"] = Utils::getNiceClosureName($object);
413 $info["referencedVars"] = [];
414 $reflect = new \ReflectionFunction($object);
415 if(($closureThis = $reflect->getClosureThis()) !== null){
416 $info["this"] = self::continueDump($closureThis, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
417 }
418
419 foreach($reflect->getStaticVariables() as $name => $variable){
420 $info["referencedVars"][$name] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
421 }
422 }else{
423 $reflection = new \ReflectionObject($object);
424
425 $info["properties"] = [];
426
427 for($original = $reflection; $reflection !== false; $reflection = $reflection->getParentClass()){
428 foreach($reflection->getProperties() as $property){
429 if($property->isStatic()){
430 continue;
431 }
432
433 $name = $property->getName();
434 if($reflection !== $original){
435 if($property->isPrivate()){
436 $name = $reflection->getName() . ":" . $name;
437 }else{
438 continue;
439 }
440 }
441 if(!$property->isInitialized($object)){
442 continue;
443 }
444
445 $info["properties"][$name] = self::continueDump($property->getValue($object), $objects, $refCounts, 0, $maxNesting, $maxStringSize);
446 }
447 }
448 }
449
450 fwrite($obData, json_encode($info, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n");
451 }
452
453 }while($continue);
454
455 $logger->info("Wrote " . count($objects) . " objects");
456
457 fclose($obData);
458
459 file_put_contents(Path::join($outputFolder, "serverEntry.js"), json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
460 file_put_contents(Path::join($outputFolder, "referenceCounts.js"), json_encode($refCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
461
462 arsort($instanceCounts, SORT_NUMERIC);
463 file_put_contents(Path::join($outputFolder, "instanceCounts.js"), json_encode($instanceCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
464
465 $logger->info("Finished!");
466
467 ini_set('memory_limit', $hardLimit);
468 gc_enable();
469 }
470
480 private static function continueDump(mixed $from, array &$objects, array &$refCounts, int $recursion, int $maxNesting, int $maxStringSize) : mixed{
481 if($maxNesting <= 0){
482 return "(error) NESTING LIMIT REACHED";
483 }
484
485 --$maxNesting;
486
487 if(is_object($from)){
488 if(!isset($objects[$hash = spl_object_hash($from)])){
489 $objects[$hash] = $from;
490 $refCounts[$hash] = 0;
491 }
492
493 ++$refCounts[$hash];
494
495 $data = "(object) $hash";
496 }elseif(is_array($from)){
497 if($recursion >= 5){
498 return "(error) ARRAY RECURSION LIMIT REACHED";
499 }
500 $data = [];
501 $numeric = 0;
502 foreach($from as $key => $value){
503 $data[$numeric] = [
504 "k" => self::continueDump($key, $objects, $refCounts, $recursion + 1, $maxNesting, $maxStringSize),
505 "v" => self::continueDump($value, $objects, $refCounts, $recursion + 1, $maxNesting, $maxStringSize),
506 ];
507 $numeric++;
508 }
509 }elseif(is_string($from)){
510 $data = "(string) len(" . strlen($from) . ") " . substr(Utils::printable($from), 0, $maxStringSize);
511 }elseif(is_resource($from)){
512 $data = "(resource) " . print_r($from, true);
513 }elseif(is_float($from)){
514 $data = "(float) $from";
515 }else{
516 $data = $from;
517 }
518
519 return $data;
520 }
521}
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)
info($message)