diff --git a/archunit/src/main/java/com/tngtech/archunit/library/metrics/MetricsComponentDependencyGraph.java b/archunit/src/main/java/com/tngtech/archunit/library/metrics/MetricsComponentDependencyGraph.java index 64d4b6bd2..8ea68bf18 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/metrics/MetricsComponentDependencyGraph.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/metrics/MetricsComponentDependencyGraph.java @@ -15,13 +15,21 @@ */ package com.tngtech.archunit.library.metrics; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; import java.util.Collection; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.SetMultimap; @@ -29,11 +37,14 @@ class MetricsComponentDependencyGraph { private final SetMultimap, MetricsComponent> outgoingComponentDependencies; private final SetMultimap, MetricsComponent> incomingComponentDependencies; + private final Map, Set>> transitiveComponentDependencies; private MetricsComponentDependencyGraph(Iterable> components, Function> getDependencies) { - ImmutableSetMultimap, MetricsComponent> componentDependencies = createComponentDependencies(components, getDependencies); + ImmutableList> componentList = ImmutableList.copyOf(components); + ImmutableSetMultimap, MetricsComponent> componentDependencies = createComponentDependencies(componentList, getDependencies); this.outgoingComponentDependencies = componentDependencies; this.incomingComponentDependencies = componentDependencies.inverse(); + this.transitiveComponentDependencies = new TransitiveDependencyPrecomputation<>(componentList, componentDependencies).getTransitiveDependenciesByComponent(); } private ImmutableSetMultimap, MetricsComponent> createComponentDependencies(Iterable> components, Function> getDependencies) { @@ -77,27 +88,316 @@ Set> getDirectDependenciesTo(MetricsComponent target) { } Set> getTransitiveDependenciesOf(MetricsComponent origin) { - ImmutableSet.Builder> transitiveDependencies = ImmutableSet.builder(); - Set> analyzedComponents = new HashSet<>(); // to avoid infinite recursion for cyclic dependencies - addTransitiveDependenciesFrom(origin, transitiveDependencies, analyzedComponents); - return transitiveDependencies.build(); + return transitiveComponentDependencies.getOrDefault(origin, ImmutableSet.of()); } - private void addTransitiveDependenciesFrom(MetricsComponent component, ImmutableSet.Builder> transitiveDependencies, Set> analyzedComponents) { - analyzedComponents.add(component); // currently being analyzed - Set> dependencyTargetsToRecurse = new HashSet<>(); - for (MetricsComponent dependency : getDirectDependenciesFrom(component)) { - transitiveDependencies.add(dependency); - dependencyTargetsToRecurse.add(dependency); + static MetricsComponentDependencyGraph of(Iterable> components, Function> getDependencies) { + return new MetricsComponentDependencyGraph<>(components, getDependencies); + } + + private static final class TransitiveDependencyPrecomputation { + private final ImmutableList> components; + private final int[][] outgoingEdgesByComponentIndex; + private final int[][] incomingEdgesByComponentIndex; + + private TransitiveDependencyPrecomputation( + ImmutableList> components, + ImmutableSetMultimap, MetricsComponent> outgoingComponentDependencies + ) { + this.components = components; + ImmutableSetMultimap, MetricsComponent> incomingComponentDependencies = outgoingComponentDependencies.inverse(); + + Map, Integer> componentIndexByComponent = new HashMap<>(); + for (int i = 0; i < components.size(); i++) { + componentIndexByComponent.put(components.get(i), i); + } + + outgoingEdgesByComponentIndex = new int[components.size()][]; + incomingEdgesByComponentIndex = new int[components.size()][]; + + for (int i = 0; i < components.size(); i++) { + MetricsComponent component = components.get(i); + outgoingEdgesByComponentIndex[i] = toIndexes(outgoingComponentDependencies.get(component), componentIndexByComponent); + incomingEdgesByComponentIndex[i] = toIndexes(incomingComponentDependencies.get(component), componentIndexByComponent); + } + } + + private int[] toIndexes(Collection> components, Map, Integer> componentIndexByComponent) { + int[] result = new int[components.size()]; + int index = 0; + for (MetricsComponent component : components) { + result[index++] = componentIndexByComponent.get(component); + } + return result; + } + + private Map, Set>> getTransitiveDependenciesByComponent() { + // Collapse cycles first, then compute reachability once on the condensed DAG. + StronglyConnectedComponents stronglyConnectedComponents = determineStronglyConnectedComponents(); + CondensedGraph condensedGraph = condense(stronglyConnectedComponents); + BitSet[] reachableStronglyConnectedComponents = determineReachableStronglyConnectedComponents(condensedGraph); + return mapToTransitiveDependenciesByComponent(stronglyConnectedComponents, reachableStronglyConnectedComponents); + } + + private StronglyConnectedComponents determineStronglyConnectedComponents() { + List nodesByFinishingOrder = determineFinishingOrder(); + int[] stronglyConnectedComponentIndexByComponentIndex = new int[components.size()]; + Arrays.fill(stronglyConnectedComponentIndexByComponentIndex, -1); + List componentIndexesByStronglyConnectedComponentIndex = new ArrayList<>(); + boolean[] isStronglyConnectedComponentSelfReachable = new boolean[components.size()]; + int stronglyConnectedComponentCount = 0; + + for (int i = nodesByFinishingOrder.size() - 1; i >= 0; i--) { + int componentIndex = nodesByFinishingOrder.get(i); + if (stronglyConnectedComponentIndexByComponentIndex[componentIndex] >= 0) { + continue; + } + + IntCollector currentStronglyConnectedComponent = new IntCollector(); + Deque stack = new ArrayDeque<>(); + stack.push(componentIndex); + stronglyConnectedComponentIndexByComponentIndex[componentIndex] = stronglyConnectedComponentCount; + + while (!stack.isEmpty()) { + int currentComponentIndex = stack.pop(); + currentStronglyConnectedComponent.add(currentComponentIndex); + + for (int incomingDependencyIndex : incomingEdgesByComponentIndex[currentComponentIndex]) { + if (stronglyConnectedComponentIndexByComponentIndex[incomingDependencyIndex] < 0) { + stronglyConnectedComponentIndexByComponentIndex[incomingDependencyIndex] = stronglyConnectedComponentCount; + stack.push(incomingDependencyIndex); + } + } + } + + int[] stronglyConnectedComponent = currentStronglyConnectedComponent.toArray(); + componentIndexesByStronglyConnectedComponentIndex.add(stronglyConnectedComponent); + isStronglyConnectedComponentSelfReachable[stronglyConnectedComponentCount] = + stronglyConnectedComponent.length > 1 || containsSelfLoop(stronglyConnectedComponent[0]); + stronglyConnectedComponentCount++; + } + + return new StronglyConnectedComponents( + stronglyConnectedComponentIndexByComponentIndex, + componentIndexesByStronglyConnectedComponentIndex, + isStronglyConnectedComponentSelfReachable, + stronglyConnectedComponentCount + ); + } + + private boolean containsSelfLoop(int componentIndex) { + for (int dependencyIndex : outgoingEdgesByComponentIndex[componentIndex]) { + if (dependencyIndex == componentIndex) { + return true; + } + } + return false; + } + + private List determineFinishingOrder() { + List nodesByFinishingOrder = new ArrayList<>(components.size()); + boolean[] visited = new boolean[components.size()]; + + for (int componentIndex = 0; componentIndex < components.size(); componentIndex++) { + if (visited[componentIndex]) { + continue; + } + depthFirstSearchOrderingByFinishTime(componentIndex, visited, nodesByFinishingOrder); + } + return nodesByFinishingOrder; + } + + private void depthFirstSearchOrderingByFinishTime(int startComponentIndex, boolean[] visited, List nodesByFinishingOrder) { + Deque stack = new ArrayDeque<>(); + stack.push(new TraversalFrame(startComponentIndex)); + + while (!stack.isEmpty()) { + TraversalFrame current = stack.peek(); + if (!visited[current.componentIndex]) { + visited[current.componentIndex] = true; + } + + if (current.nextDependencyIndex < outgoingEdgesByComponentIndex[current.componentIndex].length) { + int dependencyIndex = outgoingEdgesByComponentIndex[current.componentIndex][current.nextDependencyIndex++]; + if (!visited[dependencyIndex]) { + stack.push(new TraversalFrame(dependencyIndex)); + } + continue; + } + + nodesByFinishingOrder.add(current.componentIndex); + stack.pop(); + } + } + + private CondensedGraph condense(StronglyConnectedComponents stronglyConnectedComponents) { + List> outgoingStronglyConnectedComponents = new ArrayList<>(); + int[] incomingDegreeByStronglyConnectedComponentIndex = new int[stronglyConnectedComponents.count]; + for (int i = 0; i < stronglyConnectedComponents.count; i++) { + outgoingStronglyConnectedComponents.add(new HashSet<>()); + } + + for (int componentIndex = 0; componentIndex < components.size(); componentIndex++) { + int originStronglyConnectedComponentIndex = stronglyConnectedComponents.stronglyConnectedComponentIndexByComponentIndex[componentIndex]; + for (int dependencyIndex : outgoingEdgesByComponentIndex[componentIndex]) { + int targetStronglyConnectedComponentIndex = stronglyConnectedComponents.stronglyConnectedComponentIndexByComponentIndex[dependencyIndex]; + if (originStronglyConnectedComponentIndex != targetStronglyConnectedComponentIndex + && outgoingStronglyConnectedComponents.get(originStronglyConnectedComponentIndex).add(targetStronglyConnectedComponentIndex)) { + incomingDegreeByStronglyConnectedComponentIndex[targetStronglyConnectedComponentIndex]++; + } + } + } + + int[][] outgoingEdgesByStronglyConnectedComponentIndex = new int[stronglyConnectedComponents.count][]; + for (int i = 0; i < stronglyConnectedComponents.count; i++) { + outgoingEdgesByStronglyConnectedComponentIndex[i] = toArray(outgoingStronglyConnectedComponents.get(i)); + } + return new CondensedGraph(outgoingEdgesByStronglyConnectedComponentIndex, incomingDegreeByStronglyConnectedComponentIndex); + } + + private int[] toArray(Set values) { + int[] result = new int[values.size()]; + int index = 0; + for (Integer value : values) { + result[index++] = value; + } + return result; + } + + private BitSet[] determineReachableStronglyConnectedComponents(CondensedGraph condensedGraph) { + int[] topologicalOrder = determineTopologicalOrder(condensedGraph); + BitSet[] reachableStronglyConnectedComponentsByIndex = new BitSet[condensedGraph.outgoingEdgesByStronglyConnectedComponentIndex.length]; + + for (int i = topologicalOrder.length - 1; i >= 0; i--) { + int stronglyConnectedComponentIndex = topologicalOrder[i]; + BitSet reachable = new BitSet(condensedGraph.outgoingEdgesByStronglyConnectedComponentIndex.length); + for (int dependencyIndex : condensedGraph.outgoingEdgesByStronglyConnectedComponentIndex[stronglyConnectedComponentIndex]) { + reachable.set(dependencyIndex); + reachable.or(reachableStronglyConnectedComponentsByIndex[dependencyIndex]); + } + reachableStronglyConnectedComponentsByIndex[stronglyConnectedComponentIndex] = reachable; + } + return reachableStronglyConnectedComponentsByIndex; } - for (MetricsComponent dependency : dependencyTargetsToRecurse) { - if (!analyzedComponents.contains(dependency)) { - addTransitiveDependenciesFrom(dependency, transitiveDependencies, analyzedComponents); + + private int[] determineTopologicalOrder(CondensedGraph condensedGraph) { + int[] remainingIncomingDegree = Arrays.copyOf( + condensedGraph.incomingDegreeByStronglyConnectedComponentIndex, + condensedGraph.incomingDegreeByStronglyConnectedComponentIndex.length + ); + int[] topologicalOrder = new int[remainingIncomingDegree.length]; + Deque roots = new ArrayDeque<>(); + for (int i = 0; i < remainingIncomingDegree.length; i++) { + if (remainingIncomingDegree[i] == 0) { + roots.add(i); + } + } + + int currentIndex = 0; + while (!roots.isEmpty()) { + int currentStronglyConnectedComponentIndex = roots.removeFirst(); + topologicalOrder[currentIndex++] = currentStronglyConnectedComponentIndex; + for (int dependencyIndex : condensedGraph.outgoingEdgesByStronglyConnectedComponentIndex[currentStronglyConnectedComponentIndex]) { + remainingIncomingDegree[dependencyIndex]--; + if (remainingIncomingDegree[dependencyIndex] == 0) { + roots.addLast(dependencyIndex); + } + } + } + return topologicalOrder; + } + + private Map, Set>> mapToTransitiveDependenciesByComponent( + StronglyConnectedComponents stronglyConnectedComponents, + BitSet[] reachableStronglyConnectedComponentsByIndex + ) { + ImmutableMap.Builder, Set>> result = ImmutableMap.builder(); + List>> transitiveDependenciesByStronglyConnectedComponentIndex = new ArrayList<>(stronglyConnectedComponents.count); + + for (int stronglyConnectedComponentIndex = 0; stronglyConnectedComponentIndex < stronglyConnectedComponents.count; stronglyConnectedComponentIndex++) { + BitSet reachableStronglyConnectedComponents = (BitSet) reachableStronglyConnectedComponentsByIndex[stronglyConnectedComponentIndex].clone(); + if (stronglyConnectedComponents.isSelfReachable[stronglyConnectedComponentIndex]) { + reachableStronglyConnectedComponents.set(stronglyConnectedComponentIndex); + } + transitiveDependenciesByStronglyConnectedComponentIndex.add( + mapToComponents(reachableStronglyConnectedComponents, stronglyConnectedComponents.componentIndexesByStronglyConnectedComponentIndex) + ); + } + + for (int componentIndex = 0; componentIndex < components.size(); componentIndex++) { + int stronglyConnectedComponentIndex = stronglyConnectedComponents.stronglyConnectedComponentIndexByComponentIndex[componentIndex]; + result.put(components.get(componentIndex), transitiveDependenciesByStronglyConnectedComponentIndex.get(stronglyConnectedComponentIndex)); } + return result.build(); + } + + private ImmutableSet> mapToComponents( + BitSet reachableStronglyConnectedComponents, + List componentIndexesByStronglyConnectedComponentIndex + ) { + ImmutableSet.Builder> result = ImmutableSet.builder(); + for (int stronglyConnectedComponentIndex = reachableStronglyConnectedComponents.nextSetBit(0); + stronglyConnectedComponentIndex >= 0; + stronglyConnectedComponentIndex = reachableStronglyConnectedComponents.nextSetBit(stronglyConnectedComponentIndex + 1)) { + for (int componentIndex : componentIndexesByStronglyConnectedComponentIndex.get(stronglyConnectedComponentIndex)) { + result.add(components.get(componentIndex)); + } + } + return result.build(); } } - static MetricsComponentDependencyGraph of(Iterable> components, Function> getDependencies) { - return new MetricsComponentDependencyGraph<>(components, getDependencies); + private static final class TraversalFrame { + private final int componentIndex; + private int nextDependencyIndex = 0; + + private TraversalFrame(int componentIndex) { + this.componentIndex = componentIndex; + } + } + + private static final class StronglyConnectedComponents { + private final int[] stronglyConnectedComponentIndexByComponentIndex; + private final List componentIndexesByStronglyConnectedComponentIndex; + private final boolean[] isSelfReachable; + private final int count; + + private StronglyConnectedComponents( + int[] stronglyConnectedComponentIndexByComponentIndex, + List componentIndexesByStronglyConnectedComponentIndex, + boolean[] isSelfReachable, + int count + ) { + this.stronglyConnectedComponentIndexByComponentIndex = stronglyConnectedComponentIndexByComponentIndex; + this.componentIndexesByStronglyConnectedComponentIndex = componentIndexesByStronglyConnectedComponentIndex; + this.isSelfReachable = isSelfReachable; + this.count = count; + } + } + + private static final class CondensedGraph { + private final int[][] outgoingEdgesByStronglyConnectedComponentIndex; + private final int[] incomingDegreeByStronglyConnectedComponentIndex; + + private CondensedGraph(int[][] outgoingEdgesByStronglyConnectedComponentIndex, int[] incomingDegreeByStronglyConnectedComponentIndex) { + this.outgoingEdgesByStronglyConnectedComponentIndex = outgoingEdgesByStronglyConnectedComponentIndex; + this.incomingDegreeByStronglyConnectedComponentIndex = incomingDegreeByStronglyConnectedComponentIndex; + } + } + + private static final class IntCollector { + private int[] values = new int[4]; + private int size = 0; + + private void add(int value) { + if (size == values.length) { + values = Arrays.copyOf(values, values.length * 2); + } + values[size++] = value; + } + + private int[] toArray() { + return Arrays.copyOf(values, size); + } } }