Skip to content

Commit

Permalink
feat: add ChunkService#removeUnusedChunksImmediately method that ca…
Browse files Browse the repository at this point in the history
…n remove unused chunks immediately. Also, the `/gc` command will call this method in all dimensions now.
  • Loading branch information
smartcmd committed Jan 25, 2025
1 parent b8d2fae commit 38bac79
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 11 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Unless otherwise specified, any version comparison below is the comparison of se
and sources can be registered by plugin now.
- (API) Added `Form#onClose(Consumer<ModalFormCancelReason>)` method that can be used to set a callback which will be called
with the close reason when the form is closed. The old `Form#onClose` method is still available that just ignores the reason.
- (API) Added `ChunkService#removeUnusedChunksImmediately` method that can remove unused chunks immediately. Also, the `/gc` command
will call this method in all dimensions now.

### Changed

Expand Down
13 changes: 13 additions & 0 deletions api/src/main/java/org/allaymc/api/world/service/ChunkService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.allaymc.api.world.service;

import org.allaymc.api.server.ServerSettings;
import org.allaymc.api.world.chunk.Chunk;
import org.allaymc.api.world.chunk.ChunkLoader;
import org.allaymc.api.world.chunk.ChunkSource;
Expand Down Expand Up @@ -230,4 +231,16 @@ public interface ChunkService extends ChunkSource {
*/
@UnmodifiableView
Collection<CompletableFuture<Chunk>> getLoadingChunks();

/**
* Remove chunks that are unused immediately.
* <p>
* An unused chunk is a chunk that is loaded but is not in any chunk loader's
* range. Usually these chunks will still keep loaded for a period of time
* (the time is specified by {@link ServerSettings.WorldSettings#removeUnneededChunkCycle()}).
* <p>
* Calling this method will set the countdown of all unused chunks to zero, which
* will make these chunks be unloaded during the next tick.
*/
void removeUnusedChunksImmediately();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.allaymc.api.command.SimpleCommand;
import org.allaymc.api.command.tree.CommandTree;
import org.allaymc.api.i18n.TrKeys;
import org.allaymc.api.server.Server;

import static java.lang.Runtime.getRuntime;
import static org.allaymc.api.math.MathUtils.round;
Expand All @@ -21,6 +22,11 @@ public void prepareCommandTree(CommandTree tree) {
.exec(context -> {
var memory = getCurrentMemoryUsage();
System.gc();
for (var world : Server.getInstance().getWorldPool().getWorlds().values()) {
for (var dimension : world.getDimensions().values()) {
dimension.getChunkService().removeUnusedChunksImmediately();
}
}
var freedMemory = memory - getCurrentMemoryUsage();
context.getSender().sendTr(TrKeys.A_COMMAND_GC_COMPLETED, freedMemory);
return context.success();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package org.allaymc.server.world.service;

import com.google.common.collect.Sets;
import it.unimi.dsi.fastutil.longs.*;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
import it.unimi.dsi.fastutil.longs.LongComparator;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.allaymc.api.eventbus.event.world.ChunkLoadEvent;
Expand Down Expand Up @@ -41,22 +43,36 @@
* @author daoge_cmd
*/
@Slf4j
@RequiredArgsConstructor
public final class AllayChunkService implements ChunkService {

private static final int TICK_RADIUS_SQUARED = (int) Math.pow(Server.SETTINGS.worldSettings().tickRadius(), 2);

private final Map<Long, Chunk> loadedChunks = new Long2ObjectNonBlockingMap<>();
private final Map<Long, CompletableFuture<Chunk>> loadingChunks = new Long2ObjectNonBlockingMap<>();
private final Map<ChunkLoader, ChunkLoaderManager> chunkLoaderManagers = new Object2ObjectOpenHashMap<>();
// NOTICE: this setter is made for testing
@Setter
private int removeUnneededChunkCycle;

private final Map<Long, Chunk> loadedChunks;
private final Map<Long, CompletableFuture<Chunk>> loadingChunks;
private final Set<Long> keepLoadingChunks;
private final Map<Long, Integer> unusedChunkClearCountDown;
private final Map<ChunkLoader, ChunkLoaderManager> chunkLoaderManagers;

private final Dimension dimension;
@Getter
private final WorldGenerator worldGenerator;
private final WorldStorage worldStorage;
private final Map<Long, Integer> unusedChunkClearCountDown = new Long2IntOpenHashMap();
private final Set<Long> keepLoadingChunks = Sets.newConcurrentHashSet();
@Setter
private int removeUnneededChunkCycle = Server.SETTINGS.worldSettings().removeUnneededChunkCycle();

public AllayChunkService(Dimension dimension, WorldGenerator worldGenerator, WorldStorage worldStorage) {
this.dimension = dimension;
this.worldGenerator = worldGenerator;
this.worldStorage = worldStorage;
this.removeUnneededChunkCycle = Server.SETTINGS.worldSettings().removeUnneededChunkCycle();
this.loadedChunks = new Long2ObjectNonBlockingMap<>();
this.loadingChunks = new Long2ObjectNonBlockingMap<>();
this.keepLoadingChunks = Sets.newConcurrentHashSet();
this.unusedChunkClearCountDown = new Long2ObjectNonBlockingMap<>();
this.chunkLoaderManagers = new Object2ObjectOpenHashMap<>();
}

public void startTick() {
if (worldGenerator instanceof AllayWorldGenerator allayWorldGenerator) {
Expand Down Expand Up @@ -116,6 +132,11 @@ private void tickChunkLoaders() {
}
}

@Override
public void removeUnusedChunksImmediately() {
unusedChunkClearCountDown.replaceAll((chunkHash, countDown) -> 0);
}

private void removeUnusedChunks() {
unusedChunkClearCountDown.entrySet().removeIf(entry -> {
var chunk = getChunk(entry.getKey());
Expand All @@ -125,7 +146,8 @@ private void removeUnusedChunks() {
unusedChunkClearCountDown.replaceAll((chunkHash, countDown) -> countDown - 1);
// Remove countdown ended unused chunks
unusedChunkClearCountDown.entrySet().removeIf(entry -> {
var shouldRemove = entry.getValue() == 0;
// It is possible that the value be smaller than zero, however it is not a problem
var shouldRemove = entry.getValue() <= 0;
if (shouldRemove && !unloadChunk(entry.getKey()).getNow(true)) {
// Chunk cannot be unloaded, may because ChunkUnloadEvent is cancelled
// If chunk unloading is cancelled by a plugin, unloadChunk()
Expand Down

0 comments on commit 38bac79

Please sign in to comment.