mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-04-20 16:00:20 +00:00
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.
This commit is contained in:
parent
8f536e6f21
commit
42f90e94ff
102
src/GarbageCollectorManager.php
Normal file
102
src/GarbageCollectorManager.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?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,
|
||||
){
|
||||
gc_disable();
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -93,6 +93,7 @@ abstract class AsyncTask extends Runnable{
|
||||
|
||||
$this->finished = true;
|
||||
AsyncWorker::getNotifier()->wakeupSleeper();
|
||||
AsyncWorker::maybeCollectCycles();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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{
|
||||
|
Loading…
x
Reference in New Issue
Block a user