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
This commit is contained in:
embeddedt 2025-05-03 14:34:48 -04:00
parent 7398b48345
commit 2535174e00
No known key found for this signature in database
GPG Key ID: A69433EC199B5613
3 changed files with 66 additions and 3 deletions

View File

@ -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;
}
}

View File

@ -2,4 +2,5 @@ package org.embeddedt.modernfix.forge.recipe;
public interface ExtendedIngredient {
boolean mfix$hasNoElements();
void mfix$clearReference();
}

View File

@ -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<ItemStack[]> {
private final Ingredient ingredient;
private static final ReferenceQueue<ItemStack[]> 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<? extends ItemStack[]> 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();
}
}
}
}