Add DataSourceReferenceTracker to automatically free unused data sources
This commit is contained in:
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+235
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<IFullDataSource> FULL_DATA_GARBAGE_COLLECTED_QUEUE = new ReferenceQueue<>();
|
||||
private static final ReferenceQueue<ColumnRenderSource> RENDER_DATA_GARBAGE_COLLECTED_QUEUE = new ReferenceQueue<>();
|
||||
|
||||
// TODO using a ConcurrentHashMap may or may not be the best choice here
|
||||
private static final Set<FullDataSourceSoftRef> FULL_DATA_SOFT_REFS = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<RenderDataSourceSoftRef> 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<FullDataMetaFile, IFullDataSource>
|
||||
{
|
||||
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<RenderMetaDataFile, ColumnRenderSource>
|
||||
{
|
||||
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<TMetaFile extends AbstractMetaDataContainerFile, TDataSource> extends SoftReference<TDataSource> implements Closeable
|
||||
{
|
||||
public final TMetaFile metaFile;
|
||||
public final long createdMsTime;
|
||||
|
||||
private long expirationMsTime;
|
||||
|
||||
|
||||
|
||||
public AbstractDataSourceSoftTracker(TMetaFile metaFile, TDataSource dataSource, ReferenceQueue<TDataSource> 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(); }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+5
-4
@@ -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<IFullDataSource> cachedFullDataSourceRef = new SoftReference<>(null);
|
||||
private DataSourceReferenceTracker.FullDataSourceSoftRef cachedFullDataSourceRef = new DataSourceReferenceTracker.FullDataSourceSoftRef(this,null);
|
||||
private final AtomicReference<CompletableFuture<IFullDataSource>> 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);
|
||||
|
||||
+4
-3
@@ -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<ColumnRenderSource> cachedRenderDataSource = new SoftReference<>(null);
|
||||
private DataSourceReferenceTracker.RenderDataSourceSoftRef cachedRenderDataSource = new DataSourceReferenceTracker.RenderDataSourceSoftRef(this, null);
|
||||
private final AtomicReference<CompletableFuture<ColumnRenderSource>> 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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user