This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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,30 +33,30 @@ public class VoxelIngestService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void processJob() {
|
private void processJob() {
|
||||||
var task = this.ingestQueue.poll();
|
int batch = Math.min(64, Math.max(1, this.ingestQueue.size()));
|
||||||
if (task == null) return;
|
for (int i = 0; i < batch; i++) {
|
||||||
|
var task = this.ingestQueue.poll();
|
||||||
try {
|
if (task == null) break;
|
||||||
task.world.markActive();
|
try {
|
||||||
|
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(
|
SECTION_CACHE.get(),
|
||||||
SECTION_CACHE.get(),
|
task.world.getMapper(),
|
||||||
task.world.getMapper(),
|
section.getStates(),
|
||||||
section.getStates(),
|
section.getBiomes(),
|
||||||
section.getBiomes(),
|
getLightingSupplier(task)
|
||||||
getLightingSupplier(task)
|
);
|
||||||
);
|
WorldConversionFactory.mipSection(csec, task.world.getMapper());
|
||||||
WorldConversionFactory.mipSection(csec, task.world.getMapper());
|
WorldUpdater.insertUpdate(task.world, csec);
|
||||||
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
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user