PocketMine-MP 5.15.1 git-be6754494fdbbb9dd57c058ba0e33a4a78c4581f
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 spl_object_id;
36use function strlen;
37
41class ChunkCache implements ChunkListener{
43 private static array $instances = [];
44
48 public static function getInstance(World $world, Compressor $compressor) : self{
49 $worldId = spl_object_id($world);
50 $compressorId = spl_object_id($compressor);
51 if(!isset(self::$instances[$worldId])){
52 self::$instances[$worldId] = [];
53 $world->addOnUnloadCallback(static function() use ($worldId) : void{
54 foreach(self::$instances[$worldId] as $cache){
55 $cache->caches = [];
56 }
57 unset(self::$instances[$worldId]);
58 \GlobalLogger::get()->debug("Destroyed chunk packet caches for world#$worldId");
59 });
60 }
61 if(!isset(self::$instances[$worldId][$compressorId])){
62 \GlobalLogger::get()->debug("Created new chunk packet cache (world#$worldId, compressor#$compressorId)");
63 self::$instances[$worldId][$compressorId] = new self($world, $compressor);
64 }
65 return self::$instances[$worldId][$compressorId];
66 }
67
68 public static function pruneCaches() : void{
69 foreach(self::$instances as $compressorMap){
70 foreach($compressorMap as $chunkCache){
71 foreach($chunkCache->caches as $chunkHash => $promise){
72 if($promise->hasResult()){
73 //Do not clear promises that are not yet fulfilled; they will have requesters waiting on them
74 unset($chunkCache->caches[$chunkHash]);
75 }
76 }
77 }
78 }
79 }
80
82 private array $caches = [];
83
84 private int $hits = 0;
85 private int $misses = 0;
86
87 private function __construct(
88 private World $world,
89 private Compressor $compressor
90 ){}
91
97 public function request(int $chunkX, int $chunkZ) : CompressBatchPromise{
98 $this->world->registerChunkListener($this, $chunkX, $chunkZ);
99 $chunk = $this->world->getChunk($chunkX, $chunkZ);
100 if($chunk === null){
101 throw new \InvalidArgumentException("Cannot request an unloaded chunk");
102 }
103 $chunkHash = World::chunkHash($chunkX, $chunkZ);
104
105 if(isset($this->caches[$chunkHash])){
106 ++$this->hits;
107 return $this->caches[$chunkHash];
108 }
109
110 ++$this->misses;
111
112 $this->world->timings->syncChunkSendPrepare->startTiming();
113 try{
114 $this->caches[$chunkHash] = new CompressBatchPromise();
115
116 $this->world->getServer()->getAsyncPool()->submitTask(
118 $chunkX,
119 $chunkZ,
120 DimensionIds::OVERWORLD, //TODO: not hardcode this
121 $chunk,
122 $this->caches[$chunkHash],
123 $this->compressor
124 )
125 );
126
127 return $this->caches[$chunkHash];
128 }finally{
129 $this->world->timings->syncChunkSendPrepare->stopTiming();
130 }
131 }
132
133 private function destroy(int $chunkX, int $chunkZ) : bool{
134 $chunkHash = World::chunkHash($chunkX, $chunkZ);
135 $existing = $this->caches[$chunkHash] ?? null;
136 unset($this->caches[$chunkHash]);
137
138 return $existing !== null;
139 }
140
144 private function destroyOrRestart(int $chunkX, int $chunkZ) : void{
145 $chunkPosHash = World::chunkHash($chunkX, $chunkZ);
146 $cache = $this->caches[$chunkPosHash] ?? null;
147 if($cache !== null){
148 if(!$cache->hasResult()){
149 //some requesters are waiting for this chunk, so their request needs to be fulfilled
150 $cache->cancel();
151 unset($this->caches[$chunkPosHash]);
152
153 $this->request($chunkX, $chunkZ)->onResolve(...$cache->getResolveCallbacks());
154 }else{
155 //dump the cache, it'll be regenerated the next time it's requested
156 $this->destroy($chunkX, $chunkZ);
157 }
158 }
159 }
160
161 use ChunkListenerNoOpTrait {
162 //force overriding of these
163 onChunkChanged as private;
164 onBlockChanged as private;
165 onChunkUnloaded as private;
166 }
167
171 public function onChunkChanged(int $chunkX, int $chunkZ, Chunk $chunk) : void{
172 $this->destroyOrRestart($chunkX, $chunkZ);
173 }
174
178 public function onBlockChanged(Vector3 $block) : void{
179 //FIXME: requesters will still receive this chunk after it's been dropped, but we can't mark this for a simple
180 //sync here because it can spam the worker pool
181 $this->destroy($block->getFloorX() >> Chunk::COORD_BIT_SIZE, $block->getFloorZ() >> Chunk::COORD_BIT_SIZE);
182 }
183
187 public function onChunkUnloaded(int $chunkX, int $chunkZ, Chunk $chunk) : void{
188 $this->destroy($chunkX, $chunkZ);
189 $this->world->unregisterChunkListener($this, $chunkX, $chunkZ);
190 }
191
196 public function calculateCacheSize() : int{
197 $result = 0;
198 foreach($this->caches as $cache){
199 if($cache->hasResult()){
200 $result += strlen($cache->getResult());
201 }
202 }
203 return $result;
204 }
205
209 public function getHitPercentage() : float{
210 $total = $this->hits + $this->misses;
211 return $total > 0 ? $this->hits / $total : 0.0;
212 }
213}
onBlockChanged as onChunkUnloaded as onChunkChanged(int $chunkX, int $chunkZ, Chunk $chunk)
Definition: ChunkCache.php:171
request(int $chunkX, int $chunkZ)
Definition: ChunkCache.php:97
onChunkUnloaded(int $chunkX, int $chunkZ, Chunk $chunk)
Definition: ChunkCache.php:187
static getInstance(World $world, Compressor $compressor)
Definition: ChunkCache.php:48
addOnUnloadCallback(\Closure $callback)
Definition: World.php:662