Speed up crafting job scheduling and execution - #218
Merged
rubensworks merged 5 commits intoAug 29, 2026
Conversation
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MxquU3sDjDpKbeHrVyrV1t
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MxquU3sDjDpKbeHrVyrV1t
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MxquU3sDjDpKbeHrVyrV1t
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MxquU3sDjDpKbeHrVyrV1t
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MxquU3sDjDpKbeHrVyrV1t
rubensworks
force-pushed
the
claude/performance-optimizations-44692n
branch
from
August 26, 2026 19:40
1885c91 to
1b65458
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uses the performance benchmarks added in #217 to find and remove the largest avoidable costs in auto-crafting. A JFR profile of the whole benchmark suite showed that the mod's own time was concentrated in a few spots that do work proportional to something other than the amount of crafting.
Changes
Look crafting table recipes up through the cache (
CraftingProcessOverrideCraftingTable).CraftingHelpers.findServerRecipegoes straight toRecipeManager.getRecipeFor, which is a linear scan over every crafting recipe in the game, callingmatches()on each one. Every craft does at least two of those lookups: one to simulate the craft, and one to actually perform it.findRecipeCachedis CyclopsCore's cached equivalent, already used byRecipeHandlerRecipeType(and therefore by the crafting interface's own recipe validation). This was 9% of the mod's samples in the profile, and it scales with the number of installed recipes rather than with the amount of crafting, so the win grows in a modpack.Stop asking the network channel for its total capacity on every recipe input evaluation (
CraftingHelpers.getIngredientRecipeInputs).The "quickly return if the storage is empty" shortcut 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, and it was paid on every call for every input component of every candidate recipe. An indexed channel that 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: when the index is empty,getMaxQuantity()is still consulted. Skipping the call also skips the observation it incidentally scheduled, which is harmless because the extraction that follows schedules one itself.Classify the crafting job index (
CraftingJobIndexDefault).Crafting writers ask every tick whether an instance is already being crafted, using a quantity-less match condition. A plain
IngredientHashMapcannot hash that, so it answered by filtering over every crafting job in the network. UsingIngredientMapSingleClassified— exactly whatRecipeIndexDefaultalready does — narrows the lookup to the jobs producing that item first.Two smaller ones.
CraftingJobDependencyGraph.getDependencies/getDependentsno longer allocate a throwawayIntArrayListfor the empty case and no longer route int ids through a boxing stream;getDependentsruns for every completed craft entry. AndCraftingJobHandler.updateno longer looks the crafting network up when the handler has nothing pending, which is what every idle crafting interface does on every tick, and it no longer repeats that lookup per finished job.Measurements
Average network tick time in ms, two runs per revision, lower is better:
craft_simplecraft_recipe_indexcraft_nestedThe idle and churn presets stay within run-to-run noise, which is expected: they schedule no jobs and execute no crafts. Server tick time is dominated by chunk ticking on the test machine and is too noisy at this scale to read anything into.
Notes
The recipe cache holds entries for a minute and is not invalidated by a datapack reload, so a
/reloadcan take up to a minute to be reflected in crafting table process overrides. The crafting interface's recipe validation already goes through the same cache, so this is not a new property of the mod.Testing
All 49 game tests pass, as do the unit tests and
spotlessCheck.🤖 Generated with Claude Code
https://claude.ai/code/session_01MxquU3sDjDpKbeHrVyrV1t
Generated by Claude Code