Skip to content

Extract GeneratorExecutor system from World, v2 #6682

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: minor-next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 25 additions & 49 deletions src/world/World.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,11 @@
use pocketmine\world\format\io\WritableWorldProvider;
use pocketmine\world\format\LightArray;
use pocketmine\world\format\SubChunk;
use pocketmine\world\generator\executor\AsyncGeneratorExecutor;
use pocketmine\world\generator\executor\GeneratorExecutor;
use pocketmine\world\generator\executor\GeneratorExecutorSetupParameters;
use pocketmine\world\generator\executor\SyncGeneratorExecutor;
use pocketmine\world\generator\GeneratorManager;
use pocketmine\world\generator\GeneratorRegisterTask;
use pocketmine\world\generator\GeneratorUnregisterTask;
use pocketmine\world\generator\PopulationTask;
use pocketmine\world\light\BlockLightUpdate;
use pocketmine\world\light\LightPopulationTask;
Expand Down Expand Up @@ -336,11 +338,7 @@ class World implements ChunkManager{
*/
private array $chunkPopulationRequestQueueIndex = [];

/**
* @var true[]
* @phpstan-var array<int, true>
*/
private array $generatorRegisteredWorkers = [];
private readonly GeneratorExecutor $generatorExecutor;

private bool $autoSave = true;

Expand All @@ -360,9 +358,6 @@ class World implements ChunkManager{

private bool $doingTick = false;

/** @phpstan-var class-string<\pocketmine\world\generator\Generator> */
private string $generator;

private bool $unloaded = false;
/**
* @var \Closure[]
Expand Down Expand Up @@ -498,7 +493,23 @@ public function __construct(
$generator = GeneratorManager::getInstance()->getGenerator($this->provider->getWorldData()->getGenerator()) ??
throw new AssumptionFailedError("WorldManager should already have checked that the generator exists");
$generator->validateGeneratorOptions($this->provider->getWorldData()->getGeneratorOptions());
$this->generator = $generator->getGeneratorClass();

$executorSetupParameters = new GeneratorExecutorSetupParameters(
worldMinY: $this->minY,
worldMaxY: $this->maxY,
generatorSeed: $this->getSeed(),
generatorClass: $generator->getGeneratorClass(),
generatorSettings: $this->provider->getWorldData()->getGeneratorOptions()
);
$this->generatorExecutor = $generator->isFast() ?
new SyncGeneratorExecutor($executorSetupParameters) :
new AsyncGeneratorExecutor(
$this->logger,
$this->workerPool,
$executorSetupParameters,
$this->worldId
);

$this->chunkPopulationRequestQueue = new \SplQueue();
$this->addOnUnloadCallback(function() : void{
$this->logger->debug("Cancelling unfulfilled generation requests");
Expand Down Expand Up @@ -534,17 +545,6 @@ public function __construct(
$this->initRandomTickBlocksFromConfig($cfg);

$this->timings = new WorldTimings($this);

$this->workerPool->addWorkerStartHook($workerStartHook = function(int $workerId) : void{
if(array_key_exists($workerId, $this->generatorRegisteredWorkers)){
$this->logger->debug("Worker $workerId with previously registered generator restarted, flagging as unregistered");
unset($this->generatorRegisteredWorkers[$workerId]);
}
});
$workerPool = $this->workerPool;
$this->addOnUnloadCallback(static function() use ($workerPool, $workerStartHook) : void{
$workerPool->removeWorkerStartHook($workerStartHook);
});
}

private function initRandomTickBlocksFromConfig(ServerConfigGroup $cfg) : void{
Expand Down Expand Up @@ -585,21 +585,6 @@ public function getTickRateTime() : float{
return $this->tickRateTime;
}

public function registerGeneratorToWorker(int $worker) : void{
$this->logger->debug("Registering generator on worker $worker");
$this->workerPool->submitTaskToWorker(new GeneratorRegisterTask($this, $this->generator, $this->provider->getWorldData()->getGeneratorOptions()), $worker);
$this->generatorRegisteredWorkers[$worker] = true;
}

public function unregisterGenerator() : void{
foreach($this->workerPool->getRunningWorkers() as $i){
if(isset($this->generatorRegisteredWorkers[$i])){
$this->workerPool->submitTaskToWorker(new GeneratorUnregisterTask($this), $i);
}
}
$this->generatorRegisteredWorkers = [];
}

public function getServer() : Server{
return $this->server;
}
Expand Down Expand Up @@ -657,7 +642,7 @@ public function onUnload() : void{

$this->save();

$this->unregisterGenerator();
$this->generatorExecutor->shutdown();

$this->provider->close();
$this->blockCache = [];
Expand Down Expand Up @@ -3486,8 +3471,8 @@ private function internalOrderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLo

$centerChunk = $this->loadChunk($chunkX, $chunkZ);
$adjacentChunks = $this->getAdjacentChunks($chunkX, $chunkZ);
$task = new PopulationTask(
$this->worldId,

$this->generatorExecutor->populate(
$chunkX,
$chunkZ,
$centerChunk,
Expand All @@ -3500,15 +3485,6 @@ function(Chunk $centerChunk, array $adjacentChunks) use ($chunkPopulationLockId,
$this->generateChunkCallback($chunkPopulationLockId, $chunkX, $chunkZ, $centerChunk, $adjacentChunks, $temporaryChunkLoader);
}
);
$workerId = $this->workerPool->selectWorker();
if(!isset($this->workerPool->getRunningWorkers()[$workerId]) && isset($this->generatorRegisteredWorkers[$workerId])){
$this->logger->debug("Selected worker $workerId previously had generator registered, but is now offline");
unset($this->generatorRegisteredWorkers[$workerId]);
}
if(!isset($this->generatorRegisteredWorkers[$workerId])){
$this->registerGeneratorToWorker($workerId);
}
$this->workerPool->submitTaskToWorker($task, $workerId);

return $resolver->getPromise();
}finally{
Expand Down
7 changes: 4 additions & 3 deletions src/world/generator/GeneratorManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public function __construct(){
}catch(InvalidGeneratorOptionsException $e){
return $e;
}
});
}, fast: true);
$this->addGenerator(Normal::class, "normal", fn() => null);
$this->addAlias("normal", "default");
$this->addGenerator(Nether::class, "nether", fn() => null);
Expand All @@ -62,22 +62,23 @@ public function __construct(){
* @param string $name Alias for this generator type that can be written in configs
* @param \Closure $presetValidator Callback to validate generator options for new worlds
* @param bool $overwrite Whether to force overwriting any existing registered generator with the same name
* @param bool $fast Whether this generator is fast enough to run without async tasks
*
* @phpstan-param \Closure(string) : ?InvalidGeneratorOptionsException $presetValidator
*
* @phpstan-param class-string<Generator> $class
*
* @throws \InvalidArgumentException
*/
public function addGenerator(string $class, string $name, \Closure $presetValidator, bool $overwrite = false) : void{
public function addGenerator(string $class, string $name, \Closure $presetValidator, bool $overwrite = false, bool $fast = false) : void{
Utils::testValidInstance($class, Generator::class);

$name = strtolower($name);
if(!$overwrite && isset($this->list[$name])){
throw new \InvalidArgumentException("Alias \"$name\" is already assigned");
}

$this->list[$name] = new GeneratorManagerEntry($class, $presetValidator);
$this->list[$name] = new GeneratorManagerEntry($class, $presetValidator, $fast);
}

/**
Expand Down
5 changes: 4 additions & 1 deletion src/world/generator/GeneratorManagerEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ final class GeneratorManagerEntry{
*/
public function __construct(
private string $generatorClass,
private \Closure $presetValidator
private \Closure $presetValidator,
private readonly bool $fast
){}

/** @phpstan-return class-string<Generator> */
public function getGeneratorClass() : string{ return $this->generatorClass; }

public function isFast() : bool{ return $this->fast; }

/**
* @throws InvalidGeneratorOptionsException
*/
Expand Down
7 changes: 7 additions & 0 deletions src/world/generator/PopulationTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,18 @@
use pocketmine\utils\AssumptionFailedError;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\io\FastChunkSerializer;
use pocketmine\world\generator\executor\ThreadLocalGeneratorContext;
use function array_map;
use function igbinary_serialize;
use function igbinary_unserialize;

/**
* @internal
*
* TODO: this should be moved to the executor namespace, but plugins have unfortunately used it directly due to the
* difficulty of regenerating chunks. This should be addressed in the future.
* For the remainder of PM5, we can't relocate this class.
*
* @phpstan-type OnCompletion \Closure(Chunk $centerChunk, array<int, Chunk> $adjacentChunks) : void
*/
class PopulationTask extends AsyncTask{
Expand Down
106 changes: 106 additions & 0 deletions src/world/generator/executor/AsyncGeneratorExecutor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?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\world\generator\executor;

use pocketmine\scheduler\AsyncPool;
use pocketmine\world\format\Chunk;
use pocketmine\world\generator\PopulationTask;
use function array_key_exists;

final class AsyncGeneratorExecutor implements GeneratorExecutor{
private static int $nextAsyncContextId = 1;

private readonly \Logger $logger;

/** @phpstan-var \Closure(int) : void */
private readonly \Closure $workerStartHook;

private readonly int $asyncContextId;

/**
* @var true[]
* @phpstan-var array<int, true>
*/
private array $generatorRegisteredWorkers = [];

public function __construct(
\Logger $logger,
private readonly AsyncPool $workerPool,
private readonly GeneratorExecutorSetupParameters $setupParameters,
int $asyncContextId = null
){
$this->logger = new \PrefixedLogger($logger, "AsyncGeneratorExecutor");

//TODO: we only allow setting this for PM5 because of PopulationTask uses in plugins
$this->asyncContextId = $asyncContextId ?? self::$nextAsyncContextId++;

$this->workerStartHook = function(int $workerId) : void{
if(array_key_exists($workerId, $this->generatorRegisteredWorkers)){
$this->logger->debug("Worker $workerId with previously registered generator restarted, flagging as unregistered");
unset($this->generatorRegisteredWorkers[$workerId]);
}
};
$this->workerPool->addWorkerStartHook($this->workerStartHook);
}

private function registerGeneratorToWorker(int $worker) : void{
$this->logger->debug("Registering generator on worker $worker");
$this->workerPool->submitTaskToWorker(new AsyncGeneratorRegisterTask($this->setupParameters, $this->asyncContextId), $worker);
$this->generatorRegisteredWorkers[$worker] = true;
}

private function unregisterGenerator() : void{
foreach($this->workerPool->getRunningWorkers() as $i){
if(isset($this->generatorRegisteredWorkers[$i])){
$this->workerPool->submitTaskToWorker(new AsyncGeneratorUnregisterTask($this->asyncContextId), $i);
}
}
$this->generatorRegisteredWorkers = [];
}

public function populate(int $chunkX, int $chunkZ, ?Chunk $centerChunk, array $adjacentChunks, \Closure $onCompletion) : void{
$task = new PopulationTask(
$this->asyncContextId,
$chunkX,
$chunkZ,
$centerChunk,
$adjacentChunks,
$onCompletion
);
$workerId = $this->workerPool->selectWorker();
if(!isset($this->workerPool->getRunningWorkers()[$workerId]) && isset($this->generatorRegisteredWorkers[$workerId])){
$this->logger->debug("Selected worker $workerId previously had generator registered, but is now offline");
unset($this->generatorRegisteredWorkers[$workerId]);
}
if(!isset($this->generatorRegisteredWorkers[$workerId])){
$this->registerGeneratorToWorker($workerId);
}
$this->workerPool->submitTaskToWorker($task, $workerId);
}

public function shutdown() : void{
$this->unregisterGenerator();
$this->workerPool->removeWorkerStartHook($this->workerStartHook);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,20 @@

declare(strict_types=1);

namespace pocketmine\world\generator;
namespace pocketmine\world\generator\executor;

use pocketmine\scheduler\AsyncTask;
use pocketmine\world\World;

class GeneratorRegisterTask extends AsyncTask{
public int $seed;
public int $worldId;
public int $worldMinY;
public int $worldMaxY;
class AsyncGeneratorRegisterTask extends AsyncTask{

/**
* @phpstan-param class-string<Generator> $generatorClass
*/
public function __construct(
World $world,
public string $generatorClass,
public string $generatorSettings
){
$this->seed = $world->getSeed();
$this->worldId = $world->getId();
$this->worldMinY = $world->getMinY();
$this->worldMaxY = $world->getMaxY();
}
private readonly GeneratorExecutorSetupParameters $setupParameters,
private readonly int $contextId
){}

public function onRun() : void{
/**
* @var Generator $generator
* @see Generator::__construct()
*/
$generator = new $this->generatorClass($this->seed, $this->generatorSettings);
ThreadLocalGeneratorContext::register(new ThreadLocalGeneratorContext($generator, $this->worldMinY, $this->worldMaxY), $this->worldId);
$setupParameters = $this->setupParameters;
$generator = $setupParameters->createGenerator();
ThreadLocalGeneratorContext::register(new ThreadLocalGeneratorContext($generator, $setupParameters->worldMinY, $setupParameters->worldMaxY), $this->contextId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,16 @@

declare(strict_types=1);

namespace pocketmine\world\generator;
namespace pocketmine\world\generator\executor;

use pocketmine\scheduler\AsyncTask;
use pocketmine\world\World;

class GeneratorUnregisterTask extends AsyncTask{
public int $worldId;

public function __construct(World $world){
$this->worldId = $world->getId();
}
class AsyncGeneratorUnregisterTask extends AsyncTask{
public function __construct(
private readonly int $contextId
){}

public function onRun() : void{
ThreadLocalGeneratorContext::unregister($this->worldId);
ThreadLocalGeneratorContext::unregister($this->contextId);
}
}
Loading