Add DataSourceReferenceTracker to automatically free unused data sources

This commit is contained in:
James Seibel
2023-09-12 20:16:32 -05:00
parent 13b7a20ff6
commit 03f50b168d
4 changed files with 247 additions and 7 deletions
@@ -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();
}
}
@@ -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(); }
}
}
@@ -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);
@@ -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);
});
}