Skip to content

Commit

Permalink
Optimize channel computation (#8285)
Browse files Browse the repository at this point in the history
- Optimize the channel computation to make it go from O(N^2) to O(N).
- Cleanup related code a little bit. Most importantly, choose the same
meaning for `usedChannels` vs `lastUsedChannels` for both
`GridConnection` and `GridNode`.
- Remove booting delay. Note that the API still supports it, and that we
might want to add back some sort of delay in the future if necessary.
- Remove `pathfindingStepsPerTick` config option.
  • Loading branch information
Technici4n authored Dec 17, 2024
1 parent 5b884a3 commit 1600e93
Show file tree
Hide file tree
Showing 11 changed files with 477 additions and 162 deletions.
8 changes: 0 additions & 8 deletions src/main/java/appeng/core/AEConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,6 @@ public void setChannelModel(ChannelMode mode) {
}
}

public int getPathfindingStepsPerTick() {
return common.pathfindingStepsPerTick.get();
}

/**
* @return True if an in-world preview of parts and facade placement should be shown when holding one in hand.
*/
Expand Down Expand Up @@ -531,7 +527,6 @@ private static class CommonConfig {
public final BooleanValue matterCannonBlockDamage;
public final BooleanValue tinyTntBlockDamage;
public final EnumValue<ChannelMode> channels;
public final IntValue pathfindingStepsPerTick;
public final BooleanValue spatialAnchorEnableRandomTicks;

public final IntValue growthAcceleratorSpeed;
Expand Down Expand Up @@ -601,9 +596,6 @@ public CommonConfig() {
"Enables the ability of Tiny TNT to break blocks.");
channels = defineEnum(builder, "channels", ChannelMode.DEFAULT,
"Changes the channel capacity that cables provide in AE2.");
pathfindingStepsPerTick = define(builder, "pathfindingStepsPerTick", 4,
1, 1024,
"The number of pathfinding steps that are taken per tick and per grid that is booting. Lower numbers will mean booting takes longer, but less work is done per tick.");
spatialAnchorEnableRandomTicks = define(builder, "spatialAnchorEnableRandomTicks", true,
"Whether Spatial Anchors should force random chunk ticks and entity spawning.");
builder.pop();
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/appeng/debug/DebugCardItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ public InteractionResult onItemUseFirst(ItemStack stack, UseOnContext context) {
partHost.markForUpdate();
if (center != null) {
final GridNode n = (GridNode) center.getGridNode();
this.outputSecondaryMessage(player, "Node Channels", Integer.toString(n.usedChannels()));
this.outputSecondaryMessage(player, "Node Channels", Integer.toString(n.getUsedChannels()));
for (var entry : n.getInWorldConnections().entrySet()) {
this.outputSecondaryMessage(player, "Channels " + entry.getKey().getName(),
Integer.toString(entry.getValue().getUsedChannels()));
Expand Down
55 changes: 29 additions & 26 deletions src/main/java/appeng/me/GridConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@

public class GridConnection implements IGridConnection, IPathItem {

private int usedChannels = 0;
/**
* Will be modified during pathing and should not be exposed outside of that purpose.
*/
int usedChannels = 0;
/**
* Finalized version of {@link #usedChannels} once pathing is done.
*/
private int lastUsedChannels = 0;
private Object visitorIterationNumber = null;
/**
Expand Down Expand Up @@ -93,12 +99,12 @@ public void destroy() {
}

@Override
public IGridNode a() {
public GridNode a() {
return this.sideA;
}

@Override
public IGridNode b() {
public GridNode b() {
return this.sideB;
}

Expand All @@ -109,20 +115,22 @@ public boolean isInWorld() {

@Override
public int getUsedChannels() {
return usedChannels;
return lastUsedChannels;
}

@Override
public IPathItem getControllerRoute() {
if (this.sideA.hasFlag(GridFlags.CANNOT_CARRY)) {
return null;
}
public void setAdHocChannels(int channels) {
this.usedChannels = channels;
}

@Override
public GridNode getControllerRoute() {
return this.sideA;
}

@Override
public void setControllerRoute(IPathItem fast) {
this.lastUsedChannels = 0;
this.usedChannels = 0;

// If the shortest route to the controller is via side B, we need to flip the
// connections sides because side A should be the closest route to the controller.
Expand All @@ -136,11 +144,6 @@ public void setControllerRoute(IPathItem fast) {
}
}

@Override
public boolean canSupportMoreChannels() {
return this.getLastUsedChannels() < getMaxChannels();
}

@Override
public int getMaxChannels() {
var mode = sideB.getGrid().getPathingService().getChannelMode();
Expand All @@ -152,23 +155,27 @@ public int getMaxChannels() {

@Override
public Iterable<IPathItem> getPossibleOptions() {
return ImmutableList.of((IPathItem) this.a(), (IPathItem) this.b());
}

@Override
public void incrementChannelCount(int usedChannels) {
this.lastUsedChannels += usedChannels;
return ImmutableList.of(this.a(), this.b());
}

@Override
public boolean hasFlag(GridFlags flag) {
return false;
}

public int propagateChannelsUpwards() {
if (this.sideB.getControllerRoute() == this) { // Check that we are in B's route
this.usedChannels = this.sideB.usedChannels;
} else {
this.usedChannels = 0;
}
return this.usedChannels;
}

@Override
public void finalizeChannels() {
if (this.getUsedChannels() != this.getLastUsedChannels()) {
this.usedChannels = this.lastUsedChannels;
if (this.lastUsedChannels != this.usedChannels) {
this.lastUsedChannels = this.usedChannels;

if (this.sideA.getInternalGrid() != null) {
this.sideA.notifyStatusChange(IGridNodeListener.State.CHANNEL);
Expand All @@ -180,10 +187,6 @@ public void finalizeChannels() {
}
}

private int getLastUsedChannels() {
return this.lastUsedChannels;
}

Object getVisitorIterationNumber() {
return this.visitorIterationNumber;
}
Expand Down
110 changes: 93 additions & 17 deletions src/main/java/appeng/me/GridNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.MutableClassToInstanceMap;
import com.google.gson.stream.JsonWriter;
import com.mojang.logging.LogUtils;

import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;

import net.minecraft.CrashReportCategory;
import net.minecraft.core.Direction;
Expand Down Expand Up @@ -64,12 +66,15 @@
import appeng.api.parts.IPart;
import appeng.api.stacks.AEItemKey;
import appeng.api.util.AEColor;
import appeng.blockentity.networking.ControllerBlockEntity;
import appeng.core.AELog;
import appeng.me.pathfinding.IPathItem;
import appeng.util.IDebugExportable;
import appeng.util.JsonStreamUtil;

public class GridNode implements IGridNode, IPathItem, IDebugExportable {
private static final Logger LOGGER = LogUtils.getLogger();

private final ServerLevel level;
/**
* This is the logical host of the node, which could be any object. In many cases this will be a block entity or
Expand All @@ -96,9 +101,33 @@ public class GridNode implements IGridNode, IPathItem, IDebugExportable {
private int owningPlayerId = -1;
private Grid myGrid;
private Object visitorIterationNumber = null;
// connection criteria
private int usedChannels = 0;
/**
* Will be modified during pathing and should not be exposed outside of that purpose.
*/
int usedChannels = 0;
/**
* Finalized version of {@link #usedChannels} once pathing is done.
*/
private int lastUsedChannels = 0;
/**
* The nearest ancestor of this node which restricts the number of maximum available channels for its subtree. It is
* {@code null} if the next node is a controller.
* <p>
* Used to quickly walk the path to the controller when checking channel assignability, based on the observation
* that the max channel count increases as we get to the controller, and that we only need to check the highest node
* of each max channel count.
* <p>
* For example, on the following path:
* {@code controller - dense cable 1 - dense cable 2 - dense cable 3 - cable 1 - cable 2 - cable 3 - device}, we
* need to check that {@code dense cable 1} can accept the additional channel. If this is true then dense cables
* {@code 2} and {@code 3} can always accept it. Same for regular cables, so it is enough to check that
* {@code dense cable 1} and {@code cable 1} can accept it, massively speeding up the assignment for large trees.
*/
@Nullable
private GridNode highestSimilarAncestor = null;
private int subtreeMaxChannels;
private boolean subtreeAllowsCompressedChannels;

private final EnumSet<GridFlags> flags;
private ClassToInstanceMap<IGridNodeService> services;

Expand All @@ -124,10 +153,6 @@ Grid getMyGrid() {
return this.myGrid;
}

public int usedChannels() {
return this.lastUsedChannels;
}

@FunctionalInterface
public interface ListenerCallback<T> {
void call(IGridNodeListener<T> listener, T owner, IGridNode node);
Expand Down Expand Up @@ -560,19 +585,54 @@ private void visitorNode(Object tracker, IGridVisitor g, Deque<GridNode> nextRun
}
}

@Override
public void setAdHocChannels(int channels) {
this.usedChannels = channels;
}

@Override
public IPathItem getControllerRoute() {
if (this.connections.isEmpty() || this.hasFlag(GridFlags.CANNOT_CARRY)) {
return null;
if (this.connections.isEmpty()) {
throw new IllegalStateException(
"Node %s has no connections, cannot have a controller route!".formatted(this));
}

return this.connections.get(0);
return this.connections.getFirst();
}

public @Nullable GridNode getHighestSimilarAncestor() {
return highestSimilarAncestor;
}

public boolean getSubtreeAllowsCompressedChannels() {
return subtreeAllowsCompressedChannels;
}

@Override
public void setControllerRoute(IPathItem fast) {
this.usedChannels = 0;

var nodeParent = (GridNode) fast.getControllerRoute();
if (nodeParent.getOwner() instanceof ControllerBlockEntity) {
this.highestSimilarAncestor = null;
this.subtreeMaxChannels = getMaxChannels();
this.subtreeAllowsCompressedChannels = !hasFlag(GridFlags.CANNOT_CARRY_COMPRESSED);
} else {
if (nodeParent.highestSimilarAncestor == null) {
// Parent is connected to a controller, it is the bottleneck.
this.highestSimilarAncestor = nodeParent;
} else if (nodeParent.subtreeMaxChannels == nodeParent.highestSimilarAncestor.subtreeMaxChannels) {
// Parent is not restricting the number of channels, go as high as possible.
this.highestSimilarAncestor = nodeParent.highestSimilarAncestor;
} else {
// Parent is restricting the number of channels, link to it directly.
this.highestSimilarAncestor = nodeParent;
}
this.subtreeMaxChannels = Math.min(nodeParent.subtreeMaxChannels, getMaxChannels());
this.subtreeAllowsCompressedChannels = nodeParent.subtreeAllowsCompressedChannels
&& !hasFlag(GridFlags.CANNOT_CARRY_COMPRESSED);
}

GridConnection connection = (GridConnection) fast;

final int idx = this.connections.indexOf(connection);
Expand All @@ -582,14 +642,9 @@ public void setControllerRoute(IPathItem fast) {
}
}

@Override
public boolean canSupportMoreChannels() {
return this.getUsedChannels() < this.getMaxChannels();
}

@Override
public int getUsedChannels() {
return this.usedChannels;
return this.lastUsedChannels;
}

@Override
Expand All @@ -615,18 +670,39 @@ public Iterable<IPathItem> getPossibleOptions() {
return ImmutableList.copyOf(this.connections);
}

@Override
public int propagateChannelsUpwards(boolean consumesChannel) {
this.usedChannels = 0;
for (var connection : connections) {
if (connection.getControllerRoute() == this) {
this.usedChannels += connection.usedChannels;
}
}
if (consumesChannel) {
this.usedChannels++;
}

if (this.usedChannels > getMaxChannels()) {
LOGGER.error(
"Internal channel assignment error. Grid node {} has {} channels passing through it but it only supports up to {}. Please open an issue on the AE2 repository.",
this, this.usedChannels, getMaxChannels());
}

return this.usedChannels;
}

public void incrementChannelCount(int usedChannels) {
this.usedChannels += usedChannels;
}

@Override
public void finalizeChannels() {
this.highestSimilarAncestor = null;

if (hasFlag(GridFlags.CANNOT_CARRY)) {
return;
}

if (this.lastUsedChannels != this.getUsedChannels()) {
if (this.lastUsedChannels != this.usedChannels) {
this.lastUsedChannels = this.usedChannels;

if (this.getInternalGrid() != null) {
Expand Down
6 changes: 2 additions & 4 deletions src/main/java/appeng/me/pathfinding/AdHocChannelUpdater.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,13 @@ public AdHocChannelUpdater(int used) {
@Override
public boolean visitNode(IGridNode n) {
final GridNode gn = (GridNode) n;
gn.setControllerRoute(null);
gn.incrementChannelCount(this.usedChannels);
gn.setAdHocChannels(this.usedChannels);
return true;
}

@Override
public void visitConnection(IGridConnection gcc) {
final GridConnection gc = (GridConnection) gcc;
gc.setControllerRoute(null);
gc.incrementChannelCount(this.usedChannels);
gc.setAdHocChannels(this.usedChannels);
}
}
Loading

0 comments on commit 1600e93

Please sign in to comment.