Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ protected void regressRecipe() {
}

public @NotNull Iterator<GTRecipe> searchRecipe() {
return machine.getRecipeType().searchRecipe(machine, r -> matchRecipe(r).isSuccess());
return machine.getRecipeType().searchRecipe(machine, r -> true);
}

public void findAndHandleRecipe() {
Expand Down
181 changes: 71 additions & 110 deletions src/main/java/com/gregtechceu/gtceu/api/recipe/lookup/RecipeDB.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -63,7 +61,7 @@ public void clear() {
if (list == null) {
return null;
}
return findRecursive(list, predicate);
return find(list, predicate);
}

/**
Expand All @@ -77,7 +75,7 @@ public void clear() {
@VisibleForTesting
public @Nullable GTRecipe find(@NotNull List<List<AbstractMapIngredient>> list,
@NotNull Predicate<GTRecipe> predicate) {
return findRecursive(list, predicate);
return (new RecipeIterator(this, list, predicate)).next();
}

/**
Expand All @@ -99,7 +97,7 @@ public void clear() {
list.add(MapIngredientTypeManager.getFrom(ingredient, cap));
}
});
return findRecursive(list, predicate);
return find(list, predicate);
}

/**
Expand All @@ -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<List<AbstractMapIngredient>> ingredients,
@NotNull Predicate<GTRecipe> 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<List<AbstractMapIngredient>> ingredients,
@NotNull Branch branch, @NotNull Predicate<GTRecipe> 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<List<AbstractMapIngredient>> ingredients,
@NotNull Branch branch, @NotNull Predicate<GTRecipe> 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}
*
Expand Down Expand Up @@ -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<GTRecipe> {

private final @NotNull RecipeDB db;
private final @NotNull List<List<AbstractMapIngredient>> ingredients;
private final @NotNull Predicate<GTRecipe> predicate;
private int index;

private final Deque<SearchFrame> stack = new ArrayDeque<>();

@VisibleForTesting
public RecipeIterator(@NotNull RecipeDB db,
@NotNull List<List<AbstractMapIngredient>> ingredients,
@NotNull Predicate<GTRecipe> 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<AbstractMapIngredient> 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));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AbstractMapIngredient>> 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();
}
}