diff --git a/src/main/java/net/minecraft/server/management/PlayerManager.java b/src/main/java/net/minecraft/server/management/PlayerManager.java index 69cce6b..5a68b1d 100644 --- a/src/main/java/net/minecraft/server/management/PlayerManager.java +++ b/src/main/java/net/minecraft/server/management/PlayerManager.java @@ -270,7 +270,7 @@ PlayerManager.this.chunkWatcherWithPlayers.remove(this); } - PlayerManager.this.getWorldServer().theChunkProviderServer.unloadChunksIfNotNearSpawn(this.chunkLocation.chunkXPos, this.chunkLocation.chunkZPos); + PlayerManager.this.getWorldServer().theChunkProviderServer.unbindChunk(this.chunkLocation.chunkXPos, this.chunkLocation.chunkZPos); } } } diff --git a/src/main/java/net/minecraft/world/chunk/Chunk.java b/src/main/java/net/minecraft/world/chunk/Chunk.java index 615d022..23c5c85 100644 --- a/src/main/java/net/minecraft/world/chunk/Chunk.java +++ b/src/main/java/net/minecraft/world/chunk/Chunk.java @@ -33,6 +33,7 @@ import net.minecraft.world.ChunkPosition; import net.minecraft.world.EnumSkyBlock; import net.minecraft.world.World; +import net.minecraft.world.WorldServer; import net.minecraft.world.biome.BiomeGenBase; import net.minecraft.world.biome.WorldChunkManager; import net.minecraft.world.chunk.storage.ExtendedBlockStorage; @@ -42,6 +43,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.ultramine.server.chunk.ChunkBindState; import org.ultramine.server.chunk.ChunkHash; import org.ultramine.server.chunk.PendingBlockUpdate; @@ -925,6 +927,8 @@ this.worldObj.addLoadedEntities(this.entityLists[i]); } MinecraftForge.EVENT_BUS.post(new ChunkEvent.Load(this)); + + loadTime = unbindTime = ((WorldServer)worldObj).func_73046_m().getTickCounter(); } public void onChunkUnload() @@ -1537,6 +1541,10 @@ private Set pendingUpdatesSet; private TreeSet pendingUpdatesQueue; + private ChunkBindState bindState = ChunkBindState.NONE; + private int loadTime; + private int unbindTime; + public PendingBlockUpdate pollPending(long time) { if(pendingUpdatesQueue == null || pendingUpdatesQueue.size() == 0) return null; @@ -1578,4 +1586,31 @@ { return pendingUpdatesQueue; } + + public ChunkBindState getBindState() + { + return bindState; + } + + public void setBindState(ChunkBindState bindState) + { + this.bindState = bindState; + } + + public void unbind() + { + if(bindState.canChangeState()) + bindState = ChunkBindState.NONE; + unbindTime = ((WorldServer)worldObj).func_73046_m().getTickCounter(); + } + + public int getLoadTime() + { + return loadTime; + } + + public int getUnbindTime() + { + return unbindTime; + } } diff --git a/src/main/java/net/minecraft/world/gen/ChunkProviderServer.java b/src/main/java/net/minecraft/world/gen/ChunkProviderServer.java index f2a149c..21c5e91 100644 --- a/src/main/java/net/minecraft/world/gen/ChunkProviderServer.java +++ b/src/main/java/net/minecraft/world/gen/ChunkProviderServer.java @@ -13,7 +13,10 @@ import java.util.List; import java.util.Set; +import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.registry.GameRegistry; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReportCategory; import net.minecraft.entity.EnumCreatureType; @@ -37,6 +40,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.ultramine.server.chunk.ChunkBindState; +import org.ultramine.server.chunk.ChunkGC; import org.ultramine.server.chunk.ChunkHash; import org.ultramine.server.chunk.ChunkMap; import org.ultramine.server.chunk.IChunkLoadCallback; @@ -59,6 +64,9 @@ this.worldObj = par1WorldServer; this.currentChunkLoader = par2IChunkLoader; this.currentChunkProvider = par3IChunkProvider; + + if(isServer) + chunkGC = new ChunkGC(this); } public boolean chunkExists(int par1, int par2) @@ -124,6 +132,8 @@ else { chunk = ChunkIOExecutor.syncChunkLoad(this.worldObj, loader, this, par1, par2); + chunk.setBindState(ChunkBindState.LEAK); + logger.warn("The chunk("+par1+", "+par2+") was loaded sync", new Throwable()); } } else if (chunk == null) @@ -314,8 +324,11 @@ public boolean unloadQueuedChunks() { - if (!this.worldObj.levelSaving) +// if (!this.worldObj.levelSaving) { + if(isServer) + chunkGC.onTick(); + for (ChunkCoordIntPair forced : this.worldObj.getPersistentChunks().keySet()) { this.chunksToUnload.remove(ChunkHash.chunkToKey(forced.chunkXPos, forced.chunkZPos)); @@ -351,7 +364,7 @@ Chunk chunk = loadedChunkHashMap.get(hash); if(chunk != null) { - if(true/*chunk.getBindReason().canUnload()*/) + if(chunk.getBindState().canUnload()) { chunk.onChunkUnload(); if(true/*chunk.shouldSaveOnUnload()*/) @@ -411,6 +424,11 @@ /* ======================================== ULTRAMINE START =====================================*/ + private static final boolean isServer = FMLCommonHandler.instance().getSide().isServer(); + + @SideOnly(Side.SERVER) + private ChunkGC chunkGC; + public void loadAsync(int x, int z, IChunkLoadCallback callback) { Chunk chunk = loadedChunkHashMap.get(x, z); @@ -429,4 +447,18 @@ { return loadedChunkHashMap.get(cx, cz); } + + public void unbindChunk(int cx, int cz) + { + Chunk chunk = loadedChunkHashMap.get(cx, cz); + if(chunk != null) + unbindChunk(chunk); + } + + public void unbindChunk(Chunk chunk) + { + chunk.unbind(); + if(!isServer) + unloadChunksIfNotNearSpawn(chunk.xPosition, chunk.zPosition); + } } \ No newline at end of file diff --git a/src/main/java/org/ultramine/server/WorldsConfig.java b/src/main/java/org/ultramine/server/WorldsConfig.java index 6d89dc9..6345771 100644 --- a/src/main/java/org/ultramine/server/WorldsConfig.java +++ b/src/main/java/org/ultramine/server/WorldsConfig.java @@ -55,6 +55,7 @@ { public int viewDistance = 10; public int chunkUpdateRadius = 7; + public int chunkCacheSize; public boolean enableChunkLoaders = true; } } diff --git a/src/main/java/org/ultramine/server/chunk/ChunkBindState.java b/src/main/java/org/ultramine/server/chunk/ChunkBindState.java new file mode 100644 index 0000000..70388a4 --- /dev/null +++ b/src/main/java/org/ultramine/server/chunk/ChunkBindState.java @@ -0,0 +1,43 @@ +package org.ultramine.server.chunk; + +public enum ChunkBindState +{ + /** + * Чанк ничем не занят и может быть выгружен в любой момент. Стандартное + * значение при асинхронной загрузке чанка. + */ + NONE, + /** + * Чанк занят игроком/игроками. Как только все игроки выйдут из радиуса + * прогрузки, значение сменится на NONE. + */ + PLAYER, + /** + * Чанк был загружен синхронно, в обход менеджера загрузки чанков. По логике + * ванильного майна это является утечкой памяти - чанк не будет выгружен из + * памяти до тех пор, пока не будет помечен к отгрузке вручную (например, в + * него зайдет и выйдет игрок). Но у нас чанк будет выгружен через некоторое + * время. + */ + LEAK, + /** + * Чанку запрещено выгружаться или изменять состояние бинда. Чанк не будет + * выгружен никогда. + */ + ETERNAL; + + public boolean canUnload() + { + return this == NONE; + } + + public boolean canChangeState() + { + return this != ETERNAL; + } + + public boolean isLeak() + { + return this == LEAK; + } +} diff --git a/src/main/java/org/ultramine/server/chunk/ChunkGC.java b/src/main/java/org/ultramine/server/chunk/ChunkGC.java new file mode 100644 index 0000000..4371e25 --- /dev/null +++ b/src/main/java/org/ultramine/server/chunk/ChunkGC.java @@ -0,0 +1,111 @@ +package org.ultramine.server.chunk; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.world.WorldServer; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.gen.ChunkProviderServer; + +@SideOnly(Side.SERVER) +public class ChunkGC +{ + private static final int MIN_GC_INTERVAL = 100; + private static final int MAX_UNLOAD_QUEUE_SIZE = 128; + private static final int MAX_CHUNKS_PER_OP = 1024; + private static final int _10_MINUTES = 20*60*10; + + private final ChunkProviderServer provider; + private final WorldServer world; + + private int lastGCTime; + private int lastChunkCount; + private int minChunkDiff; + + public ChunkGC(ChunkProviderServer provider) + { + this.provider = provider; + this.world = provider.worldObj; + } + + public void onTick() + { + int confCacheSize = world.getConfig().chunkLoading.chunkCacheSize; + int chunksPerPlayer = (int)Math.pow(world.getConfig().chunkLoading.viewDistance*2 + 1, 2); + int boundChunks = world.playerEntities.size()*chunksPerPlayer + world.getPersistentChunks().size(); + int chunkLimit = boundChunks + confCacheSize + MAX_CHUNKS_PER_OP; + + int curTime = world.func_73046_m().getTickCounter(); + int unloadQueueSize = provider.chunksToUnload.size(); + int chunkCount = provider.loadedChunkHashMap.size() - unloadQueueSize; + int timePassed = curTime - lastGCTime; + int chunkDiff = chunkCount - lastChunkCount; + + if(chunkCount > chunkLimit && timePassed > MIN_GC_INTERVAL && unloadQueueSize < MAX_UNLOAD_QUEUE_SIZE && (minChunkDiff == 0 || chunkDiff > minChunkDiff)) + { + Collection all = provider.loadedChunkHashMap.valueCollection(); + List unbound = new ArrayList(all.size() - boundChunks); + for(Chunk chunk : all) + { + if(chunk.getBindState().canUnload()) + { + unbound.add(chunk); + } + else if(chunk.getBindState().isLeak() && curTime - chunk.getUnbindTime() > _10_MINUTES) + { + chunk.unbind(); + unbound.add(chunk); + } + } + + int unboundLimit = confCacheSize + MAX_CHUNKS_PER_OP + unloadQueueSize; + + if(unbound.size() > unboundLimit) + { + //performing GC + Collections.sort(unbound, new ChunkComparator(curTime)); + + for(int i = 0, s = Math.min(unbound.size(), MAX_CHUNKS_PER_OP); i < s; i++) + { + Chunk chunk = unbound.get(i); + provider.chunksToUnload.add(ChunkHash.chunkToKey(chunk.xPosition, chunk.zPosition)); + } + + if(unbound.size() - Math.min(unbound.size(), MAX_CHUNKS_PER_OP) > unboundLimit) + minChunkDiff = 0; + else + minChunkDiff = unboundLimit - unbound.size(); + } + else + { + minChunkDiff = unboundLimit - unbound.size(); + } + + lastGCTime = curTime; + lastChunkCount = provider.loadedChunkHashMap.size() - provider.chunksToUnload.size(); + } + } + + private static class ChunkComparator implements Comparator + { + private int curTime; + + public ChunkComparator(int curTime) + { + this.curTime = curTime; + } + + @Override + public int compare(Chunk c1, Chunk c2) + { + float c = (float)Math.max(curTime - c1.getLoadTime(), _10_MINUTES)/(float)Math.max(curTime - c1.getUnbindTime(), 1) + - (float)Math.max(curTime - c2.getLoadTime(), _10_MINUTES)/(float)Math.max(curTime - c2.getUnbindTime(), 1); + return c == 0 ? 0 : c < 0 ? -1 : 1; + } + }; +} diff --git a/src/main/java/org/ultramine/server/chunk/ChunkSendManager.java b/src/main/java/org/ultramine/server/chunk/ChunkSendManager.java index e64b310..da0c716 100644 --- a/src/main/java/org/ultramine/server/chunk/ChunkSendManager.java +++ b/src/main/java/org/ultramine/server/chunk/ChunkSendManager.java @@ -248,7 +248,7 @@ PlayerManager.PlayerInstance pi = manager.getOrCreateChunkWatcher(chunk.xPosition, chunk.zPosition, false); if (pi == null) - ((WorldServer)chunk.worldObj).theChunkProviderServer.unloadChunksIfNotNearSpawn(chunk.xPosition, chunk.zPosition); + ((WorldServer)chunk.worldObj).theChunkProviderServer.unbindChunk(chunk); } } @@ -258,7 +258,7 @@ PlayerManager.PlayerInstance pi = manager.getOrCreateChunkWatcher(chunk.xPosition, chunk.zPosition, false); if (pi == null) - ((WorldServer)chunk.worldObj).theChunkProviderServer.unloadChunksIfNotNearSpawn(chunk.xPosition, chunk.zPosition); + ((WorldServer)chunk.worldObj).theChunkProviderServer.unbindChunk(chunk); } } @@ -360,6 +360,7 @@ @Override public void onChunkLoaded(Chunk chunk) { + chunk.setBindState(ChunkBindState.PLAYER); executor.execute(new CompressAndSendChunkTask(chunk)); } }; diff --git a/src/main/resources/org/ultramine/defaults/defaultworlds.yml b/src/main/resources/org/ultramine/defaults/defaultworlds.yml index 9d40e1f..39ba8ea 100644 --- a/src/main/resources/org/ultramine/defaults/defaultworlds.yml +++ b/src/main/resources/org/ultramine/defaults/defaultworlds.yml @@ -16,10 +16,12 @@ pvp: true time: NORMAL weather: NORMAL - chunkLoading: + chunkLoading: &global_cl viewDistance: 10 chunkUpdateRadius: 7 + chunkCacheSize: 1024 enableChunkLoaders: true + worlds: world: <<: *global @@ -27,6 +29,9 @@ generation: <<: *global_gen providerID: 0 + chunkLoading: + <<: *global_cl + chunkCacheSize: 4096 world_nether: <<: *global dimension: -1