From 4c0399ca4001824e330e694a09e53a4fff894928 Mon Sep 17 00:00:00 2001 From: spdis Date: Wed, 7 Jan 2026 12:23:29 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B5=84=E6=BA=90=E9=87=8A=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../voxy/common/config/VoxyServerConfig.java | 14 +++++ .../common/world/ActiveSectionTracker.java | 46 +++++++++++++++- .../cortex/voxy/common/world/WorldEngine.java | 9 ++++ .../voxy/common/world/WorldSection.java | 26 ++++++++- .../world/service/VoxelIngestService.java | 53 ++++++++++--------- .../cortex/voxy/server/VisibleBandCache.java | 37 +++++++++++++ .../voxy/server/VoxyServerInstance.java | 27 ++++++++++ 7 files changed, 185 insertions(+), 27 deletions(-) diff --git a/src/main/java/me/cortex/voxy/common/config/VoxyServerConfig.java b/src/main/java/me/cortex/voxy/common/config/VoxyServerConfig.java index 52358560..c862f3ef 100644 --- a/src/main/java/me/cortex/voxy/common/config/VoxyServerConfig.java +++ b/src/main/java/me/cortex/voxy/common/config/VoxyServerConfig.java @@ -30,6 +30,20 @@ public class VoxyServerConfig { public int dirtyUpdateDelay = 20; @SerializedName(value = "ingest_enabled", alternate = {"ingestEnabled"}) public boolean ingestEnabled = true; + @SerializedName(value = "reuse_cache_max", alternate = {"reuseCacheMax"}) + public int reuseCacheMax = 64; + @SerializedName(value = "visible_band_cache_max", alternate = {"visibleBandCacheMax"}) + public int visibleBandCacheMax = 10000; + @SerializedName(value = "visible_band_cache_ttl_seconds", alternate = {"visibleBandCacheTtlSeconds"}) + public int visibleBandCacheTtlSeconds = 300; + @SerializedName(value = "lru_cache_max", alternate = {"lruCacheMax"}) + public int lruCacheMax = 2048; + @SerializedName(value = "lru_cache_ttl_seconds", alternate = {"lruCacheTtlSeconds"}) + public int lruCacheTtlSeconds = 180; + @SerializedName(value = "ingest_queue_max", alternate = {"ingestQueueMax"}) + public int ingestQueueMax = 8192; + @SerializedName(value = "debug_memory_stats", alternate = {"debugMemoryStats"}) + public boolean debugMemoryStats = false; private static VoxyServerConfig loadOrCreate() { Path path = getConfigPath(); diff --git a/src/main/java/me/cortex/voxy/common/world/ActiveSectionTracker.java b/src/main/java/me/cortex/voxy/common/world/ActiveSectionTracker.java index 64ccb0e9..12904a6c 100644 --- a/src/main/java/me/cortex/voxy/common/world/ActiveSectionTracker.java +++ b/src/main/java/me/cortex/voxy/common/world/ActiveSectionTracker.java @@ -41,7 +41,10 @@ public class ActiveSectionTracker { private final int lruSize; private final StampedLock lruLock = new StampedLock(); - private final Long2ObjectLinkedOpenHashMap lruSecondaryCache;//TODO: THIS NEEDS TO BECOME A GLOBAL STATIC CACHE + private final Long2ObjectLinkedOpenHashMap lruSecondaryCache; + private final it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap lruTimestamps = new it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap(); + private volatile int lruMaxOverride = -1; + private volatile long lruTtlMillis = -1; @Nullable public final WorldEngine engine; @@ -254,12 +257,14 @@ public class ActiveSectionTracker { long stamp2 = this.lruLock.writeLock(); lock.unlockWrite(stamp); WorldSection a = this.lruSecondaryCache.put(section.key, section); + this.lruTimestamps.put(section.key, System.currentTimeMillis()); if (a != null) { throw new IllegalStateException("duplicate sections in cache is impossible"); } //If cache is bigger than its ment to be, remove the least recently used and free it if (this.lruSize < this.lruSecondaryCache.size()) { aa = this.lruSecondaryCache.removeFirst(); + this.lruTimestamps.remove(aa.key); } this.lruLock.unlockWrite(stamp2); @@ -295,6 +300,45 @@ public class ActiveSectionTracker { return this.lruSecondaryCache.size(); } + public void setLruPolicy(int maxOverride, int ttlSeconds) { + this.lruMaxOverride = maxOverride; + this.lruTtlMillis = ttlSeconds > 0 ? ttlSeconds * 1000L : -1; + } + + public void trimLruByPolicy() { + long stamp = this.lruLock.writeLock(); + try { + if (this.lruMaxOverride > 0) { + while (this.lruSecondaryCache.size() > this.lruMaxOverride) { + WorldSection s = this.lruSecondaryCache.removeFirst(); + if (s != null) { + this.lruTimestamps.remove(s.key); + s._releaseArray(); + } else { + break; + } + } + } + if (this.lruTtlMillis > 0) { + long now = System.currentTimeMillis(); + var it = this.lruSecondaryCache.long2ObjectEntrySet().fastIterator(); + while (it.hasNext()) { + var e = it.next(); + long k = e.getLongKey(); + long t = this.lruTimestamps.getOrDefault(k, 0L); + if (t != 0L && now - t > this.lruTtlMillis) { + WorldSection s = e.getValue(); + it.remove(); + this.lruTimestamps.remove(k); + if (s != null) s._releaseArray(); + } + } + } + } finally { + this.lruLock.unlockWrite(stamp); + } + } + public static void main(String[] args) throws InterruptedException { var tracker = new ActiveSectionTracker(6, a->0, 2<<10); var bean = tracker.acquire(0, 0, 0, 9, false); diff --git a/src/main/java/me/cortex/voxy/common/world/WorldEngine.java b/src/main/java/me/cortex/voxy/common/world/WorldEngine.java index 328b8d97..0f83dc36 100644 --- a/src/main/java/me/cortex/voxy/common/world/WorldEngine.java +++ b/src/main/java/me/cortex/voxy/common/world/WorldEngine.java @@ -62,6 +62,7 @@ public class WorldEngine { this.mapper = new Mapper(this.storage); //5 cache size bits means that the section tracker has 32 separate maps that it uses this.sectionTracker = new ActiveSectionTracker(6, storage::loadSection, cacheSize, this); + this.sectionTracker.setLruPolicy(-1, -1); } public WorldSection acquireIfExists(int lvl, int x, int y, int z) { @@ -194,4 +195,12 @@ public class WorldEngine { this.saveCallback.save(this, section); } } + + public void setLruPolicy(int maxOverride, int ttlSeconds) { + this.sectionTracker.setLruPolicy(maxOverride, ttlSeconds); + } + + public void trimCaches() { + this.sectionTracker.trimLruByPolicy(); + } } diff --git a/src/main/java/me/cortex/voxy/common/world/WorldSection.java b/src/main/java/me/cortex/voxy/common/world/WorldSection.java index 6ab6aff5..5d9ea11c 100644 --- a/src/main/java/me/cortex/voxy/common/world/WorldSection.java +++ b/src/main/java/me/cortex/voxy/common/world/WorldSection.java @@ -36,7 +36,7 @@ public final class WorldSection { //TODO: should make it dynamically adjust the size allowance based on memory pressure/WorldSection allocation rate (e.g. is it doing a world import) - private static final int ARRAY_REUSE_CACHE_SIZE = 400;//500;//32*32*32*8*ARRAY_REUSE_CACHE_SIZE == number of bytes + private static volatile int ARRAY_REUSE_CACHE_SIZE = 400; //TODO: maybe just swap this to a ConcurrentLinkedDeque private static final AtomicInteger ARRAY_REUSE_CACHE_COUNT = new AtomicInteger(0); private static final ConcurrentLinkedDeque ARRAY_REUSE_CACHE = new ConcurrentLinkedDeque<>(); @@ -293,4 +293,26 @@ public final class WorldSection { public boolean isFreed() { return (((int)ATOMIC_STATE_HANDLE.get(this))&1)==0; } -} \ No newline at end of file + + public static void setArrayReuseCacheTarget(int size) { + if (size < 0) size = 0; + ARRAY_REUSE_CACHE_SIZE = size; + } + + public static int getArrayReuseCacheTarget() { + return ARRAY_REUSE_CACHE_SIZE; + } + + public static int getArrayReuseCacheCount() { + return ARRAY_REUSE_CACHE_COUNT.get(); + } + + public static void trimReuseCacheToTarget() { + int target = ARRAY_REUSE_CACHE_SIZE; + while (ARRAY_REUSE_CACHE_COUNT.get() > target) { + long[] a = ARRAY_REUSE_CACHE.pollFirst(); + if (a == null) break; + ARRAY_REUSE_CACHE_COUNT.decrementAndGet(); + } + } +} diff --git a/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java b/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java index d0205f2f..7118a022 100644 --- a/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java +++ b/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java @@ -25,7 +25,7 @@ public class VoxelIngestService { private final Service service; private record IngestSection(int cx, int cy, int cz, WorldEngine world, LevelChunkSection section, DataLayer blockLight, DataLayer skyLight){} private final ConcurrentLinkedDeque ingestQueue = new ConcurrentLinkedDeque<>(); - private static final int MAX_QUEUE_SIZE = 16384; + private static volatile int MAX_QUEUE_SIZE = 16384; private int dropCount = 0; public VoxelIngestService(ServiceManager pool) { @@ -33,30 +33,30 @@ public class VoxelIngestService { } private void processJob() { - var task = this.ingestQueue.poll(); - if (task == null) return; - - try { - task.world.markActive(); - - var section = task.section; - var vs = SECTION_CACHE.get().setPosition(task.cx, task.cy, task.cz); - - if (section.hasOnlyAir() && task.blockLight==null && task.skyLight==null) {//If the chunk section has lighting data, propagate it - WorldUpdater.insertUpdate(task.world, vs.zero()); - } else { - VoxelizedSection csec = WorldConversionFactory.convert( - SECTION_CACHE.get(), - task.world.getMapper(), - section.getStates(), - section.getBiomes(), - getLightingSupplier(task) - ); - WorldConversionFactory.mipSection(csec, task.world.getMapper()); - WorldUpdater.insertUpdate(task.world, csec); + int batch = Math.min(64, Math.max(1, this.ingestQueue.size())); + for (int i = 0; i < batch; i++) { + var task = this.ingestQueue.poll(); + if (task == null) break; + try { + task.world.markActive(); + var section = task.section; + var vs = SECTION_CACHE.get().setPosition(task.cx, task.cy, task.cz); + if (section.hasOnlyAir() && task.blockLight==null && task.skyLight==null) { + WorldUpdater.insertUpdate(task.world, vs.zero()); + } else { + VoxelizedSection csec = WorldConversionFactory.convert( + SECTION_CACHE.get(), + task.world.getMapper(), + section.getStates(), + section.getBiomes(), + getLightingSupplier(task) + ); + WorldConversionFactory.mipSection(csec, task.world.getMapper()); + WorldUpdater.insertUpdate(task.world, csec); + } + } catch (Exception e) { + Logger.error("Error ingesting section " + task.cx + ", " + task.cy + ", " + task.cz, e); } - } catch (Exception e) { - Logger.error("Error ingesting section " + task.cx + ", " + task.cy + ", " + task.cz, e); } } @@ -234,4 +234,9 @@ public class VoxelIngestService { if (!engine.instanceIn.isIngestEnabled(null)) return false;//TODO: dont pass in null return engine.instanceIn.getIngestService().rawIngest0(engine, section, x, y, z, bl, sl); } + + public void setMaxQueueSize(int size) { + if (size < 0) size = 0; + MAX_QUEUE_SIZE = size; + } } diff --git a/src/main/java/me/cortex/voxy/server/VisibleBandCache.java b/src/main/java/me/cortex/voxy/server/VisibleBandCache.java index 1b4ac3e4..882a2780 100644 --- a/src/main/java/me/cortex/voxy/server/VisibleBandCache.java +++ b/src/main/java/me/cortex/voxy/server/VisibleBandCache.java @@ -9,6 +9,9 @@ final class VisibleBandCache { private final VoxyServerInstance instance; private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); private final ConcurrentHashMap pending = new ConcurrentHashMap<>(); + private final java.util.concurrent.ConcurrentHashMap access = new java.util.concurrent.ConcurrentHashMap<>(); + private volatile int maxSize = 10000; + private volatile long ttlMillis = 300_000L; VisibleBandCache(VoxyServerInstance instance) { this.instance = instance; @@ -23,6 +26,10 @@ final class VisibleBandCache { try { int[] result = VisibleBandUtil.computeVisibleBands(level, engine, chunkX, chunkZ); cache.put(key, result); + access.put(key, System.currentTimeMillis()); + if (cache.size() > maxSize) { + purgeExpired(); + } } finally { pending.remove(key); } @@ -35,5 +42,35 @@ final class VisibleBandCache { long key = (((long) chunkX) << 32) ^ (chunkZ & 0xffffffffL); cache.remove(key); pending.remove(key); + access.remove(key); + } + + void setPolicy(int maxSize, int ttlSeconds) { + if (maxSize < 0) maxSize = 0; + this.maxSize = maxSize; + this.ttlMillis = ttlSeconds > 0 ? ttlSeconds * 1000L : -1; + } + + void purgeExpired() { + long now = System.currentTimeMillis(); + if (this.ttlMillis > 0) { + for (var e : access.entrySet()) { + if (now - e.getValue() > this.ttlMillis) { + long k = e.getKey(); + cache.remove(k); + pending.remove(k); + access.remove(k); + } + } + } + while (cache.size() > maxSize) { + var it = access.entrySet().iterator(); + if (!it.hasNext()) break; + var e = it.next(); + long k = e.getKey(); + it.remove(); + cache.remove(k); + pending.remove(k); + } } } diff --git a/src/main/java/me/cortex/voxy/server/VoxyServerInstance.java b/src/main/java/me/cortex/voxy/server/VoxyServerInstance.java index db423587..3f713643 100644 --- a/src/main/java/me/cortex/voxy/server/VoxyServerInstance.java +++ b/src/main/java/me/cortex/voxy/server/VoxyServerInstance.java @@ -37,6 +37,8 @@ public class VoxyServerInstance extends VoxyInstance { this.dirtyUpdateService = new DirtyUpdateService(this); this.playerLodTracker = new PlayerLodTracker(this); this.visibleBandCache = new VisibleBandCache(this); + me.cortex.voxy.common.world.WorldSection.setArrayReuseCacheTarget(VoxyServerConfig.CONFIG.reuseCacheMax); + this.threadPool.serviceManager.createServiceNoCleanup(() -> me.cortex.voxy.common.world.WorldSection::trimReuseCacheToTarget, 60000, "ReuseCacheTrim"); // Start a service to tick the dirty service this.threadPool.serviceManager.createServiceNoCleanup(() -> this.dirtyUpdateService::tick, VoxyServerConfig.CONFIG.dirtyUpdateDelay * 50, "DirtyUpdateService"); @@ -66,6 +68,18 @@ public class VoxyServerInstance extends VoxyInstance { this.dirtyUpdateService.tick(); this.playerLodTracker.tick(); }); + + me.cortex.voxy.common.world.service.VoxelIngestService svc = this.getIngestService(); + if (svc != null) { + svc.setMaxQueueSize(VoxyServerConfig.CONFIG.ingestQueueMax); + } + if (VoxyServerConfig.CONFIG.debugMemoryStats) { + this.threadPool.serviceManager.createServiceNoCleanup(() -> this::logMemoryStats, 60000, "VoxyMemoryStats"); + } + int ttl = VoxyServerConfig.CONFIG.visibleBandCacheTtlSeconds; + int max = VoxyServerConfig.CONFIG.visibleBandCacheMax; + this.threadPool.serviceManager.createServiceNoCleanup(() -> this.visibleBandCache::purgeExpired, 60000, "VisibleBandPurge"); + this.visibleBandCache.setPolicy(max, ttl); } @Override @@ -116,4 +130,17 @@ public class VoxyServerInstance extends VoxyInstance { VisibleBandCache getVisibleBandCache() { return this.visibleBandCache; } + + private void logMemoryStats() { + for (var level : this.server.getAllLevels()) { + var wi = WorldIdentifier.of(level); + var w = this.getNullable(wi); + if (w != null) { + me.cortex.voxy.common.Logger.info("World active sections: " + w.getActiveSectionCount()); + w.setLruPolicy(VoxyServerConfig.CONFIG.lruCacheMax, VoxyServerConfig.CONFIG.lruCacheTtlSeconds); + w.trimCaches(); + } + } + me.cortex.voxy.common.Logger.info("ReuseCache count: " + me.cortex.voxy.common.world.WorldSection.getArrayReuseCacheCount() + "/" + me.cortex.voxy.common.world.WorldSection.getArrayReuseCacheTarget()); + } }