Bug 538752 - Improve buildFilteredDigraph algorithm
Use a deep traverse of the graph with dynamic recursivity.
This leads to much better results in worst case.
Change-Id: If127067c00bd5ad0fad0b68319eb91dc166be5ca
Signed-off-by: Mickael Istria <mistria@redhat.com>
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ComputeProjectOrder.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ComputeProjectOrder.java
index 49da2b8..7c27d9a 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ComputeProjectOrder.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ComputeProjectOrder.java
@@ -18,9 +18,8 @@
import java.lang.reflect.Array;
import java.util.*;
+import java.util.function.Function;
import java.util.function.Predicate;
-import java.util.stream.Collectors;
-import org.eclipse.core.internal.resources.ComputeProjectOrder.Digraph.Edge;
import org.eclipse.core.internal.resources.ComputeProjectOrder.Digraph.Vertex;
import org.eclipse.core.runtime.Assert;
@@ -678,7 +677,8 @@
* Builds a digraph excluding the nodes that do not match filter, but keeps transitive edges. Ie if A->B->C and B is removed,
* result would be A->C.
*
- * This is an expensive operation (worst case is O(v^3) with v number of vertexes, or O(e^2) with e number of edges.
+ * Complexity is O(#edge + #vertex). It implements a dynamic recursive deep-first graph traversing algorithm to compute
+ * resutling edges.
*
* @param initialGraph
* @param filterOut a filter to exclude nodes.
@@ -686,58 +686,53 @@
* @return the filtered graph.
*/
public static <T> Digraph<T> buildFilteredDigraph(Digraph<T> initialGraph, Predicate<T> filterOut, Class<T> clazz) {
- Digraph<T> res = new Digraph<>(clazz);
- // Filter removed vs remaining vertices and initialize result graph with remaining vertices
- Set<T> toRemoveIds = new LinkedHashSet<>();
+ Digraph<T> filteredGraph = new Digraph<>(clazz);
+ // build vertices
for (Vertex<T> vertex : initialGraph.vertexList) {
T id = vertex.id;
if (!filterOut.test(id)) {
- res.addVertex(id);
- } else {
- toRemoveIds.add(id);
+ filteredGraph.addVertex(id);
}
}
- if (res.vertexList.size() < 2) { // 0 or 1 node -> no edge
- return res;
- }
- // filter "invalid" edges (a bound is filtered out) vs remaining edge.
- // Invalid edge are stored for further processing. Valid edges are added
- // directly to result graph.
- final Set<Edge<T>> edgesToReplace = new LinkedHashSet<>();
- initialGraph.getEdges().forEach(edge -> {
- if (toRemoveIds.contains(edge.from) || toRemoveIds.contains(edge.to)) {
- edgesToReplace.add(edge);
- } else {
- res.addEdge(edge.from, edge.to);
- }
- });
- // Remove filtered node, 1 by 1, and update the related edges
- for (T toRemove : toRemoveIds) {
- // Discard edges that loop on the removed vertex, as it won't participate in filtered graph
- // Note: this can be a common case for subgraph when there are cycles in the initial graph.
- edgesToReplace.removeIf(edge -> edge.from == toRemove && edge.to == toRemove);
- // Pick related edges
- Set<Edge<T>> incoming = edgesToReplace.stream().filter(edge -> edge.to == toRemove).collect(Collectors.toSet());
- Set<Edge<T>> outgoing = edgesToReplace.stream().filter(edge -> edge.from == toRemove).collect(Collectors.toSet());
- // Remove related edges from the set of remaining edges to consider
- edgesToReplace.removeAll(incoming);
- edgesToReplace.removeAll(outgoing);
- // Replace all occurrences of A->toRemove and toRemove->C couple by A->C
- incoming.forEach(incomingEdge -> outgoing.forEach(outgoingEdge -> {
- Edge<T> edge = new Edge<>(incomingEdge.from, outgoingEdge.to);
- if (toRemoveIds.contains(edge.from) || toRemoveIds.contains(edge.to)) {
- // This edge is not valid and still needs to be considered for replacement
- edgesToReplace.add(edge);
- } else {
- // Edge is valid and can be pushed in the result graph
- res.addEdge(edge.from, edge.to);
+ // Takes an id as input, and returns the nodes in the filteredGraph that are adjacent to this node
+ // so that if initial graph has A->B and B->C and B->D and B is removed, this function return C and D
+ // when invoked on A.
+ Function<T, Set<T>> computeAdjacents = new Function<T, Set<T>>() {
+ private Set<T> processing = new HashSet<>();
+ // Store intermediary results to not repeat same computations with the same expected results
+ private Map<T, Set<T>> adjacentsMap = new HashMap<>(initialGraph.vertexList.size(), 1.f);
+
+ @Override
+ public Set<T> apply(T id) {
+ if (adjacentsMap.containsKey(id)) {
+ return adjacentsMap.get(id);
+ } else if (processing.contains(id)) {
+ // in a cycle, skip processing as no new edge is to expect.
+ // But don't store result, return directly!
+ return Collections.emptySet();
}
- }));
- }
- if (!edgesToReplace.isEmpty()) {
- throw new IllegalStateException(edgesToReplace.size() + " edges are remaining at this point: " + //$NON-NLS-1$
- edgesToReplace.toString() + "\nThere should be none. This indicates a bug in the graph filtering algorithm"); //$NON-NLS-1$
- }
- return res;
+ processing.add(id);
+ Set<T> resolvedAdjacents = new HashSet<>();
+ initialGraph.vertexMap.get(id).adjacent.forEach(adjacent -> {
+ if (filteredGraph.vertexMap.keySet().contains(adjacent.id)) {
+ // adjacent in target graph, just take it.
+ resolvedAdjacents.add(adjacent.id);
+ } else {
+ // adjacent filtered out, take its resolved existing adjacents
+ resolvedAdjacents.addAll(apply(adjacent.id));
+ }
+ });
+ adjacentsMap.put(id, resolvedAdjacents);
+ processing.remove(id);
+ return resolvedAdjacents;
+ }
+ };
+ filteredGraph.vertexMap.keySet().forEach(id -> {
+ computeAdjacents.apply(id).forEach(adjacent -> {
+ filteredGraph.addEdge(id, adjacent);
+ });
+ });
+
+ return filteredGraph;
}
}