From 42f90e94ffb7cdd30162eb450be073e2be6e5a06 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sun, 15 Dec 2024 21:25:32 +0000 Subject: [PATCH] AsyncWorker now manually triggers GC at the end of each task run, similar to the main thread this avoids costly GC runs during hot code. --- src/GarbageCollectorManager.php | 102 +++++++++++++++++++++++ src/MemoryManager.php | 48 +---------- src/network/mcpe/raklib/RakLibServer.php | 4 + src/scheduler/AsyncTask.php | 1 + src/scheduler/AsyncWorker.php | 13 ++- 5 files changed, 121 insertions(+), 47 deletions(-) create mode 100644 src/GarbageCollectorManager.php diff --git a/src/GarbageCollectorManager.php b/src/GarbageCollectorManager.php new file mode 100644 index 000000000..c5cd1f9ed --- /dev/null +++ b/src/GarbageCollectorManager.php @@ -0,0 +1,102 @@ +logger = new \PrefixedLogger($logger, "Cyclic Garbage Collector"); + $this->timings = new TimingsHandler("Cyclic Garbage Collector"); + } + + 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; + } +} diff --git a/src/MemoryManager.php b/src/MemoryManager.php index b83e99fa5..f541514dd 100644 --- a/src/MemoryManager.php +++ b/src/MemoryManager.php @@ -31,16 +31,11 @@ use pocketmine\timings\Timings; use pocketmine\utils\Process; use pocketmine\YmlServerProperties as Yml; use function gc_collect_cycles; -use function gc_disable; use function gc_mem_caches; -use function gc_status; -use function hrtime; use function ini_set; use function intdiv; -use function max; use function mb_strtoupper; use function min; -use function number_format; use function preg_match; use function round; use function sprintf; @@ -50,13 +45,7 @@ class MemoryManager{ 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; - //These constants are copied from Zend/zend_gc.c as of PHP 8.3.14 - //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 GarbageCollectorManager $cycleGcManager; private int $memoryLimit; private int $globalMemoryLimit; @@ -72,9 +61,6 @@ class MemoryManager{ private int $garbageCollectionPeriod; private int $garbageCollectionTicker = 0; - private int $cycleCollectionThreshold = self::GC_THRESHOLD_DEFAULT; - private int $cycleCollectionTimeTotalNs = 0; - private int $lowMemChunkRadiusOverride; private bool $dumpWorkers = true; @@ -85,9 +71,9 @@ class MemoryManager{ private Server $server ){ $this->logger = new \PrefixedLogger($server->getLogger(), "Memory Manager"); + $this->cycleGcManager = new GarbageCollectorManager($this->logger); $this->init($server->getConfigGroup()); - gc_disable(); } private function init(ServerConfigGroup $config) : void{ @@ -174,18 +160,6 @@ class MemoryManager{ $this->logger->debug(sprintf("Freed %gMB, $cycles cycles", round(($ev->getMemoryFreed() / 1024) / 1024, 2))); } - 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->cycleCollectionThreshold){ - $this->cycleCollectionThreshold = min(self::GC_THRESHOLD_MAX, $this->cycleCollectionThreshold + self::GC_THRESHOLD_STEP); - }elseif($this->cycleCollectionThreshold > self::GC_THRESHOLD_DEFAULT){ - $this->cycleCollectionThreshold = max(self::GC_THRESHOLD_DEFAULT, $this->cycleCollectionThreshold - self::GC_THRESHOLD_STEP); - } - } - /** * Called every tick to update the memory manager state. */ @@ -218,25 +192,11 @@ class MemoryManager{ } } - $rootsBefore = gc_status()["roots"]; if($this->garbageCollectionPeriod > 0 && ++$this->garbageCollectionTicker >= $this->garbageCollectionPeriod){ $this->garbageCollectionTicker = 0; $this->triggerGarbageCollector(); - }elseif($rootsBefore >= $this->cycleCollectionThreshold){ - Timings::$garbageCollector->startTiming(); - - $start = hrtime(true); - $cycles = gc_collect_cycles(); - $end = hrtime(true); - - $rootsAfter = gc_status()["roots"]; - $this->adjustGcThreshold($cycles, $rootsAfter); - - Timings::$garbageCollector->stopTiming(); - - $time = $end - $start; - $this->cycleCollectionTimeTotalNs += $time; - $this->logger->debug("gc_collect_cycles: " . number_format($time) . " ns ($rootsBefore -> $rootsAfter roots, $cycles cycles collected) - total GC time: " . number_format($this->cycleCollectionTimeTotalNs) . " ns"); + }else{ + $this->cycleGcManager->maybeCollectCycles(); } Timings::$memoryManager->stopTiming(); diff --git a/src/network/mcpe/raklib/RakLibServer.php b/src/network/mcpe/raklib/RakLibServer.php index 5137b94ba..3b4b8da74 100644 --- a/src/network/mcpe/raklib/RakLibServer.php +++ b/src/network/mcpe/raklib/RakLibServer.php @@ -82,7 +82,11 @@ class RakLibServer extends Thread{ } 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(); + ini_set("display_errors", '1'); ini_set("display_startup_errors", '1'); \GlobalLogger::set($this->logger); diff --git a/src/scheduler/AsyncTask.php b/src/scheduler/AsyncTask.php index 1175b6fc4..a9b466f40 100644 --- a/src/scheduler/AsyncTask.php +++ b/src/scheduler/AsyncTask.php @@ -93,6 +93,7 @@ abstract class AsyncTask extends Runnable{ $this->finished = true; AsyncWorker::getNotifier()->wakeupSleeper(); + AsyncWorker::maybeCollectCycles(); } /** diff --git a/src/scheduler/AsyncWorker.php b/src/scheduler/AsyncWorker.php index 5fdfb1ebb..5f911bc1c 100644 --- a/src/scheduler/AsyncWorker.php +++ b/src/scheduler/AsyncWorker.php @@ -24,12 +24,12 @@ declare(strict_types=1); namespace pocketmine\scheduler; use pmmp\thread\Thread as NativeThread; +use pocketmine\GarbageCollectorManager; use pocketmine\snooze\SleeperHandlerEntry; use pocketmine\snooze\SleeperNotifier; use pocketmine\thread\log\ThreadSafeLogger; use pocketmine\thread\Worker; use pocketmine\utils\AssumptionFailedError; -use function gc_enable; use function ini_set; class AsyncWorker extends Worker{ @@ -37,6 +37,7 @@ class AsyncWorker extends Worker{ private static array $store = []; private static ?SleeperNotifier $notifier = null; + private static ?GarbageCollectorManager $cycleGcManager = null; public function __construct( private ThreadSafeLogger $logger, @@ -52,11 +53,16 @@ class AsyncWorker extends Worker{ 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{ \GlobalLogger::set($this->logger); - gc_enable(); - if($this->memoryLimit > 0){ ini_set('memory_limit', $this->memoryLimit . 'M'); $this->logger->debug("Set memory limit to " . $this->memoryLimit . " MB"); @@ -66,6 +72,7 @@ class AsyncWorker extends Worker{ } self::$notifier = $this->sleeperEntry->createNotifier(); + self::$cycleGcManager = new GarbageCollectorManager($this->logger); } public function getLogger() : ThreadSafeLogger{