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); }); }