PocketMine-MP 5.25.1 git-694aa17cc916a954b10fe12721c81b1dc73eecd5
Loading...
Searching...
No Matches
ChunkCache.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\network\mcpe\cache;
25
32use pocketmine\world\ChunkListenerNoOpTrait;
35use function is_string;
36use function spl_object_id;
37use function strlen;
38
42class ChunkCache implements ChunkListener{
44 private static array $instances = [];
45
49 public static function getInstance(World $world, Compressor $compressor) : self{
50 $worldId = spl_object_id($world);
51 $compressorId = spl_object_id($compressor);
52 if(!isset(self::$instances[$worldId])){
53 self::$instances[$worldId] = [];
54 $world->addOnUnloadCallback(static function() use ($worldId) : void{
55 foreach(self::$instances[$worldId] as $cache){
56 $cache->caches = [];
57 }
58 unset(self::$instances[$worldId]);
59 \GlobalLogger::get()->debug("Destroyed chunk packet caches for world#$worldId");
60 });
61 }
62 if(!isset(self::$instances[$worldId][$compressorId])){
63 \GlobalLogger::get()->debug("Created new chunk packet cache (world#$worldId, compressor#$compressorId)");
64 self::$instances[$worldId][$compressorId] = new self($world, $compressor);
65 }
66 return self::$instances[$worldId][$compressorId];
67 }
68
69 public static function pruneCaches() : void{
70 foreach(self::$instances as $compressorMap){
71 foreach($compressorMap as $chunkCache){
72 foreach($chunkCache->caches as $chunkHash => $promise){
73 if(is_string($promise)){
74 //Do not clear promises that are not yet fulfilled; they will have requesters waiting on them
75 unset($chunkCache->caches[$chunkHash]);
76 }
77 }
78 }
79 }
80 }
81
86 private array $caches = [];
87
88 private int $hits = 0;
89 private int $misses = 0;
90
94 private function __construct(
95 private World $world,
96 private Compressor $compressor,
97 private int $dimensionId = DimensionIds::OVERWORLD
98 ){}
99
100 private function prepareChunkAsync(int $chunkX, int $chunkZ, int $chunkHash) : CompressBatchPromise{
101 $this->world->registerChunkListener($this, $chunkX, $chunkZ);
102 $chunk = $this->world->getChunk($chunkX, $chunkZ);
103 if($chunk === null){
104 throw new \InvalidArgumentException("Cannot request an unloaded chunk");
105 }
106 ++$this->misses;
107
108 $this->world->timings->syncChunkSendPrepare->startTiming();
109 try{
110 $promise = new CompressBatchPromise();
111
112 $this->world->getServer()->getAsyncPool()->submitTask(
113 new ChunkRequestTask(
114 $chunkX,
115 $chunkZ,
116 $this->dimensionId,
117 $chunk,
118 $promise,
119 $this->compressor
120 )
121 );
122 $this->caches[$chunkHash] = $promise;
123 $promise->onResolve(function(CompressBatchPromise $promise) use ($chunkHash) : void{
124 //the promise may have been discarded or replaced if the chunk was unloaded or modified in the meantime
125 if(($this->caches[$chunkHash] ?? null) === $promise){
126 $this->caches[$chunkHash] = $promise->getResult();
127 }
128 });
129
130 return $promise;
131 }finally{
132 $this->world->timings->syncChunkSendPrepare->stopTiming();
133 }
134 }
135
141 public function request(int $chunkX, int $chunkZ) : CompressBatchPromise|string{
142 $chunkHash = World::chunkHash($chunkX, $chunkZ);
143 if(isset($this->caches[$chunkHash])){
144 ++$this->hits;
145 return $this->caches[$chunkHash];
146 }
147
148 return $this->prepareChunkAsync($chunkX, $chunkZ, $chunkHash);
149 }
150
151 private function destroy(int $chunkX, int $chunkZ) : bool{
152 $chunkHash = World::chunkHash($chunkX, $chunkZ);
153 $existing = $this->caches[$chunkHash] ?? null;
154 unset($this->caches[$chunkHash]);
155
156 return $existing !== null;
157 }
158
162 private function destroyOrRestart(int $chunkX, int $chunkZ) : void{
163 $chunkPosHash = World::chunkHash($chunkX, $chunkZ);
164 $cache = $this->caches[$chunkPosHash] ?? null;
165 if($cache !== null){
166 if(!is_string($cache)){
167 //some requesters are waiting for this chunk, so their request needs to be fulfilled
168 $cache->cancel();
169 unset($this->caches[$chunkPosHash]);
170
171 $this->prepareChunkAsync($chunkX, $chunkZ, $chunkPosHash)->onResolve(...$cache->getResolveCallbacks());
172 }else{
173 //dump the cache, it'll be regenerated the next time it's requested
174 $this->destroy($chunkX, $chunkZ);
175 }
176 }
177 }
178
179 use ChunkListenerNoOpTrait {
180 //force overriding of these
181 onChunkChanged as private;
182 onBlockChanged as private;
183 onChunkUnloaded as private;
184 }
185
189 public function onChunkChanged(int $chunkX, int $chunkZ, Chunk $chunk) : void{
190 $this->destroyOrRestart($chunkX, $chunkZ);
191 }
192
196 public function onBlockChanged(Vector3 $block) : void{
197 //FIXME: requesters will still receive this chunk after it's been dropped, but we can't mark this for a simple
198 //sync here because it can spam the worker pool
199 $this->destroy($block->getFloorX() >> Chunk::COORD_BIT_SIZE, $block->getFloorZ() >> Chunk::COORD_BIT_SIZE);
200 }
201
205 public function onChunkUnloaded(int $chunkX, int $chunkZ, Chunk $chunk) : void{
206 $this->destroy($chunkX, $chunkZ);
207 $this->world->unregisterChunkListener($this, $chunkX, $chunkZ);
208 }
209
214 public function calculateCacheSize() : int{
215 $result = 0;
216 foreach($this->caches as $cache){
217 if(is_string($cache)){
218 $result += strlen($cache);
219 }
220 }
221 return $result;
222 }
223
227 public function getHitPercentage() : float{
228 $total = $this->hits + $this->misses;
229 return $total > 0 ? $this->hits / $total : 0.0;
230 }
231}
onBlockChanged as onChunkUnloaded as onChunkChanged(int $chunkX, int $chunkZ, Chunk $chunk)
request(int $chunkX, int $chunkZ)
onChunkUnloaded(int $chunkX, int $chunkZ, Chunk $chunk)
static getInstance(World $world, Compressor $compressor)
addOnUnloadCallback(\Closure $callback)
Definition World.php:671