diff --git a/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/faster_ingredients/IngredientMixin.java b/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/faster_ingredients/IngredientMixin.java index 5fa2ab02..5e9b0a15 100644 --- a/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/faster_ingredients/IngredientMixin.java +++ b/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/faster_ingredients/IngredientMixin.java @@ -7,6 +7,7 @@ import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; import org.embeddedt.modernfix.forge.recipe.ExtendedIngredient; +import org.embeddedt.modernfix.forge.recipe.IngredientItemStacksSoftReference; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; @@ -31,6 +32,8 @@ public abstract class IngredientMixin implements ExtendedIngredient { @Shadow @Nullable private ItemStack[] itemStacks; + private volatile IngredientItemStacksSoftReference mfix$cachedItemStacks; + /** * @author embeddedt * @reason tag ingredients can be tested without iterating over all items @@ -95,9 +98,8 @@ public abstract class IngredientMixin implements ExtendedIngredient { /** * @author embeddedt - * @reason remove caching of the item stacks, it won't deduplicate anything with tags (since each Ingredient - * instance would make new item stacks anyway, and storing them permanently takes up a lot of memory). - * We implement an optimized version of some functions that avoids needing to call this entirely. + * @reason Change caching of item stacks to use a soft reference, which allows the GC to evict the array under + * memory pressure/when it hasn't been used. */ @Overwrite public ItemStack[] getItems() { @@ -105,6 +107,19 @@ public abstract class IngredientMixin implements ExtendedIngredient { if (this.itemStacks != null) { return this.itemStacks; } + var cache = this.mfix$cachedItemStacks; + if (cache != null) { + var stacks = cache.get(); + if (stacks != null) { + return stacks; + } + } + ItemStack[] result = computeItemsArray(); + this.mfix$cachedItemStacks = new IngredientItemStacksSoftReference((Ingredient)(Object)this, result); + return result; + } + + private ItemStack[] computeItemsArray() { // Fast path for case with one item if (this.values.length == 1) { if (this.values[0] instanceof Ingredient.ItemValue itemValue) { @@ -131,4 +146,9 @@ public abstract class IngredientMixin implements ExtendedIngredient { } return itemList.toArray(ItemStack[]::new); } + + @Override + public void mfix$clearReference() { + this.mfix$cachedItemStacks = null; + } } diff --git a/forge/src/main/java/org/embeddedt/modernfix/forge/recipe/ExtendedIngredient.java b/forge/src/main/java/org/embeddedt/modernfix/forge/recipe/ExtendedIngredient.java index db7014b4..1544cac1 100644 --- a/forge/src/main/java/org/embeddedt/modernfix/forge/recipe/ExtendedIngredient.java +++ b/forge/src/main/java/org/embeddedt/modernfix/forge/recipe/ExtendedIngredient.java @@ -2,4 +2,5 @@ package org.embeddedt.modernfix.forge.recipe; public interface ExtendedIngredient { boolean mfix$hasNoElements(); + void mfix$clearReference(); } diff --git a/forge/src/main/java/org/embeddedt/modernfix/forge/recipe/IngredientItemStacksSoftReference.java b/forge/src/main/java/org/embeddedt/modernfix/forge/recipe/IngredientItemStacksSoftReference.java new file mode 100644 index 00000000..bfb0c94a --- /dev/null +++ b/forge/src/main/java/org/embeddedt/modernfix/forge/recipe/IngredientItemStacksSoftReference.java @@ -0,0 +1,42 @@ +package org.embeddedt.modernfix.forge.recipe; + +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; + +public class IngredientItemStacksSoftReference extends SoftReference { + private final Ingredient ingredient; + + private static final ReferenceQueue QUEUE = new ReferenceQueue<>(); + private static final Thread DISCARD_THREAD = new Thread(IngredientItemStacksSoftReference::clearReferences, "Ingredient reference clearing thread"); + + static { + DISCARD_THREAD.setPriority(Thread.NORM_PRIORITY + 2); + DISCARD_THREAD.setDaemon(true); + DISCARD_THREAD.start(); + } + + public IngredientItemStacksSoftReference(Ingredient ingredient, ItemStack[] stacks) { + super(stacks, QUEUE); + this.ingredient = ingredient; + } + + private static void clearReferences() { + while (true) { + Reference ref; + try { + ref = QUEUE.remove(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + if (ref instanceof IngredientItemStacksSoftReference ingRef && ingRef.ingredient instanceof ExtendedIngredient extIng) { + // Null out the reference to the SoftReference object, to allow the SoftReference itself to be garbage collected. + extIng.mfix$clearReference(); + } + } + } +}