From 6bcfc8e17beb0d807c954aa74d34513872c710bc Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 19:36:43 +0000 Subject: [PATCH 1/5] Look crafting table recipes up through the recipe cache CraftingHelpers.findServerRecipe goes straight to RecipeManager.getRecipeFor, which is a linear scan over every crafting recipe in the game that calls matches() on each one. Every craft does at least two of those lookups: one to simulate the craft, and one to actually perform it. findRecipeCached is CyclopsCore's cached equivalent, and is already what the crafting interface's own recipe validation ends up using through RecipeHandlerRecipeType. Its input is copied into the cache key, so the item stacks of the grid can not corrupt an entry afterwards. This was 9% of the mod's samples in a profile of the benchmark suite. It is also the part of a craft that scales with the number of installed recipes rather than with the amount of crafting, so it grows in a modpack. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MxquU3sDjDpKbeHrVyrV1t --- .../CraftingProcessOverrideCraftingTable.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/cyclops/integratedcrafting/core/crafting/processoverride/CraftingProcessOverrideCraftingTable.java b/src/main/java/org/cyclops/integratedcrafting/core/crafting/processoverride/CraftingProcessOverrideCraftingTable.java index 484d76038..492d6c4c9 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/crafting/processoverride/CraftingProcessOverrideCraftingTable.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/crafting/processoverride/CraftingProcessOverrideCraftingTable.java @@ -60,11 +60,14 @@ public boolean craft(Function, PartPos> targetGetter, CraftingInput gridInput = gridFull.asCraftInput(); Level level = target.getPos().getLevel(true); - return CraftingHelpers.findServerRecipe(RecipeType.CRAFTING, gridInput, level) + // Look the recipe up via the cache, as an uncached lookup is a linear scan over every crafting recipe. + // This method is called at least twice for every craft: + // once to simulate the craft, and once to actually perform it. + return CraftingHelpers.findRecipeCached(RecipeType.CRAFTING, gridInput, level, false) .or(() -> { try { CraftingGrid gridSmall = new CraftingGrid(ingredients, 2, 2); - return CraftingHelpers.findServerRecipe(RecipeType.CRAFTING, gridSmall.asCraftInput(), level); + return CraftingHelpers.findRecipeCached(RecipeType.CRAFTING, gridSmall.asCraftInput(), level, false); } catch (IllegalArgumentException e) { // This can occur if the ingredients don't fit in a 2x2 grid. return Optional.empty(); From 0551473d1c2af29563fc2fb3ab1e6136ce591840 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 19:37:30 +0000 Subject: [PATCH 2/5] Stop aggregating network capacity on every recipe input evaluation The "quickly return if the storage is empty" shortcut in getIngredientRecipeInputs led with storage.getMaxQuantity() == 0. For a network channel that is not a cheap call: IngredientChannelAdapter.getMaxQuantity() aggregates the capacity of every storage position in the channel, costing a capability lookup and a slot enumeration per position. It was paid on every call, for every input component of every candidate recipe, while the shortcut it guards can only trigger for a network that has no storage at all. A channel that is indexed and holds at least one instance is guaranteed to have capacity, so its index now answers this without touching any position. The condition is otherwise unchanged: an empty index still consults getMaxQuantity(). Skipping the call also skips the observation it incidentally scheduled, which is harmless, as the extraction that follows schedules one itself. This was 6% of the mod's samples in a profile of the benchmark suite. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MxquU3sDjDpKbeHrVyrV1t --- .../core/CraftingHelpers.java | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java b/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java index 187a84dcc..35cccac3a 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java @@ -37,7 +37,6 @@ import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.IntStream; /** * Helpers related to handling crafting jobs. @@ -986,6 +985,45 @@ public static List getIngredientRecipeInputs(IIngredientComponentStora extractionMemoryReusable, collectMissingIngredients, recipeOutputQuantity, false); } + /** + * Check if the given storage has no capacity at all, and can therefore never contain anything. + * + * {@link IIngredientComponentStorage#getMaxQuantity()} is not a cheap call for a network channel: + * it aggregates the capacity of every storage position in the channel, + * which costs a capability lookup and a slot enumeration per position. + * A channel that is indexed and holds at least one instance is guaranteed to have capacity, + * so its index can answer this question without touching any position. + * + * @param storage A storage. + * @param The instance type. + * @param The matching condition parameter. + * @return If the storage has no capacity. + */ + protected static boolean hasNoStorageCapacity(IIngredientComponentStorage storage) { + if (storage instanceof IngredientChannelIndexed + && !((IngredientChannelIndexed) storage).getIndex().isEmpty()) { + return false; + } + return storage.getMaxQuantity() == 0; + } + + /** + * @param recipe A recipe. + * @param ingredientComponent An ingredient component type. + * @param The instance type. + * @param The matching condition parameter. + * @return If at least one of the recipe's inputs of the given component type is reusable. + */ + protected static boolean hasReusableInput(IRecipeDefinition recipe, IngredientComponent ingredientComponent) { + int inputCount = recipe.getInputs(ingredientComponent).size(); + for (int i = 0; i < inputCount; i++) { + if (recipe.isInputReusable(ingredientComponent, i)) { + return true; + } + } + return false; + } + public static Pair, MissingIngredients> getIngredientRecipeInputs(IIngredientComponentStorage storage, IngredientComponent ingredientComponent, IRecipeDefinition recipe, boolean simulate, @@ -997,10 +1035,9 @@ public static List getIngredientRecipeInputs(IIngredientComponentStora // Quickly return if the storage is empty // We can't take this shortcut if we have a reusable ingredient AND extractionMemoryReusable is not empty - if (storage.getMaxQuantity() == 0 && + if (hasNoStorageCapacity(storage) && extractionMemoryReusable.isEmpty() && - IntStream.range(0, recipe.getInputs(ingredientComponent).size()) - .noneMatch(i -> recipe.isInputReusable(ingredientComponent, i))) { + !hasReusableInput(recipe, ingredientComponent)) { if (collectMissingIngredients) { List> recipeInputs = recipe.getInputs(ingredientComponent); MissingIngredients missing = new MissingIngredients<>(recipeInputs.stream().map(IPrototypedIngredientAlternatives::getAlternatives) From 14f228b158a45c0cfd2615d552cd4cbc7fa434bd Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 19:37:42 +0000 Subject: [PATCH 3/5] Classify the crafting job index by ingredient category Crafting writers ask the index on every tick whether an instance is already being crafted, using a quantity-less match condition. A plain IngredientHashMap can not hash that condition, so it answered by filtering over every crafting job in the network. Use IngredientMapSingleClassified, exactly as RecipeIndexDefault already does, so that such a lookup narrows to the jobs producing that item first. This was 5% of the mod's samples in a profile of the benchmark suite, and it scales with the number of crafting jobs a network is running. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MxquU3sDjDpKbeHrVyrV1t --- .../integratedcrafting/core/CraftingJobIndexDefault.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobIndexDefault.java b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobIndexDefault.java index d1310ca73..b3a8e1d8e 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobIndexDefault.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobIndexDefault.java @@ -9,6 +9,7 @@ import org.cyclops.cyclopscore.datastructure.MultitransformIterator; import org.cyclops.cyclopscore.ingredient.collection.IIngredientMapMutable; import org.cyclops.cyclopscore.ingredient.collection.IngredientHashMap; +import org.cyclops.cyclopscore.ingredient.collection.IngredientMapSingleClassified; import org.cyclops.integratedcrafting.api.crafting.CraftingJob; import org.cyclops.integratedcrafting.api.recipe.ICraftingJobIndex; import org.cyclops.integratedcrafting.api.recipe.ICraftingJobIndexModifiable; @@ -57,7 +58,13 @@ public CraftingJob getCraftingJob(int craftingJobId) { @Nullable protected IIngredientMapMutable> initializeIndex(IngredientComponent recipeComponent) { - return new IngredientHashMap<>(recipeComponent); + // Classify by the component's primary category, just like RecipeIndexDefault does. + // Lookups in this index are done with quantity-less match conditions, + // which a plain hash map can only answer by filtering over every indexed crafting job. + if (recipeComponent.getCategoryTypes().size() == 1) { + return new IngredientHashMap<>(recipeComponent); + } + return new IngredientMapSingleClassified<>(recipeComponent, () -> new IngredientHashMap<>(recipeComponent), recipeComponent.getCategoryTypes().get(0)); } @Override From ccd3929da3428644d16001f6b992c64b9e8f7059 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 19:37:53 +0000 Subject: [PATCH 4/5] Resolve crafting job dependency edges without boxing getDependencies and getDependents allocated a throwaway IntArrayList for the common case of a job without edges, and routed the int ids they do have through a stream that boxes every one of them. getDependents runs for every completed crafting job entry. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MxquU3sDjDpKbeHrVyrV1t --- .../crafting/CraftingJobDependencyGraph.java | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/cyclops/integratedcrafting/api/crafting/CraftingJobDependencyGraph.java b/src/main/java/org/cyclops/integratedcrafting/api/crafting/CraftingJobDependencyGraph.java index 79b207b44..d94287320 100644 --- a/src/main/java/org/cyclops/integratedcrafting/api/crafting/CraftingJobDependencyGraph.java +++ b/src/main/java/org/cyclops/integratedcrafting/api/crafting/CraftingJobDependencyGraph.java @@ -1,5 +1,6 @@ package org.cyclops.integratedcrafting.api.crafting; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; @@ -16,9 +17,9 @@ import javax.annotation.Nullable; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; /** * A CraftingJobDependencyGraph stores dependencies between crafting jobs based on their unique ID. @@ -52,11 +53,7 @@ public CraftingJob getCraftingJob(int id) { } public Collection getDependencies(CraftingJob craftingJob) { - return dependencies.getOrDefault(craftingJob.getId(), new IntArrayList()) - .stream() - .map(craftingJobs::get) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + return getCraftingJobs(dependencies.get(craftingJob.getId())); } public boolean hasDependencies(CraftingJob craftingJob) { @@ -77,11 +74,27 @@ public boolean hasDependencies(int craftingJobId) { } public Collection getDependents(CraftingJob craftingJob) { - return dependents.getOrDefault(craftingJob.getId(), new IntArrayList()) - .stream() - .map(craftingJobs::get) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + return getCraftingJobs(dependents.get(craftingJob.getId())); + } + + /** + * Resolve the given crafting job ids into their crafting jobs, skipping the ids that are unknown. + * @param craftingJobIds Crafting job ids, may be null if no ids are stored. + * @return A new collection with the resolved crafting jobs. + */ + protected Collection getCraftingJobs(@Nullable IntCollection craftingJobIds) { + if (craftingJobIds == null || craftingJobIds.isEmpty()) { + return Collections.emptyList(); + } + List resolved = Lists.newArrayListWithCapacity(craftingJobIds.size()); + IntIterator it = craftingJobIds.iterator(); + while (it.hasNext()) { + CraftingJob craftingJob = this.craftingJobs.get(it.nextInt()); + if (craftingJob != null) { + resolved.add(craftingJob); + } + } + return resolved; } public void addCraftingJobId(CraftingJob craftingJob) { From 1b654585f3f1b8d5f99dba7898c3719106eaa0b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 19:38:04 +0000 Subject: [PATCH 5/5] Skip the crafting network lookup of an idle crafting interface CraftingJobHandler.update looked the crafting network capability up to find a job to start, even when the handler has no pending jobs at all, which is what every idle crafting interface does on every tick. It also repeated that same lookup for every finished job instead of once. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MxquU3sDjDpKbeHrVyrV1t --- .../cyclops/integratedcrafting/core/CraftingJobHandler.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java index e72a8d591..f38d56de7 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java @@ -429,10 +429,10 @@ public void update(INetwork network, int channel, PartPos targetPos) { // Notify the network of finalized crafting jobs if (finishedCraftingJobs.size() > 0) { + ICraftingNetwork craftingNetwork = CraftingHelpers.getCraftingNetworkChecked(network); for (CraftingJob finishedCraftingJob : finishedCraftingJobs.values()) { if (finishedCraftingJob.getAmount() == 0) { // If the job is fully finished, remove it from the network - ICraftingNetwork craftingNetwork = CraftingHelpers.getCraftingNetworkChecked(network); craftingNetwork.onCraftingJobFinished(finishedCraftingJob); allCraftingJobs.remove(finishedCraftingJob.getId()); nonBlockingJobsRunningAmount.remove(finishedCraftingJob.getId()); @@ -471,7 +471,9 @@ public void update(INetwork network, int channel, PartPos targetPos) { } } - if (processingJobs < this.maxProcessingJobs) { + // Only look for a job to start if this handler has room for one, and has something to start. + // Skipping this block for an idle handler avoids a crafting network lookup for every idle tick. + if (processingJobs < this.maxProcessingJobs && !this.pendingCraftingJobs.isEmpty()) { // Handle crafting jobs CraftingJob startingCraftingJob = null; ICraftingNetwork craftingNetwork = CraftingHelpers.getCraftingNetworkChecked(network);