diff --git a/src/MemoryManager.php b/src/MemoryManager.php index 4308167d37..638a2613ae 100644 --- a/src/MemoryManager.php +++ b/src/MemoryManager.php @@ -41,11 +41,12 @@ use function fopen; use function fwrite; use function gc_collect_cycles; use function gc_disable; -use function gc_enable; use function gc_mem_caches; +use function gc_status; use function get_class; use function get_declared_classes; use function get_defined_functions; +use function hrtime; use function ini_get; use function ini_set; use function intdiv; @@ -55,9 +56,11 @@ use function is_object; use function is_resource; use function is_string; use function json_encode; +use function max; use function mb_strtoupper; use function min; use function mkdir; +use function number_format; use function preg_match; use function print_r; use function round; @@ -75,6 +78,14 @@ 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 int $memoryLimit; private int $globalMemoryLimit; private int $checkRate; @@ -91,6 +102,9 @@ class MemoryManager{ private bool $garbageCollectionTrigger; private bool $garbageCollectionAsync; + private int $cycleCollectionThreshold = self::GC_THRESHOLD_DEFAULT; + private int $cycleCollectionTimeTotalNs = 0; + private int $lowMemChunkRadiusOverride; private bool $lowMemChunkGC; @@ -107,6 +121,7 @@ class MemoryManager{ $this->logger = new \PrefixedLogger($server->getLogger(), "Memory Manager"); $this->init($server->getConfigGroup()); + gc_disable(); } private function init(ServerConfigGroup $config) : void{ @@ -152,7 +167,6 @@ class MemoryManager{ $this->lowMemClearWorldCache = $config->getPropertyBool(Yml::MEMORY_WORLD_CACHES_LOW_MEMORY_TRIGGER, true); $this->dumpWorkers = $config->getPropertyBool(Yml::MEMORY_MEMORY_DUMP_DUMP_ASYNC_WORKER, true); - gc_enable(); } public function isLowMemory() : bool{ @@ -204,6 +218,18 @@ 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. */ @@ -236,9 +262,25 @@ 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"); } Timings::$memoryManager->stopTiming(); @@ -465,7 +507,6 @@ class MemoryManager{ $logger->info("Finished!"); ini_set('memory_limit', $hardLimit); - gc_enable(); } /**