diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeLogic.java b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeLogic.java index 772eef9c6c0..2b97ff75302 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeLogic.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeLogic.java @@ -316,7 +316,7 @@ protected void regressRecipe() { } public @NotNull Iterator searchRecipe() { - return machine.getRecipeType().searchRecipe(machine, r -> matchRecipe(r).isSuccess()); + return machine.getRecipeType().searchRecipe(machine, r -> true); } public void findAndHandleRecipe() { diff --git a/src/main/java/com/gregtechceu/gtceu/api/recipe/lookup/RecipeDB.java b/src/main/java/com/gregtechceu/gtceu/api/recipe/lookup/RecipeDB.java index 1d9850a7236..a7862af48d3 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/recipe/lookup/RecipeDB.java +++ b/src/main/java/com/gregtechceu/gtceu/api/recipe/lookup/RecipeDB.java @@ -19,8 +19,6 @@ import com.mojang.datafixers.util.Either; import it.unimi.dsi.fastutil.objects.ObjectArrayList; -import lombok.AccessLevel; -import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.*; import java.util.*; @@ -63,7 +61,7 @@ public void clear() { if (list == null) { return null; } - return findRecursive(list, predicate); + return find(list, predicate); } /** @@ -77,7 +75,7 @@ public void clear() { @VisibleForTesting public @Nullable GTRecipe find(@NotNull List> list, @NotNull Predicate predicate) { - return findRecursive(list, predicate); + return (new RecipeIterator(this, list, predicate)).next(); } /** @@ -99,7 +97,7 @@ public void clear() { list.add(MapIngredientTypeManager.getFrom(ingredient, cap)); } }); - return findRecursive(list, predicate); + return find(list, predicate); } /** @@ -118,98 +116,6 @@ public void clear() { return new RecipeIterator(this, list, predicate); } - /** - * Recursively finds a recipe. - * - * @param ingredients the ingredients to search with - * @param predicate if the found recipe is valid - * @return the recipe - */ - private @Nullable GTRecipe findRecursive(@NotNull List> ingredients, - @NotNull Predicate predicate) { - // Try each ingredient as a starting point, adding it to the skip-list. - // The skip-list is a packed long, where each 1 bit represents an index to skip - for (int i = 0; i < ingredients.size(); i++) { - BitSet skipSet = new BitSet(ingredients.size()); - skipSet.set(i); - GTRecipe r = findRecursive(ingredients, rootBranch, predicate, i, 0, skipSet); - if (r != null) { - return r; - } - } - return null; - } - - /** - * Recursively finds a recipe by checking the current branch's nodes. - * - * @param ingredients the ingredients to search with - * @param branch the branch to search - * @param predicate if the found recipe is valid - * @param index the index of the ingredient list to check - * @param count how deep we are in recursion, < ingredients.length - * @param skip bitmask of ingredients already checked - * @return the recipe - */ - private @Nullable GTRecipe findRecursive(@NotNull List> ingredients, - @NotNull Branch branch, @NotNull Predicate predicate, - int index, int count, @NotNull BitSet skip) { - // exhausted all the ingredients, and didn't find anything - if (count == ingredients.size()) { - return null; - } - - // Iterate over current level of nodes. - for (AbstractMapIngredient obj : ingredients.get(index)) { - // determine the root nodes - var nodes = nodesForIngredient(obj, branch); - var result = nodes.get(obj); - if (result == null) { - continue; - } - // if there is a recipe (left mapping), return it immediately as found, if it can be handled - // Otherwise, recurse and go to the next branch. - GTRecipe recipe = result.map(r -> predicate.test(r) ? r : null, - b -> findRecursiveDive(ingredients, b, predicate, index, count, skip)); - if (recipe != null) { - return recipe; - } - } - return null; - } - - /** - * Recursively finds a recipe by diving deeper down a path. - * - * @param ingredients the ingredients to search with - * @param branch the branch to search - * @param predicate if the found recipe is valid - * @param index the index of the ingredient list to check - * @param count how deep we are in recursion, must be < ingredients.length - * @param skip bitmask of ingredients already checked - * @return the recipe - */ - private @Nullable GTRecipe findRecursiveDive(@NotNull List> ingredients, - @NotNull Branch branch, @NotNull Predicate predicate, - int index, int count, @NotNull BitSet skip) { - // loop through all ingredients, wrapping around the end until all are tried. - for (int i = (index + 1) % ingredients.size(); i != index; i = (i + 1) % ingredients.size()) { - if (skip.get(i)) { - continue; - } - // Recursive call - // Append the current index to the skip list - skip.set(i); - // Increase the count, so the recursion can terminate if needed (ingredients is exhausted) - GTRecipe r = findRecursive(ingredients, branch, predicate, i, count + 1, skip); - if (r != null) { - return r; - } - skip.clear(i); - } - return null; - } - /** * Converts a Recipe Capability holder's handlers into a list of {@link AbstractMapIngredient} * @@ -358,38 +264,93 @@ private boolean addRecursive(@NotNull GTRecipe recipe, return true; } - @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + private static class SearchFrame { + + int index; // ingredient slot we’re exploring + int ingredientIndex; // position within ingredients[index] + Branch branch; // branch in the recipe DB + + public SearchFrame(int index, Branch branch) { + this.index = index; + this.ingredientIndex = 0; + this.branch = branch; + } + } + public static class RecipeIterator implements Iterator { private final @NotNull RecipeDB db; private final @NotNull List> ingredients; private final @NotNull Predicate predicate; - private int index; + + private final Deque stack = new ArrayDeque<>(); + + @VisibleForTesting + public RecipeIterator(@NotNull RecipeDB db, + @NotNull List> ingredients, + @NotNull Predicate predicate) { + this.db = db; + this.ingredients = ingredients; + this.predicate = predicate; + + for (int i = ingredients.size() - 1; i >= 0; i--) { + stack.push(new SearchFrame(i, db.rootBranch)); + } + } @Override public boolean hasNext() { - return index < ingredients.size(); + return !stack.isEmpty(); } @Override - public @Nullable GTRecipe next() { - while (index < ingredients.size()) { - BitSet skipSet = new BitSet(ingredients.size()); - skipSet.set(index); - GTRecipe r = db.findRecursive(ingredients, db.rootBranch, predicate, index, 0, skipSet); - index++; - if (r != null) { - return r; + public GTRecipe next() { + while (!stack.isEmpty()) { + // We stay on one frame until all ingredients have been checked + SearchFrame frame = stack.peek(); + + if (frame.ingredientIndex >= ingredients.get(frame.index).size()) { + stack.pop(); + continue; } + + List ingredientList = ingredients.get(frame.index); + AbstractMapIngredient ingredient = ingredientList.get(frame.ingredientIndex); + // Increment candidate pos for next iteration + frame.ingredientIndex++; + var nodes = nodesForIngredient(ingredient, frame.branch); + var result = nodes.get(ingredient); + if (result == null) { + continue; + } + + // Option 1: It's a recipe + if (result.left().isPresent()) { + var recipe = result.left().get(); + if (predicate.test(recipe)) { + return recipe; + } + } + + // Option 2: It's a branch, dive deeper + result.ifRight(b -> { + for (int j = ingredients.size() - 1; j >= 0; j--) { + stack.push(new SearchFrame(j, b)); + } + }); } - return null; + + return null; // no more recipes } /** * Reset the iterator */ public void reset() { - this.index = 0; + stack.clear(); + for (int i = ingredients.size() - 1; i >= 0; i--) { + stack.push(new SearchFrame(i, db.rootBranch)); + } } } } diff --git a/src/test/java/com/gregtechceu/gtceu/api/recipe/lookup/GTRecipeLookupTest.java b/src/test/java/com/gregtechceu/gtceu/api/recipe/lookup/GTRecipeLookupTest.java index 0cde5b0ac50..dcd992640e5 100644 --- a/src/test/java/com/gregtechceu/gtceu/api/recipe/lookup/GTRecipeLookupTest.java +++ b/src/test/java/com/gregtechceu/gtceu/api/recipe/lookup/GTRecipeLookupTest.java @@ -47,7 +47,7 @@ public static void prepare(ServerLevel level) { .inputItems(Items.COBBLESTONE, 1) .outputItems(Items.STONE, 1) .buildRawRecipe(); - GTRecipe SMELT_ACACIA_WOOD = recipeType.recipeBuilder("smelt_acacia_wood") + SMELT_ACACIA_WOOD = recipeType.recipeBuilder("smelt_acacia_wood") .inputItems(Items.ACACIA_WOOD, 1) .outputItems(Items.CHARCOAL, 1) .buildRawRecipe(); diff --git a/src/test/java/com/gregtechceu/gtceu/gametest/stresstest/RecipeIteratorStressTest.java b/src/test/java/com/gregtechceu/gtceu/gametest/stresstest/RecipeIteratorStressTest.java new file mode 100644 index 00000000000..943fc583cf5 --- /dev/null +++ b/src/test/java/com/gregtechceu/gtceu/gametest/stresstest/RecipeIteratorStressTest.java @@ -0,0 +1,128 @@ +package com.gregtechceu.gtceu.gametest.stresstest; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.GTValues; +import com.gregtechceu.gtceu.api.capability.recipe.ItemRecipeCapability; +import com.gregtechceu.gtceu.api.machine.multiblock.WorkableMultiblockMachine; +import com.gregtechceu.gtceu.api.recipe.GTRecipeType; +import com.gregtechceu.gtceu.api.recipe.lookup.RecipeDB; +import com.gregtechceu.gtceu.api.recipe.lookup.ingredient.AbstractMapIngredient; +import com.gregtechceu.gtceu.api.recipe.lookup.ingredient.MapIngredientTypeManager; +import com.gregtechceu.gtceu.common.data.GTRecipeTypes; +import com.gregtechceu.gtceu.common.machine.multiblock.part.FluidHatchPartMachine; +import com.gregtechceu.gtceu.common.machine.multiblock.part.ItemBusPartMachine; +import com.gregtechceu.gtceu.gametest.util.TestUtils; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.gametest.framework.BeforeBatch; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.block.Blocks; +import net.minecraftforge.gametest.GameTestHolder; +import net.minecraftforge.gametest.PrefixGameTestTemplate; + +import java.util.ArrayList; +import java.util.List; + +import static com.gregtechceu.gtceu.gametest.util.TestUtils.getMetaMachine; + +@PrefixGameTestTemplate(false) +@GameTestHolder(GTCEu.MOD_ID) +public class RecipeIteratorStressTest { + + private static final boolean DO_RUN_RECIPE_ITERATOR_STRESSTEST = false; + + private static GTRecipeType LCR_RECIPE_TYPE; + + @BeforeBatch(batch = "StressTests") + public static void prepare(ServerLevel level) { + LCR_RECIPE_TYPE = TestUtils.createRecipeType("stress_tests", 3, 3, 3, 3); + // Force insert the recipe into the manager. + LCR_RECIPE_TYPE.getAdditionHandler().beginStaging(); + LCR_RECIPE_TYPE.getAdditionHandler().addStaging(LCR_RECIPE_TYPE + .recipeBuilder(GTCEu.id("test_multiblock_stress_tests")) + .inputItems(new ItemStack(Blocks.COBBLESTONE), new ItemStack(Blocks.ACACIA_WOOD)) + .outputItems(new ItemStack(Blocks.STONE)) + .EUt(GTValues.VA[GTValues.HV]).duration(1) + // NBT has a schematic in it with an HV energy input hatch + .buildRawRecipe()); + LCR_RECIPE_TYPE.getAdditionHandler().completeStaging(); + } + + private static BusHolder getBussesAndForm(GameTestHelper helper) { + WorkableMultiblockMachine controller = (WorkableMultiblockMachine) getMetaMachine( + helper.getBlockEntity(new BlockPos(1, 2, 0))); + TestUtils.formMultiblock(controller); + controller.setRecipeType(LCR_RECIPE_TYPE); + ItemBusPartMachine inputBus1 = (ItemBusPartMachine) getMetaMachine( + helper.getBlockEntity(new BlockPos(2, 1, 0))); + ItemBusPartMachine inputBus2 = (ItemBusPartMachine) getMetaMachine( + helper.getBlockEntity(new BlockPos(2, 2, 0))); + ItemBusPartMachine outputBus1 = (ItemBusPartMachine) getMetaMachine( + helper.getBlockEntity(new BlockPos(0, 1, 0))); + FluidHatchPartMachine outputHatch1 = (FluidHatchPartMachine) getMetaMachine( + helper.getBlockEntity(new BlockPos(0, 2, 0))); + return new BusHolder(inputBus1, inputBus2, outputBus1, outputHatch1, controller); + } + + private record BusHolder(ItemBusPartMachine inputBus1, ItemBusPartMachine inputBus2, ItemBusPartMachine outputBus1, + FluidHatchPartMachine outputHatch1, WorkableMultiblockMachine controller) {} + + @GameTest(template = "empty", batch = "StressTests") + public static void iteratorStressTest(GameTestHelper helper) { + if (!DO_RUN_RECIPE_ITERATOR_STRESSTEST) { + helper.succeed(); + return; + } + List> list = new ArrayList(); + for (var item : BuiltInRegistries.ITEM) { + list.add(MapIngredientTypeManager.getFrom(Ingredient.of(item), ItemRecipeCapability.CAP)); + } + for (var block : BuiltInRegistries.BLOCK) { + list.add(MapIngredientTypeManager.getFrom(Ingredient.of(block), ItemRecipeCapability.CAP)); + } + + long start = System.nanoTime(); + + long currentIterator = 0; + for (int i = 0; i < 20; i++) { + RecipeDB.RecipeIterator iterator = new RecipeDB.RecipeIterator(GTRecipeTypes.ASSEMBLER_RECIPES.db(), list, + (ignored) -> true); + while (iterator.hasNext()) { + var recipe = iterator.next(); + currentIterator++; + } + } + long end = System.nanoTime(); + GTCEu.LOGGER.info("current iterator recipes: " + currentIterator / 100); + GTCEu.LOGGER.info("Lookup in big tree Took " + (end - start) / 1_000_000.0 + " ms"); + helper.succeed(); + } + + @GameTest(template = "lcr_input_separation", batch = "StressTests") + public static void iteratorOnMachineStressTest(GameTestHelper helper) { + if (!DO_RUN_RECIPE_ITERATOR_STRESSTEST) { + helper.succeed(); + return; + } + + BusHolder busHolder = getBussesAndForm(helper); + busHolder.inputBus1.getInventory().setStackInSlot(0, new ItemStack(Items.COBBLESTONE)); + busHolder.inputBus1.getInventory().setStackInSlot(1, new ItemStack(Blocks.ACACIA_WOOD)); + busHolder.controller.recipeLogic.searchRecipe(); + long start = System.nanoTime(); + + for (int i = 0; i < 1000; i++) { + busHolder.controller.recipeLogic.findAndHandleRecipe(); + busHolder.controller.recipeLogic.markLastRecipeDirty(); + } + long end = System.nanoTime(); + GTCEu.LOGGER.info("On machine took " + (end - start) / 1_000_000.0 + " ms"); + helper.succeed(); + } +}