From 2535174e00b56d4b77426e64634be13de1e81d1f Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 3 May 2025 14:34:48 -0400 Subject: [PATCH] Implement advanced caching of ingredient item stacks using soft references This gives the GC control over when to evict the cache, which combines the benefits of removing the cache entirely with the speed of keeping it --- .../faster_ingredients/IngredientMixin.java | 26 ++++++++++-- .../forge/recipe/ExtendedIngredient.java | 1 + .../IngredientItemStacksSoftReference.java | 42 +++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 forge/src/main/java/org/embeddedt/modernfix/forge/recipe/IngredientItemStacksSoftReference.java 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(); + } + } + } +}