Make event phase ordering logic usable in other contexts (internally only) ()

* Make event phase ordering logic usable in other contexts (internally only)

* Rename and move to toposort package
This commit is contained in:
Technici4n 2023-07-18 13:54:12 +02:00 committed by modmuss50
parent 53c11dad6d
commit 737a6ee8bf
6 changed files with 246 additions and 180 deletions
fabric-api-base/src
main/java/net/fabricmc/fabric/impl/base
testmod/java/net/fabricmc/fabric/test/base

View file

@ -18,22 +18,19 @@ package net.fabricmc.fabric.impl.base.event;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.impl.base.toposort.NodeSorting;
class ArrayBackedEvent<T> extends Event<T> {
static final Logger LOGGER = LoggerFactory.getLogger("fabric-api-base");
private final Function<T[], T> invokerFactory;
private final Object lock = new Object();
private T[] handlers;
@ -82,7 +79,7 @@ class ArrayBackedEvent<T> extends Event<T> {
sortedPhases.add(phase);
if (sortIfCreate) {
PhaseSorting.sortPhases(sortedPhases);
NodeSorting.sort(sortedPhases, "event phases", Comparator.comparing(data -> data.id));
}
}
@ -121,9 +118,8 @@ class ArrayBackedEvent<T> extends Event<T> {
synchronized (lock) {
EventPhaseData<T> first = getOrCreatePhase(firstPhase, false);
EventPhaseData<T> second = getOrCreatePhase(secondPhase, false);
first.subsequentPhases.add(second);
second.previousPhases.add(first);
PhaseSorting.sortPhases(this.sortedPhases);
EventPhaseData.link(first, second);
NodeSorting.sort(this.sortedPhases, "event phases", Comparator.comparing(data -> data.id));
rebuildInvoker(handlers.length);
}
}

View file

@ -17,21 +17,18 @@
package net.fabricmc.fabric.impl.base.event;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.impl.base.toposort.SortableNode;
/**
* Data of an {@link ArrayBackedEvent} phase.
*/
class EventPhaseData<T> {
class EventPhaseData<T> extends SortableNode<EventPhaseData<T>> {
final Identifier id;
T[] listeners;
final List<EventPhaseData<T>> subsequentPhases = new ArrayList<>();
final List<EventPhaseData<T>> previousPhases = new ArrayList<>();
int visitStatus = 0; // 0: not visited, 1: visiting, 2: visited
@SuppressWarnings("unchecked")
EventPhaseData(Identifier id, Class<?> listenerClass) {
@ -44,4 +41,9 @@ class EventPhaseData<T> {
listeners = Arrays.copyOf(listeners, oldLength + 1);
listeners[oldLength] = listener;
}
@Override
protected String getDescription() {
return id.toString();
}
}

View file

@ -1,162 +0,0 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.base.event;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import com.google.common.annotations.VisibleForTesting;
/**
* Contains phase-sorting logic for {@link ArrayBackedEvent}.
*/
public class PhaseSorting {
@VisibleForTesting
public static boolean ENABLE_CYCLE_WARNING = true;
/**
* Deterministically sort a list of phases.
* 1) Compute phase SCCs (i.e. cycles).
* 2) Sort phases by id within SCCs.
* 3) Sort SCCs with respect to each other by respecting constraints, and by id in case of a tie.
*/
static <T> void sortPhases(List<EventPhaseData<T>> sortedPhases) {
// FIRST KOSARAJU SCC VISIT
List<EventPhaseData<T>> toposort = new ArrayList<>(sortedPhases.size());
for (EventPhaseData<T> phase : sortedPhases) {
forwardVisit(phase, null, toposort);
}
clearStatus(toposort);
Collections.reverse(toposort);
// SECOND KOSARAJU SCC VISIT
Map<EventPhaseData<T>, PhaseScc<T>> phaseToScc = new IdentityHashMap<>();
for (EventPhaseData<T> phase : toposort) {
if (phase.visitStatus == 0) {
List<EventPhaseData<T>> sccPhases = new ArrayList<>();
// Collect phases in SCC.
backwardVisit(phase, sccPhases);
// Sort phases by id.
sccPhases.sort(Comparator.comparing(p -> p.id));
// Mark phases as belonging to this SCC.
PhaseScc<T> scc = new PhaseScc<>(sccPhases);
for (EventPhaseData<T> phaseInScc : sccPhases) {
phaseToScc.put(phaseInScc, scc);
}
}
}
clearStatus(toposort);
// Build SCC graph
for (PhaseScc<T> scc : phaseToScc.values()) {
for (EventPhaseData<T> phase : scc.phases) {
for (EventPhaseData<T> subsequentPhase : phase.subsequentPhases) {
PhaseScc<T> subsequentScc = phaseToScc.get(subsequentPhase);
if (subsequentScc != scc) {
scc.subsequentSccs.add(subsequentScc);
subsequentScc.inDegree++;
}
}
}
}
// Order SCCs according to priorities. When there is a choice, use the SCC with the lowest id.
// The priority queue contains all SCCs that currently have 0 in-degree.
PriorityQueue<PhaseScc<T>> pq = new PriorityQueue<>(Comparator.comparing(scc -> scc.phases.get(0).id));
sortedPhases.clear();
for (PhaseScc<T> scc : phaseToScc.values()) {
if (scc.inDegree == 0) {
pq.add(scc);
// Prevent adding the same SCC multiple times, as phaseToScc may contain the same value multiple times.
scc.inDegree = -1;
}
}
while (!pq.isEmpty()) {
PhaseScc<T> scc = pq.poll();
sortedPhases.addAll(scc.phases);
for (PhaseScc<T> subsequentScc : scc.subsequentSccs) {
subsequentScc.inDegree--;
if (subsequentScc.inDegree == 0) {
pq.add(subsequentScc);
}
}
}
}
private static <T> void forwardVisit(EventPhaseData<T> phase, EventPhaseData<T> parent, List<EventPhaseData<T>> toposort) {
if (phase.visitStatus == 0) {
// Not yet visited.
phase.visitStatus = 1;
for (EventPhaseData<T> data : phase.subsequentPhases) {
forwardVisit(data, phase, toposort);
}
toposort.add(phase);
phase.visitStatus = 2;
} else if (phase.visitStatus == 1 && ENABLE_CYCLE_WARNING) {
// Already visiting, so we have found a cycle.
ArrayBackedEvent.LOGGER.warn(String.format(
"Event phase ordering conflict detected.%nEvent phase %s is ordered both before and after event phase %s.",
phase.id,
parent.id
));
}
}
private static <T> void clearStatus(List<EventPhaseData<T>> phases) {
for (EventPhaseData<T> phase : phases) {
phase.visitStatus = 0;
}
}
private static <T> void backwardVisit(EventPhaseData<T> phase, List<EventPhaseData<T>> sccPhases) {
if (phase.visitStatus == 0) {
phase.visitStatus = 1;
sccPhases.add(phase);
for (EventPhaseData<T> data : phase.previousPhases) {
backwardVisit(data, sccPhases);
}
}
}
private static class PhaseScc<T> {
final List<EventPhaseData<T>> phases;
final List<PhaseScc<T>> subsequentSccs = new ArrayList<>();
int inDegree = 0;
private PhaseScc(List<EventPhaseData<T>> phases) {
this.phases = phases;
}
}
}

View file

@ -0,0 +1,190 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.base.toposort;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Contains a topological sort implementation, with tie breaking using a {@link Comparator}.
*
* <p>The final order is always deterministic (i.e. doesn't change with the order of the input elements or the edges),
* assuming that they are all different according to the comparator. This also holds in the presence of cycles.
*
* <p>The steps are as follows:
* <ol>
* <li>Compute node SCCs (Strongly Connected Components, i.e. cycles).</li>
* <li>Sort nodes within SCCs using the comparator.</li>
* <li>Sort SCCs with respect to each other by respecting constraints, and using the comparator in case of a tie.</li>
* </ol>
*/
public class NodeSorting {
private static final Logger LOGGER = LoggerFactory.getLogger("fabric-api-base");
@VisibleForTesting
public static boolean ENABLE_CYCLE_WARNING = true;
/**
* Sort a list of nodes.
*
* @param sortedNodes The list of nodes to sort. Will be modified in-place.
* @param elementDescription A description of the elements, used for logging in the presence of cycles.
* @param comparator The comparator to break ties and to order elements within a cycle.
* @return {@code true} if all the constraints were satisfied, {@code false} if there was at least one cycle.
*/
public static <N extends SortableNode<N>> boolean sort(List<N> sortedNodes, String elementDescription, Comparator<N> comparator) {
// FIRST KOSARAJU SCC VISIT
List<N> toposort = new ArrayList<>(sortedNodes.size());
for (N node : sortedNodes) {
forwardVisit(node, null, toposort);
}
clearStatus(toposort);
Collections.reverse(toposort);
// SECOND KOSARAJU SCC VISIT
Map<N, NodeScc<N>> nodeToScc = new IdentityHashMap<>();
for (N node : toposort) {
if (!node.visited) {
List<N> sccNodes = new ArrayList<>();
// Collect nodes in SCC.
backwardVisit(node, sccNodes);
// Sort nodes by id.
sccNodes.sort(comparator);
// Mark nodes as belonging to this SCC.
NodeScc<N> scc = new NodeScc<>(sccNodes);
for (N nodeInScc : sccNodes) {
nodeToScc.put(nodeInScc, scc);
}
}
}
clearStatus(toposort);
// Build SCC graph
for (NodeScc<N> scc : nodeToScc.values()) {
for (N node : scc.nodes) {
for (N subsequentNode : node.subsequentNodes) {
NodeScc<N> subsequentScc = nodeToScc.get(subsequentNode);
if (subsequentScc != scc) {
scc.subsequentSccs.add(subsequentScc);
subsequentScc.inDegree++;
}
}
}
}
// Order SCCs according to priorities. When there is a choice, use the SCC with the lowest id.
// The priority queue contains all SCCs that currently have 0 in-degree.
PriorityQueue<NodeScc<N>> pq = new PriorityQueue<>(Comparator.comparing(scc -> scc.nodes.get(0), comparator));
sortedNodes.clear();
for (NodeScc<N> scc : nodeToScc.values()) {
if (scc.inDegree == 0) {
pq.add(scc);
// Prevent adding the same SCC multiple times, as nodeToScc may contain the same value multiple times.
scc.inDegree = -1;
}
}
boolean noCycle = true;
while (!pq.isEmpty()) {
NodeScc<N> scc = pq.poll();
sortedNodes.addAll(scc.nodes);
if (scc.nodes.size() > 1) {
noCycle = false;
if (ENABLE_CYCLE_WARNING) {
// Print cycle warning
StringBuilder builder = new StringBuilder();
builder.append("Found cycle while sorting ").append(elementDescription).append(":\n");
for (N node : scc.nodes) {
builder.append("\t").append(node.getDescription()).append("\n");
}
LOGGER.warn(builder.toString());
}
}
for (NodeScc<N> subsequentScc : scc.subsequentSccs) {
subsequentScc.inDegree--;
if (subsequentScc.inDegree == 0) {
pq.add(subsequentScc);
}
}
}
return noCycle;
}
private static <N extends SortableNode<N>> void forwardVisit(N node, N parent, List<N> toposort) {
if (!node.visited) {
// Not yet visited.
node.visited = true;
for (N data : node.subsequentNodes) {
forwardVisit(data, node, toposort);
}
toposort.add(node);
}
}
private static <N extends SortableNode<N>> void clearStatus(List<N> nodes) {
for (N node : nodes) {
node.visited = false;
}
}
private static <N extends SortableNode<N>> void backwardVisit(N node, List<N> sccNodes) {
if (!node.visited) {
node.visited = true;
sccNodes.add(node);
for (N data : node.previousNodes) {
backwardVisit(data, sccNodes);
}
}
}
private static class NodeScc<N extends SortableNode<N>> {
final List<N> nodes;
final List<NodeScc<N>> subsequentSccs = new ArrayList<>();
int inDegree = 0;
private NodeScc(List<N> nodes) {
this.nodes = nodes;
}
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.base.toposort;
import java.util.ArrayList;
import java.util.List;
public abstract class SortableNode<N extends SortableNode<N>> {
final List<N> subsequentNodes = new ArrayList<>();
final List<N> previousNodes = new ArrayList<>();
boolean visited = false;
/**
* @return Description of this node, used to print the cycle warning.
*/
protected abstract String getDescription();
public static <N extends SortableNode<N>> void link(N first, N second) {
if (first == second) {
throw new IllegalArgumentException("Cannot link a node to itself!");
}
first.subsequentNodes.add(second);
second.previousNodes.add(first);
}
}

View file

@ -29,7 +29,7 @@ import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.fabricmc.fabric.impl.base.event.PhaseSorting;
import net.fabricmc.fabric.impl.base.toposort.NodeSorting;
public class EventTests {
private static final Logger LOGGER = LoggerFactory.getLogger("fabric-api-base");
@ -41,10 +41,10 @@ public class EventTests {
testMultipleDefaultPhases();
testAddedPhases();
testCycle();
PhaseSorting.ENABLE_CYCLE_WARNING = false;
NodeSorting.ENABLE_CYCLE_WARNING = false;
testDeterministicOrdering();
testTwoCycles();
PhaseSorting.ENABLE_CYCLE_WARNING = true;
NodeSorting.ENABLE_CYCLE_WARNING = true;
long time2 = System.currentTimeMillis();
LOGGER.info("Event unit tests succeeded in {} milliseconds.", time2 - time1);