PocketMine-MP 5.23.3 git-f7687af337d001ddbcc47b8e773f014a33faa662
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 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
85 private array $caches = [];
86
87 private int $hits = 0;
88 private int $misses = 0;
89
90 private function __construct(
91 private World $world,
92 private Compressor $compressor
93 ){}
94
100 public function request(int $chunkX, int $chunkZ) : 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 $chunkHash = World::chunkHash($chunkX, $chunkZ);
107
108 if(isset($this->caches[$chunkHash])){
109 ++$this->hits;
110 return $this->caches[$chunkHash];
111 }
112
113 ++$this->misses;
114
115 $this->world->timings->syncChunkSendPrepare->startTiming();
116 try{
117 $this->caches[$chunkHash] = new CompressBatchPromise();
118
119 $this->world->getServer()->getAsyncPool()->submitTask(
121 $chunkX,
122 $chunkZ,
123 DimensionIds::OVERWORLD, //TODO: not hardcode this
124 $chunk,
125 $this->caches[$chunkHash],
126 $this->compressor
127 )
128 );
129
130 return $this->caches[$chunkHash];
131 }finally{
132 $this->world->timings->syncChunkSendPrepare->stopTiming();
133 }
134 }
135
136 private function destroy(int $chunkX, int $chunkZ) : bool{
137 $chunkHash = World::chunkHash($chunkX, $chunkZ);
138 $existing = $this->caches[$chunkHash] ?? null;
139 unset($this->caches[$chunkHash]);
140
141 return $existing !== null;
142 }
143
147 private function destroyOrRestart(int $chunkX, int $chunkZ) : void{
148 $chunkPosHash = World::chunkHash($chunkX, $chunkZ);
149 $cache = $this->caches[$chunkPosHash] ?? null;
150 if($cache !== null){
151 if(!$cache->hasResult()){
152 //some requesters are waiting for this chunk, so their request needs to be fulfilled
153 $cache->cancel();
154 unset($this->caches[$chunkPosHash]);
155
156 $this->request($chunkX, $chunkZ)->onResolve(...$cache->getResolveCallbacks());
157 }else{
158 //dump the cache, it'll be regenerated the next time it's requested
159 $this->destroy($chunkX, $chunkZ);
160 }
161 }
162 }
163
164 use ChunkListenerNoOpTrait {
165 //force overriding of these
166 onChunkChanged as private;
167 onBlockChanged as private;
168 onChunkUnloaded as private;
169 }
170
174 public function onChunkChanged(int $chunkX, int $chunkZ, Chunk $chunk) : void{
175 $this->destroyOrRestart($chunkX, $chunkZ);
176 }
177
181 public function onBlockChanged(Vector3 $block) : void{
182 //FIXME: requesters will still receive this chunk after it's been dropped, but we can't mark this for a simple
183 //sync here because it can spam the worker pool
184 $this->destroy($block->getFloorX() >> Chunk::COORD_BIT_SIZE, $block->getFloorZ() >> Chunk::COORD_BIT_SIZE);
185 }
186
190 public function onChunkUnloaded(int $chunkX, int $chunkZ, Chunk $chunk) : void{
191 $this->destroy($chunkX, $chunkZ);
192 $this->world->unregisterChunkListener($this, $chunkX, $chunkZ);
193 }
194
199 public function calculateCacheSize() : int{
200 $result = 0;
201 foreach($this->caches as $cache){
202 if($cache->hasResult()){
203 $result += strlen($cache->getResult());
204 }
205 }
206 return $result;
207 }
208
212 public function getHitPercentage() : float{
213 $total = $this->hits + $this->misses;
214 return $total > 0 ? $this->hits / $total : 0.0;
215 }
216}
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:662