diff --git a/core/src/main/java/com/seibel/distanthorizons/core/Initializer.java b/core/src/main/java/com/seibel/distanthorizons/core/Initializer.java
index 254db658e..c6b34eac1 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/Initializer.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/Initializer.java
@@ -19,6 +19,7 @@
package com.seibel.distanthorizons.core;
+import com.seibel.distanthorizons.core.file.DataSourceReferenceTracker;
import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.core.world.DhApiWorldProxy;
import com.seibel.distanthorizons.core.api.external.methods.config.DhApiConfig;
@@ -67,6 +68,8 @@ public class Initializer
DhApi.Delayed.worldProxy = DhApiWorldProxy.INSTANCE;
DhApi.Delayed.renderProxy = DhApiRenderProxy.INSTANCE;
+ DataSourceReferenceTracker.startGarbageCollectorBackgroundThread();
+
}
}
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/DataSourceReferenceTracker.java b/core/src/main/java/com/seibel/distanthorizons/core/file/DataSourceReferenceTracker.java
new file mode 100644
index 000000000..640701b48
--- /dev/null
+++ b/core/src/main/java/com/seibel/distanthorizons/core/file/DataSourceReferenceTracker.java
@@ -0,0 +1,235 @@
+/*
+ * This file is part of the Distant Horizons mod
+ * licensed under the GNU LGPL v3 License.
+ *
+ * Copyright (C) 2020-2023 James Seibel
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package com.seibel.distanthorizons.core.file;
+
+import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource;
+import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource;
+import com.seibel.distanthorizons.core.file.fullDatafile.FullDataMetaFile;
+import com.seibel.distanthorizons.core.file.metaData.AbstractMetaDataContainerFile;
+import com.seibel.distanthorizons.core.file.renderfile.RenderMetaDataFile;
+import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
+import com.seibel.distanthorizons.core.util.ThreadUtil;
+import org.apache.logging.log4j.Logger;
+
+import java.io.Closeable;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.SoftReference;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+
+/**
+ * Keeps track of {@link FullDataMetaFile} and {@link RenderMetaDataFile}'s
+ * and handles freeing their underlying data sources if they go unused for a certain amount of time.
+ */
+public class DataSourceReferenceTracker
+{
+ private static final Logger LOGGER = DhLoggerBuilder.getLogger();
+ private static final boolean LOG_GARBAGE_COLLECTIONS = false;
+
+ /** How often the garbage collector thread will run */
+ private static final long MS_BETWEEN_GARBAGE_CHECKS = TimeUnit.SECONDS.toMillis(30);
+ /** How long a data source has to go unused before it can be freed */
+ private static final long MS_TO_EXPIRE_DATA_SOURCE = TimeUnit.SECONDS.toMillis(15);
+
+
+ // these queues are populated by the JVM's garbage collector after the assigned soft reference is freed
+ private static final ReferenceQueue FULL_DATA_GARBAGE_COLLECTED_QUEUE = new ReferenceQueue<>();
+ private static final ReferenceQueue RENDER_DATA_GARBAGE_COLLECTED_QUEUE = new ReferenceQueue<>();
+
+ // TODO using a ConcurrentHashMap may or may not be the best choice here
+ private static final Set FULL_DATA_SOFT_REFS = ConcurrentHashMap.newKeySet();
+ private static final Set RENDER_DATA_SOFT_REFS = ConcurrentHashMap.newKeySet();
+
+ private static final ThreadPoolExecutor GARBAGE_COLLECTOR_THREAD = ThreadUtil.makeSingleThreadPool("DataSourceReferenceTracker", ThreadUtil.MINIMUM_RELATIVE_PRIORITY);
+
+
+
+ //=================//
+ // collector logic //
+ //=================//
+
+ /** Warning: this should not be called more than once. */
+ public static void startGarbageCollectorBackgroundThread() { GARBAGE_COLLECTOR_THREAD.execute(() -> garbageCollectorLoop()); }
+ private static void garbageCollectorLoop()
+ {
+ while(true)
+ {
+ try
+ {
+ runGarbageCollection();
+ Thread.sleep(MS_BETWEEN_GARBAGE_CHECKS);
+ }
+ catch (InterruptedException e)
+ {
+ LOGGER.error("Garbage collector thread interrupted.", e);
+ }
+ catch (Exception e)
+ {
+ LOGGER.error("Unexpected data source garbage collector exception: " + e.getMessage(), e);
+ }
+ }
+ }
+
+ public static void runGarbageCollection()
+ {
+ removeGarbageCollectedDataSources();
+ removeExpiredDataSources();
+ }
+ private static void removeGarbageCollectedDataSources()
+ {
+ FullDataSourceSoftRef garbageCollectedFullDataSoftRef = (FullDataSourceSoftRef) FULL_DATA_GARBAGE_COLLECTED_QUEUE.poll();
+ while (garbageCollectedFullDataSoftRef != null)
+ {
+ if (LOG_GARBAGE_COLLECTIONS)
+ {
+ LOGGER.info("Full Data at pos: " + garbageCollectedFullDataSoftRef.metaFile.pos + " has been soft released.");
+ }
+ garbageCollectedFullDataSoftRef.close();
+
+ garbageCollectedFullDataSoftRef = (FullDataSourceSoftRef) FULL_DATA_GARBAGE_COLLECTED_QUEUE.poll();
+ }
+
+ RenderDataSourceSoftRef renderSoftRef = (RenderDataSourceSoftRef) RENDER_DATA_GARBAGE_COLLECTED_QUEUE.poll();
+ while (renderSoftRef != null)
+ {
+ if (LOG_GARBAGE_COLLECTIONS)
+ {
+ LOGGER.info("Render Data at pos: " + renderSoftRef.metaFile.pos + " has been soft released.");
+ }
+ renderSoftRef.close();
+
+ renderSoftRef = (RenderDataSourceSoftRef) RENDER_DATA_GARBAGE_COLLECTED_QUEUE.poll();
+ }
+ }
+ private static void removeExpiredDataSources()
+ {
+ // TODO merge these loops
+ FULL_DATA_SOFT_REFS.removeIf((fullDataSoftRef) ->
+ {
+ boolean remove = fullDataSoftRef.isDataSourceExpired() || (fullDataSoftRef.silentGet() == null);
+ if (remove)
+ {
+ fullDataSoftRef.clear();
+ fullDataSoftRef.close();
+
+ if (LOG_GARBAGE_COLLECTIONS)
+ {
+ LOGGER.info("Full Data at pos: " + fullDataSoftRef.metaFile.pos + " has expired and will be released at the next GC. ["+FULL_DATA_SOFT_REFS.size()+"] Full data sources remain.");
+ }
+ }
+
+ return remove;
+ });
+
+ // TODO merge these loops
+ RENDER_DATA_SOFT_REFS.removeIf((renderDataSoftRef) ->
+ {
+ boolean remove = renderDataSoftRef.isDataSourceExpired() || (renderDataSoftRef.silentGet() == null);
+ if (remove)
+ {
+ renderDataSoftRef.clear();
+ renderDataSoftRef.close();
+
+ if (LOG_GARBAGE_COLLECTIONS)
+ {
+ LOGGER.info("Render Data at pos: " + renderDataSoftRef.metaFile.pos + " has expired and will be released at the next GC. ["+RENDER_DATA_SOFT_REFS.size()+"] Render data sources remain.");
+ }
+ }
+
+ return remove;
+ });
+ }
+
+
+
+ //================//
+ // helper classes //
+ //================//
+
+ public static class FullDataSourceSoftRef extends AbstractDataSourceSoftTracker
+ {
+ public FullDataSourceSoftRef(FullDataMetaFile metaFile, IFullDataSource data)
+ {
+ super(metaFile, data, FULL_DATA_GARBAGE_COLLECTED_QUEUE);
+ FULL_DATA_SOFT_REFS.add(this);
+ }
+
+ @Override
+ public void close() { FULL_DATA_SOFT_REFS.remove(this); }
+ }
+ public static class RenderDataSourceSoftRef extends AbstractDataSourceSoftTracker
+ {
+ public RenderDataSourceSoftRef(RenderMetaDataFile metaFile, ColumnRenderSource data)
+ {
+ super(metaFile, data, RENDER_DATA_GARBAGE_COLLECTED_QUEUE);
+ RENDER_DATA_SOFT_REFS.add(this);
+ }
+
+ @Override
+ public void close() { RENDER_DATA_SOFT_REFS.remove(this); }
+ }
+
+ /** wrapper for a {@link SoftReference} so we can track and manually remove unused sources */
+ public static abstract class AbstractDataSourceSoftTracker extends SoftReference implements Closeable
+ {
+ public final TMetaFile metaFile;
+ public final long createdMsTime;
+
+ private long expirationMsTime;
+
+
+
+ public AbstractDataSourceSoftTracker(TMetaFile metaFile, TDataSource dataSource, ReferenceQueue referenceQueue)
+ {
+ super(dataSource, referenceQueue);
+ this.metaFile = metaFile;
+
+ this.createdMsTime = System.currentTimeMillis();
+ this.expirationMsTime = System.currentTimeMillis();
+ }
+
+
+
+ public void updateLastAccessedTime() { this.expirationMsTime = System.currentTimeMillis() + MS_TO_EXPIRE_DATA_SOURCE; }
+ public long getExpirationMsTime() { return this.expirationMsTime; }
+ public boolean isDataSourceExpired() { return this.expirationMsTime > System.currentTimeMillis(); }
+
+
+ @Override
+ public TDataSource get()
+ {
+ this.updateLastAccessedTime();
+ return super.get();
+ }
+
+ /**
+ * Gets the underlying datasource without updating the {@link AbstractDataSourceSoftTracker#expirationMsTime}
+ * Note: this still updates {@link SoftReference}'s timestamp variable which may prevent the JVM from
+ * marking this reference as valid for deletion.
+ */
+ public TDataSource silentGet() { return super.get(); }
+
+ }
+
+}
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataMetaFile.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataMetaFile.java
index 43fa3825a..3fa1cc495 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataMetaFile.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataMetaFile.java
@@ -33,6 +33,7 @@ import com.seibel.distanthorizons.core.dataObjects.fullData.sources.CompleteFull
import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IIncompleteFullDataSource;
+import com.seibel.distanthorizons.core.file.DataSourceReferenceTracker;
import com.seibel.distanthorizons.core.file.metaData.AbstractMetaDataContainerFile;
import com.seibel.distanthorizons.core.file.metaData.BaseMetaData;
import com.seibel.distanthorizons.core.level.IDhLevel;
@@ -84,7 +85,7 @@ public class FullDataMetaFile extends AbstractMetaDataContainerFile implements I
* When clearing, don't set to null, instead create a SoftReference containing null.
* This makes null checks simpler.
*/
- private SoftReference cachedFullDataSourceRef = new SoftReference<>(null);
+ private DataSourceReferenceTracker.FullDataSourceSoftRef cachedFullDataSourceRef = new DataSourceReferenceTracker.FullDataSourceSoftRef(this,null);
private final AtomicReference> dataSourceLoadFutureRef = new AtomicReference<>(null);
// === Concurrent Write tracking ===
@@ -401,7 +402,7 @@ public class FullDataMetaFile extends AbstractMetaDataContainerFile implements I
public static void checkAndLogPhantomDataSourceLifeCycles()
{
DataObjTracker phantomRef = (DataObjTracker) LIFE_CYCLE_DEBUG_QUEUE.poll();
- // wait for the tracker to be garbage collected(?)
+ // wait for the tracker to be garbage collected
while (phantomRef != null)
{
if (LOG_DATA_SOURCE_LIVES)
@@ -563,7 +564,7 @@ public class FullDataMetaFile extends AbstractMetaDataContainerFile implements I
// save the updated data source
- this.cachedFullDataSourceRef = new SoftReference<>(fullDataSource);
+ this.cachedFullDataSourceRef = new DataSourceReferenceTracker.FullDataSourceSoftRef(this, fullDataSource);
// the task is complete
completionFuture.complete(fullDataSource);
@@ -685,7 +686,7 @@ public class FullDataMetaFile extends AbstractMetaDataContainerFile implements I
if (LOG_DATA_SOURCE_LIVES)
{
- LOGGER.info("Phantom created on {}! count: {}", data.getSectionPos(), LIFE_CYCLE_DEBUG_SET.size());
+ //LOGGER.info("Phantom created on "+data.getSectionPos()+"! count: "+LIFE_CYCLE_DEBUG_SET.size());
}
LIFE_CYCLE_DEBUG_SET.add(this);
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderMetaDataFile.java b/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderMetaDataFile.java
index d5e739570..7430509c3 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderMetaDataFile.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderMetaDataFile.java
@@ -23,6 +23,7 @@ import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource;
import com.seibel.distanthorizons.core.dataObjects.transformers.FullDataToRenderDataTransformer;
+import com.seibel.distanthorizons.core.file.DataSourceReferenceTracker;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataMetaFile;
import com.seibel.distanthorizons.core.file.fullDatafile.IFullDataSourceProvider;
import com.seibel.distanthorizons.core.file.metaData.AbstractMetaDataContainerFile;
@@ -68,7 +69,7 @@ public class RenderMetaDataFile extends AbstractMetaDataContainerFile implements
* When clearing, don't set to null, instead create a SoftReference containing null.
* This makes null checks simpler.
*/
- private SoftReference cachedRenderDataSource = new SoftReference<>(null);
+ private DataSourceReferenceTracker.RenderDataSourceSoftRef cachedRenderDataSource = new DataSourceReferenceTracker.RenderDataSourceSoftRef(this, null);
private final AtomicReference> renderSourceLoadFutureRef = new AtomicReference<>(null);
private final IDhClientLevel clientLevel;
@@ -210,7 +211,7 @@ public class RenderMetaDataFile extends AbstractMetaDataContainerFile implements
this.updateRenderCacheAsync(newColumnRenderSource).whenComplete((voidObj, ex) ->
{
- this.cachedRenderDataSource = new SoftReference<>(newColumnRenderSource);
+ this.cachedRenderDataSource = new DataSourceReferenceTracker.RenderDataSourceSoftRef(this, newColumnRenderSource);
this.renderSourceLoadFutureRef.set(null);
getSourceFuture.complete(newColumnRenderSource);
@@ -258,7 +259,7 @@ public class RenderMetaDataFile extends AbstractMetaDataContainerFile implements
this.renderSourceLoadFutureRef.set(null);
- this.cachedRenderDataSource = new SoftReference<>(renderSource);
+ this.cachedRenderDataSource = new DataSourceReferenceTracker.RenderDataSourceSoftRef(this, renderSource);
getSourceFuture.complete(renderSource);
});
}