From 772ec27ea1c0ee66f50d33dedc49692dad643320 Mon Sep 17 00:00:00 2001 From: mcrcortex <18544518+MCRcortex@users.noreply.github.com> Date: Thu, 30 Jan 2025 05:15:55 +1000 Subject: [PATCH] Secondary caching system implemented with ~1gb of heap allocated to it --- .../me/cortex/voxy/client/core/VoxelCore.java | 2 +- .../client/core/rendering/RenderService.java | 2 +- .../common/world/ActiveSectionTracker.java | 90 ++++++++++++------- .../voxy/common/world/L2SectionCache.java | 16 ---- .../cortex/voxy/common/world/WorldEngine.java | 7 +- .../voxy/common/world/WorldSection.java | 30 ++++--- 6 files changed, 84 insertions(+), 63 deletions(-) delete mode 100644 src/main/java/me/cortex/voxy/common/world/L2SectionCache.java diff --git a/src/main/java/me/cortex/voxy/client/core/VoxelCore.java b/src/main/java/me/cortex/voxy/client/core/VoxelCore.java index f5b621ca..69eca9e8 100644 --- a/src/main/java/me/cortex/voxy/client/core/VoxelCore.java +++ b/src/main/java/me/cortex/voxy/client/core/VoxelCore.java @@ -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); diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java b/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java index 656a78fb..ca553ca0 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java @@ -49,7 +49,7 @@ public class RenderService, 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(); 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 12ae98f8..97b6939e 100644 --- a/src/main/java/me/cortex/voxy/common/world/ActiveSectionTracker.java +++ b/src/main/java/me/cortex/voxy/common/world/ActiveSectionTracker.java @@ -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>[] loadedSectionCache; private final SectionLoader loader; - //private static final int SECONDARY_CACHE_CAPACITY = 256; - //private final Long2ObjectLinkedOpenHashMap secondaryDataCache = new Long2ObjectLinkedOpenHashMap<>(SECONDARY_CACHE_CAPACITY*2);//Its x2 due to race conditions - + private final int maxLRUSectionPerSlice; + private final Long2ObjectLinkedOpenHashMap[] 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<(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 holder = null; boolean isLoader = false; + WorldSection cachedSection = null; synchronized (cache) { holder = cache.get(key); if (holder == null) { @@ -49,32 +54,39 @@ 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), - WorldEngine.getX(key), - WorldEngine.getY(key), - WorldEngine.getZ(key), - this); + 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 - //throw new IllegalStateException("Unable to load section: "); - System.err.println("Unable to load section " + section.key + " setting to air"); - status = 1; - } + if (status < 0) { + //TODO: Instead if throwing an exception do something better, like attempting to regen + //throw new IllegalStateException("Unable to load section: "); + System.err.println("Unable to load section " + section.key + " setting to air"); + status = 1; + } - //TODO: REWRITE THE section tracker _again_ to not be so shit and jank, and so that Arrays.fill is not 10% of the execution time - if (status == 1) { - //We need to set the data to air as it is undefined state - Arrays.fill(section.data, 0); + //TODO: REWRITE THE section tracker _again_ to not be so shit and jank, and so that Arrays.fill is not 10% of the execution time + if (status == 1) { + //We need to set the data to air as it is undefined state + Arrays.fill(section.data, 0); + } } section.acquire(); holder.obj = section; @@ -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(); diff --git a/src/main/java/me/cortex/voxy/common/world/L2SectionCache.java b/src/main/java/me/cortex/voxy/common/world/L2SectionCache.java deleted file mode 100644 index b0468423..00000000 --- a/src/main/java/me/cortex/voxy/common/world/L2SectionCache.java +++ /dev/null @@ -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 - } -} 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 54f16d10..87777aba 100644 --- a/src/main/java/me/cortex/voxy/common/world/WorldEngine.java +++ b/src/main/java/me/cortex/voxy/common/world/WorldEngine.java @@ -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 debug) { + debug.add("ACC/SCC: " + this.sectionTracker.getLoadedCacheCount()+"/"+this.sectionTracker.getSecondaryCacheSize());//Active cache count, Secondary cache counts } public void shutdown() { 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 289cf3f5..253a6050 100644 --- a/src/main/java/me/cortex/voxy/common/world/WorldSection.java +++ b/src/main/java/me/cortex/voxy/common/world/WorldSection.java @@ -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 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,18 +133,22 @@ public final class WorldSection { throw new IllegalStateException("Section marked as free but has refs"); } } - boolean isFreed = witness == 1; - if (isFreed) { - if (ARRAY_REUSE_CACHE.size() < ARRAY_REUSE_CACHE_SIZE) { - synchronized (ARRAY_REUSE_CACHE) { - ARRAY_REUSE_CACHE.add(this.data); - } - } - this.data = null; - } - return 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); + } + } + this.data = null; + } + + public void assertNotFree() { if (VERIFY_WORLD_SECTION_EXECUTION) { if ((((int) ATOMIC_STATE_HANDLE.get(this)) & 1) == 0) {