From 9622fc3bd720598a4ca474e3b980a4429af34ced Mon Sep 17 00:00:00 2001 From: James Seibel Date: Sat, 4 Jan 2025 09:31:42 -0600 Subject: [PATCH] Use soft references in array pool to prevent some memory crashes Also log if there isn't enough memory --- .../distanthorizons/core/config/Config.java | 8 + .../pooling/PhantomArrayListCheckout.java | 10 +- .../core/pooling/PhantomArrayListPool.java | 162 +++++++++++++++--- .../assets/distanthorizons/lang/en_us.json | 6 +- 4 files changed, 157 insertions(+), 29 deletions(-) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java b/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java index f46a66adb..1f5bc3600 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java @@ -1439,6 +1439,14 @@ public class Config + "") .build(); + public static ConfigEntry showPoolInsufficientMemoryWarning = new ConfigEntry.Builder() + .set(true) + .comment("" + + "If enabled, a chat message will be displayed if DH detects \n" + + "that any pooled objects have been garbage collected. \n" + + "") + .build(); + public static ConfigEntry showReplayWarningOnStartup = new ConfigEntry.Builder() .set(true) .comment("" diff --git a/core/src/main/java/com/seibel/distanthorizons/core/pooling/PhantomArrayListCheckout.java b/core/src/main/java/com/seibel/distanthorizons/core/pooling/PhantomArrayListCheckout.java index 52ebe0091..e0ceba276 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/pooling/PhantomArrayListCheckout.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/pooling/PhantomArrayListCheckout.java @@ -2,10 +2,10 @@ package com.seibel.distanthorizons.core.pooling; import com.seibel.distanthorizons.core.util.ListUtil; import it.unimi.dsi.fastutil.bytes.ByteArrayList; -import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap; import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.shorts.ShortArrayList; +import java.lang.ref.SoftReference; import java.util.ArrayList; /** @@ -23,6 +23,7 @@ public class PhantomArrayListCheckout implements AutoCloseable private final ArrayList byteArrayLists = new ArrayList<>(); private final ArrayList shortArrayLists = new ArrayList<>(); private final ArrayList longArrayLists = new ArrayList<>(); + private final ArrayList> longArrayRefLists = new ArrayList<>(); @@ -43,7 +44,11 @@ public class PhantomArrayListCheckout implements AutoCloseable public void addByteArrayList(ByteArrayList list) { this.byteArrayLists.add(list); } public void addShortArrayList(ShortArrayList list) { this.shortArrayLists.add(list); } - public void addLongArrayList(LongArrayList list) { this.longArrayLists.add(list); } + public void addLongArrayListRef(LongArrayList list, SoftReference listRef) + { + this.longArrayLists.add(list); + this.longArrayRefLists.add(listRef); + } @@ -79,6 +84,7 @@ public class PhantomArrayListCheckout implements AutoCloseable public ArrayList getAllByteArrays() { return this.byteArrayLists; } public ArrayList getAllShortArrays() { return this.shortArrayLists; } public ArrayList getAllLongArrays() { return this.longArrayLists; } + public ArrayList> getAllLongArrayRefs() { return this.longArrayRefLists; } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/pooling/PhantomArrayListPool.java b/core/src/main/java/com/seibel/distanthorizons/core/pooling/PhantomArrayListPool.java index f87bb0830..e17fd16b5 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/pooling/PhantomArrayListPool.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/pooling/PhantomArrayListPool.java @@ -1,25 +1,29 @@ package com.seibel.distanthorizons.core.pooling; +import com.seibel.distanthorizons.core.api.internal.ClientApi; +import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.f3.F3Screen; +import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.coreapi.util.StringUtil; import it.unimi.dsi.fastutil.bytes.ByteArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.shorts.ShortArrayList; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiConsumer; import java.util.function.Supplier; /** @@ -57,6 +61,9 @@ public class PhantomArrayListPool private static final boolean LOG_ARRAY_RECOVERY = false; + private static boolean lowMemoryWarningLogged = false; + + /** used for debugging and tracking what the pool contains */ public final String name; @@ -70,7 +77,7 @@ public class PhantomArrayListPool private final ConcurrentLinkedQueue pooledByteArrays = new ConcurrentLinkedQueue<>(); private final ConcurrentLinkedQueue pooledShortArrays = new ConcurrentLinkedQueue<>(); - private final ConcurrentLinkedQueue pooledLongArrays = new ConcurrentLinkedQueue<>(); + private final ConcurrentLinkedQueue> pooledLongArrays = new ConcurrentLinkedQueue<>(); /** counts how many byte arrays have been created by this pool */ private final AtomicInteger totalByteArrayCountRef = new AtomicInteger(0); @@ -86,6 +93,9 @@ public class PhantomArrayListPool /** used for debugging, represents an estimate for how many bytes the long[] pool contains */ private long lastLongPoolSizeInBytes = -1; + /** For pools backed by {@link SoftReference}'s we may need to decrease the size when elements are garbage collected */ + private boolean clearLastRefPoolSizes = false; + //=============// @@ -194,7 +204,11 @@ public class PhantomArrayListPool // long for (int i = 0; i < longArrayCount; i++) { - checkout.addLongArrayList(getPooledArray(this.pooledLongArrays, () -> this.createEmptyLongArrayList())); + addRefPooledArray( + this.pooledLongArrays, + this::createEmptyLongArrayList, + this::onLongArrayListGarbageCollected, + checkout::addLongArrayListRef); } return checkout; @@ -215,7 +229,7 @@ public class PhantomArrayListPool this.totalShortArrayCountRef.getAndIncrement(); return new ShortArrayList(0); } - public LongArrayList createEmptyLongArrayList() + private LongArrayList createEmptyLongArrayList() { //LOGGER.error("created new long array"); this.totalLongArrayCountRef.getAndIncrement(); @@ -223,7 +237,17 @@ public class PhantomArrayListPool } - // internal pool handler // + // garbage collection handlers // + + /** should only happen if Java doesn't have enough memory */ + private void onLongArrayListGarbageCollected() + { + this.clearLastRefPoolSizes = true; + this.totalLongArrayCountRef.getAndDecrement(); + } + + + // internal pool handlers // private static > T getPooledArray(ConcurrentLinkedQueue pool, Supplier emptyArrayCreatorFunc) { @@ -239,6 +263,63 @@ public class PhantomArrayListPool return emptyArrayCreatorFunc.get(); } } + private static > void addRefPooledArray( + ConcurrentLinkedQueue> arrayPool, + Supplier emptyArrayCreatorFunc, + Runnable arrayGarbageCollectedFunc, + BiConsumer> putArrayFunc) + { + T array = null; + SoftReference arrayRef = arrayPool.poll(); + + // find the first non-null pooled array + while (arrayRef != null && array == null) + { + array = arrayRef.get(); + if (array == null) + { + // this reference is pointing to null, + // the array must have been garbage collected, + // that means we don't have enough memory + if (!lowMemoryWarningLogged) + { + lowMemoryWarningLogged = true; + + // orange text + String message = "\u00A76" + "Distant Horizons: Insufficient memory detected." + "\u00A7r \n" + + "This may cause stuttering or crashing. \n" + + "Either: your allocated memory isn't high enough, \n" + + "your DH CPU preset is too high, or your DH quality preset is too high."; + + LOGGER.warn(message); + if (Config.Common.Logging.Warning.showPoolInsufficientMemoryWarning.get()) + { + ClientApi.INSTANCE.showChatMessageNextFrame(message); + } + } + + arrayGarbageCollectedFunc.run(); + + // try the next reference + arrayRef = arrayPool.poll(); + } + } + + + if (array != null) + { + LodUtil.assertTrue(arrayRef != null, "How did we get an array without it's reference?"); + array.clear(); + } + else + { + // no pooled sources exist + array = emptyArrayCreatorFunc.get(); + arrayRef = new SoftReference<>(array); + } + + putArrayFunc.accept(array, arrayRef); + } @@ -260,7 +341,7 @@ public class PhantomArrayListPool this.pooledByteArrays.addAll(checkout.getAllByteArrays()); this.pooledShortArrays.addAll(checkout.getAllShortArrays()); - this.pooledLongArrays.addAll(checkout.getAllLongArrays()); + this.pooledLongArrays.addAll(checkout.getAllLongArrayRefs()); //LOGGER.info("Returned ["+checkout.byteArrayLists.size()+"/"+this.pooledByteArrays.size()+"] bytes and ["+checkout.longArrayLists.size()+"/"+this.pooledLongArrays.size()+"] longs.");\ } @@ -379,7 +460,12 @@ public class PhantomArrayListPool this.lastShortPoolSizeInBytes = Math.max(shortPoolByteSize, this.lastShortPoolSizeInBytes); // long - long longPoolByteSize = estimateMemoryUsage(this.pooledLongArrays, Long.BYTES); + if (this.clearLastRefPoolSizes) + { + this.lastLongPoolSizeInBytes = 0; + this.clearLastRefPoolSizes = false; + } + long longPoolByteSize = estimateRefMemoryUsage(this.pooledLongArrays, Long.BYTES); this.lastLongPoolSizeInBytes = Math.max(longPoolByteSize, this.lastLongPoolSizeInBytes); } @@ -391,29 +477,53 @@ public class PhantomArrayListPool // Object overhead + capacity of underlying array * size of Long (8 bytes) long overhead = Byte.SIZE * 4; - long elementCount; - if (array instanceof ByteArrayList) - { - elementCount = ((ByteArrayList)array).elements().length; - } - else if (array instanceof ShortArrayList) - { - elementCount = ((ShortArrayList)array).elements().length; - } - else if (array instanceof LongArrayList) - { - elementCount = ((LongArrayList)array).elements().length; - } - else - { - throw new UnsupportedOperationException("Not implemented for type ["+array.getClass().getSimpleName()+"]."); - } - + long elementCount = getCollectionCount(array); long arraySize = elementCount * elementSizeInBytes; longByteSize += overhead + arraySize; } return longByteSize; } + private static > long estimateRefMemoryUsage(ConcurrentLinkedQueue> pool, long elementSizeInBytes) + { + long longByteSize = 0; + for (SoftReference arrayRef : pool) + { + // Object overhead + capacity of underlying array * size of Long (8 bytes) + long overhead = Byte.SIZE * 4; + T array = arrayRef.get(); + if (array == null) + { + continue; + } + + long elementCount = getCollectionCount(array); + long arraySize = elementCount * elementSizeInBytes; + longByteSize += overhead + arraySize; + } + return longByteSize; + } + private static long getCollectionCount(@NotNull Collection array) + { + long elementCount; + if (array instanceof ByteArrayList) + { + elementCount = ((ByteArrayList)array).elements().length; + } + else if (array instanceof ShortArrayList) + { + elementCount = ((ShortArrayList)array).elements().length; + } + else if (array instanceof LongArrayList) + { + elementCount = ((LongArrayList)array).elements().length; + } + else + { + throw new UnsupportedOperationException("Not implemented for type ["+array.getClass().getSimpleName()+"]."); + } + + return elementCount; + } diff --git a/core/src/main/resources/assets/distanthorizons/lang/en_us.json b/core/src/main/resources/assets/distanthorizons/lang/en_us.json index 15eb13031..1c0d1af44 100644 --- a/core/src/main/resources/assets/distanthorizons/lang/en_us.json +++ b/core/src/main/resources/assets/distanthorizons/lang/en_us.json @@ -643,7 +643,11 @@ "distanthorizons.config.common.logging.warning": "Warnings", "distanthorizons.config.common.logging.warning.showLowMemoryWarningOnStartup": - "Show Low Memory Warning", + "Show Low Memory Warning On Startup", + "distanthorizons.config.common.logging.warning.showPoolInsufficientMemoryWarning": + "Show Pool Insufficient Memory Warning", + "distanthorizons.config.common.logging.warning.showPoolInsufficientMemoryWarning.@tooltip": + "If DH detects that pooled objects are being garbage collected this will send a chat warning.", "distanthorizons.config.common.logging.warning.showReplayWarningOnStartup": "Show Replay Warning", "distanthorizons.config.common.logging.warning.showUpdateQueueOverloadedChatWarning":