debug("Created new chunk packet cache (world#$worldId, compressor#$compressorId)"); self::$instances[$worldId][$compressorId] = new self($world, $compressor); } return self::$instances[$worldId][$compressorId]; } /** @var World */ private $world; /** @var Compressor */ private $compressor; /** @var CompressBatchPromise[] */ private $caches = []; /** @var int */ private $hits = 0; /** @var int */ private $misses = 0; private function __construct(World $world, Compressor $compressor){ $this->world = $world; $this->compressor = $compressor; } /** * Requests asynchronous preparation of the chunk at the given coordinates. * * @return CompressBatchPromise a promise of resolution which will contain a compressed chunk packet. */ public function request(int $chunkX, int $chunkZ) : CompressBatchPromise{ $this->world->registerChunkListener($this, $chunkX, $chunkZ); $chunkHash = World::chunkHash($chunkX, $chunkZ); if(isset($this->caches[$chunkHash])){ ++$this->hits; return $this->caches[$chunkHash]; } ++$this->misses; $this->world->timings->syncChunkSendPrepareTimer->startTiming(); try{ $this->caches[$chunkHash] = new CompressBatchPromise(); $this->world->getServer()->getAsyncPool()->submitTask( new ChunkRequestTask( $chunkX, $chunkZ, $this->world->getChunk($chunkX, $chunkZ), $this->caches[$chunkHash], $this->compressor, function() use ($chunkX, $chunkZ) : void{ $this->world->getLogger()->error("Failed preparing chunk $chunkX $chunkZ, retrying"); $this->restartPendingRequest($chunkX, $chunkZ); } ) ); return $this->caches[$chunkHash]; }finally{ $this->world->timings->syncChunkSendPrepareTimer->stopTiming(); } } 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; } /** * Restarts an async request for an unresolved chunk. * * @throws \InvalidArgumentException */ private function restartPendingRequest(int $chunkX, int $chunkZ) : void{ $chunkHash = World::chunkHash($chunkX, $chunkZ); $existing = $this->caches[$chunkHash] ?? null; if($existing === null or $existing->hasResult()){ throw new \InvalidArgumentException("Restart can only be applied to unresolved promises"); } $existing->cancel(); unset($this->caches[$chunkHash]); $this->request($chunkX, $chunkZ)->onResolve(...$existing->getResolveCallbacks()); } /** * @throws \InvalidArgumentException */ private function destroyOrRestart(int $chunkX, int $chunkZ) : void{ $cache = $this->caches[World::chunkHash($chunkX, $chunkZ)] ?? null; if($cache !== null){ if(!$cache->hasResult()){ //some requesters are waiting for this chunk, so their request needs to be fulfilled $this->restartPendingRequest($chunkX, $chunkZ); }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(Chunk $chunk) : void{ //FIXME: this gets fired for stuff that doesn't change terrain related things (like lighting updates) $this->destroyOrRestart($chunk->getX(), $chunk->getZ()); } /** * @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() >> 4, $block->getFloorZ() >> 4); } /** * @see ChunkListener::onChunkUnloaded() */ public function onChunkUnloaded(Chunk $chunk) : void{ $this->destroy($chunk->getX(), $chunk->getZ()); $this->world->unregisterChunkListener($this, $chunk->getX(), $chunk->getZ()); } /** * 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($cache->hasResult()){ $result += strlen($cache->getResult()); } } 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; } }