Use soft references in array pool to prevent some memory crashes

Also log if there isn't enough memory
This commit is contained in:
James Seibel
2025-01-04 09:31:42 -06:00
parent 31490f97b9
commit 9622fc3bd7
4 changed files with 157 additions and 29 deletions
@@ -1439,6 +1439,14 @@ public class Config
+ "")
.build();
public static ConfigEntry<Boolean> showPoolInsufficientMemoryWarning = new ConfigEntry.Builder<Boolean>()
.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<Boolean> showReplayWarningOnStartup = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
@@ -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<ByteArrayList> byteArrayLists = new ArrayList<>();
private final ArrayList<ShortArrayList> shortArrayLists = new ArrayList<>();
private final ArrayList<LongArrayList> longArrayLists = new ArrayList<>();
private final ArrayList<SoftReference<LongArrayList>> 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<LongArrayList> listRef)
{
this.longArrayLists.add(list);
this.longArrayRefLists.add(listRef);
}
@@ -79,6 +84,7 @@ public class PhantomArrayListCheckout implements AutoCloseable
public ArrayList<ByteArrayList> getAllByteArrays() { return this.byteArrayLists; }
public ArrayList<ShortArrayList> getAllShortArrays() { return this.shortArrayLists; }
public ArrayList<LongArrayList> getAllLongArrays() { return this.longArrayLists; }
public ArrayList<SoftReference<LongArrayList>> getAllLongArrayRefs() { return this.longArrayRefLists; }
@@ -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<ByteArrayList> pooledByteArrays = new ConcurrentLinkedQueue<>();
private final ConcurrentLinkedQueue<ShortArrayList> pooledShortArrays = new ConcurrentLinkedQueue<>();
private final ConcurrentLinkedQueue<LongArrayList> pooledLongArrays = new ConcurrentLinkedQueue<>();
private final ConcurrentLinkedQueue<SoftReference<LongArrayList>> 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 extends List<?>> T getPooledArray(ConcurrentLinkedQueue<T> pool, Supplier<T> emptyArrayCreatorFunc)
{
@@ -239,6 +263,63 @@ public class PhantomArrayListPool
return emptyArrayCreatorFunc.get();
}
}
private static <T extends List<?>> void addRefPooledArray(
ConcurrentLinkedQueue<SoftReference<T>> arrayPool,
Supplier<T> emptyArrayCreatorFunc,
Runnable arrayGarbageCollectedFunc,
BiConsumer<T, SoftReference<T>> putArrayFunc)
{
T array = null;
SoftReference<T> 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 <T extends Collection<?>> long estimateRefMemoryUsage(ConcurrentLinkedQueue<SoftReference<T>> pool, long elementSizeInBytes)
{
long longByteSize = 0;
for (SoftReference<T> 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;
}
@@ -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":