Merge 'minor-next' into 'major-next'

Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/12344337356
This commit is contained in:
pmmp-admin-bot[bot] 2024-12-16 01:42:31 +00:00
commit 3a0f15ef0d
11 changed files with 486 additions and 330 deletions

View File

@ -54,12 +54,6 @@ memory:
#This only affects the main thread. Other threads should fire their own collections #This only affects the main thread. Other threads should fire their own collections
period: 36000 period: 36000
#Fire asynchronous tasks to collect garbage from workers
collect-async-worker: true
#Trigger on low memory
low-memory-trigger: true
#Settings controlling memory dump handling. #Settings controlling memory dump handling.
memory-dump: memory-dump:
#Dump memory from async workers as well as the main thread. If you have issues with segfaults when dumping memory, disable this setting. #Dump memory from async workers as well as the main thread. If you have issues with segfaults when dumping memory, disable this setting.
@ -69,16 +63,6 @@ memory:
#Cap maximum render distance per player when low memory is triggered. Set to 0 to disable cap. #Cap maximum render distance per player when low memory is triggered. Set to 0 to disable cap.
chunk-radius: 4 chunk-radius: 4
#Do chunk garbage collection on trigger
trigger-chunk-collect: true
world-caches:
#Disallow adding to world chunk-packet caches when memory is low
disable-chunk-cache: true
#Clear world caches when memory is low
low-memory-trigger: true
network: network:
#Threshold for batching packets, in bytes. Only these packets will be compressed #Threshold for batching packets, in bytes. Only these packets will be compressed
#Set to 0 to compress everything, -1 to disable. #Set to 0 to compress everything, -1 to disable.

View File

@ -0,0 +1,103 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine;
use pocketmine\timings\TimingsHandler;
use function gc_collect_cycles;
use function gc_disable;
use function gc_status;
use function hrtime;
use function max;
use function min;
use function number_format;
/**
* Allows threads to manually trigger the cyclic garbage collector using a threshold like PHP's own garbage collector,
* but triggered at a time that suits the thread instead of in random code pathways.
*
* The GC trigger behaviour in this class was adapted from Zend/zend_gc.c as of PHP 8.3.14.
*/
final class GarbageCollectorManager{
//TODO: These values could be adjusted to better suit PM, but for now we just want to mirror PHP GC to minimize
//behavioural changes.
private const GC_THRESHOLD_TRIGGER = 100;
private const GC_THRESHOLD_MAX = 1_000_000_000;
private const GC_THRESHOLD_DEFAULT = 10_001;
private const GC_THRESHOLD_STEP = 10_000;
private int $threshold = self::GC_THRESHOLD_DEFAULT;
private int $collectionTimeTotalNs = 0;
private \Logger $logger;
private TimingsHandler $timings;
public function __construct(
\Logger $logger,
?TimingsHandler $parentTimings,
){
gc_disable();
$this->logger = new \PrefixedLogger($logger, "Cyclic Garbage Collector");
$this->timings = new TimingsHandler("Cyclic Garbage Collector", $parentTimings);
}
private function adjustGcThreshold(int $cyclesCollected, int $rootsAfterGC) : void{
//TODO Very simple heuristic for dynamic GC buffer resizing:
//If there are "too few" collections, increase the collection threshold
//by a fixed step
//Adapted from zend_gc.c/gc_adjust_threshold() as of PHP 8.3.14
if($cyclesCollected < self::GC_THRESHOLD_TRIGGER || $rootsAfterGC >= $this->threshold){
$this->threshold = min(self::GC_THRESHOLD_MAX, $this->threshold + self::GC_THRESHOLD_STEP);
}elseif($this->threshold > self::GC_THRESHOLD_DEFAULT){
$this->threshold = max(self::GC_THRESHOLD_DEFAULT, $this->threshold - self::GC_THRESHOLD_STEP);
}
}
public function getThreshold() : int{ return $this->threshold; }
public function getCollectionTimeTotalNs() : int{ return $this->collectionTimeTotalNs; }
public function maybeCollectCycles() : int{
$rootsBefore = gc_status()["roots"];
if($rootsBefore < $this->threshold){
return 0;
}
$this->timings->startTiming();
$start = hrtime(true);
$cycles = gc_collect_cycles();
$end = hrtime(true);
$rootsAfter = gc_status()["roots"];
$this->adjustGcThreshold($cycles, $rootsAfter);
$this->timings->stopTiming();
$time = $end - $start;
$this->collectionTimeTotalNs += $time;
$this->logger->debug("gc_collect_cycles: " . number_format($time) . " ns ($rootsBefore -> $rootsAfter roots, $cycles cycles collected) - total GC time: " . number_format($this->collectionTimeTotalNs) . " ns");
return $cycles;
}
}

305
src/MemoryDump.php Normal file
View File

@ -0,0 +1,305 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine;
use pocketmine\utils\Utils;
use Symfony\Component\Filesystem\Path;
use function arsort;
use function count;
use function fclose;
use function file_exists;
use function file_put_contents;
use function fopen;
use function fwrite;
use function gc_disable;
use function gc_enable;
use function gc_enabled;
use function get_class;
use function get_declared_classes;
use function get_defined_functions;
use function ini_get;
use function ini_set;
use function is_array;
use function is_float;
use function is_object;
use function is_resource;
use function is_string;
use function json_encode;
use function mkdir;
use function print_r;
use function spl_object_hash;
use function strlen;
use function substr;
use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const SORT_NUMERIC;
final class MemoryDump{
private function __construct(){
//NOOP
}
/**
* Static memory dumper accessible from any thread.
*/
public static function dumpMemory(mixed $startingObject, string $outputFolder, int $maxNesting, int $maxStringSize, \Logger $logger) : void{
$hardLimit = Utils::assumeNotFalse(ini_get('memory_limit'), "memory_limit INI directive should always exist");
ini_set('memory_limit', '-1');
$gcEnabled = gc_enabled();
gc_disable();
if(!file_exists($outputFolder)){
mkdir($outputFolder, 0777, true);
}
$obData = Utils::assumeNotFalse(fopen(Path::join($outputFolder, "objects.js"), "wb+"));
$objects = [];
$refCounts = [];
$instanceCounts = [];
$staticProperties = [];
$staticCount = 0;
$functionStaticVars = [];
$functionStaticVarsCount = 0;
foreach(get_declared_classes() as $className){
$reflection = new \ReflectionClass($className);
$staticProperties[$className] = [];
foreach($reflection->getProperties() as $property){
if(!$property->isStatic() || $property->getDeclaringClass()->getName() !== $className){
continue;
}
if(!$property->isInitialized()){
continue;
}
$staticCount++;
$staticProperties[$className][$property->getName()] = self::continueDump($property->getValue(), $objects, $refCounts, 0, $maxNesting, $maxStringSize);
}
if(count($staticProperties[$className]) === 0){
unset($staticProperties[$className]);
}
foreach($reflection->getMethods() as $method){
if($method->getDeclaringClass()->getName() !== $reflection->getName()){
continue;
}
$methodStatics = [];
foreach(Utils::promoteKeys($method->getStaticVariables()) as $name => $variable){
$methodStatics[$name] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
}
if(count($methodStatics) > 0){
$functionStaticVars[$className . "::" . $method->getName()] = $methodStatics;
$functionStaticVarsCount += count($functionStaticVars);
}
}
}
file_put_contents(Path::join($outputFolder, "staticProperties.js"), json_encode($staticProperties, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$logger->info("Wrote $staticCount static properties");
$globalVariables = [];
$globalCount = 0;
$ignoredGlobals = [
'GLOBALS' => true,
'_SERVER' => true,
'_REQUEST' => true,
'_POST' => true,
'_GET' => true,
'_FILES' => true,
'_ENV' => true,
'_COOKIE' => true,
'_SESSION' => true
];
foreach(Utils::promoteKeys($GLOBALS) as $varName => $value){
if(isset($ignoredGlobals[$varName])){
continue;
}
$globalCount++;
$globalVariables[$varName] = self::continueDump($value, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
}
file_put_contents(Path::join($outputFolder, "globalVariables.js"), json_encode($globalVariables, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$logger->info("Wrote $globalCount global variables");
foreach(get_defined_functions()["user"] as $function){
$reflect = new \ReflectionFunction($function);
$vars = [];
foreach(Utils::promoteKeys($reflect->getStaticVariables()) as $varName => $variable){
$vars[$varName] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
}
if(count($vars) > 0){
$functionStaticVars[$function] = $vars;
$functionStaticVarsCount += count($vars);
}
}
file_put_contents(Path::join($outputFolder, 'functionStaticVars.js'), json_encode($functionStaticVars, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$logger->info("Wrote $functionStaticVarsCount function static variables");
$data = self::continueDump($startingObject, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
do{
$continue = false;
foreach(Utils::stringifyKeys($objects) as $hash => $object){
if(!is_object($object)){
continue;
}
$continue = true;
$className = get_class($object);
if(!isset($instanceCounts[$className])){
$instanceCounts[$className] = 1;
}else{
$instanceCounts[$className]++;
}
$objects[$hash] = true;
$info = [
"information" => "$hash@$className",
];
if($object instanceof \Closure){
$info["definition"] = Utils::getNiceClosureName($object);
$info["referencedVars"] = [];
$reflect = new \ReflectionFunction($object);
if(($closureThis = $reflect->getClosureThis()) !== null){
$info["this"] = self::continueDump($closureThis, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
}
foreach(Utils::promoteKeys($reflect->getStaticVariables()) as $name => $variable){
$info["referencedVars"][$name] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
}
}else{
$reflection = new \ReflectionObject($object);
$info["properties"] = [];
for($original = $reflection; $reflection !== false; $reflection = $reflection->getParentClass()){
foreach($reflection->getProperties() as $property){
if($property->isStatic()){
continue;
}
$name = $property->getName();
if($reflection !== $original){
if($property->isPrivate()){
$name = $reflection->getName() . ":" . $name;
}else{
continue;
}
}
if(!$property->isInitialized($object)){
continue;
}
$info["properties"][$name] = self::continueDump($property->getValue($object), $objects, $refCounts, 0, $maxNesting, $maxStringSize);
}
}
}
fwrite($obData, json_encode($info, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n");
}
}while($continue);
$logger->info("Wrote " . count($objects) . " objects");
fclose($obData);
file_put_contents(Path::join($outputFolder, "serverEntry.js"), json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
file_put_contents(Path::join($outputFolder, "referenceCounts.js"), json_encode($refCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
arsort($instanceCounts, SORT_NUMERIC);
file_put_contents(Path::join($outputFolder, "instanceCounts.js"), json_encode($instanceCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$logger->info("Finished!");
ini_set('memory_limit', $hardLimit);
if($gcEnabled){
gc_enable();
}
}
/**
* @param object[] $objects reference parameter
* @param int[] $refCounts reference parameter
*
* @phpstan-param array<string, object> $objects
* @phpstan-param array<string, int> $refCounts
* @phpstan-param-out array<string, object> $objects
* @phpstan-param-out array<string, int> $refCounts
*/
private static function continueDump(mixed $from, array &$objects, array &$refCounts, int $recursion, int $maxNesting, int $maxStringSize) : mixed{
if($maxNesting <= 0){
return "(error) NESTING LIMIT REACHED";
}
--$maxNesting;
if(is_object($from)){
if(!isset($objects[$hash = spl_object_hash($from)])){
$objects[$hash] = $from;
$refCounts[$hash] = 0;
}
++$refCounts[$hash];
$data = "(object) $hash";
}elseif(is_array($from)){
if($recursion >= 5){
return "(error) ARRAY RECURSION LIMIT REACHED";
}
$data = [];
$numeric = 0;
foreach(Utils::promoteKeys($from) as $key => $value){
$data[$numeric] = [
"k" => self::continueDump($key, $objects, $refCounts, $recursion + 1, $maxNesting, $maxStringSize),
"v" => self::continueDump($value, $objects, $refCounts, $recursion + 1, $maxNesting, $maxStringSize),
];
$numeric++;
}
}elseif(is_string($from)){
$data = "(string) len(" . strlen($from) . ") " . substr(Utils::printable($from), 0, $maxStringSize);
}elseif(is_resource($from)){
$data = "(resource) " . print_r($from, true);
}elseif(is_float($from)){
$data = "(float) $from";
}else{
$data = $from;
}
return $data;
}
}

View File

@ -29,52 +29,24 @@ use pocketmine\scheduler\DumpWorkerMemoryTask;
use pocketmine\scheduler\GarbageCollectionTask; use pocketmine\scheduler\GarbageCollectionTask;
use pocketmine\timings\Timings; use pocketmine\timings\Timings;
use pocketmine\utils\Process; use pocketmine\utils\Process;
use pocketmine\utils\Utils;
use pocketmine\YmlServerProperties as Yml; use pocketmine\YmlServerProperties as Yml;
use Symfony\Component\Filesystem\Path;
use function arsort;
use function count;
use function fclose;
use function file_exists;
use function file_put_contents;
use function fopen;
use function fwrite;
use function gc_collect_cycles; use function gc_collect_cycles;
use function gc_disable;
use function gc_enable;
use function gc_mem_caches; use function gc_mem_caches;
use function get_class;
use function get_declared_classes;
use function get_defined_functions;
use function ini_get;
use function ini_set; use function ini_set;
use function intdiv; use function intdiv;
use function is_array;
use function is_float;
use function is_object;
use function is_resource;
use function is_string;
use function json_encode;
use function mb_strtoupper; use function mb_strtoupper;
use function min; use function min;
use function mkdir;
use function preg_match; use function preg_match;
use function print_r;
use function round; use function round;
use function spl_object_hash;
use function sprintf; use function sprintf;
use function strlen;
use function substr;
use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const SORT_NUMERIC;
class MemoryManager{ class MemoryManager{
private const DEFAULT_CHECK_RATE = Server::TARGET_TICKS_PER_SECOND; private const DEFAULT_CHECK_RATE = Server::TARGET_TICKS_PER_SECOND;
private const DEFAULT_CONTINUOUS_TRIGGER_RATE = Server::TARGET_TICKS_PER_SECOND * 2; private const DEFAULT_CONTINUOUS_TRIGGER_RATE = Server::TARGET_TICKS_PER_SECOND * 2;
private const DEFAULT_TICKS_PER_GC = 30 * 60 * Server::TARGET_TICKS_PER_SECOND; private const DEFAULT_TICKS_PER_GC = 30 * 60 * Server::TARGET_TICKS_PER_SECOND;
private GarbageCollectorManager $cycleGcManager;
private int $memoryLimit; private int $memoryLimit;
private int $globalMemoryLimit; private int $globalMemoryLimit;
private int $checkRate; private int $checkRate;
@ -88,14 +60,8 @@ class MemoryManager{
private int $garbageCollectionPeriod; private int $garbageCollectionPeriod;
private int $garbageCollectionTicker = 0; private int $garbageCollectionTicker = 0;
private bool $garbageCollectionTrigger;
private bool $garbageCollectionAsync;
private int $lowMemChunkRadiusOverride; private int $lowMemChunkRadiusOverride;
private bool $lowMemChunkGC;
private bool $lowMemDisableChunkCache;
private bool $lowMemClearWorldCache;
private bool $dumpWorkers = true; private bool $dumpWorkers = true;
@ -105,6 +71,7 @@ class MemoryManager{
private Server $server private Server $server
){ ){
$this->logger = new \PrefixedLogger($server->getLogger(), "Memory Manager"); $this->logger = new \PrefixedLogger($server->getLogger(), "Memory Manager");
$this->cycleGcManager = new GarbageCollectorManager($this->logger, Timings::$memoryManager);
$this->init($server->getConfigGroup()); $this->init($server->getConfigGroup());
} }
@ -142,17 +109,10 @@ class MemoryManager{
$this->continuousTriggerRate = $config->getPropertyInt(Yml::MEMORY_CONTINUOUS_TRIGGER_RATE, self::DEFAULT_CONTINUOUS_TRIGGER_RATE); $this->continuousTriggerRate = $config->getPropertyInt(Yml::MEMORY_CONTINUOUS_TRIGGER_RATE, self::DEFAULT_CONTINUOUS_TRIGGER_RATE);
$this->garbageCollectionPeriod = $config->getPropertyInt(Yml::MEMORY_GARBAGE_COLLECTION_PERIOD, self::DEFAULT_TICKS_PER_GC); $this->garbageCollectionPeriod = $config->getPropertyInt(Yml::MEMORY_GARBAGE_COLLECTION_PERIOD, self::DEFAULT_TICKS_PER_GC);
$this->garbageCollectionTrigger = $config->getPropertyBool(Yml::MEMORY_GARBAGE_COLLECTION_LOW_MEMORY_TRIGGER, true);
$this->garbageCollectionAsync = $config->getPropertyBool(Yml::MEMORY_GARBAGE_COLLECTION_COLLECT_ASYNC_WORKER, true);
$this->lowMemChunkRadiusOverride = $config->getPropertyInt(Yml::MEMORY_MAX_CHUNKS_CHUNK_RADIUS, 4); $this->lowMemChunkRadiusOverride = $config->getPropertyInt(Yml::MEMORY_MAX_CHUNKS_CHUNK_RADIUS, 4);
$this->lowMemChunkGC = $config->getPropertyBool(Yml::MEMORY_MAX_CHUNKS_TRIGGER_CHUNK_COLLECT, true);
$this->lowMemDisableChunkCache = $config->getPropertyBool(Yml::MEMORY_WORLD_CACHES_DISABLE_CHUNK_CACHE, true);
$this->lowMemClearWorldCache = $config->getPropertyBool(Yml::MEMORY_WORLD_CACHES_LOW_MEMORY_TRIGGER, true);
$this->dumpWorkers = $config->getPropertyBool(Yml::MEMORY_MEMORY_DUMP_DUMP_ASYNC_WORKER, true); $this->dumpWorkers = $config->getPropertyBool(Yml::MEMORY_MEMORY_DUMP_DUMP_ASYNC_WORKER, true);
gc_enable();
} }
public function isLowMemory() : bool{ public function isLowMemory() : bool{
@ -163,8 +123,11 @@ class MemoryManager{
return $this->globalMemoryLimit; return $this->globalMemoryLimit;
} }
/**
* @deprecated
*/
public function canUseChunkCache() : bool{ public function canUseChunkCache() : bool{
return !$this->lowMemory || !$this->lowMemDisableChunkCache; return !$this->lowMemory;
} }
/** /**
@ -180,26 +143,19 @@ class MemoryManager{
public function trigger(int $memory, int $limit, bool $global = false, int $triggerCount = 0) : void{ public function trigger(int $memory, int $limit, bool $global = false, int $triggerCount = 0) : void{
$this->logger->debug(sprintf("%sLow memory triggered, limit %gMB, using %gMB", $this->logger->debug(sprintf("%sLow memory triggered, limit %gMB, using %gMB",
$global ? "Global " : "", round(($limit / 1024) / 1024, 2), round(($memory / 1024) / 1024, 2))); $global ? "Global " : "", round(($limit / 1024) / 1024, 2), round(($memory / 1024) / 1024, 2)));
if($this->lowMemClearWorldCache){ foreach($this->server->getWorldManager()->getWorlds() as $world){
foreach($this->server->getWorldManager()->getWorlds() as $world){ $world->clearCache(true);
$world->clearCache(true);
}
ChunkCache::pruneCaches();
} }
ChunkCache::pruneCaches();
if($this->lowMemChunkGC){ foreach($this->server->getWorldManager()->getWorlds() as $world){
foreach($this->server->getWorldManager()->getWorlds() as $world){ $world->doChunkGarbageCollection();
$world->doChunkGarbageCollection();
}
} }
$ev = new LowMemoryEvent($memory, $limit, $global, $triggerCount); $ev = new LowMemoryEvent($memory, $limit, $global, $triggerCount);
$ev->call(); $ev->call();
$cycles = 0; $cycles = $this->triggerGarbageCollector();
if($this->garbageCollectionTrigger){
$cycles = $this->triggerGarbageCollector();
}
$this->logger->debug(sprintf("Freed %gMB, $cycles cycles", round(($ev->getMemoryFreed() / 1024) / 1024, 2))); $this->logger->debug(sprintf("Freed %gMB, $cycles cycles", round(($ev->getMemoryFreed() / 1024) / 1024, 2)));
} }
@ -239,6 +195,8 @@ class MemoryManager{
if($this->garbageCollectionPeriod > 0 && ++$this->garbageCollectionTicker >= $this->garbageCollectionPeriod){ if($this->garbageCollectionPeriod > 0 && ++$this->garbageCollectionTicker >= $this->garbageCollectionPeriod){
$this->garbageCollectionTicker = 0; $this->garbageCollectionTicker = 0;
$this->triggerGarbageCollector(); $this->triggerGarbageCollector();
}else{
$this->cycleGcManager->maybeCollectCycles();
} }
Timings::$memoryManager->stopTiming(); Timings::$memoryManager->stopTiming();
@ -247,14 +205,12 @@ class MemoryManager{
public function triggerGarbageCollector() : int{ public function triggerGarbageCollector() : int{
Timings::$garbageCollector->startTiming(); Timings::$garbageCollector->startTiming();
if($this->garbageCollectionAsync){ $pool = $this->server->getAsyncPool();
$pool = $this->server->getAsyncPool(); if(($w = $pool->shutdownUnusedWorkers()) > 0){
if(($w = $pool->shutdownUnusedWorkers()) > 0){ $this->logger->debug("Shut down $w idle async pool workers");
$this->logger->debug("Shut down $w idle async pool workers"); }
} foreach($pool->getRunningWorkers() as $i){
foreach($pool->getRunningWorkers() as $i){ $pool->submitTaskToWorker(new GarbageCollectionTask(), $i);
$pool->submitTaskToWorker(new GarbageCollectionTask(), $i);
}
} }
$cycles = gc_collect_cycles(); $cycles = gc_collect_cycles();
@ -271,7 +227,7 @@ class MemoryManager{
public function dumpServerMemory(string $outputFolder, int $maxNesting, int $maxStringSize) : void{ public function dumpServerMemory(string $outputFolder, int $maxNesting, int $maxStringSize) : void{
$logger = new \PrefixedLogger($this->server->getLogger(), "Memory Dump"); $logger = new \PrefixedLogger($this->server->getLogger(), "Memory Dump");
$logger->notice("After the memory dump is done, the server might crash"); $logger->notice("After the memory dump is done, the server might crash");
self::dumpMemory($this->server, $outputFolder, $maxNesting, $maxStringSize, $logger); MemoryDump::dumpMemory($this->server, $outputFolder, $maxNesting, $maxStringSize, $logger);
if($this->dumpWorkers){ if($this->dumpWorkers){
$pool = $this->server->getAsyncPool(); $pool = $this->server->getAsyncPool();
@ -283,239 +239,10 @@ class MemoryManager{
/** /**
* Static memory dumper accessible from any thread. * Static memory dumper accessible from any thread.
* @deprecated
* @see MemoryDump
*/ */
public static function dumpMemory(mixed $startingObject, string $outputFolder, int $maxNesting, int $maxStringSize, \Logger $logger) : void{ public static function dumpMemory(mixed $startingObject, string $outputFolder, int $maxNesting, int $maxStringSize, \Logger $logger) : void{
$hardLimit = Utils::assumeNotFalse(ini_get('memory_limit'), "memory_limit INI directive should always exist"); MemoryDump::dumpMemory($startingObject, $outputFolder, $maxNesting, $maxStringSize, $logger);
ini_set('memory_limit', '-1');
gc_disable();
if(!file_exists($outputFolder)){
mkdir($outputFolder, 0777, true);
}
$obData = Utils::assumeNotFalse(fopen(Path::join($outputFolder, "objects.js"), "wb+"));
$objects = [];
$refCounts = [];
$instanceCounts = [];
$staticProperties = [];
$staticCount = 0;
$functionStaticVars = [];
$functionStaticVarsCount = 0;
foreach(get_declared_classes() as $className){
$reflection = new \ReflectionClass($className);
$staticProperties[$className] = [];
foreach($reflection->getProperties() as $property){
if(!$property->isStatic() || $property->getDeclaringClass()->getName() !== $className){
continue;
}
if(!$property->isInitialized()){
continue;
}
$staticCount++;
$staticProperties[$className][$property->getName()] = self::continueDump($property->getValue(), $objects, $refCounts, 0, $maxNesting, $maxStringSize);
}
if(count($staticProperties[$className]) === 0){
unset($staticProperties[$className]);
}
foreach($reflection->getMethods() as $method){
if($method->getDeclaringClass()->getName() !== $reflection->getName()){
continue;
}
$methodStatics = [];
foreach(Utils::promoteKeys($method->getStaticVariables()) as $name => $variable){
$methodStatics[$name] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
}
if(count($methodStatics) > 0){
$functionStaticVars[$className . "::" . $method->getName()] = $methodStatics;
$functionStaticVarsCount += count($functionStaticVars);
}
}
}
file_put_contents(Path::join($outputFolder, "staticProperties.js"), json_encode($staticProperties, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$logger->info("Wrote $staticCount static properties");
$globalVariables = [];
$globalCount = 0;
$ignoredGlobals = [
'GLOBALS' => true,
'_SERVER' => true,
'_REQUEST' => true,
'_POST' => true,
'_GET' => true,
'_FILES' => true,
'_ENV' => true,
'_COOKIE' => true,
'_SESSION' => true
];
foreach(Utils::promoteKeys($GLOBALS) as $varName => $value){
if(isset($ignoredGlobals[$varName])){
continue;
}
$globalCount++;
$globalVariables[$varName] = self::continueDump($value, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
}
file_put_contents(Path::join($outputFolder, "globalVariables.js"), json_encode($globalVariables, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$logger->info("Wrote $globalCount global variables");
foreach(get_defined_functions()["user"] as $function){
$reflect = new \ReflectionFunction($function);
$vars = [];
foreach(Utils::promoteKeys($reflect->getStaticVariables()) as $varName => $variable){
$vars[$varName] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
}
if(count($vars) > 0){
$functionStaticVars[$function] = $vars;
$functionStaticVarsCount += count($vars);
}
}
file_put_contents(Path::join($outputFolder, 'functionStaticVars.js'), json_encode($functionStaticVars, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$logger->info("Wrote $functionStaticVarsCount function static variables");
$data = self::continueDump($startingObject, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
do{
$continue = false;
foreach(Utils::stringifyKeys($objects) as $hash => $object){
if(!is_object($object)){
continue;
}
$continue = true;
$className = get_class($object);
if(!isset($instanceCounts[$className])){
$instanceCounts[$className] = 1;
}else{
$instanceCounts[$className]++;
}
$objects[$hash] = true;
$info = [
"information" => "$hash@$className",
];
if($object instanceof \Closure){
$info["definition"] = Utils::getNiceClosureName($object);
$info["referencedVars"] = [];
$reflect = new \ReflectionFunction($object);
if(($closureThis = $reflect->getClosureThis()) !== null){
$info["this"] = self::continueDump($closureThis, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
}
foreach(Utils::promoteKeys($reflect->getStaticVariables()) as $name => $variable){
$info["referencedVars"][$name] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
}
}else{
$reflection = new \ReflectionObject($object);
$info["properties"] = [];
for($original = $reflection; $reflection !== false; $reflection = $reflection->getParentClass()){
foreach($reflection->getProperties() as $property){
if($property->isStatic()){
continue;
}
$name = $property->getName();
if($reflection !== $original){
if($property->isPrivate()){
$name = $reflection->getName() . ":" . $name;
}else{
continue;
}
}
if(!$property->isInitialized($object)){
continue;
}
$info["properties"][$name] = self::continueDump($property->getValue($object), $objects, $refCounts, 0, $maxNesting, $maxStringSize);
}
}
}
fwrite($obData, json_encode($info, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n");
}
}while($continue);
$logger->info("Wrote " . count($objects) . " objects");
fclose($obData);
file_put_contents(Path::join($outputFolder, "serverEntry.js"), json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
file_put_contents(Path::join($outputFolder, "referenceCounts.js"), json_encode($refCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
arsort($instanceCounts, SORT_NUMERIC);
file_put_contents(Path::join($outputFolder, "instanceCounts.js"), json_encode($instanceCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$logger->info("Finished!");
ini_set('memory_limit', $hardLimit);
gc_enable();
}
/**
* @param object[] $objects reference parameter
* @param int[] $refCounts reference parameter
*
* @phpstan-param array<string, object> $objects
* @phpstan-param array<string, int> $refCounts
* @phpstan-param-out array<string, object> $objects
* @phpstan-param-out array<string, int> $refCounts
*/
private static function continueDump(mixed $from, array &$objects, array &$refCounts, int $recursion, int $maxNesting, int $maxStringSize) : mixed{
if($maxNesting <= 0){
return "(error) NESTING LIMIT REACHED";
}
--$maxNesting;
if(is_object($from)){
if(!isset($objects[$hash = spl_object_hash($from)])){
$objects[$hash] = $from;
$refCounts[$hash] = 0;
}
++$refCounts[$hash];
$data = "(object) $hash";
}elseif(is_array($from)){
if($recursion >= 5){
return "(error) ARRAY RECURSION LIMIT REACHED";
}
$data = [];
$numeric = 0;
foreach(Utils::promoteKeys($from) as $key => $value){
$data[$numeric] = [
"k" => self::continueDump($key, $objects, $refCounts, $recursion + 1, $maxNesting, $maxStringSize),
"v" => self::continueDump($value, $objects, $refCounts, $recursion + 1, $maxNesting, $maxStringSize),
];
$numeric++;
}
}elseif(is_string($from)){
$data = "(string) len(" . strlen($from) . ") " . substr(Utils::printable($from), 0, $maxStringSize);
}elseif(is_resource($from)){
$data = "(resource) " . print_r($from, true);
}elseif(is_float($from)){
$data = "(float) $from";
}else{
$data = $from;
}
return $data;
} }
} }

View File

@ -75,20 +75,14 @@ final class YmlServerProperties{
public const MEMORY_CONTINUOUS_TRIGGER = 'memory.continuous-trigger'; public const MEMORY_CONTINUOUS_TRIGGER = 'memory.continuous-trigger';
public const MEMORY_CONTINUOUS_TRIGGER_RATE = 'memory.continuous-trigger-rate'; public const MEMORY_CONTINUOUS_TRIGGER_RATE = 'memory.continuous-trigger-rate';
public const MEMORY_GARBAGE_COLLECTION = 'memory.garbage-collection'; public const MEMORY_GARBAGE_COLLECTION = 'memory.garbage-collection';
public const MEMORY_GARBAGE_COLLECTION_COLLECT_ASYNC_WORKER = 'memory.garbage-collection.collect-async-worker';
public const MEMORY_GARBAGE_COLLECTION_LOW_MEMORY_TRIGGER = 'memory.garbage-collection.low-memory-trigger';
public const MEMORY_GARBAGE_COLLECTION_PERIOD = 'memory.garbage-collection.period'; public const MEMORY_GARBAGE_COLLECTION_PERIOD = 'memory.garbage-collection.period';
public const MEMORY_GLOBAL_LIMIT = 'memory.global-limit'; public const MEMORY_GLOBAL_LIMIT = 'memory.global-limit';
public const MEMORY_MAIN_HARD_LIMIT = 'memory.main-hard-limit'; public const MEMORY_MAIN_HARD_LIMIT = 'memory.main-hard-limit';
public const MEMORY_MAIN_LIMIT = 'memory.main-limit'; public const MEMORY_MAIN_LIMIT = 'memory.main-limit';
public const MEMORY_MAX_CHUNKS = 'memory.max-chunks'; public const MEMORY_MAX_CHUNKS = 'memory.max-chunks';
public const MEMORY_MAX_CHUNKS_CHUNK_RADIUS = 'memory.max-chunks.chunk-radius'; public const MEMORY_MAX_CHUNKS_CHUNK_RADIUS = 'memory.max-chunks.chunk-radius';
public const MEMORY_MAX_CHUNKS_TRIGGER_CHUNK_COLLECT = 'memory.max-chunks.trigger-chunk-collect';
public const MEMORY_MEMORY_DUMP = 'memory.memory-dump'; public const MEMORY_MEMORY_DUMP = 'memory.memory-dump';
public const MEMORY_MEMORY_DUMP_DUMP_ASYNC_WORKER = 'memory.memory-dump.dump-async-worker'; public const MEMORY_MEMORY_DUMP_DUMP_ASYNC_WORKER = 'memory.memory-dump.dump-async-worker';
public const MEMORY_WORLD_CACHES = 'memory.world-caches';
public const MEMORY_WORLD_CACHES_DISABLE_CHUNK_CACHE = 'memory.world-caches.disable-chunk-cache';
public const MEMORY_WORLD_CACHES_LOW_MEMORY_TRIGGER = 'memory.world-caches.low-memory-trigger';
public const NETWORK = 'network'; public const NETWORK = 'network';
public const NETWORK_ASYNC_COMPRESSION = 'network.async-compression'; public const NETWORK_ASYNC_COMPRESSION = 'network.async-compression';
public const NETWORK_ASYNC_COMPRESSION_THRESHOLD = 'network.async-compression-threshold'; public const NETWORK_ASYNC_COMPRESSION_THRESHOLD = 'network.async-compression-threshold';

View File

@ -82,7 +82,11 @@ class RakLibServer extends Thread{
} }
protected function onRun() : void{ protected function onRun() : void{
//TODO: switch to manually triggered GC
//the best time to do it is between ticks when the server would otherwise be sleeping, but RakLib's current
//design doesn't allow this as of 1.1.1
gc_enable(); gc_enable();
ini_set("display_errors", '1'); ini_set("display_errors", '1');
ini_set("display_startup_errors", '1'); ini_set("display_startup_errors", '1');
\GlobalLogger::set($this->logger); \GlobalLogger::set($this->logger);

View File

@ -93,6 +93,7 @@ abstract class AsyncTask extends Runnable{
$this->finished = true; $this->finished = true;
AsyncWorker::getNotifier()->wakeupSleeper(); AsyncWorker::getNotifier()->wakeupSleeper();
AsyncWorker::maybeCollectCycles();
} }
/** /**

View File

@ -24,12 +24,13 @@ declare(strict_types=1);
namespace pocketmine\scheduler; namespace pocketmine\scheduler;
use pmmp\thread\Thread as NativeThread; use pmmp\thread\Thread as NativeThread;
use pocketmine\GarbageCollectorManager;
use pocketmine\snooze\SleeperHandlerEntry; use pocketmine\snooze\SleeperHandlerEntry;
use pocketmine\snooze\SleeperNotifier; use pocketmine\snooze\SleeperNotifier;
use pocketmine\thread\log\ThreadSafeLogger; use pocketmine\thread\log\ThreadSafeLogger;
use pocketmine\thread\Worker; use pocketmine\thread\Worker;
use pocketmine\timings\Timings;
use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\AssumptionFailedError;
use function gc_enable;
use function ini_set; use function ini_set;
class AsyncWorker extends Worker{ class AsyncWorker extends Worker{
@ -37,6 +38,7 @@ class AsyncWorker extends Worker{
private static array $store = []; private static array $store = [];
private static ?SleeperNotifier $notifier = null; private static ?SleeperNotifier $notifier = null;
private static ?GarbageCollectorManager $cycleGcManager = null;
public function __construct( public function __construct(
private ThreadSafeLogger $logger, private ThreadSafeLogger $logger,
@ -52,11 +54,16 @@ class AsyncWorker extends Worker{
throw new AssumptionFailedError("SleeperNotifier not found in thread-local storage"); throw new AssumptionFailedError("SleeperNotifier not found in thread-local storage");
} }
public static function maybeCollectCycles() : void{
if(self::$cycleGcManager === null){
throw new AssumptionFailedError("GarbageCollectorManager not found in thread-local storage");
}
self::$cycleGcManager->maybeCollectCycles();
}
protected function onRun() : void{ protected function onRun() : void{
\GlobalLogger::set($this->logger); \GlobalLogger::set($this->logger);
gc_enable();
if($this->memoryLimit > 0){ if($this->memoryLimit > 0){
ini_set('memory_limit', $this->memoryLimit . 'M'); ini_set('memory_limit', $this->memoryLimit . 'M');
$this->logger->debug("Set memory limit to " . $this->memoryLimit . " MB"); $this->logger->debug("Set memory limit to " . $this->memoryLimit . " MB");
@ -66,6 +73,7 @@ class AsyncWorker extends Worker{
} }
self::$notifier = $this->sleeperEntry->createNotifier(); self::$notifier = $this->sleeperEntry->createNotifier();
self::$cycleGcManager = new GarbageCollectorManager($this->logger, Timings::$asyncTaskWorkers);
} }
public function getLogger() : ThreadSafeLogger{ public function getLogger() : ThreadSafeLogger{

View File

@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\scheduler; namespace pocketmine\scheduler;
use pmmp\thread\Thread as NativeThread; use pmmp\thread\Thread as NativeThread;
use pocketmine\MemoryManager; use pocketmine\MemoryDump;
use Symfony\Component\Filesystem\Path; use Symfony\Component\Filesystem\Path;
use function assert; use function assert;
@ -41,7 +41,7 @@ class DumpWorkerMemoryTask extends AsyncTask{
public function onRun() : void{ public function onRun() : void{
$worker = NativeThread::getCurrentThread(); $worker = NativeThread::getCurrentThread();
assert($worker instanceof AsyncWorker); assert($worker instanceof AsyncWorker);
MemoryManager::dumpMemory( MemoryDump::dumpMemory(
$worker, $worker,
Path::join($this->outputFolder, "AsyncWorker#" . $worker->getAsyncWorkerId()), Path::join($this->outputFolder, "AsyncWorker#" . $worker->getAsyncWorkerId()),
$this->maxNesting, $this->maxNesting,

View File

@ -131,7 +131,7 @@ abstract class Timings{
/** @var TimingsHandler[] */ /** @var TimingsHandler[] */
private static array $asyncTaskError = []; private static array $asyncTaskError = [];
private static TimingsHandler $asyncTaskWorkers; public static TimingsHandler $asyncTaskWorkers;
/** @var TimingsHandler[] */ /** @var TimingsHandler[] */
private static array $asyncTaskRun = []; private static array $asyncTaskRun = [];

View File

@ -167,6 +167,9 @@ class World implements ChunkManager{
public const DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK = 3; public const DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK = 3;
//TODO: this could probably do with being a lot bigger
private const BLOCK_CACHE_SIZE_CAP = 2048;
/** /**
* @var Player[] entity runtime ID => Player * @var Player[] entity runtime ID => Player
* @phpstan-var array<int, Player> * @phpstan-var array<int, Player>
@ -202,6 +205,7 @@ class World implements ChunkManager{
* @phpstan-var array<ChunkPosHash, array<ChunkBlockPosHash, Block>> * @phpstan-var array<ChunkPosHash, array<ChunkBlockPosHash, Block>>
*/ */
private array $blockCache = []; private array $blockCache = [];
private int $blockCacheSize = 0;
/** /**
* @var AxisAlignedBB[][][] chunkHash => [relativeBlockHash => AxisAlignedBB[]] * @var AxisAlignedBB[][][] chunkHash => [relativeBlockHash => AxisAlignedBB[]]
* @phpstan-var array<ChunkPosHash, array<ChunkBlockPosHash, list<AxisAlignedBB>>> * @phpstan-var array<ChunkPosHash, array<ChunkBlockPosHash, list<AxisAlignedBB>>>
@ -653,6 +657,7 @@ class World implements ChunkManager{
$this->provider->close(); $this->provider->close();
$this->blockCache = []; $this->blockCache = [];
$this->blockCacheSize = 0;
$this->blockCollisionBoxCache = []; $this->blockCollisionBoxCache = [];
$this->unloaded = true; $this->unloaded = true;
@ -1138,13 +1143,16 @@ class World implements ChunkManager{
public function clearCache(bool $force = false) : void{ public function clearCache(bool $force = false) : void{
if($force){ if($force){
$this->blockCache = []; $this->blockCache = [];
$this->blockCacheSize = 0;
$this->blockCollisionBoxCache = []; $this->blockCollisionBoxCache = [];
}else{ }else{
$count = 0; //Recalculate this when we're asked - blockCacheSize may be higher than the real size
$this->blockCacheSize = 0;
foreach($this->blockCache as $list){ foreach($this->blockCache as $list){
$count += count($list); $this->blockCacheSize += count($list);
if($count > 2048){ if($this->blockCacheSize > self::BLOCK_CACHE_SIZE_CAP){
$this->blockCache = []; $this->blockCache = [];
$this->blockCacheSize = 0;
break; break;
} }
} }
@ -1152,7 +1160,7 @@ class World implements ChunkManager{
$count = 0; $count = 0;
foreach($this->blockCollisionBoxCache as $list){ foreach($this->blockCollisionBoxCache as $list){
$count += count($list); $count += count($list);
if($count > 2048){ if($count > self::BLOCK_CACHE_SIZE_CAP){
//TODO: Is this really the best logic? //TODO: Is this really the best logic?
$this->blockCollisionBoxCache = []; $this->blockCollisionBoxCache = [];
break; break;
@ -1161,6 +1169,19 @@ class World implements ChunkManager{
} }
} }
private function trimBlockCache() : void{
$before = $this->blockCacheSize;
//Since PHP maintains key order, earliest in foreach should be the oldest entries
//Older entries are less likely to be hot, so destroying these should usually have the lowest impact on performance
foreach($this->blockCache as $chunkHash => $blocks){
unset($this->blockCache[$chunkHash]);
$this->blockCacheSize -= count($blocks);
if($this->blockCacheSize < self::BLOCK_CACHE_SIZE_CAP){
break;
}
}
}
/** /**
* @return true[] fullID => dummy * @return true[] fullID => dummy
* @phpstan-return array<int, true> * @phpstan-return array<int, true>
@ -1921,6 +1942,10 @@ class World implements ChunkManager{
if($addToCache && $relativeBlockHash !== null){ if($addToCache && $relativeBlockHash !== null){
$this->blockCache[$chunkHash][$relativeBlockHash] = $block; $this->blockCache[$chunkHash][$relativeBlockHash] = $block;
if(++$this->blockCacheSize >= self::BLOCK_CACHE_SIZE_CAP){
$this->trimBlockCache();
}
} }
return $block; return $block;
@ -1967,6 +1992,7 @@ class World implements ChunkManager{
$relativeBlockHash = World::chunkBlockHash($x, $y, $z); $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
unset($this->blockCache[$chunkHash][$relativeBlockHash]); unset($this->blockCache[$chunkHash][$relativeBlockHash]);
$this->blockCacheSize--;
unset($this->blockCollisionBoxCache[$chunkHash][$relativeBlockHash]); unset($this->blockCollisionBoxCache[$chunkHash][$relativeBlockHash]);
//blocks like fences have collision boxes that reach into neighbouring blocks, so we need to invalidate the //blocks like fences have collision boxes that reach into neighbouring blocks, so we need to invalidate the
//caches for those blocks as well //caches for those blocks as well
@ -2570,6 +2596,7 @@ class World implements ChunkManager{
$this->chunks[$chunkHash] = $chunk; $this->chunks[$chunkHash] = $chunk;
$this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
unset($this->blockCache[$chunkHash]); unset($this->blockCache[$chunkHash]);
unset($this->blockCollisionBoxCache[$chunkHash]); unset($this->blockCollisionBoxCache[$chunkHash]);
unset($this->changedBlocks[$chunkHash]); unset($this->changedBlocks[$chunkHash]);
@ -2854,6 +2881,8 @@ class World implements ChunkManager{
$this->logger->debug("Chunk $x $z has been upgraded, will be saved at the next autosave opportunity"); $this->logger->debug("Chunk $x $z has been upgraded, will be saved at the next autosave opportunity");
} }
$this->chunks[$chunkHash] = $chunk; $this->chunks[$chunkHash] = $chunk;
$this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
unset($this->blockCache[$chunkHash]); unset($this->blockCache[$chunkHash]);
unset($this->blockCollisionBoxCache[$chunkHash]); unset($this->blockCollisionBoxCache[$chunkHash]);
@ -3013,6 +3042,7 @@ class World implements ChunkManager{
} }
unset($this->chunks[$chunkHash]); unset($this->chunks[$chunkHash]);
$this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
unset($this->blockCache[$chunkHash]); unset($this->blockCache[$chunkHash]);
unset($this->blockCollisionBoxCache[$chunkHash]); unset($this->blockCollisionBoxCache[$chunkHash]);
unset($this->changedBlocks[$chunkHash]); unset($this->changedBlocks[$chunkHash]);