addOnUnloadCallback(static function() use ($worldId) : void{ foreach(self::$instances[$worldId] as $cache){ $cache->caches = []; } unset(self::$instances[$worldId]); \GlobalLogger::get()->debug("Destroyed chunk packet caches for world#$worldId"); }); } if(!isset(self::$instances[$worldId][$compressorId])){ \GlobalLogger::get()->debug("Created new chunk packet cache (world#$worldId, compressor#$compressorId)"); self::$instances[$worldId][$compressorId] = new self($world, $compressor); } return self::$instances[$worldId][$compressorId]; } public static function pruneCaches() : void{ foreach(self::$instances as $compressorMap){ foreach($compressorMap as $chunkCache){ foreach($chunkCache->caches as $chunkHash => $promise){ if(is_string($promise)){ //Do not clear promises that are not yet fulfilled; they will have requesters waiting on them unset($chunkCache->caches[$chunkHash]); } } } } } /** * @var CompressBatchPromise[]|string[] * @phpstan-var array */ private array $caches = []; private int $hits = 0; private int $misses = 0; private function __construct( private World $world, private Compressor $compressor ){} private function prepareChunkAsync(int $chunkX, int $chunkZ, int $chunkHash) : CompressBatchPromise{ $this->world->registerChunkListener($this, $chunkX, $chunkZ); $chunk = $this->world->getChunk($chunkX, $chunkZ); if($chunk === null){ throw new \InvalidArgumentException("Cannot request an unloaded chunk"); } ++$this->misses; $this->world->timings->syncChunkSendPrepare->startTiming(); try{ $promise = new CompressBatchPromise(); $this->world->getServer()->getAsyncPool()->submitTask( new ChunkRequestTask( $chunkX, $chunkZ, DimensionIds::OVERWORLD, //TODO: not hardcode this $chunk, $promise, $this->compressor ) ); $this->caches[$chunkHash] = $promise; $promise->onResolve(function(CompressBatchPromise $promise) use ($chunkHash) : void{ //the promise may have been discarded or replaced if the chunk was unloaded or modified in the meantime if(($this->caches[$chunkHash] ?? null) === $promise){ $this->caches[$chunkHash] = $promise->getResult(); } }); return $promise; }finally{ $this->world->timings->syncChunkSendPrepare->stopTiming(); } } /** * Requests asynchronous preparation of the chunk at the given coordinates. * * @return CompressBatchPromise|string Compressed chunk packet, or a promise for one to be resolved asynchronously. */ public function request(int $chunkX, int $chunkZ) : CompressBatchPromise|string{ $chunkHash = World::chunkHash($chunkX, $chunkZ); if(isset($this->caches[$chunkHash])){ ++$this->hits; return $this->caches[$chunkHash]; } return $this->prepareChunkAsync($chunkX, $chunkZ, $chunkHash); } private function destroy(int $chunkX, int $chunkZ) : bool{ $chunkHash = World::chunkHash($chunkX, $chunkZ); $existing = $this->caches[$chunkHash] ?? null; unset($this->caches[$chunkHash]); return $existing !== null; } /** * @throws \InvalidArgumentException */ private function destroyOrRestart(int $chunkX, int $chunkZ) : void{ $chunkPosHash = World::chunkHash($chunkX, $chunkZ); $cache = $this->caches[$chunkPosHash] ?? null; if($cache !== null){ if(!is_string($cache)){ //some requesters are waiting for this chunk, so their request needs to be fulfilled $cache->cancel(); unset($this->caches[$chunkPosHash]); $this->prepareChunkAsync($chunkX, $chunkZ, $chunkPosHash)->onResolve(...$cache->getResolveCallbacks()); }else{ //dump the cache, it'll be regenerated the next time it's requested $this->destroy($chunkX, $chunkZ); } } } use ChunkListenerNoOpTrait { //force overriding of these onChunkChanged as private; onBlockChanged as private; onChunkUnloaded as private; } /** * @see ChunkListener::onChunkChanged() */ public function onChunkChanged(int $chunkX, int $chunkZ, Chunk $chunk) : void{ $this->destroyOrRestart($chunkX, $chunkZ); } /** * @see ChunkListener::onBlockChanged() */ public function onBlockChanged(Vector3 $block) : void{ //FIXME: requesters will still receive this chunk after it's been dropped, but we can't mark this for a simple //sync here because it can spam the worker pool $this->destroy($block->getFloorX() >> Chunk::COORD_BIT_SIZE, $block->getFloorZ() >> Chunk::COORD_BIT_SIZE); } /** * @see ChunkListener::onChunkUnloaded() */ public function onChunkUnloaded(int $chunkX, int $chunkZ, Chunk $chunk) : void{ $this->destroy($chunkX, $chunkZ); $this->world->unregisterChunkListener($this, $chunkX, $chunkZ); } /** * Returns the number of bytes occupied by the cache data in this cache. This does not include the size of any * promises referenced by the cache. */ public function calculateCacheSize() : int{ $result = 0; foreach($this->caches as $cache){ if(is_string($cache)){ $result += strlen($cache); } } return $result; } /** * Returns the percentage of requests to the cache which resulted in a cache hit. */ public function getHitPercentage() : float{ $total = $this->hits + $this->misses; return $total > 0 ? $this->hits / $total : 0.0; } }