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/forge/src/main/java/org/embeddedt/modernfix/forge/config/NightConfigWatchThrottler.java b/forge/src/main/java/org/embeddedt/modernfix/forge/config/NightConfigWatchThrottler.java new file mode 100644 index 00000000..a0283bd2 --- /dev/null +++ b/forge/src/main/java/org/embeddedt/modernfix/forge/config/NightConfigWatchThrottler.java @@ -0,0 +1,56 @@ +package org.embeddedt.modernfix.forge.config; + +import com.electronwill.nightconfig.core.file.FileWatcher; +import com.google.common.collect.ForwardingCollection; +import com.google.common.collect.ForwardingMap; +import net.minecraftforge.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/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/potential_spawns_alloc/ForgeEventFactoryMixin.java b/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/potential_spawns_alloc/ForgeEventFactoryMixin.java new file mode 100644 index 00000000..ca203b86 --- /dev/null +++ b/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/potential_spawns_alloc/ForgeEventFactoryMixin.java @@ -0,0 +1,25 @@ +package org.embeddedt.modernfix.forge.mixin.perf.potential_spawns_alloc; + +import net.minecraft.core.BlockPos; +import net.minecraft.util.random.WeightedRandomList; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.biome.MobSpawnSettings; +import net.minecraftforge.event.ForgeEventFactory; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.util.List; + +@Mixin(ForgeEventFactory.class) +public class ForgeEventFactoryMixin { + @Redirect(method = "getPotentialSpawns", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/random/WeightedRandomList;create(Ljava/util/List;)Lnet/minecraft/util/random/WeightedRandomList;")) + private static WeightedRandomList reuseOldList(List items, LevelAccessor level, MobCategory category, BlockPos pos, WeightedRandomList oldList) { + // Our patched version of PotentialSpawns will return the same list as unwrap() if no one mutated the list + if(items == oldList.unwrap()) { + return oldList; + } + return WeightedRandomList.create(items); + } +} diff --git a/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/potential_spawns_alloc/PotentialSpawnsMixin.java b/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/potential_spawns_alloc/PotentialSpawnsMixin.java new file mode 100644 index 00000000..9fac5799 --- /dev/null +++ b/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/potential_spawns_alloc/PotentialSpawnsMixin.java @@ -0,0 +1,68 @@ +package org.embeddedt.modernfix.forge.mixin.perf.potential_spawns_alloc; + +import net.minecraft.core.BlockPos; +import net.minecraft.util.random.WeightedRandomList; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.biome.MobSpawnSettings; +import net.minecraftforge.event.world.WorldEvent; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +@Mixin(WorldEvent.PotentialSpawns.class) +public class PotentialSpawnsMixin { + @Shadow(remap = false) @Final @Mutable private List view; + @Shadow(remap = false) @Final @Mutable private List list; + + private static final ArrayList SENTINEL = new ArrayList<>(); + + @Redirect(method = "", at = @At(value = "NEW", target = "java/util/ArrayList", ordinal = 1)) + private ArrayList avoidListAlloc1() { + return SENTINEL; + } + + @Redirect(method = "", at = @At(value = "NEW", target = "java/util/ArrayList", ordinal = 0)) + private ArrayList avoidListAlloc2(Collection c) { + return SENTINEL; + } + + @Redirect(method = "", at = @At(value = "INVOKE", target = "Ljava/util/Collections;unmodifiableList(Ljava/util/List;)Ljava/util/List;")) + private List avoidListAlloc3(List l) { + return null; + } + + @Inject(method = "", at = @At("RETURN")) + private void initializeSmartLists(LevelAccessor level, MobCategory category, BlockPos pos, WeightedRandomList oldList, CallbackInfo ci) { + this.view = oldList.unwrap(); + this.list = null; + } + + private void mfix$populateList() { + if(this.list == null) { + this.list = new ArrayList<>(this.view); + this.view = Collections.unmodifiableList(this.list); + } + } + + @Inject(method = {"addSpawnerData" }, at = @At("HEAD"), remap = false) + private void populateList(MobSpawnSettings.SpawnerData data, CallbackInfo ci) { + mfix$populateList(); + } + + @Inject(method = {"removeSpawnerData" }, at = @At("HEAD"), remap = false) + private void populateList(MobSpawnSettings.SpawnerData data, CallbackInfoReturnable cir) { + mfix$populateList(); + } +} diff --git a/forge/src/main/java/org/embeddedt/modernfix/platform/forge/ModernFixPlatformHooksImpl.java b/forge/src/main/java/org/embeddedt/modernfix/platform/forge/ModernFixPlatformHooksImpl.java index f5e958bd..e697e44d 100644 --- a/forge/src/main/java/org/embeddedt/modernfix/platform/forge/ModernFixPlatformHooksImpl.java +++ b/forge/src/main/java/org/embeddedt/modernfix/platform/forge/ModernFixPlatformHooksImpl.java @@ -27,6 +27,7 @@ import org.embeddedt.modernfix.core.ModernFixMixinPlugin; import org.embeddedt.modernfix.forge.classloading.ATInjector; import org.embeddedt.modernfix.forge.classloading.FastAccessTransformerList; import org.embeddedt.modernfix.forge.config.NightConfigFixer; +import org.embeddedt.modernfix.forge.config.NightConfigWatchThrottler; import org.embeddedt.modernfix.forge.init.ModernFixForge; import org.embeddedt.modernfix.forge.packet.PacketHandler; import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; @@ -159,6 +160,7 @@ public class ModernFixPlatformHooksImpl implements ModernFixPlatformHooks { } NightConfigFixer.monitorFileWatcher(); + NightConfigWatchThrottler.throttle(); } public void applyASMTransformers(String mixinClassName, ClassNode targetClass) {