Secondary caching system implemented with ~1gb of heap allocated to it
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user