Secondary caching system implemented with ~1gb of heap allocated to it

This commit is contained in:
mcrcortex
2025-01-30 05:15:55 +10:00
parent 5421452429
commit 772ec27ea1
6 changed files with 84 additions and 63 deletions

View File

@@ -190,7 +190,7 @@ public class VoxelCore {
debug.add("Render service tasks: " + this.renderGen.getTaskCount());
*/
debug.add("I/S tasks: " + this.world.ingestService.getTaskCount() + "/"+this.world.savingService.getTaskCount());
debug.add("SCS: " + Arrays.toString(this.world.getLoadedSectionCacheSizes()));
this.world.addDebugData(debug);
this.renderer.addDebugData(debug);
PrintfDebugUtil.addToOut(debug);

View File

@@ -49,7 +49,7 @@ public class RenderService<T extends AbstractSectionRenderer<J, ?>, J extends Vi
//Max sections: ~500k
//Max geometry: 1 gb
this.sectionRenderer = (T) createSectionRenderer(this.modelService.getStore(),1<<20, (1L<<32)-1024);
this.sectionRenderer = (T) createSectionRenderer(this.modelService.getStore(),1<<20, (1L<<31)-1024);
//Do something incredibly hacky, we dont need to keep the reference to this around, so just connect and discard
var router = new SectionUpdateRouter();

View File

@@ -8,6 +8,7 @@ import me.cortex.voxy.common.world.other.Mapper;
import java.util.Arrays;
public class ActiveSectionTracker {
//Deserialize into the supplied section, returns true on success, false on failure
public interface SectionLoader {int load(WorldSection section);}
@@ -16,16 +17,18 @@ public class ActiveSectionTracker {
private final Long2ObjectOpenHashMap<VolatileHolder<WorldSection>>[] loadedSectionCache;
private final SectionLoader loader;
//private static final int SECONDARY_CACHE_CAPACITY = 256;
//private final Long2ObjectLinkedOpenHashMap<long[]> secondaryDataCache = new Long2ObjectLinkedOpenHashMap<>(SECONDARY_CACHE_CAPACITY*2);//Its x2 due to race conditions
private final int maxLRUSectionPerSlice;
private final Long2ObjectLinkedOpenHashMap<WorldSection>[] lruSecondaryCache;
@SuppressWarnings("unchecked")
public ActiveSectionTracker(int numSlicesBits, SectionLoader loader) {
public ActiveSectionTracker(int numSlicesBits, SectionLoader loader, int cacheSize) {
this.loader = loader;
this.loadedSectionCache = new Long2ObjectOpenHashMap[1<<numSlicesBits];
this.lruSecondaryCache = new Long2ObjectLinkedOpenHashMap[1<<numSlicesBits];
this.maxLRUSectionPerSlice = (cacheSize+(1<<numSlicesBits)-1)/(1<<numSlicesBits);
for (int i = 0; i < this.loadedSectionCache.length; i++) {
this.loadedSectionCache[i] = new Long2ObjectOpenHashMap<>(1024);
this.lruSecondaryCache[i] = new Long2ObjectLinkedOpenHashMap<>(this.maxLRUSectionPerSlice);
}
}
@@ -34,9 +37,11 @@ public class ActiveSectionTracker {
}
public WorldSection acquire(long key, boolean nullOnEmpty) {
var cache = this.loadedSectionCache[this.getCacheArrayIndex(key)];
int index = this.getCacheArrayIndex(key);
var cache = this.loadedSectionCache[index];
VolatileHolder<WorldSection> holder = null;
boolean isLoader = false;
WorldSection cachedSection = null;
synchronized (cache) {
holder = cache.get(key);
if (holder == null) {
@@ -49,20 +54,26 @@ public class ActiveSectionTracker {
section.acquire();
return section;
}
if (isLoader) {
cachedSection = this.lruSecondaryCache[index].remove(key);
if (cachedSection != null) {
cachedSection.primeForReuse();
}
}
}
//If this thread was the one to create the reference then its the thread to load the section
if (isLoader) {
var section = new WorldSection(WorldEngine.getLevel(key),
int status = 0;
var section = cachedSection;
if (section == null) {//Secondary cache miss
section = new WorldSection(WorldEngine.getLevel(key),
WorldEngine.getX(key),
WorldEngine.getY(key),
WorldEngine.getZ(key),
this);
int status = -1;//this.dataCache.load(section);
if (status == -1) {//Cache miss
status = this.loader.load(section);
}
if (status < 0) {
//TODO: Instead if throwing an exception do something better, like attempting to regen
@@ -76,6 +87,7 @@ public class ActiveSectionTracker {
//We need to set the data to air as it is undefined state
Arrays.fill(section.data, 0);
}
}
section.acquire();
holder.obj = section;
if (nullOnEmpty && status == 1) {//If its air return null as stated, release the section aswell
@@ -98,14 +110,23 @@ public class ActiveSectionTracker {
}
void tryUnload(WorldSection section) {
var cache = this.loadedSectionCache[this.getCacheArrayIndex(section.key)];
boolean removed = false;
int index = this.getCacheArrayIndex(section.key);
var cache = this.loadedSectionCache[index];
synchronized (cache) {
if (section.trySetFreed()) {
if (cache.remove(section.key).obj != section) {
throw new IllegalStateException("Removed section not the same as the referenced section in the cache");
}
removed = true;
//Add section to secondary cache while primary is locked
var lruCache = this.lruSecondaryCache[index];
var prev = lruCache.put(section.key, section);
if (prev != null) {
prev._releaseArray();
}
//If cache is bigger than its ment to be, remove the least recently used and free it
if (this.maxLRUSectionPerSlice < lruCache.size()) {
lruCache.removeFirst()._releaseArray();
}
}
}
}
@@ -120,17 +141,24 @@ public class ActiveSectionTracker {
return seed ^ seed >>> 31;
}
public int[] getCacheCounts() {
int[] res = new int[this.loadedSectionCache.length];
for (int i = 0; i < this.loadedSectionCache.length; i++) {
res[i] = this.loadedSectionCache[i].size();
public int getLoadedCacheCount() {
int res = 0;
for (var cache : this.loadedSectionCache) {
res += cache.size();
}
return res;
}
public int getSecondaryCacheSize() {
int res = 0;
for (var cache : this.lruSecondaryCache) {
res += cache.size();
}
return res;
}
public static void main(String[] args) {
var tracker = new ActiveSectionTracker(1, a->0);
var tracker = new ActiveSectionTracker(1, a->0, 1<<10);
var section = tracker.acquire(0,0,0,0, false);
section.acquire();

View File

@@ -1,16 +0,0 @@
package me.cortex.voxy.common.world;
public class L2SectionCache {
//Sections may go here before they goto die
public L2SectionCache(int size) {
}
public void put(WorldSection worldSection) {
}
public WorldSection reacquire(long pos) {
//Try to re-acquire a section from the cache and revive it
}
}

View File

@@ -10,6 +10,7 @@ import me.cortex.voxy.common.storage.StorageBackend;
import me.cortex.voxy.common.thread.ServiceThreadPool;
import java.util.Arrays;
import java.util.List;
//Use an LMDB backend to store the world, use a local inmemory cache for lod sections
// automatically manages and invalidates sections of the world as needed
@@ -47,7 +48,7 @@ public class WorldEngine {
this.storage = storageBackend;
this.mapper = new Mapper(this.storage);
//4 cache size bits means that the section tracker has 16 separate maps that it uses
this.sectionTracker = new ActiveSectionTracker(4, this::unsafeLoadSection);
this.sectionTracker = new ActiveSectionTracker(4, this::unsafeLoadSection, 1<<12);//1 gb of cpu section cache
this.savingService = new SectionSavingService(this, serviceThreadPool);
this.ingestService = new VoxelIngestService(this, serviceThreadPool);
@@ -205,8 +206,8 @@ public class WorldEngine {
}
}
public int[] getLoadedSectionCacheSizes() {
return this.sectionTracker.getCacheCounts();
public void addDebugData(List<String> debug) {
debug.add("ACC/SCC: " + this.sectionTracker.getLoadedCacheCount()+"/"+this.sectionTracker.getSecondaryCacheSize());//Active cache count, Secondary cache counts
}
public void shutdown() {

View File

@@ -33,7 +33,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 = 300;//500;//32*32*32*8*ARRAY_REUSE_CACHE_SIZE == number of bytes
private static final int ARRAY_REUSE_CACHE_SIZE = 200;//500;//32*32*32*8*ARRAY_REUSE_CACHE_SIZE == number of bytes
//TODO: maybe just swap this to a ConcurrentLinkedDeque
private static final Deque<long[]> ARRAY_REUSE_CACHE = new ArrayDeque<>(1024);
@@ -76,6 +76,10 @@ public final class WorldSection {
}
}
void primeForReuse() {
ATOMIC_STATE_HANDLE.set(this, 1);
}
@Override
public int hashCode() {
return ((x*1235641+y)*8127451+z)*918267913+lvl;
@@ -129,8 +133,13 @@ public final class WorldSection {
throw new IllegalStateException("Section marked as free but has refs");
}
}
boolean isFreed = witness == 1;
if (isFreed) {
return witness == 1;
}
void _releaseArray() {
if (VERIFY_WORLD_SECTION_EXECUTION && this.data == null) {
throw new IllegalStateException();
}
if (ARRAY_REUSE_CACHE.size() < ARRAY_REUSE_CACHE_SIZE) {
synchronized (ARRAY_REUSE_CACHE) {
ARRAY_REUSE_CACHE.add(this.data);
@@ -138,8 +147,7 @@ public final class WorldSection {
}
this.data = null;
}
return isFreed;
}
public void assertNotFree() {
if (VERIFY_WORLD_SECTION_EXECUTION) {