Skip to content

Commit 529e8f1

Browse files
authored
Improve runtime of ModuleClassLoader constructor (#48)
In old startup profiles from 1.19/1.20, `ModuleClassLoader` often takes at least a second or two to be constructed in large modpacks. This likely boils down to the high amounts of iteration being done over the module list - in particular, constructing `parentLoaders` would have O(n^2) runtime in a scenario where every module reads the other modules. This PR makes some changes to the constructor to avoid redundant iteration. I have not profiled the changes, but I don't see a reason why they could be slower than the status quo. In particular, the `parentLoaders` loop has been heavily optimized to cache data for each module, which should provide noticeable speedup to the automatic module case.
1 parent 420e984 commit 529e8f1

1 file changed

Lines changed: 31 additions & 25 deletions

File tree

src/main/java/cpw/mods/cl/ModuleClassLoader.java

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.net.URL;
1313
import java.util.*;
1414
import java.util.function.BiFunction;
15+
import java.util.function.Function;
1516
import java.util.function.Supplier;
1617
import java.util.stream.Collectors;
1718
import java.util.stream.Stream;
@@ -32,39 +33,44 @@ public class ModuleClassLoader extends ClassLoader {
3233
public ModuleClassLoader(final String name, final Configuration configuration, final List<ModuleLayer> parentLayers) {
3334
super(name, null);
3435
this.configuration = configuration;
35-
this.resolvedRoots = configuration.modules().stream()
36-
.map(ResolvedModule::reference)
37-
.filter(JarModuleFinder.JarModuleReference.class::isInstance)
38-
.collect(Collectors.toMap(r -> r.descriptor().name(), r -> (JarModuleFinder.JarModuleReference) r));
39-
4036
this.packageLookup = new HashMap<>();
41-
for (var mod : this.configuration.modules()) {
42-
if (this.resolvedRoots.containsKey(mod.name())) {
43-
mod.reference().descriptor().packages().forEach(pk->this.packageLookup.put(pk, mod));
44-
}
45-
}
37+
this.resolvedRoots = configuration.modules().stream()
38+
.filter(m -> m.reference() instanceof JarModuleFinder.JarModuleReference)
39+
.peek(mod -> {
40+
// Populate packageLookup at the same time, for speed
41+
mod.reference().descriptor().packages().forEach(pk->this.packageLookup.put(pk, mod));
42+
})
43+
.collect(Collectors.toMap(mod -> mod.reference().descriptor().name(), mod -> (JarModuleFinder.JarModuleReference)mod.reference()));
4644

4745
this.parentLoaders = new HashMap<>();
46+
Set<ModuleDescriptor> processedAutomaticDescriptors = new HashSet<>();
47+
Map<ResolvedModule, ClassLoader> classLoaderMap = new HashMap<>();
48+
Function<ResolvedModule, ClassLoader> findClassLoader = k -> {
49+
// Loading a class requires its module to be part of resolvedRoots
50+
// If it's not, we delegate loading to its module's classloader
51+
if (!this.resolvedRoots.containsKey(k.name())) {
52+
return parentLayers.stream()
53+
.filter(l -> l.configuration() == k.configuration())
54+
.flatMap(layer->Optional.ofNullable(layer.findLoader(k.name())).stream())
55+
.findFirst()
56+
.orElse(ClassLoader.getPlatformClassLoader());
57+
} else {
58+
return ModuleClassLoader.this;
59+
}
60+
};
61+
// This loop will be O(n^2) for the average set of mods, since they all read one another.
62+
// However, we amortize some of the cost by optimizing the common automatic module path.
4863
for (var rm : configuration.modules()) {
4964
for (var other : rm.reads()) {
50-
Supplier<ClassLoader> findClassLoader = ()->{
51-
// Loading a class requires its module to be part of resolvedRoots
52-
// If it's not, we delegate loading to its module's classloader
53-
if (!this.resolvedRoots.containsKey(other.name())) {
54-
return parentLayers.stream()
55-
.filter(l -> l.configuration() == other.configuration())
56-
.flatMap(layer->Optional.ofNullable(layer.findLoader(other.name())).stream())
57-
.findFirst()
58-
.orElse(ClassLoader.getPlatformClassLoader());
59-
} else {
60-
return ModuleClassLoader.this;
61-
}
62-
};
63-
var cl = findClassLoader.get();
65+
ClassLoader cl = classLoaderMap.computeIfAbsent(other, findClassLoader);
6466
final var descriptor = other.reference().descriptor();
6567
if (descriptor.isAutomatic()) {
66-
descriptor.packages().forEach(pn->this.parentLoaders.put(pn, cl));
68+
// No need to run this logic more than once per automatic module
69+
if (processedAutomaticDescriptors.add(descriptor)) {
70+
descriptor.packages().forEach(pn->this.parentLoaders.put(pn, cl));
71+
}
6772
} else {
73+
// We actually use "rm" for this path, so we have to run it each time
6874
descriptor.exports().stream()
6975
.filter(e -> !e.isQualified() || (e.isQualified() && other.configuration() == configuration && e.targets().contains(rm.name())))
7076
.map(ModuleDescriptor.Exports::source)

0 commit comments

Comments
 (0)