资源释放
Some checks failed
check-does-build / build (push) Failing after 8s

This commit is contained in:
2026-01-07 12:23:29 +08:00
parent 49e92b4190
commit 4c0399ca40
7 changed files with 185 additions and 27 deletions

View File

@@ -30,6 +30,20 @@ public class VoxyServerConfig {
public int dirtyUpdateDelay = 20; public int dirtyUpdateDelay = 20;
@SerializedName(value = "ingest_enabled", alternate = {"ingestEnabled"}) @SerializedName(value = "ingest_enabled", alternate = {"ingestEnabled"})
public boolean ingestEnabled = true; 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() { private static VoxyServerConfig loadOrCreate() {
Path path = getConfigPath(); Path path = getConfigPath();

View File

@@ -41,7 +41,10 @@ public class ActiveSectionTracker {
private final int lruSize; private final int lruSize;
private final StampedLock lruLock = new StampedLock(); private final StampedLock lruLock = new StampedLock();
private final Long2ObjectLinkedOpenHashMap<WorldSection> lruSecondaryCache;//TODO: THIS NEEDS TO BECOME A GLOBAL STATIC CACHE private final Long2ObjectLinkedOpenHashMap<WorldSection> 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 @Nullable
public final WorldEngine engine; public final WorldEngine engine;
@@ -254,12 +257,14 @@ public class ActiveSectionTracker {
long stamp2 = this.lruLock.writeLock(); long stamp2 = this.lruLock.writeLock();
lock.unlockWrite(stamp); lock.unlockWrite(stamp);
WorldSection a = this.lruSecondaryCache.put(section.key, section); WorldSection a = this.lruSecondaryCache.put(section.key, section);
this.lruTimestamps.put(section.key, System.currentTimeMillis());
if (a != null) { if (a != null) {
throw new IllegalStateException("duplicate sections in cache is impossible"); 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 cache is bigger than its ment to be, remove the least recently used and free it
if (this.lruSize < this.lruSecondaryCache.size()) { if (this.lruSize < this.lruSecondaryCache.size()) {
aa = this.lruSecondaryCache.removeFirst(); aa = this.lruSecondaryCache.removeFirst();
this.lruTimestamps.remove(aa.key);
} }
this.lruLock.unlockWrite(stamp2); this.lruLock.unlockWrite(stamp2);
@@ -295,6 +300,45 @@ public class ActiveSectionTracker {
return this.lruSecondaryCache.size(); 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 { public static void main(String[] args) throws InterruptedException {
var tracker = new ActiveSectionTracker(6, a->0, 2<<10); var tracker = new ActiveSectionTracker(6, a->0, 2<<10);
var bean = tracker.acquire(0, 0, 0, 9, false); var bean = tracker.acquire(0, 0, 0, 9, false);

View File

@@ -62,6 +62,7 @@ public class WorldEngine {
this.mapper = new Mapper(this.storage); this.mapper = new Mapper(this.storage);
//5 cache size bits means that the section tracker has 32 separate maps that it uses //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 = new ActiveSectionTracker(6, storage::loadSection, cacheSize, this);
this.sectionTracker.setLruPolicy(-1, -1);
} }
public WorldSection acquireIfExists(int lvl, int x, int y, int z) { public WorldSection acquireIfExists(int lvl, int x, int y, int z) {
@@ -194,4 +195,12 @@ public class WorldEngine {
this.saveCallback.save(this, section); this.saveCallback.save(this, section);
} }
} }
public void setLruPolicy(int maxOverride, int ttlSeconds) {
this.sectionTracker.setLruPolicy(maxOverride, ttlSeconds);
}
public void trimCaches() {
this.sectionTracker.trimLruByPolicy();
}
} }

View File

@@ -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) //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 //TODO: maybe just swap this to a ConcurrentLinkedDeque
private static final AtomicInteger ARRAY_REUSE_CACHE_COUNT = new AtomicInteger(0); private static final AtomicInteger ARRAY_REUSE_CACHE_COUNT = new AtomicInteger(0);
private static final ConcurrentLinkedDeque<long[]> ARRAY_REUSE_CACHE = new ConcurrentLinkedDeque<>(); private static final ConcurrentLinkedDeque<long[]> ARRAY_REUSE_CACHE = new ConcurrentLinkedDeque<>();
@@ -293,4 +293,26 @@ public final class WorldSection {
public boolean isFreed() { public boolean isFreed() {
return (((int)ATOMIC_STATE_HANDLE.get(this))&1)==0; return (((int)ATOMIC_STATE_HANDLE.get(this))&1)==0;
} }
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();
}
}
} }

View File

@@ -25,7 +25,7 @@ public class VoxelIngestService {
private final Service service; private final Service service;
private record IngestSection(int cx, int cy, int cz, WorldEngine world, LevelChunkSection section, DataLayer blockLight, DataLayer skyLight){} private record IngestSection(int cx, int cy, int cz, WorldEngine world, LevelChunkSection section, DataLayer blockLight, DataLayer skyLight){}
private final ConcurrentLinkedDeque<IngestSection> ingestQueue = new ConcurrentLinkedDeque<>(); private final ConcurrentLinkedDeque<IngestSection> ingestQueue = new ConcurrentLinkedDeque<>();
private static final int MAX_QUEUE_SIZE = 16384; private static volatile int MAX_QUEUE_SIZE = 16384;
private int dropCount = 0; private int dropCount = 0;
public VoxelIngestService(ServiceManager pool) { public VoxelIngestService(ServiceManager pool) {
@@ -33,16 +33,15 @@ public class VoxelIngestService {
} }
private void processJob() { private void processJob() {
int batch = Math.min(64, Math.max(1, this.ingestQueue.size()));
for (int i = 0; i < batch; i++) {
var task = this.ingestQueue.poll(); var task = this.ingestQueue.poll();
if (task == null) return; if (task == null) break;
try { try {
task.world.markActive(); task.world.markActive();
var section = task.section; var section = task.section;
var vs = SECTION_CACHE.get().setPosition(task.cx, task.cy, task.cz); var vs = SECTION_CACHE.get().setPosition(task.cx, task.cy, task.cz);
if (section.hasOnlyAir() && task.blockLight==null && task.skyLight==null) {
if (section.hasOnlyAir() && task.blockLight==null && task.skyLight==null) {//If the chunk section has lighting data, propagate it
WorldUpdater.insertUpdate(task.world, vs.zero()); WorldUpdater.insertUpdate(task.world, vs.zero());
} else { } else {
VoxelizedSection csec = WorldConversionFactory.convert( VoxelizedSection csec = WorldConversionFactory.convert(
@@ -59,6 +58,7 @@ public class VoxelIngestService {
Logger.error("Error ingesting section " + task.cx + ", " + task.cy + ", " + task.cz, e); Logger.error("Error ingesting section " + task.cx + ", " + task.cy + ", " + task.cz, e);
} }
} }
}
@NotNull @NotNull
private static ILightingSupplier getLightingSupplier(IngestSection task) { private static ILightingSupplier getLightingSupplier(IngestSection task) {
@@ -234,4 +234,9 @@ public class VoxelIngestService {
if (!engine.instanceIn.isIngestEnabled(null)) return false;//TODO: dont pass in null if (!engine.instanceIn.isIngestEnabled(null)) return false;//TODO: dont pass in null
return engine.instanceIn.getIngestService().rawIngest0(engine, section, x, y, z, bl, sl); 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;
}
} }

View File

@@ -9,6 +9,9 @@ final class VisibleBandCache {
private final VoxyServerInstance instance; private final VoxyServerInstance instance;
private final ConcurrentHashMap<Long, int[]> cache = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Long, int[]> cache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Boolean> pending = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Long, Boolean> pending = new ConcurrentHashMap<>();
private final java.util.concurrent.ConcurrentHashMap<Long, Long> access = new java.util.concurrent.ConcurrentHashMap<>();
private volatile int maxSize = 10000;
private volatile long ttlMillis = 300_000L;
VisibleBandCache(VoxyServerInstance instance) { VisibleBandCache(VoxyServerInstance instance) {
this.instance = instance; this.instance = instance;
@@ -23,6 +26,10 @@ final class VisibleBandCache {
try { try {
int[] result = VisibleBandUtil.computeVisibleBands(level, engine, chunkX, chunkZ); int[] result = VisibleBandUtil.computeVisibleBands(level, engine, chunkX, chunkZ);
cache.put(key, result); cache.put(key, result);
access.put(key, System.currentTimeMillis());
if (cache.size() > maxSize) {
purgeExpired();
}
} finally { } finally {
pending.remove(key); pending.remove(key);
} }
@@ -35,5 +42,35 @@ final class VisibleBandCache {
long key = (((long) chunkX) << 32) ^ (chunkZ & 0xffffffffL); long key = (((long) chunkX) << 32) ^ (chunkZ & 0xffffffffL);
cache.remove(key); cache.remove(key);
pending.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);
}
} }
} }

View File

@@ -37,6 +37,8 @@ public class VoxyServerInstance extends VoxyInstance {
this.dirtyUpdateService = new DirtyUpdateService(this); this.dirtyUpdateService = new DirtyUpdateService(this);
this.playerLodTracker = new PlayerLodTracker(this); this.playerLodTracker = new PlayerLodTracker(this);
this.visibleBandCache = new VisibleBandCache(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 // Start a service to tick the dirty service
this.threadPool.serviceManager.createServiceNoCleanup(() -> this.dirtyUpdateService::tick, VoxyServerConfig.CONFIG.dirtyUpdateDelay * 50, "DirtyUpdateService"); 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.dirtyUpdateService.tick();
this.playerLodTracker.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 @Override
@@ -116,4 +130,17 @@ public class VoxyServerInstance extends VoxyInstance {
VisibleBandCache getVisibleBandCache() { VisibleBandCache getVisibleBandCache() {
return this.visibleBandCache; 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());
}
} }