mirror of
https://github.com/FabricMC/fabric.git
synced 2025-04-21 11:20:55 -04:00
Make event phase ordering logic usable in other contexts (internally only) (#3183)
* Make event phase ordering logic usable in other contexts (internally only) * Rename and move to toposort package
This commit is contained in:
parent
53c11dad6d
commit
737a6ee8bf
6 changed files with 246 additions and 180 deletions
fabric-api-base/src
main/java/net/fabricmc/fabric/impl/base
event
toposort
testmod/java/net/fabricmc/fabric/test/base
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue