From 1a277e6db0e11720a33298e7c6da7aa2b8db3dc9 Mon Sep 17 00:00:00 2001 From: shartte Date: Fri, 9 Aug 2024 12:49:33 +0200 Subject: [PATCH] Network Ticking Optimizations (#8131) --- .../appeng/api/networking/GridServices.java | 55 ++++++++++- .../api/networking/GridServicesInternal.java | 4 +- .../crafting/execution/CraftingCpuLogic.java | 10 ++ src/main/java/appeng/me/Grid.java | 26 +++--- .../me/helpers/GridServiceContainer.java | 13 +++ .../appeng/me/service/CraftingService.java | 91 +++++++++++++------ .../appeng/me/service/TickManagerService.java | 14 +-- .../helpers/NetworkCraftingProviders.java | 27 ++++++ .../api/networking/GridServicesTest.java | 56 +++++++++++- 9 files changed, 233 insertions(+), 63 deletions(-) create mode 100644 src/main/java/appeng/me/helpers/GridServiceContainer.java diff --git a/src/main/java/appeng/api/networking/GridServices.java b/src/main/java/appeng/api/networking/GridServices.java index 5351bd9a8f2..884313a64b0 100644 --- a/src/main/java/appeng/api/networking/GridServices.java +++ b/src/main/java/appeng/api/networking/GridServices.java @@ -27,12 +27,16 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashMap; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import net.minecraft.world.level.Level; + +import appeng.me.helpers.GridServiceContainer; + /** * A registry of grid services to extend grid functionality. */ @@ -92,14 +96,37 @@ private static boolean isRegistered(Class publicInterface) { *

* This is used by AE2 internally to initialize the services for a grid. */ - static Map, IGridServiceProvider> createServices(IGrid g) { - var result = new LinkedHashMap, IGridServiceProvider>(registry.size()); + static GridServiceContainer createServices(IGrid g) { + var services = new IdentityHashMap, IGridServiceProvider>(registry.size()); + var serverStartTickServices = new ArrayList(registry.size()); + var levelStartTickServices = new ArrayList(registry.size()); + var levelEndTickServices = new ArrayList(registry.size()); + var serverEndTickServices = new ArrayList(registry.size()); for (var registration : registry) { - result.put(registration.publicInterface, registration.construct(g, result)); + var service = registration.construct(g, services); + services.put(registration.publicInterface, service); + + if (registration.hasServerStartTick) { + serverStartTickServices.add(service); + } + if (registration.hasLevelStartTick) { + levelStartTickServices.add(service); + } + if (registration.hasLevelEndTick) { + levelEndTickServices.add(service); + } + if (registration.hasServerEndTick) { + serverEndTickServices.add(service); + } } - return result; + return new GridServiceContainer( + services, + serverStartTickServices.toArray(IGridServiceProvider[]::new), + levelStartTickServices.toArray(IGridServiceProvider[]::new), + levelEndTickServices.toArray(IGridServiceProvider[]::new), + serverEndTickServices.toArray(IGridServiceProvider[]::new)); } private static class GridCacheRegistration { @@ -114,6 +141,11 @@ private static class GridCacheRegistration { private final Set> dependencies; + private final boolean hasServerStartTick; + private final boolean hasLevelStartTick; + private final boolean hasLevelEndTick; + private final boolean hasServerEndTick; + @SuppressWarnings("unchecked") public GridCacheRegistration(Class implClass, Class publicInterface) { this.publicInterface = publicInterface; @@ -130,6 +162,19 @@ public GridCacheRegistration(Class implClass, Class publicInterface) { this.dependencies = Arrays.stream(this.constructorParameterTypes) .filter(t -> !t.equals(IGrid.class)) .collect(Collectors.toSet()); + + try { + this.hasServerStartTick = implClass.getMethod("onServerStartTick") + .getDeclaringClass() != IGridServiceProvider.class; + this.hasLevelStartTick = implClass.getMethod("onLevelStartTick", Level.class) + .getDeclaringClass() != IGridServiceProvider.class; + this.hasLevelEndTick = implClass.getMethod("onLevelEndTick", Level.class) + .getDeclaringClass() != IGridServiceProvider.class; + this.hasServerEndTick = implClass.getMethod("onServerEndTick") + .getDeclaringClass() != IGridServiceProvider.class; + } catch (NoSuchMethodException exception) { + throw new RuntimeException("Failed to check which methods the grid service implements", exception); + } } public IGridServiceProvider construct(IGrid g, Map, IGridServiceProvider> createdServices) { diff --git a/src/main/java/appeng/api/networking/GridServicesInternal.java b/src/main/java/appeng/api/networking/GridServicesInternal.java index 1dc48c057a0..1ec2dc67ae4 100644 --- a/src/main/java/appeng/api/networking/GridServicesInternal.java +++ b/src/main/java/appeng/api/networking/GridServicesInternal.java @@ -18,14 +18,14 @@ package appeng.api.networking; -import java.util.Map; +import appeng.me.helpers.GridServiceContainer; /** * Allows access to non-public features of {@link GridServices}. */ public class GridServicesInternal { - public static Map, IGridServiceProvider> createServices(IGrid g) { + public static GridServiceContainer createServices(IGrid g) { return GridServices.createServices(g); } diff --git a/src/main/java/appeng/crafting/execution/CraftingCpuLogic.java b/src/main/java/appeng/crafting/execution/CraftingCpuLogic.java index da0a3878c98..2c881cf1453 100644 --- a/src/main/java/appeng/crafting/execution/CraftingCpuLogic.java +++ b/src/main/java/appeng/crafting/execution/CraftingCpuLogic.java @@ -49,6 +49,7 @@ import appeng.core.network.clientbound.CraftingJobStatusPacket; import appeng.crafting.CraftingLink; import appeng.crafting.inv.ListCraftingInventory; +import appeng.hooks.ticking.TickHandler; import appeng.me.cluster.implementations.CraftingCPUCluster; import appeng.me.service.CraftingService; @@ -75,6 +76,8 @@ public class CraftingCpuLogic { */ private boolean cantStoreItems = false; + private long lastModifiedOnTick = TickHandler.instance().getCurrentTick(); + public CraftingCpuLogic(CraftingCPUCluster cluster) { this.cluster = cluster; } @@ -390,11 +393,16 @@ public void storeItems() { } private void postChange(AEKey what) { + lastModifiedOnTick = TickHandler.instance().getCurrentTick(); for (var listener : listeners) { listener.accept(what); } } + public long getLastModifiedOnTick() { + return lastModifiedOnTick; + } + public boolean hasJob() { return this.job != null; } @@ -509,6 +517,8 @@ public boolean isCantStoreItems() { } private void notifyJobOwner(ExecutingCraftingJob job, CraftingJobStatusPacket.Status status) { + this.lastModifiedOnTick = TickHandler.instance().getCurrentTick(); + var playerId = job.playerId; if (playerId == null) { return; diff --git a/src/main/java/appeng/me/Grid.java b/src/main/java/appeng/me/Grid.java index a60f4b6de8f..1522869dcb3 100644 --- a/src/main/java/appeng/me/Grid.java +++ b/src/main/java/appeng/me/Grid.java @@ -48,7 +48,6 @@ import appeng.api.networking.IGridNode; import appeng.api.networking.IGridNodeListener; import appeng.api.networking.IGridService; -import appeng.api.networking.IGridServiceProvider; import appeng.api.networking.crafting.ICraftingService; import appeng.api.networking.energy.IEnergyService; import appeng.api.networking.events.GridEvent; @@ -58,6 +57,7 @@ import appeng.api.networking.ticking.ITickManager; import appeng.core.AELog; import appeng.hooks.ticking.TickHandler; +import appeng.me.helpers.GridServiceContainer; import appeng.me.service.P2PService; import appeng.parts.AEBasePart; import appeng.util.IDebugExportable; @@ -71,7 +71,7 @@ public class Grid implements IGrid { private static int nextSerial = 0; private final SetMultimap, IGridNode> machines = MultimapBuilder.hashKeys().hashSetValues().build(); - private final Map, IGridServiceProvider> services; + private final GridServiceContainer services; // Becomes null after the last node has left the grid. @Nullable private GridNode pivot; @@ -103,17 +103,13 @@ int getPriority() { return this.priority; } - Collection getProviders() { - return this.services.values(); - } - @Override public int size() { return this.machines.size(); } void remove(GridNode gridNode) { - for (var c : this.services.values()) { + for (var c : services.services().values()) { c.removeNode(gridNode); } @@ -137,13 +133,13 @@ void add(GridNode gridNode, @Nullable CompoundTag savedData) { // track node. this.machines.put(gridNode.getOwner().getClass(), gridNode); - for (var service : this.services.values()) { + for (var service : services.services().values()) { service.addNode(gridNode, savedData); } } void saveNodeData(GridNode gridNode, CompoundTag savedData) { - for (var service : this.services.values()) { + for (var service : services.services().values()) { service.saveNodeData(gridNode, savedData); } } @@ -151,7 +147,7 @@ void saveNodeData(GridNode gridNode, CompoundTag savedData) { @SuppressWarnings("unchecked") @Override public C getService(Class iface) { - var service = this.services.get(iface); + var service = this.services.services().get(iface); if (service == null) { throw new IllegalArgumentException("Service " + iface + " is not registered"); } @@ -224,7 +220,7 @@ public void onServerStartTick() { return; } - for (var gc : this.services.values()) { + for (var gc : this.services.serverStartTickServices()) { gc.onServerStartTick(); } } @@ -234,7 +230,7 @@ public void onLevelStartTick(Level level) { return; } - for (var gc : this.services.values()) { + for (var gc : this.services.levelStartTickServices()) { gc.onLevelStartTick(level); } } @@ -244,7 +240,7 @@ public void onLevelEndTick(Level level) { return; } - for (var gc : this.services.values()) { + for (var gc : this.services.levelEndtickServices()) { gc.onLevelEndTick(level); } } @@ -254,7 +250,7 @@ public void onServerEndTick() { return; } - for (var gc : this.services.values()) { + for (var gc : this.services.serverEndTickServices()) { gc.onServerEndTick(); } } @@ -354,7 +350,7 @@ public void export(JsonWriter jsonWriter) throws IOException { jsonWriter.name("services"); jsonWriter.beginObject(); - for (var entry : services.entrySet()) { + for (var entry : services.services().entrySet()) { jsonWriter.name(getServiceExportKey(entry.getKey())); jsonWriter.beginObject(); entry.getValue().debugDump(jsonWriter, registries); diff --git a/src/main/java/appeng/me/helpers/GridServiceContainer.java b/src/main/java/appeng/me/helpers/GridServiceContainer.java new file mode 100644 index 00000000000..7c2f17b21ff --- /dev/null +++ b/src/main/java/appeng/me/helpers/GridServiceContainer.java @@ -0,0 +1,13 @@ +package appeng.me.helpers; + +import java.util.Map; + +import appeng.api.networking.IGridServiceProvider; + +public record GridServiceContainer( + Map, IGridServiceProvider> services, + IGridServiceProvider[] serverStartTickServices, + IGridServiceProvider[] levelStartTickServices, + IGridServiceProvider[] levelEndtickServices, + IGridServiceProvider[] serverEndTickServices) { +} diff --git a/src/main/java/appeng/me/service/CraftingService.java b/src/main/java/appeng/me/service/CraftingService.java index 63ad0f4013b..24ecdd70aa5 100644 --- a/src/main/java/appeng/me/service/CraftingService.java +++ b/src/main/java/appeng/me/service/CraftingService.java @@ -71,6 +71,7 @@ import appeng.crafting.CraftingLink; import appeng.crafting.CraftingLinkNexus; import appeng.crafting.execution.CraftingSubmitResult; +import appeng.hooks.ticking.TickHandler; import appeng.me.cluster.implementations.CraftingCPUCluster; import appeng.me.helpers.InterestManager; import appeng.me.helpers.StackWatcher; @@ -123,11 +124,15 @@ public class CraftingService implements ICraftingService, IGridServiceProvider { private final IEnergyService energyGrid; private final Set currentlyCrafting = new HashSet<>(); private final Set currentlyCraftable = new HashSet<>(); + private long lastProcessedCraftingLogicChangeTick; + private long lastProcessedCraftableChangeTick; private boolean updateList = false; public CraftingService(IGrid grid, IStorageService storageGrid, IEnergyService energyGrid) { this.grid = grid; this.energyGrid = energyGrid; + this.lastProcessedCraftingLogicChangeTick = TickHandler.instance().getCurrentTick(); + this.lastProcessedCraftableChangeTick = TickHandler.instance().getCurrentTick(); storageGrid.addGlobalStorageProvider(new CraftingServiceStorage(this)); } @@ -137,44 +142,74 @@ public void onServerEndTick() { if (this.updateList) { this.updateList = false; this.updateCPUClusters(); + lastProcessedCraftingLogicChangeTick = -1; // Ensure caches below are also updated } this.craftingLinks.values().removeIf(nexus -> nexus.isDead(this.grid, this)); - var previouslyCrafting = new HashSet<>(currentlyCrafting); - var previouslyCraftable = new HashSet<>(currentlyCraftable); - this.currentlyCrafting.clear(); - this.currentlyCraftable.clear(); - - for (CraftingCPUCluster cpu : this.craftingCPUClusters) { + long latestChange = 0; + for (var cpu : this.craftingCPUClusters) { cpu.craftingLogic.tickCraftingLogic(energyGrid, this); - cpu.craftingLogic.getAllWaitingFor(this.currentlyCrafting); + latestChange = Math.max( + latestChange, + cpu.craftingLogic.getLastModifiedOnTick()); } - currentlyCraftable.addAll(getCraftables(k -> true)); - - // Notify watchers about items no longer being crafted - var changed = new HashSet(); - changed.addAll(Sets.difference(previouslyCrafting, currentlyCrafting)); - changed.addAll(Sets.difference(currentlyCrafting, previouslyCrafting)); - for (var what : changed) { - for (var watcher : interestManager.get(what)) { - watcher.getHost().onRequestChange(what); + + // There's nothing to do if we weren't crafting anything and we don't have any CPUs that could craft + if (latestChange != lastProcessedCraftingLogicChangeTick) { + lastProcessedCraftingLogicChangeTick = latestChange; + + Set previouslyCrafting = currentlyCrafting.isEmpty() ? Set.of() : new HashSet<>(currentlyCrafting); + this.currentlyCrafting.clear(); + + for (var cpu : this.craftingCPUClusters) { + cpu.craftingLogic.getAllWaitingFor(this.currentlyCrafting); } - for (var watcher : interestManager.getAllStacksWatchers()) { - watcher.getHost().onRequestChange(what); + + // Notify watchers about items no longer being crafted, but only if there can be changes and there are + // watchers + if (!interests.isEmpty() && !(previouslyCrafting.isEmpty() && currentlyCrafting.isEmpty())) { + var changed = new HashSet(); + changed.addAll(Sets.difference(previouslyCrafting, currentlyCrafting)); + changed.addAll(Sets.difference(currentlyCrafting, previouslyCrafting)); + for (var what : changed) { + for (var watcher : interestManager.get(what)) { + watcher.getHost().onRequestChange(what); + } + for (var watcher : interestManager.getAllStacksWatchers()) { + watcher.getHost().onRequestChange(what); + } + } } } - // Notify watchers about items no longer craftable - var changedCraftable = new HashSet(); - changedCraftable.addAll(Sets.difference(previouslyCraftable, currentlyCraftable)); - changedCraftable.addAll(Sets.difference(currentlyCraftable, previouslyCraftable)); - for (var what : changedCraftable) { - for (var watcher : interestManager.get(what)) { - watcher.getHost().onCraftableChange(what); - } - for (var watcher : interestManager.getAllStacksWatchers()) { - watcher.getHost().onCraftableChange(what); + // Throttle updates of craftables to once every 10 ticks + if (lastProcessedCraftableChangeTick != craftingProviders.getLastModifiedOnTick()) { + lastProcessedCraftableChangeTick = craftingProviders.getLastModifiedOnTick(); + + // If everything is empty, there's nothing to do + if (!currentlyCraftable.isEmpty() || !craftingProviders.getCraftableKeys().isEmpty() + || !craftingProviders.getEmittableKeys().isEmpty()) { + Set previouslyCraftable = currentlyCraftable.isEmpty() ? Set.of() + : new HashSet<>(currentlyCraftable); + this.currentlyCraftable.clear(); + currentlyCraftable.addAll(craftingProviders.getCraftableKeys()); + currentlyCraftable.addAll(craftingProviders.getEmittableKeys()); + + // Only perform the change tracking if there are watchers + if (!interests.isEmpty()) { + var changedCraftable = new HashSet(); + changedCraftable.addAll(Sets.difference(previouslyCraftable, currentlyCraftable)); + changedCraftable.addAll(Sets.difference(currentlyCraftable, previouslyCraftable)); + for (var what : changedCraftable) { + for (var watcher : interestManager.get(what)) { + watcher.getHost().onCraftableChange(what); + } + for (var watcher : interestManager.getAllStacksWatchers()) { + watcher.getHost().onCraftableChange(what); + } + } + } } } } diff --git a/src/main/java/appeng/me/service/TickManagerService.java b/src/main/java/appeng/me/service/TickManagerService.java index 09cf496cdf7..554b628d47e 100644 --- a/src/main/java/appeng/me/service/TickManagerService.java +++ b/src/main/java/appeng/me/service/TickManagerService.java @@ -18,7 +18,7 @@ package appeng.me.service; -import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.LongSummaryStatistics; import java.util.Map; import java.util.Objects; @@ -50,10 +50,10 @@ public class TickManagerService implements ITickManager, IGridServiceProvider { private static final int TICK_RATE_SPEED_UP_FACTOR = 2; private static final int TICK_RATE_SLOW_DOWN_FACTOR = 1; - private final Map alertable = new HashMap<>(); - private final Map sleeping = new HashMap<>(); - private final Map awake = new HashMap<>(); - private final Map> upcomingTicks = new HashMap<>(); + private final Map alertable = new IdentityHashMap<>(); + private final Map sleeping = new IdentityHashMap<>(); + private final Map awake = new IdentityHashMap<>(); + private final Map> upcomingTicks = new IdentityHashMap<>(); private PriorityQueue currentlyTickingQueue = null; @@ -70,10 +70,6 @@ public void onServerStartTick() { this.currentTick++; } - @Override - public void onLevelStartTick(Level level) { - } - @Override public void onLevelEndTick(Level level) { this.tickLevelQueue(level); diff --git a/src/main/java/appeng/me/service/helpers/NetworkCraftingProviders.java b/src/main/java/appeng/me/service/helpers/NetworkCraftingProviders.java index 0ab6b33dca3..5bfabc0697d 100644 --- a/src/main/java/appeng/me/service/helpers/NetworkCraftingProviders.java +++ b/src/main/java/appeng/me/service/helpers/NetworkCraftingProviders.java @@ -23,6 +23,7 @@ import appeng.api.stacks.AEKey; import appeng.api.stacks.KeyCounter; import appeng.api.storage.AEKeyFilter; +import appeng.hooks.ticking.TickHandler; /** * Keeps track of the crafting patterns in the network, and related information. @@ -37,6 +38,11 @@ public class NetworkCraftingProviders { private final KeyCounter craftableItemsList = new KeyCounter(); private final Map emitableItems = new HashMap<>(); + private final Set craftableKeys = Collections.unmodifiableSet(craftableItems.keySet()); + private final Set emittableKeys = Collections.unmodifiableSet(emitableItems.keySet()); + + private long lastModifiedOnTick = TickHandler.instance().getCurrentTick(); + public void addProvider(IGridNode node) { var provider = node.getService(ICraftingProvider.class); if (provider != null) { @@ -46,6 +52,7 @@ public void addProvider(IGridNode node) { var state = new ProviderState(provider); state.mount(this); craftingProviders.put(node, state); + setLastModifiedOnTick(); } } @@ -55,6 +62,7 @@ public void removeProvider(IGridNode node) { var state = craftingProviders.remove(node); if (state != null) { state.unmount(this); + setLastModifiedOnTick(); } } } @@ -78,6 +86,14 @@ public Set getCraftables(AEKeyFilter filter) { return result; } + public Set getCraftableKeys() { + return craftableKeys; + } + + public Set getEmittableKeys() { + return emittableKeys; + } + public Collection getCraftingFor(AEKey whatToCraft) { var patterns = this.craftableItems.get(whatToCraft); if (patterns != null) { @@ -208,4 +224,15 @@ private List getSortedPatterns() { private record PatternInfo(IPatternDetails pattern, ProviderState state) { } + + private void setLastModifiedOnTick() { + lastModifiedOnTick = TickHandler.instance().getCurrentTick(); + } + + /** + * @see TickHandler#getCurrentTick() + */ + public long getLastModifiedOnTick() { + return lastModifiedOnTick; + } } diff --git a/src/test/java/appeng/api/networking/GridServicesTest.java b/src/test/java/appeng/api/networking/GridServicesTest.java index 9d625a1e583..a5145b263f3 100644 --- a/src/test/java/appeng/api/networking/GridServicesTest.java +++ b/src/test/java/appeng/api/networking/GridServicesTest.java @@ -30,6 +30,8 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoSettings; +import net.minecraft.world.level.Level; + import appeng.util.BootstrapMinecraft; @MockitoSettings @@ -62,7 +64,7 @@ void restoreRegistry() throws Exception { @Test void testEmptyRegistry() { - var services = GridServices.createServices(grid); + var services = GridServices.createServices(grid).services(); assertThat(services).isEmpty(); } @@ -70,7 +72,7 @@ void testEmptyRegistry() { void testGridServiceWithDefaultConstructor() { GridServices.register(PublicInterface.class, GridService1.class); - var services = GridServices.createServices(grid); + var services = GridServices.createServices(grid).services(); assertThat(services).containsOnlyKeys(PublicInterface.class); assertThat(services.get(PublicInterface.class)).isInstanceOf(GridService1.class); } @@ -79,7 +81,7 @@ void testGridServiceWithDefaultConstructor() { void testGridServiceWithGridDependency() { GridServices.register(GridService2.class, GridService2.class); - var services = GridServices.createServices(grid); + var services = GridServices.createServices(grid).services(); assertThat(services).containsOnlyKeys(GridService2.class); var actual = (GridService2) services.get(GridService2.class); assertThat(actual.grid).isSameAs(grid); @@ -92,7 +94,7 @@ void testGridServicesWithDependencies() { GridServices.register(GridService3.class, GridService3.class); GridServices.register(GridService4.class, GridService4.class); - var services = GridServices.createServices(grid); + var services = GridServices.createServices(grid).services(); assertThat(services).containsOnlyKeys( PublicInterface.class, GridService2.class, @@ -176,4 +178,50 @@ public AmbiguousConstructorClass(GridService2 gc2) { } } + @Test + void testTickingServices() { + GridServices.register(ServerStartTickOnly.class, ServerStartTickOnly.class); + GridServices.register(LevelStartTickOnly.class, LevelStartTickOnly.class); + GridServices.register(LevelEndTickOnly.class, LevelEndTickOnly.class); + GridServices.register(ServerEndTickOnly.class, ServerEndTickOnly.class); + + var services = GridServicesInternal.createServices(grid); + var map = services.services(); + assertThat(services.serverStartTickServices()) + .containsExactly(map.get(ServerStartTickOnly.class)); + assertThat(services.levelStartTickServices()) + .containsExactly(map.get(LevelStartTickOnly.class)); + assertThat(services.levelEndtickServices()) + .containsExactly(map.get(LevelEndTickOnly.class)); + assertThat(services.serverEndTickServices()) + .containsExactly(map.get(ServerEndTickOnly.class)); + } + + public static class ServerStartTickOnly implements IGridServiceProvider { + @Override + public void onServerStartTick() { + IGridServiceProvider.super.onServerStartTick(); + } + } + + public static class LevelStartTickOnly implements IGridServiceProvider { + @Override + public void onLevelStartTick(Level level) { + IGridServiceProvider.super.onLevelStartTick(level); + } + } + + public static class LevelEndTickOnly implements IGridServiceProvider { + @Override + public void onLevelEndTick(Level level) { + IGridServiceProvider.super.onLevelEndTick(level); + } + } + + public static class ServerEndTickOnly implements IGridServiceProvider { + @Override + public void onServerEndTick() { + IGridServiceProvider.super.onServerEndTick(); + } + } }