From e7892efca8dff8ae5004c2d89360d55f75d103cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Sun, 20 Dec 2020 20:18:53 +0100 Subject: [PATCH] Computer tiles now also get cleaned up properly on world unload. Needed to properly unload all devices, always. Avoids warnings of leaked allocation on shutdown, but more importantly avoids memory leaking when running a multi-world game where worlds may repeatedly be loaded and unloaded. --- .../block/entity/ComputerTileEntity.java | 10 ++ .../cil/oc2/common/util/ServerScheduler.java | 105 +++++++++++++++--- 2 files changed, 101 insertions(+), 14 deletions(-) diff --git a/src/main/java/li/cil/oc2/common/block/entity/ComputerTileEntity.java b/src/main/java/li/cil/oc2/common/block/entity/ComputerTileEntity.java index e7d01e50..cfdbe73a 100644 --- a/src/main/java/li/cil/oc2/common/block/entity/ComputerTileEntity.java +++ b/src/main/java/li/cil/oc2/common/block/entity/ComputerTileEntity.java @@ -81,6 +81,7 @@ public final class ComputerTileEntity extends AbstractTileEntity implements ITic /////////////////////////////////////////////////////////////////// + private final Runnable onWorldUnloaded = this::onWorldUnloaded; private Chunk chunk; private final AbstractDeviceBusController busController; private AbstractDeviceBusController.BusState busState; @@ -363,6 +364,9 @@ public final class ComputerTileEntity extends AbstractTileEntity implements ITic protected void initializeServer() { super.initializeServer(); + final World world = requireNonNull(getWorld()); + ServerScheduler.scheduleOnUnload(world, onWorldUnloaded); + busElement.initialize(); virtualMachine.rtc.setWorld(requireNonNull(getWorld())); ServerScheduler.schedule(() -> chunk = requireNonNull(getWorld()).getChunkAt(getPos())); @@ -372,6 +376,8 @@ public final class ComputerTileEntity extends AbstractTileEntity implements ITic protected void disposeServer() { super.disposeServer(); + ServerScheduler.removeOnUnload(getWorld(), onWorldUnloaded); + stopRunnerAndResetVM(); busController.dispose(); busElement.dispose(); @@ -426,6 +432,10 @@ public final class ComputerTileEntity extends AbstractTileEntity implements ITic MemoryMaps.store(virtualMachine.board.getMemoryMap(), 0x80000000L + offset, stream); } + private void onWorldUnloaded() { + disposeServer(); + } + /////////////////////////////////////////////////////////////////// private final class BusController extends AbstractDeviceBusController { diff --git a/src/main/java/li/cil/oc2/common/util/ServerScheduler.java b/src/main/java/li/cil/oc2/common/util/ServerScheduler.java index 095e6823..930bf0d2 100644 --- a/src/main/java/li/cil/oc2/common/util/ServerScheduler.java +++ b/src/main/java/li/cil/oc2/common/util/ServerScheduler.java @@ -1,19 +1,24 @@ package li.cil.oc2.common.util; import net.minecraft.world.IWorld; +import net.minecraft.world.chunk.IChunk; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.world.ChunkEvent; import net.minecraftforge.event.world.WorldEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.event.server.FMLServerStoppedEvent; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.util.PriorityQueue; -import java.util.WeakHashMap; +import java.util.*; +import java.util.stream.Collectors; public final class ServerScheduler { - private static final Scheduler serverScheduler = new Scheduler(); - private static final WeakHashMap worldSchedulers = new WeakHashMap<>(); + private static final TickScheduler globalTickScheduler = new TickScheduler(); + private static final WeakHashMap worldTickSchedulers = new WeakHashMap<>(); + private static final WeakHashMap worldUnloadSchedulers = new WeakHashMap<>(); + private static final WeakHashMap chunkUnloadSchedulers = new WeakHashMap<>(); /////////////////////////////////////////////////////////////////// @@ -26,7 +31,7 @@ public final class ServerScheduler { } public static void schedule(final Runnable runnable, final int afterTicks) { - serverScheduler.schedule(runnable, afterTicks); + globalTickScheduler.schedule(runnable, afterTicks); } public static void schedule(final IWorld world, final Runnable runnable) { @@ -34,30 +39,82 @@ public final class ServerScheduler { } public static void schedule(final IWorld world, final Runnable runnable, final int afterTicks) { - final Scheduler scheduler = worldSchedulers.computeIfAbsent(world, w -> new Scheduler()); + final TickScheduler scheduler = worldTickSchedulers.computeIfAbsent(world, w -> new TickScheduler()); scheduler.schedule(runnable, afterTicks); } + public static void scheduleOnUnload(final IWorld world, final Runnable listener) { + worldUnloadSchedulers.computeIfAbsent(world, unused -> new UnloadScheduler()).add(listener); + } + + public static void removeOnUnload(@Nullable final IWorld world, final Runnable listener) { + if (world == null) { + return; + } + + final UnloadScheduler scheduler = worldUnloadSchedulers.get(world); + if (scheduler != null) { + scheduler.remove(listener); + } + } + + public static void scheduleOnUnload(final IChunk chunk, final Runnable listener) { + chunkUnloadSchedulers.computeIfAbsent(chunk, unused -> new UnloadScheduler()).add(listener); + } + + public static void removeOnUnload(@Nullable final IChunk chunk, final Runnable listener) { + if (chunk == null) { + return; + } + + final UnloadScheduler scheduler = chunkUnloadSchedulers.get(chunk); + if (scheduler != null) { + scheduler.remove(listener); + } + } + /////////////////////////////////////////////////////////////////// private static final class EventHandler { @SubscribeEvent public static void handleServerStoppedEvent(final FMLServerStoppedEvent event) { - serverScheduler.clear(); - worldSchedulers.clear(); + globalTickScheduler.clear(); + worldTickSchedulers.clear(); + worldUnloadSchedulers.clear(); + chunkUnloadSchedulers.clear(); } @SubscribeEvent public static void handleWorldUnload(final WorldEvent.Unload event) { - worldSchedulers.remove(event.getWorld()); + worldTickSchedulers.remove(event.getWorld()); + + final List unloadedChunks = chunkUnloadSchedulers.keySet().stream() + .filter(chunk -> chunk.getWorldForge() == event.getWorld()) + .collect(Collectors.toList()); + for (final IChunk chunk : unloadedChunks) { + chunkUnloadSchedulers.remove(chunk); + } + + final UnloadScheduler scheduler = worldUnloadSchedulers.remove(event.getWorld()); + if (scheduler != null) { + scheduler.run(); + } + } + + @SubscribeEvent + public static void handleChunkUnload(final ChunkEvent.Unload event) { + final UnloadScheduler scheduler = chunkUnloadSchedulers.remove(event.getChunk()); + if (scheduler != null) { + scheduler.run(); + } } @SubscribeEvent public static void handleServerTick(final TickEvent.ServerTickEvent event) { if (event.phase == TickEvent.Phase.START) { - serverScheduler.tick(); + globalTickScheduler.tick(); - for (final Scheduler scheduler : worldSchedulers.values()) { + for (final TickScheduler scheduler : worldTickSchedulers.values()) { scheduler.tick(); } } @@ -69,16 +126,16 @@ public final class ServerScheduler { return; } - serverScheduler.processQueue(); + globalTickScheduler.processQueue(); - final Scheduler scheduler = worldSchedulers.get(event.world); + final TickScheduler scheduler = worldTickSchedulers.get(event.world); if (scheduler != null) { scheduler.processQueue(); } } } - private static final class Scheduler { + private static final class TickScheduler { private final PriorityQueue queue = new PriorityQueue<>(); private int currentTick; @@ -116,4 +173,24 @@ public final class ServerScheduler { return Integer.compare(tick, o.tick); } } + + private static final class UnloadScheduler { + private final Set listeners = Collections.newSetFromMap(new WeakHashMap<>()); + + public void add(final Runnable listener) { + listeners.add(listener); + } + + public void remove(final Runnable listener) { + listeners.remove(listener); + } + + public void run() { + for (final Runnable runnable : listeners) { + runnable.run(); + } + + listeners.clear(); + } + } }