From 9a2c53c744e894630b1ccc6e16ffcaaa1d94d0d5 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Mon, 2 Dec 2024 20:59:45 +0000 Subject: [PATCH 1/7] Attempt at having PM trigger threshold GC instead of letting PHP do it the reasoning for this was described in #5628 --- src/MemoryManager.php | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/MemoryManager.php b/src/MemoryManager.php index 4308167d37..384ea13e52 100644 --- a/src/MemoryManager.php +++ b/src/MemoryManager.php @@ -41,11 +41,12 @@ 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_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; @@ -107,6 +110,7 @@ public function __construct( $this->logger = new \PrefixedLogger($server->getLogger(), "Memory Manager"); $this->init($server->getConfigGroup()); + gc_disable(); } private function init(ServerConfigGroup $config) : void{ @@ -152,7 +156,6 @@ private function init(ServerConfigGroup $config) : void{ $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 +207,28 @@ public function trigger(int $memory, int $limit, bool $global = false, int $trig $this->logger->debug(sprintf("Freed %gMB, $cycles cycles", round(($ev->getMemoryFreed() / 1024) / 1024, 2))); } + 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 $gcThreshold = self::GC_THRESHOLD_DEFAULT; + + private function adjustGcThreshold(int $count, int $num_roots) : 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($count < self::GC_THRESHOLD_TRIGGER || $num_roots >= $this->gcThreshold){ + /* increase */ + if($this->gcThreshold < self::GC_THRESHOLD_MAX){ + $this->gcThreshold = min(self::GC_THRESHOLD_MAX, $this->gcThreshold + self::GC_THRESHOLD_STEP); + } + }elseif($this->gcThreshold > self::GC_THRESHOLD_DEFAULT){ + $this->gcThreshold = max(self::GC_THRESHOLD_DEFAULT, $this->gcThreshold - self::GC_THRESHOLD_STEP); + } + } + /** * Called every tick to update the memory manager state. */ @@ -239,6 +264,15 @@ public function check() : void{ if($this->garbageCollectionPeriod > 0 && ++$this->garbageCollectionTicker >= $this->garbageCollectionPeriod){ $this->garbageCollectionTicker = 0; $this->triggerGarbageCollector(); + }else{ + $status = gc_status(); + $roots = $status["roots"]; + if($roots >= $this->gcThreshold){ + Timings::$garbageCollector->startTiming(); + $cycles = gc_collect_cycles(); + $this->adjustGcThreshold($cycles, $roots); + Timings::$garbageCollector->stopTiming(); + } } Timings::$memoryManager->stopTiming(); @@ -465,7 +499,6 @@ public static function dumpMemory(mixed $startingObject, string $outputFolder, i $logger->info("Finished!"); ini_set('memory_limit', $hardLimit); - gc_enable(); } /** From 45cd6f2aa0fcbb910166488a5d5c5879bed84329 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Mon, 2 Dec 2024 21:01:35 +0000 Subject: [PATCH 2/7] Remove debug imports --- src/MemoryManager.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/MemoryManager.php b/src/MemoryManager.php index 384ea13e52..cd7b8aef78 100644 --- a/src/MemoryManager.php +++ b/src/MemoryManager.php @@ -46,7 +46,6 @@ 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; @@ -60,7 +59,6 @@ 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; From 62b3855dcb5f986d82b1abe0b71ae947ca813aff Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Mon, 2 Dec 2024 21:06:39 +0000 Subject: [PATCH 3/7] tidy --- src/MemoryManager.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/MemoryManager.php b/src/MemoryManager.php index cd7b8aef78..107f5dbb7e 100644 --- a/src/MemoryManager.php +++ b/src/MemoryManager.php @@ -218,10 +218,7 @@ private function adjustGcThreshold(int $count, int $num_roots) : void{ //by a fixed step //Adapted from zend_gc.c/gc_adjust_threshold() as of PHP 8.3.14 if($count < self::GC_THRESHOLD_TRIGGER || $num_roots >= $this->gcThreshold){ - /* increase */ - if($this->gcThreshold < self::GC_THRESHOLD_MAX){ - $this->gcThreshold = min(self::GC_THRESHOLD_MAX, $this->gcThreshold + self::GC_THRESHOLD_STEP); - } + $this->gcThreshold = min(self::GC_THRESHOLD_MAX, $this->gcThreshold + self::GC_THRESHOLD_STEP); }elseif($this->gcThreshold > self::GC_THRESHOLD_DEFAULT){ $this->gcThreshold = max(self::GC_THRESHOLD_DEFAULT, $this->gcThreshold - self::GC_THRESHOLD_STEP); } From 6d7f05dd7d4757f46a4d208e136dbb3514928095 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Mon, 2 Dec 2024 21:07:33 +0000 Subject: [PATCH 4/7] rename variable --- src/MemoryManager.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MemoryManager.php b/src/MemoryManager.php index 107f5dbb7e..821b6c82e4 100644 --- a/src/MemoryManager.php +++ b/src/MemoryManager.php @@ -212,12 +212,12 @@ public function trigger(int $memory, int $limit, bool $global = false, int $trig private int $gcThreshold = self::GC_THRESHOLD_DEFAULT; - private function adjustGcThreshold(int $count, int $num_roots) : void{ + private function adjustGcThreshold(int $count, int $roots) : 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($count < self::GC_THRESHOLD_TRIGGER || $num_roots >= $this->gcThreshold){ + if($count < self::GC_THRESHOLD_TRIGGER || $roots >= $this->gcThreshold){ $this->gcThreshold = min(self::GC_THRESHOLD_MAX, $this->gcThreshold + self::GC_THRESHOLD_STEP); }elseif($this->gcThreshold > self::GC_THRESHOLD_DEFAULT){ $this->gcThreshold = max(self::GC_THRESHOLD_DEFAULT, $this->gcThreshold - self::GC_THRESHOLD_STEP); From 9ecc23cb3a438b042183cfd6367dde27113f8634 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Tue, 3 Dec 2024 16:41:23 +0000 Subject: [PATCH 5/7] Fixed logic mismatch GC threshold recalculation is supposed to use the root count _after_ GC, not before (presumably so it doesn't count non-cyclic objects). --- src/MemoryManager.php | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/MemoryManager.php b/src/MemoryManager.php index 821b6c82e4..33c5595cb8 100644 --- a/src/MemoryManager.php +++ b/src/MemoryManager.php @@ -212,11 +212,12 @@ public function trigger(int $memory, int $limit, bool $global = false, int $trig private int $gcThreshold = self::GC_THRESHOLD_DEFAULT; - private function adjustGcThreshold(int $count, int $roots) : void{ + private function adjustGcThreshold(int $count) : 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 + $roots = gc_status()["roots"]; if($count < self::GC_THRESHOLD_TRIGGER || $roots >= $this->gcThreshold){ $this->gcThreshold = min(self::GC_THRESHOLD_MAX, $this->gcThreshold + self::GC_THRESHOLD_STEP); }elseif($this->gcThreshold > self::GC_THRESHOLD_DEFAULT){ @@ -259,15 +260,11 @@ public function check() : void{ if($this->garbageCollectionPeriod > 0 && ++$this->garbageCollectionTicker >= $this->garbageCollectionPeriod){ $this->garbageCollectionTicker = 0; $this->triggerGarbageCollector(); - }else{ - $status = gc_status(); - $roots = $status["roots"]; - if($roots >= $this->gcThreshold){ - Timings::$garbageCollector->startTiming(); - $cycles = gc_collect_cycles(); - $this->adjustGcThreshold($cycles, $roots); - Timings::$garbageCollector->stopTiming(); - } + }elseif(gc_status()["roots"] >= $this->gcThreshold){ + Timings::$garbageCollector->startTiming(); + $cycles = gc_collect_cycles(); + $this->adjustGcThreshold($cycles); + Timings::$garbageCollector->stopTiming(); } Timings::$memoryManager->stopTiming(); From 87aa346cb2f0f1e770fac39afcd52a2913634686 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sun, 15 Dec 2024 15:51:14 +0000 Subject: [PATCH 6/7] Clean up, add debug --- src/MemoryManager.php | 46 +++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/src/MemoryManager.php b/src/MemoryManager.php index 33c5595cb8..47dbb1cd8c 100644 --- a/src/MemoryManager.php +++ b/src/MemoryManager.php @@ -46,6 +46,7 @@ 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; @@ -59,6 +60,7 @@ 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; @@ -76,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; @@ -92,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; @@ -205,23 +218,15 @@ public function trigger(int $memory, int $limit, bool $global = false, int $trig $this->logger->debug(sprintf("Freed %gMB, $cycles cycles", round(($ev->getMemoryFreed() / 1024) / 1024, 2))); } - 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 $gcThreshold = self::GC_THRESHOLD_DEFAULT; - - private function adjustGcThreshold(int $count) : void{ + 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 - $roots = gc_status()["roots"]; - if($count < self::GC_THRESHOLD_TRIGGER || $roots >= $this->gcThreshold){ - $this->gcThreshold = min(self::GC_THRESHOLD_MAX, $this->gcThreshold + self::GC_THRESHOLD_STEP); - }elseif($this->gcThreshold > self::GC_THRESHOLD_DEFAULT){ - $this->gcThreshold = max(self::GC_THRESHOLD_DEFAULT, $this->gcThreshold - self::GC_THRESHOLD_STEP); + 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); } } @@ -257,14 +262,25 @@ public function check() : void{ } } + $rootsBefore = gc_status()["roots"]; if($this->garbageCollectionPeriod > 0 && ++$this->garbageCollectionTicker >= $this->garbageCollectionPeriod){ $this->garbageCollectionTicker = 0; $this->triggerGarbageCollector(); - }elseif(gc_status()["roots"] >= $this->gcThreshold){ + }elseif($rootsBefore >= $this->cycleCollectionThreshold){ Timings::$garbageCollector->startTiming(); + + $start = hrtime(true); $cycles = gc_collect_cycles(); - $this->adjustGcThreshold($cycles); + $end = hrtime(true); + + $rootsAfter = gc_status()["roots"]; + $this->adjustGcThreshold($rootsBefore - $rootsAfter, $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(); From f21b000ea593ca2c2e6b6688534217ddd3a386dc Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sun, 15 Dec 2024 15:53:52 +0000 Subject: [PATCH 7/7] Fix disparity I theorized that the number of roots removed should be a better heuristic than cycles, but in practice it caused GC to run a lot more often. The cost of expensive objects like Server is parasitic, so the less we run GC, the more the cost is amortized. There are ways we can eliminate Server's cost from GC, but that's a task for another time. --- src/MemoryManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MemoryManager.php b/src/MemoryManager.php index 47dbb1cd8c..638a2613ae 100644 --- a/src/MemoryManager.php +++ b/src/MemoryManager.php @@ -274,7 +274,7 @@ public function check() : void{ $end = hrtime(true); $rootsAfter = gc_status()["roots"]; - $this->adjustGcThreshold($rootsBefore - $rootsAfter, $rootsAfter); + $this->adjustGcThreshold($cycles, $rootsAfter); Timings::$garbageCollector->stopTiming();