diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/pooling/PhantomLoggingHelper.java b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/pooling/PhantomLoggingHelper.java index 022708a51..4adeab225 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/pooling/PhantomLoggingHelper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/pooling/PhantomLoggingHelper.java @@ -1,9 +1,17 @@ package com.seibel.distanthorizons.core.util.objects.pooling; import com.seibel.distanthorizons.core.logging.DhLogger; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.logging.f3.F3Screen; +import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.objects.Pair; +import java.lang.ref.PhantomReference; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; import java.util.ArrayList; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.atomic.AtomicInteger; public class PhantomLoggingHelper @@ -57,4 +65,168 @@ public class PhantomLoggingHelper + //================// + // helper classes // + //================// + //region + + /** + * Can be quickly added to a {@link AutoCloseable} implementing + * class to confirm it's being properly closed. + */ + public static class BasicPhantomReference implements AutoCloseable + { + private static final DhLogger LOGGER = new DhLoggerBuilder().build(); + + /** if enabled the number of GC'ed buffers will be logged */ + private static final boolean LOG_PHANTOM_RECOVERY = true; + /** + * If enabled the GC'ed buffers allocation/upload stacks will be logged. + * Note: due to how the buffers are often run on the render thread, + * these stacks will likely only be of limited use. + * For more robust debugging it would likely be best to somehow track + * the stacks of where these calls are happening before they're queued + * for the render thread. + */ + private static final boolean LOG_PHANTOM_ALLOCATION_STACKS = true; + + private static final int PHANTOM_REF_CHECK_TIME_IN_MS = 5 * 1000; + private static final ReferenceQueue PHANTOM_REFERENCE_QUEUE = new ReferenceQueue<>(); + private static final ConcurrentHashMap, Class> PHANTOM_TO_PARENT_CLASS = new ConcurrentHashMap<>(); + + private static final ThreadPoolExecutor CLEANUP_THREAD = ThreadUtil.makeSingleDaemonThreadPool("BasicPhantom Cleanup"); + + + private final Class parentClass; + private final PhantomReference phantomReference; + + + + //==============// + // constructors // + //==============// + //region + + static { CLEANUP_THREAD.execute(() -> runPhantomReferenceCleanupLoop()); } + + public BasicPhantomReference(Class parentClass) + { + this.parentClass = parentClass; + this.phantomReference = new PhantomReference<>(this, PHANTOM_REFERENCE_QUEUE); + PHANTOM_TO_PARENT_CLASS.put(this.phantomReference, this.parentClass); + } + + //endregion + + + + //================// + // base overrides // + //================// + //region + + @Override + public void close() + { + this.phantomReference.clear(); + PHANTOM_TO_PARENT_CLASS.remove(this.phantomReference); + } + + //endregion + + + + //================// + // static cleanup // + //================// + //region + + private static void runPhantomReferenceCleanupLoop() + { + // these arrays are stored here so they don't have to be re-allocated each loop + ArrayList> allocationStackTraceCountPairList = new ArrayList<>(); + ArrayList> parentClassNameCountPairList = new ArrayList<>(); + + while (true) + { + allocationStackTraceCountPairList.clear(); + parentClassNameCountPairList.clear(); + + try + { + try + { + Thread.sleep(PHANTOM_REF_CHECK_TIME_IN_MS); + } + catch (InterruptedException ignore) { } + + int collectedCount = 0; + + Reference phantomRef = PHANTOM_REFERENCE_QUEUE.poll(); + while (phantomRef != null) + { + // destroy the buffer if it hasn't been cleared yet + Class parentClass = PHANTOM_TO_PARENT_CLASS.remove((PhantomReference)phantomRef); // cast to make IntelliJ happy + { + String parentClassName = "NULL"; + if (parentClass != null) + { + parentClassName = parentClass.getSimpleName(); + } + + PhantomLoggingHelper.putAndIncrementTrackingString(parentClassName, parentClassNameCountPairList); + //LOGGER.info("Phantom collected for class: [" + parentClassName + "]"); + } + + + //if (LOG_PHANTOM_ALLOCATION_STACKS) // stack trace shouldn't be null, but just in case + //{ + // String stack = BUFFER_ID_TO_ALLOCATION_STRING.get(idRef); + // if (stack != null) + // { + // PhantomLoggingHelper.putAndIncrementTrackingString(stack, allocationStackTraceCountPairList); + // } + //} + + + collectedCount++; + phantomRef = PHANTOM_REFERENCE_QUEUE.poll(); + } + + + + if (LOG_PHANTOM_RECOVERY) + { + // we only want to log when something has been returned + if (collectedCount != 0) + { + LOGGER.warn("Phantoms collected: ["+ F3Screen.NUMBER_FORMAT.format(collectedCount)+"]."); + + PhantomLoggingHelper.LogAllocationStackTracePairCounts(LOGGER, parentClassNameCountPairList); + + //// log stack traces if present + //if (LOG_PHANTOM_ALLOCATION_STACKS) + //{ + // PhantomLoggingHelper.LogAllocationStackTracePairCounts(LOGGER, allocationStackTraceCountPairList); + //} + } + } + + } + catch (Exception e) + { + LOGGER.error("Unexpected error in buffer cleanup thread: [" + e.getMessage() + "].", e); + } + } + } + + //endregion + + + } + + //endregion + + + }