diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/ticking_chunk_alloc/BatMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/ticking_chunk_alloc/BatMixin.java new file mode 100644 index 00000000..e3cc0ddc --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/ticking_chunk_alloc/BatMixin.java @@ -0,0 +1,28 @@ +package org.embeddedt.modernfix.common.mixin.perf.ticking_chunk_alloc; + +import net.minecraft.world.entity.ambient.Bat; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.time.LocalDate; + +@Mixin(value = Bat.class, priority = 1200) +public class BatMixin { + private static long mfix$lastQueriedTime = -1L; + private static LocalDate mfix$lastQueriedDate = null; + + /** + * @author embeddedt + * @reason avoid excessive allocations from continuously querying the date, only get a new date once every 30 seconds + */ + @Redirect(method = "isHalloween", at = @At(value = "INVOKE", target = "Ljava/time/LocalDate;now()Ljava/time/LocalDate;"), require = 0) + private static LocalDate useCachedLocalDate() { + LocalDate date = mfix$lastQueriedDate; + if(date == null || Math.abs(System.currentTimeMillis() - mfix$lastQueriedTime) > 30000) { + mfix$lastQueriedDate = date = LocalDate.now(); + mfix$lastQueriedTime = System.currentTimeMillis(); + } + return date; + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/ticking_chunk_alloc/ChunkGeneratorMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/ticking_chunk_alloc/ChunkGeneratorMixin.java new file mode 100644 index 00000000..193995f4 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/ticking_chunk_alloc/ChunkGeneratorMixin.java @@ -0,0 +1,26 @@ +package org.embeddedt.modernfix.common.mixin.perf.ticking_chunk_alloc; + +import net.minecraft.world.level.chunk.ChunkGenerator; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +@Mixin(ChunkGenerator.class) +public class ChunkGeneratorMixin { + /** + * @author embeddedt + * @reason Avoid allocation if the chunk contains no structures + */ + @Redirect(method = "getMobsAt", at = @At(value = "INVOKE", target = "Ljava/util/Map;entrySet()Ljava/util/Set;"), require = 0) + private Set avoidSetAllocation(Map instance) { + if(instance.isEmpty()) { + return Collections.emptySet(); + } else { + return instance.entrySet(); + } + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/ticking_chunk_alloc/ChunkHolderMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/ticking_chunk_alloc/ChunkHolderMixin.java new file mode 100644 index 00000000..2ea7c2de --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/ticking_chunk_alloc/ChunkHolderMixin.java @@ -0,0 +1,40 @@ +package org.embeddedt.modernfix.common.mixin.perf.ticking_chunk_alloc; + +import com.mojang.datafixers.util.Either; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.world.level.chunk.LevelChunk; +import org.embeddedt.modernfix.util.EitherUtil; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.concurrent.CompletableFuture; + +@Mixin(value = ChunkHolder.class, priority = 500) +public abstract class ChunkHolderMixin { + @Shadow public abstract CompletableFuture> getTickingChunkFuture(); + + @Shadow public abstract CompletableFuture> getFullChunkFuture(); + + /** + * @author embeddedt + * @reason avoid Optional allocation + */ + @Overwrite + public LevelChunk getTickingChunk() { + CompletableFuture> completableFuture = this.getTickingChunkFuture(); + Either either = completableFuture.getNow(null); + return either == null ? null : EitherUtil.leftOrNull(either); + } + + /** + * @author embeddedt + * @reason avoid Optional allocation + */ + @Overwrite + public LevelChunk getFullChunk() { + CompletableFuture> completableFuture = this.getFullChunkFuture(); + Either either = completableFuture.getNow(null); + return either == null ? null : EitherUtil.leftOrNull(either); + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/util/EitherUtil.java b/common/src/main/java/org/embeddedt/modernfix/util/EitherUtil.java new file mode 100644 index 00000000..52bc52c2 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/util/EitherUtil.java @@ -0,0 +1,52 @@ +package org.embeddedt.modernfix.util; + +import com.mojang.datafixers.util.Either; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Field; + +public class EitherUtil { + private static final Class LEFT, RIGHT; + private static final MethodHandle LEFT_VAL, RIGHT_VAL; + + static { + try { + LEFT = Class.forName("com.mojang.datafixers.util.Either$Left"); + RIGHT = Class.forName("com.mojang.datafixers.util.Either$Right"); + Field lvalue = LEFT.getDeclaredField("value"); + lvalue.setAccessible(true); + Field rvalue = RIGHT.getDeclaredField("value"); + rvalue.setAccessible(true); + LEFT_VAL = MethodHandles.publicLookup().unreflectGetter(lvalue).asType(MethodType.methodType(Object.class, Either.class)); + RIGHT_VAL = MethodHandles.publicLookup().unreflectGetter(rvalue).asType(MethodType.methodType(Object.class, Either.class)); + } catch(ReflectiveOperationException e) { + throw new AssertionError("Failed to hook DFU Either", e); + } + } + + @SuppressWarnings("unchecked") + public static L leftOrNull(Either either) { + if(either.getClass() == LEFT) { + try { + return (L)LEFT_VAL.invokeExact(either); + } catch(Throwable e) { + throw new RuntimeException(e); + } + } + return null; + } + + @SuppressWarnings("unchecked") + public static R rightOrNull(Either either) { + if(either.getClass() == RIGHT) { + try { + return (R)RIGHT_VAL.invokeExact(either); + } catch(Throwable e) { + throw new RuntimeException(e); + } + } + return null; + } +} diff --git a/fabric/src/main/java/org/embeddedt/modernfix/fabric/mixin/perf/dynamic_resources/ModelBakerImplMixin.java b/fabric/src/main/java/org/embeddedt/modernfix/fabric/mixin/perf/dynamic_resources/ModelBakerImplMixin.java index 9ed0bd79..43c86168 100644 --- a/fabric/src/main/java/org/embeddedt/modernfix/fabric/mixin/perf/dynamic_resources/ModelBakerImplMixin.java +++ b/fabric/src/main/java/org/embeddedt/modernfix/fabric/mixin/perf/dynamic_resources/ModelBakerImplMixin.java @@ -1,5 +1,6 @@ package org.embeddedt.modernfix.fabric.mixin.perf.dynamic_resources; +import com.google.common.collect.ImmutableList; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import net.fabricmc.loader.api.FabricLoader; @@ -75,9 +76,19 @@ public abstract class ModelBakerImplMixin implements IExtendedModelBaker { synchronized (this.field_40571) { /* to emulate vanilla model loading, treat as top-level */ Optional blockOpt = Objects.equals(((ModelResourceLocation)arg).getVariant(), "inventory") ? Optional.empty() : BuiltInRegistries.BLOCK.getOptional(new ResourceLocation(arg.getNamespace(), arg.getPath())); + boolean invalidMRL = false; if(blockOpt.isPresent()) { /* load via lambda for mods that expect blockstate to get loaded */ - for(BlockState state : extendedBakery.getBlockStatesForMRL(blockOpt.get().getStateDefinition(), (ModelResourceLocation)arg)) { + ImmutableList states; + try { + states = extendedBakery.getBlockStatesForMRL(blockOpt.get().getStateDefinition(), (ModelResourceLocation)arg); + } catch(RuntimeException e) { + states = ImmutableList.of(); + invalidMRL = true; + // Fall back to getModel + cir.setReturnValue(this.field_40571.getModel(arg)); + } + for(BlockState state : states) { try { blockStateLoaderHandle.invokeExact(this.field_40571, state); } catch(Throwable e) { @@ -87,9 +98,11 @@ public abstract class ModelBakerImplMixin implements IExtendedModelBaker { } else { this.field_40571.loadTopLevel((ModelResourceLocation)arg); } - cir.setReturnValue(this.field_40571.topLevelModels.getOrDefault(arg, extendedBakery.mfix$getUnbakedMissingModel())); - // avoid leaks - this.field_40571.topLevelModels.clear(); + if(!invalidMRL) { + cir.setReturnValue(this.field_40571.topLevelModels.getOrDefault(arg, extendedBakery.mfix$getUnbakedMissingModel())); + // avoid leaks + this.field_40571.topLevelModels.clear(); + } } } else cir.setReturnValue(this.field_40571.getModel(arg)); diff --git a/neoforge/src/main/java/org/embeddedt/modernfix/neoforge/config/NightConfigWatchThrottler.java b/neoforge/src/main/java/org/embeddedt/modernfix/neoforge/config/NightConfigWatchThrottler.java new file mode 100644 index 00000000..049575da --- /dev/null +++ b/neoforge/src/main/java/org/embeddedt/modernfix/neoforge/config/NightConfigWatchThrottler.java @@ -0,0 +1,56 @@ +package org.embeddedt.modernfix.neoforge.config; + +import com.electronwill.nightconfig.core.file.FileWatcher; +import com.google.common.collect.ForwardingCollection; +import com.google.common.collect.ForwardingMap; +import net.neoforged.fml.util.ObfuscationReflectionHelper; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; + +/** + * Throttle NightConfig's file watching. There are reports of this consuming excessive CPU time + * (example) and the spammed iterator calls + * end up being 10% of allocations when testing in a dev environment. + */ +public class NightConfigWatchThrottler { + private static final long DELAY = TimeUnit.MILLISECONDS.toNanos(1000); + + @SuppressWarnings("rawtypes") + public static void throttle() { + Map watchedDirs = ObfuscationReflectionHelper.getPrivateValue(FileWatcher.class, FileWatcher.defaultInstance(), "watchedDirs"); + ObfuscationReflectionHelper.setPrivateValue(FileWatcher.class, FileWatcher.defaultInstance(), new ForwardingMap() { + @Override + protected Map delegate() { + return watchedDirs; + } + + private Collection cachedValues; + + @Override + public Collection values() { + if(cachedValues == null) { + Collection values = super.values(); + cachedValues = new ForwardingCollection() { + @Override + protected Collection delegate() { + return values; + } + + @Override + public Iterator iterator() { + // iterator() is called at the beginning of each iteration of the watch loop, + // so it is a good spot to inject the delay. + LockSupport.parkNanos(DELAY); + return super.iterator(); + } + }; + } + return cachedValues; + } + }, "watchedDirs"); + } +} diff --git a/neoforge/src/main/java/org/embeddedt/modernfix/platform/neoforge/ModernFixPlatformHooksImpl.java b/neoforge/src/main/java/org/embeddedt/modernfix/platform/neoforge/ModernFixPlatformHooksImpl.java index d203ae4b..5f5d1931 100644 --- a/neoforge/src/main/java/org/embeddedt/modernfix/platform/neoforge/ModernFixPlatformHooksImpl.java +++ b/neoforge/src/main/java/org/embeddedt/modernfix/platform/neoforge/ModernFixPlatformHooksImpl.java @@ -24,6 +24,7 @@ import net.neoforged.neoforge.server.ServerLifecycleHooks; import org.embeddedt.modernfix.api.constants.IntegrationConstants; import org.embeddedt.modernfix.core.ModernFixMixinPlugin; import org.embeddedt.modernfix.neoforge.config.NightConfigFixer; +import org.embeddedt.modernfix.neoforge.config.NightConfigWatchThrottler; import org.embeddedt.modernfix.neoforge.init.ModernFixForge; import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; import org.embeddedt.modernfix.spark.SparkLaunchProfiler; @@ -106,6 +107,7 @@ public class ModernFixPlatformHooksImpl implements ModernFixPlatformHooks { } NightConfigFixer.monitorFileWatcher(); + NightConfigWatchThrottler.throttle(); } public void applyASMTransformers(String mixinClassName, ClassNode targetClass) {