Skip to content

Commit db7b22d

Browse files
committed
Improve chunk loading/saving performance
1 parent db2c2e3 commit db7b22d

19 files changed

+436
-361
lines changed

server/src/main/java/org/cloudburstmc/server/level/CloudLevel.java

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -627,9 +627,7 @@ public void doTick(int currentTick) {
627627
}
628628

629629
try (Timing ignored2 = this.timings.tickChunks.startTiming()) {
630-
try (Timing ignored3 = this.timings.tickChunks.startTiming()) {
631-
this.tickChunks();
632-
}
630+
this.tickChunks();
633631

634632
synchronized (changedBlocks) {
635633
ConcurrentMap<Long, IntSet> changedBlocks = this.changedBlocks.asMap();
@@ -1488,14 +1486,12 @@ private void addBlockChange(int x, int y, int z, int layer) {
14881486
}
14891487

14901488
private void addBlockChange(long index, int x, int y, int z, int layer) {
1491-
IntSet current;
1492-
try {
1493-
current = this.changedBlocks.get(index, IntOpenHashSet::new);
1494-
} catch (ExecutionException e) {
1495-
throw new IllegalStateException("Unable to get block changes", e);
1496-
}
14971489
synchronized (changedBlocks) {
1498-
current.add(CloudChunk.blockKeyWithLayer(x, y, z, layer, this.getMinHeight()));
1490+
try {
1491+
this.changedBlocks.get(index, IntOpenHashSet::new).add(CloudChunk.blockKeyWithLayer(x, y, z, layer, this.getMinHeight()));
1492+
} catch (ExecutionException e) {
1493+
throw new IllegalStateException("Unable to get block changes", e);
1494+
}
14991495
}
15001496
}
15011497

@@ -2106,12 +2102,12 @@ public int getHighestBlock(int x, int z) {
21062102

21072103
@Override
21082104
public CloudChunk getLoadedChunk(long chunkKey) {
2109-
return (CloudChunk) this.chunkManager.getLoadedChunk(chunkKey);
2105+
return this.chunkManager.getLoadedChunk(chunkKey);
21102106
}
21112107

21122108
@Override
21132109
public CloudChunk getLoadedChunk(int chunkX, int chunkZ) {
2114-
return (CloudChunk) this.chunkManager.getLoadedChunk(chunkX, chunkZ);
2110+
return this.chunkManager.getLoadedChunk(chunkX, chunkZ);
21152111
}
21162112

21172113
@Override

server/src/main/java/org/cloudburstmc/server/level/LevelBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public CompletableFuture<CloudLevel> load() {
7777
}
7878
}
7979

80-
final Executor executor = this.server.getLevelManager().getChunkExecutor();
80+
final Executor executor = this.server.getLevelManager().getIoExecutor();
8181

8282
// Load chunk provider
8383
CompletableFuture<LevelProvider> providerFuture = CompletableFuture.supplyAsync(() -> {

server/src/main/java/org/cloudburstmc/server/level/LevelManager.java

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,13 @@
1818
import java.util.Map;
1919
import java.util.Set;
2020
import java.util.concurrent.ExecutorService;
21-
import java.util.concurrent.ForkJoinPool;
22-
import java.util.concurrent.ForkJoinWorkerThread;
21+
import java.util.concurrent.Executors;
2322
import java.util.concurrent.TimeUnit;
2423

2524
@Log4j2
2625
@Singleton
2726
public class LevelManager implements Closeable {
28-
private final ExecutorService chunkExecutor = new ForkJoinPool(
29-
Runtime.getRuntime().availableProcessors(),
30-
pool -> {
31-
ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
32-
thread.setDaemon(true);
33-
thread.setName("chunk-worker-" + thread.getPoolIndex());
34-
return thread;
35-
},
36-
null, true
37-
);
27+
private final ExecutorService ioExecutor = Executors.newVirtualThreadPerTaskExecutor();
3828
private final CloudServer server;
3929
private final Set<CloudLevel> levels = new HashSet<>();
4030
private final Map<String, CloudLevel> levelIds = new HashMap<>();
@@ -119,14 +109,14 @@ public synchronized void close() {
119109
}
120110
}
121111

122-
this.chunkExecutor.shutdown();
112+
this.ioExecutor.shutdown();
123113
try {
124-
if (!this.chunkExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
125-
log.warn("Chunk executor did not terminate in time, forcing shutdown");
126-
this.chunkExecutor.shutdownNow();
114+
if (!this.ioExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
115+
log.warn("I/O executor did not terminate in time, forcing shutdown");
116+
this.ioExecutor.shutdownNow();
127117
}
128118
} catch (InterruptedException e) {
129-
this.chunkExecutor.shutdownNow();
119+
this.ioExecutor.shutdownNow();
130120
Thread.currentThread().interrupt();
131121
}
132122
}
@@ -168,7 +158,7 @@ public void tick(int currentTick) {
168158
}
169159
}
170160

171-
public ExecutorService getChunkExecutor() {
172-
return chunkExecutor;
161+
public ExecutorService getIoExecutor() {
162+
return ioExecutor;
173163
}
174164
}

server/src/main/java/org/cloudburstmc/server/level/chunk/BiomeStorage.java

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.cloudburstmc.server.level.chunk;
22

33
import io.netty.buffer.ByteBuf;
4+
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
45
import it.unimi.dsi.fastutil.ints.IntArrayList;
56
import it.unimi.dsi.fastutil.ints.IntList;
67
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -14,7 +15,7 @@
1415
* Per-section 3D biome storage backed by a paletted {@link BitArray}.
1516
* <p>
1617
* The index mapping is identical to {@link CloudChunkSection#blockIndex(int, int, int)}:
17-
* {@code index = (x << 8) | (z << 4) | y}, where x, y, z are each 015 within the section.
18+
* {@code index = (x << 8) | (z << 4) | y}, where x, y, z are each 0-15 within the section.
1819
* <p>
1920
* Wire and disk format mirrors the {@link BlockStorage} paletted format, but uses raw
2021
* integer biome IDs in the palette rather than NBT. The copy-last optimization is
@@ -28,7 +29,9 @@ public class BiomeStorage {
2829
static final int SIZE = 4096;
2930

3031
private final IntList palette;
32+
private final Int2IntOpenHashMap paletteIndex;
3133
private BitArray bitArray;
34+
private boolean singleValue;
3235

3336
/**
3437
* Create a new storage defaulting every position to biome ID {@code defaultBiomeId}.
@@ -38,12 +41,22 @@ public class BiomeStorage {
3841
public BiomeStorage(int defaultBiomeId) {
3942
this.bitArray = null;
4043
this.palette = new IntArrayList(4);
44+
this.paletteIndex = new Int2IntOpenHashMap(4);
45+
this.paletteIndex.defaultReturnValue(-1);
4146
this.palette.add(defaultBiomeId);
47+
this.paletteIndex.put(defaultBiomeId, 0);
48+
this.singleValue = true;
4249
}
4350

4451
private BiomeStorage(BitArray bitArray, IntList palette) {
4552
this.bitArray = bitArray;
4653
this.palette = palette;
54+
this.paletteIndex = new Int2IntOpenHashMap(palette.size());
55+
this.paletteIndex.defaultReturnValue(-1);
56+
for (int i = 0; i < palette.size(); i++) {
57+
this.paletteIndex.put(palette.getInt(i), i);
58+
}
59+
this.singleValue = (palette.size() == 1);
4760
}
4861

4962
/**
@@ -65,12 +78,13 @@ public int getBiome(int index) {
6578
* @param biomeId raw biome integer ID
6679
*/
6780
public void setBiome(int index, int biomeId) {
68-
int paletteIdx = this.palette.indexOf(biomeId);
81+
int paletteIdx = this.paletteIndex.get(biomeId);
6982
if (paletteIdx == -1) {
7083
paletteIdx = this.palette.size();
7184
if (this.bitArray == null) {
7285
// Promote from 0-bit singleton to V1 now that we have a second value
7386
this.bitArray = BitArrayVersion.V1.createPalette(SIZE);
87+
this.singleValue = false;
7488
} else {
7589
BitArrayVersion version = this.bitArray.getVersion();
7690
if (paletteIdx > version.getMaxEntryValue()) {
@@ -80,6 +94,7 @@ public void setBiome(int index, int biomeId) {
8094
}
8195
}
8296
}
97+
this.paletteIndex.put(biomeId, paletteIdx);
8398
this.palette.add(biomeId);
8499
}
85100

@@ -277,20 +292,6 @@ public BiomeStorage copy() {
277292
* Returns {@code true} if all positions share the same single palette entry.
278293
*/
279294
public boolean isSingleValue() {
280-
if (this.bitArray == null) {
281-
return true;
282-
}
283-
284-
if (this.palette.size() == 1) {
285-
return true;
286-
}
287-
288-
for (int word : this.bitArray.getWords()) {
289-
if (Integer.toUnsignedLong(word) != 0L) {
290-
return false;
291-
}
292-
}
293-
294-
return true;
295+
return this.singleValue;
295296
}
296297
}

server/src/main/java/org/cloudburstmc/server/level/chunk/BlockStorage.java

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ public class BlockStorage {
3030
private final List<BlockState> palette;
3131
private final Reference2IntOpenHashMap<BlockState> paletteIndex;
3232
private BitArray bitArray;
33+
private int nonAirCount;
34+
private boolean needsCompact;
35+
private boolean dirty;
3336

3437
public BlockStorage() {
3538
this(BitArrayVersion.V1);
@@ -42,6 +45,8 @@ public BlockStorage(BitArrayVersion version) {
4245
this.paletteIndex.defaultReturnValue(-1);
4346
this.palette.add(AIR);
4447
this.paletteIndex.put(AIR, 0);
48+
this.nonAirCount = 0;
49+
this.dirty = true;
4550
}
4651

4752
private BlockStorage(BitArray bitArray, List<BlockState> palette) {
@@ -52,6 +57,8 @@ private BlockStorage(BitArray bitArray, List<BlockState> palette) {
5257
for (int i = 0; i < palette.size(); i++) {
5358
this.paletteIndex.put(palette.get(i), i);
5459
}
60+
this.nonAirCount = countNonAir();
61+
this.dirty = true;
5562
}
5663

5764
private static BitArrayVersion getVersionFromHeader(byte header) {
@@ -68,8 +75,15 @@ public BlockState getBlock(int index) {
6875

6976
public void setBlock(int index, BlockState blockState) {
7077
try {
78+
BlockState old = this.blockFor(this.bitArray.get(index));
7179
int idx = this.idFor(blockState);
7280
this.bitArray.set(index, idx);
81+
if (old == AIR && blockState != AIR) {
82+
this.nonAirCount++;
83+
} else if (old != AIR && blockState == AIR) {
84+
this.nonAirCount--;
85+
}
86+
this.dirty = true;
7387
} catch (IllegalArgumentException e) {
7488
throw new IllegalArgumentException("Unable to set block: " + blockState + ", palette: " + palette, e);
7589
}
@@ -144,6 +158,8 @@ public void readFromStorage(ByteBuf buffer) {
144158
} catch (IOException e) {
145159
throw new RuntimeException(e);
146160
}
161+
this.nonAirCount = (this.palette.size() == 1 && this.palette.get(0) == AIR) ? 0 : SIZE;
162+
this.dirty = false;
147163
return;
148164
}
149165

@@ -184,6 +200,9 @@ public void readFromStorage(ByteBuf buffer) {
184200
} catch (IOException e) {
185201
throw new RuntimeException(e);
186202
}
203+
204+
this.nonAirCount = countNonAir();
205+
this.dirty = false;
187206
}
188207

189208
private void onResize(BitArrayVersion version) {
@@ -211,6 +230,7 @@ private int idFor(BlockState blockState) {
211230
}
212231
this.palette.add(blockState);
213232
this.paletteIndex.put(blockState, index);
233+
this.needsCompact = true;
214234
return index;
215235
}
216236

@@ -219,15 +239,7 @@ private BlockState blockFor(int index) {
219239
}
220240

221241
public boolean isEmpty() {
222-
if (this.palette.size() == 1) {
223-
return true;
224-
}
225-
for (int word : this.bitArray.getWords()) {
226-
if (Integer.toUnsignedLong(word) != 0L) {
227-
return false;
228-
}
229-
}
230-
return true;
242+
return this.nonAirCount == 0;
231243
}
232244

233245
/**
@@ -239,26 +251,21 @@ public void compact() {
239251
List<BlockState> newPalette = new ReferenceArrayList<>(this.palette.size());
240252
newPalette.add(AIR);
241253

242-
// Maps old palette index to new palette index.
243254
int[] indexMap = new int[this.palette.size()];
244255

245-
// Scan every block slot and collect live palette entries.
246256
for (int i = 0; i < SIZE; i++) {
247257
int oldIdx = this.bitArray.get(i);
248258
if (indexMap[oldIdx] == 0 && oldIdx != 0) {
249-
// Not yet mapped; assign next slot in the new palette.
250259
BlockState state = this.palette.get(oldIdx);
251260
int newIdx = newPalette.size();
252261
newPalette.add(state);
253262
indexMap[oldIdx] = newIdx;
254263
}
255264
}
256265

257-
// Pick the smallest BitArrayVersion that accommodates newPalette.size() entries.
258266
int liveCount = newPalette.size();
259267
BitArrayVersion minVersion = BitArrayVersion.getMinimalVersion(liveCount);
260268

261-
// Re-index all blocks into the new bit-array.
262269
BitArray newBitArray = minVersion.createPalette(SIZE);
263270
for (int i = 0; i < SIZE; i++) {
264271
newBitArray.set(i, indexMap[this.bitArray.get(i)]);
@@ -271,9 +278,53 @@ public void compact() {
271278
for (int i = 0; i < this.palette.size(); i++) {
272279
this.paletteIndex.put(this.palette.get(i), i);
273280
}
281+
282+
this.nonAirCount = countNonAir();
283+
this.needsCompact = false;
284+
}
285+
286+
/**
287+
* Returns true if the palette has grown since the last compact().
288+
*/
289+
public boolean isCompactNeeded() {
290+
return this.needsCompact;
291+
}
292+
293+
/**
294+
* Returns true if this storage has been written to since it was last loaded from or saved to disk.
295+
*/
296+
public boolean isDirty() {
297+
return this.dirty;
298+
}
299+
300+
/**
301+
* Clears the dirty flag after the storage has been successfully persisted.
302+
*/
303+
public void clearDirty() {
304+
this.dirty = false;
274305
}
275306

276307
public BlockStorage copy() {
277308
return new BlockStorage(this.bitArray.copy(), new ReferenceArrayList<>(this.palette));
278309
}
310+
311+
private int countNonAir() {
312+
if (this.palette.size() == 1) {
313+
return this.palette.getFirst() == AIR ? 0 : SIZE;
314+
}
315+
316+
int airIndex = this.paletteIndex.getInt(AIR);
317+
if (airIndex == -1) {
318+
return SIZE;
319+
}
320+
321+
int count = 0;
322+
for (int i = 0; i < SIZE; i++) {
323+
if (this.bitArray.get(i) != airIndex) {
324+
count++;
325+
}
326+
}
327+
328+
return count;
329+
}
279330
}

0 commit comments

Comments
 (0)