diff --git a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java index 2a76970b..462fd03f 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -30,6 +30,7 @@ public class ModernFixEarlyConfig { this.addMixinRule("perf.async_jei", true); this.addMixinRule("perf.thread_priorities", true); this.addMixinRule("perf.preload_block_classes", true); + this.addMixinRule("perf.parallel_deferred_suppliers", true); /* Mod compat */ if(FMLLoader.getLoadingModList().getModFileById("smoothboot") != null) { diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/parallel_deferred_suppliers/DeferredRegisterMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/parallel_deferred_suppliers/DeferredRegisterMixin.java new file mode 100644 index 00000000..9bccfc1f --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/parallel_deferred_suppliers/DeferredRegisterMixin.java @@ -0,0 +1,27 @@ +package org.embeddedt.modernfix.mixin.perf.parallel_deferred_suppliers; + +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.IForgeRegistry; +import org.embeddedt.modernfix.registry.DeferredRegisterBaker; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +import java.util.function.Supplier; + +@Mixin(DeferredRegister.class) +public class DeferredRegisterMixin { + @Shadow(remap = false) private IForgeRegistry type; + + @Shadow(remap = false) @Final private String modid; + + @ModifyArg(method = "register(Ljava/lang/String;Ljava/util/function/Supplier;)Lnet/minecraftforge/fml/RegistryObject;", at = @At(value = "INVOKE", target = "Ljava/util/Map;putIfAbsent(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"), index = 1, remap = false) + private Object swapForCachedSupplier(Object original) { + if(this.type != null) + return DeferredRegisterBaker.cacheForComputationLater(this.type.getRegistryName(), this.modid, (Supplier)original); + else + return original; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/parallel_deferred_suppliers/GameDataMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/parallel_deferred_suppliers/GameDataMixin.java new file mode 100644 index 00000000..3f653b31 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/parallel_deferred_suppliers/GameDataMixin.java @@ -0,0 +1,24 @@ +package org.embeddedt.modernfix.mixin.perf.parallel_deferred_suppliers; + +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.ModLoadingStage; +import net.minecraftforge.registries.GameData; +import org.embeddedt.modernfix.registry.DeferredRegisterBaker; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Stream; + +@Mixin(GameData.class) +public class GameDataMixin { + @Inject(method = "generateRegistryEvents", at = @At(value = "INVOKE", target = "Ljava/util/List;stream()Ljava/util/stream/Stream;"), locals = LocalCapture.CAPTURE_FAILHARD, remap = false) + private static void bakeDeferredBeforeSendingEvents(CallbackInfoReturnable>> cir, List keys) { + /* TODO also bake items, maybe? */ + DeferredRegisterBaker.bakeSuppliers(new ResourceLocation("minecraft", "block")); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/registry/DeferredRegisterBaker.java b/src/main/java/org/embeddedt/modernfix/registry/DeferredRegisterBaker.java new file mode 100644 index 00000000..ec07e74d --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/registry/DeferredRegisterBaker.java @@ -0,0 +1,103 @@ +package org.embeddedt.modernfix.registry; + +import com.google.common.base.Stopwatch; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.ModContainer; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.fml.ModWorkManager; +import net.minecraftforge.fml.common.ObfuscationReflectionHelper; +import net.minecraftforge.fml.loading.moddiscovery.ModInfo; +import net.minecraftforge.forgespi.language.IModInfo; +import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.util.CachedSupplier; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +public class DeferredRegisterBaker { + private static final HashMap>>> supplierMap = new HashMap<>(); + public static Supplier cacheForComputationLater(ResourceLocation registry, String modid, Supplier supplier) { + synchronized (supplierMap) { + HashMap>> registrySupplierMap = supplierMap.computeIfAbsent(registry, reg -> new HashMap<>()); + List> modSupplierList = registrySupplierMap.computeIfAbsent(modid, id -> new ArrayList<>()); + CachedSupplier cacher = new CachedSupplier<>(supplier); + modSupplierList.add(cacher); + return cacher; + } + } + + public static void bakeSuppliers(ResourceLocation registry) { + synchronized (supplierMap) { + HashMap>> registrySupplierMap = supplierMap.get(registry); + if(registrySupplierMap == null) + return; + HashSet finishedMods = new HashSet<>(); + finishedMods.add("minecraft"); + finishedMods.add("forge"); + HashMap> submittedFutures = new HashMap<>(); + int numMods = ModList.get().getMods().size(); + Semaphore jobWaitingSemaphore = new Semaphore(0); + ArrayList remainingModList = new ArrayList<>(ModList.get().getMods()); + Stopwatch realtimeStopwatch = Stopwatch.createStarted(); + AtomicLong cpuLong = new AtomicLong(0); + while(finishedMods.size() < numMods) { + remainingModList.removeIf(modInfo -> { + if(finishedMods.contains(modInfo.getModId())) + return true; + boolean allDependenciesLoaded = true; + for(IModInfo.ModVersion dep : modInfo.getDependencies()) { + if(dep.isMandatory() && !finishedMods.contains(dep.getModId())) { + allDependenciesLoaded = false; + break; + } + } + if(!allDependenciesLoaded) + return false; + /* Submit job */ + List> suppliersToCompute = registrySupplierMap.get(modInfo.getModId()); + if (suppliersToCompute == null || suppliersToCompute.size() == 0) { + finishedMods.add(modInfo.getModId()); + return true; + } + Optional modContainerOpt = ModList.get().getModContainerById(modInfo.getModId()); + if(!modContainerOpt.isPresent()) + throw new IllegalStateException("Can't find mod container"); + ModContainer container = modContainerOpt.get(); + submittedFutures.put(modInfo.getModId(), CompletableFuture.runAsync(() -> { + Supplier contextExtension = ObfuscationReflectionHelper.getPrivateValue(ModContainer.class, container, "contextExtension"); + ModLoadingContext.get().setActiveContainer(container, contextExtension.get()); + Stopwatch stopwatch = Stopwatch.createStarted(); + for (CachedSupplier supplier : suppliersToCompute) { + supplier.compute(); + } + stopwatch.stop(); + cpuLong.addAndGet(stopwatch.elapsed(TimeUnit.MILLISECONDS)); + jobWaitingSemaphore.release(); + }, ModWorkManager.parallelExecutor())); + return true; + }); + try { + jobWaitingSemaphore.acquire(); + } catch(InterruptedException e) { + throw new RuntimeException("Unexpected interruption", e); + } + submittedFutures.entrySet().removeIf(entry -> { + if(entry.getValue().isDone()) { + finishedMods.add(entry.getKey()); + return true; + } + return false; + }); + } + realtimeStopwatch.stop(); + ModernFix.LOGGER.info("CPU time spent constructing " + registry + " suppliers: " + cpuLong.get()/1000f + " seconds"); + ModernFix.LOGGER.info("Real time spent constructing " + registry + " suppliers: " + realtimeStopwatch.elapsed(TimeUnit.MILLISECONDS)/1000f + " seconds"); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/util/CachedSupplier.java b/src/main/java/org/embeddedt/modernfix/util/CachedSupplier.java new file mode 100644 index 00000000..50b2303d --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/util/CachedSupplier.java @@ -0,0 +1,33 @@ +package org.embeddedt.modernfix.util; + +import java.util.function.Supplier; + +/** + * An implementation of Supplier that allows separating the time at which the value is computed from when it is + * retrieved. + */ +public class CachedSupplier implements Supplier { + private T value = null; + + private boolean hasBeenComputed; + private final Supplier delegate; + + public CachedSupplier(Supplier delegate) { + this.delegate = delegate; + } + + public synchronized void compute() { + this.value = this.delegate.get(); + this.hasBeenComputed = true; + } + + @Override + public synchronized T get() { + if(this.hasBeenComputed) { + this.hasBeenComputed = false; + return this.value; + } else { + return this.delegate.get(); + } + } +} diff --git a/src/main/resources/modernfix.mixins.json b/src/main/resources/modernfix.mixins.json index 44849ee1..8961694b 100644 --- a/src/main/resources/modernfix.mixins.json +++ b/src/main/resources/modernfix.mixins.json @@ -18,7 +18,9 @@ "perf.reduce_blockstate_cache_rebuilds.BlockCallbacksMixin", "perf.boost_worker_count.UtilMixin", "perf.thread_priorities.UtilMixin", - "perf.preload_block_classes.GameDataMixin" + "perf.preload_block_classes.GameDataMixin", + "perf.parallel_deferred_suppliers.DeferredRegisterMixin", + "perf.parallel_deferred_suppliers.GameDataMixin" ], "client": [ "perf.skip_first_datapack_reload.MinecraftMixin",