-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize startup time #97
base: 1.21.1
Are you sure you want to change the base?
Conversation
@@ -60,20 +60,35 @@ public boolean shouldIgnoreRecipe(RecipeLink recipe) { | |||
* @return True if the recipe type is ignored, false otherwise | |||
*/ | |||
private boolean isRecipeTypeIgnored(RecipeLink recipe) { | |||
return ignoredRecipeTypesCache.computeIfAbsent(recipe.getType(), type -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this gets called so much that the capturing lambda actually caused a minor performance hit, so I turned this computeIfAbsent
into a regular get/put
sequence
Set<String> compareFields = curRecipe.getActual().keySet(); | ||
if (!settings.ignoredFields.isEmpty()) { | ||
compareFields = new HashSet<>(compareFields); | ||
compareFields.removeAll(settings.ignoredFields); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking the ignored fields happens so frequently that it slows down loading.
It is much faster to do it once here and keeping a list of the fields we need to compare.
Thanks a lot for this pull request and the explanation. Since I don't have a lot of time right now, it might take me a bit to look at this more closely. A lot of this is new to me. I would have assumed the JIT optimizes things like the lambda. And I also need to learn how to read flame graphs. 😄 About the equality checks of The comparison also has the huge advantage of simplicity. With a proper API introduced in 1.0, mod authors can register their own custom unifiers that tell Almost Unified how to treat their recipe JSONs. Since we compare the JSON afterward, we can tell whether something changed without it being a burden for mod authors to track whether their unifier changed something. |
No rush! A flame graph is like a stack trace. The bottom is the first parent method, and then on top of it are the children methods called from it. The capturing lambda is pretty fast in Java but it has to create something like an object in order to store (capture) the current state outside of itself when it is created, in order to access the variables it wants to use when it is called. Normally you can ignore the performance hit, it really doesn't matter, but in this specific case it's being called an incredible number of times so it actually has some measurable impact.
We can do better than the Gson implementation because we already know the actual type of the values after parsing the Json, so we can create and do comparisons on a simple |
updated to resolve merge conflicts |
@@ -270,6 +268,17 @@ public String getName() { | |||
} | |||
} | |||
|
|||
public record CompareContext(CompareSettings settings, List<String> compareFields) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a specific reason you are using a list instead of a set here? Could even be a collection or iterable only. I am just asking because inside the create
method, you retrieve the recipe key set and when the ignored fields are not empty, you copy the collection twice. Once inside the new HashSet
and then with List.copyOf
inside the CTor call. Is is because you want to make it immutable again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR is focused on efficiency of the hot path, which in this case (according to profiling) is the path that iterates over the compareFields
. We can sloppily make lots of copies elsewhere and it'll take less than 1ms in total, but if we iterate over a HashSet
in a hot loop it will take longer than a List
for example, so the main requirement is to enforce compareFields
is a type that is fast to iterate over.
The Set
is used earlier because it's a quick way to remove any duplicates and remove the ignored fields. You can do the same thing in many ways but there are tradeoffs between copying, contains
performance, and clarity. Since the code there isn't called as much, I think I just tried to make it easy to read.
Hello!
Proposed Changes
This PR optimizes some very hot areas when starting AlmostUnified in a large pack.
I tested this against ATM10 and was able to speed up this mod's loading by about 40%, from 9.5 seconds to 5.7 seconds on my machine. This is a really imprecise measurement but I am confident that it will be at least a little faster for everyone.
Additional Context
Future Work
Almost all the remaining time is spent in
com.google.gson.JsonObject#equals
, but trying to avoid that would require a big structural change so I stopped here. I think using java maps of values instead ofJsonObject
forRecipeLink#getActual
could give a massive speedup, but there are probably things here I haven't considered so I did not try it.Performance Comparison (flame graphs)
Before:
(9.5 seconds)
After:
(5.7 seconds)