Skip to content

Commit 5edc6ce

Browse files
perf: dynamically calculate topological order to prevent cache invalidation (#1968)
1 parent 97db0c1 commit 5edc6ce

File tree

8 files changed

+53
-22
lines changed

8 files changed

+53
-22
lines changed

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/AbstractVariableReferenceGraph.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public abstract sealed class AbstractVariableReferenceGraph<Solution_, ChangeSet
2121

2222
// These structures are immutable.
2323
protected final List<GraphNode<Solution_>> nodeList;
24+
protected final BaseTopologicalOrderGraph.NodeTopologicalOrder[] nodeTopologicalOrders;
2425
protected final Map<VariableMetaModel<?, ?, ?>, Map<Object, GraphNode<Solution_>>> variableReferenceToContainingNodeMap;
2526
protected final Map<VariableMetaModel<?, ?, ?>, List<BiConsumer<AbstractVariableReferenceGraph<Solution_, ?>, Object>>> variableReferenceToBeforeProcessor;
2627
protected final Map<VariableMetaModel<?, ?, ?>, List<BiConsumer<AbstractVariableReferenceGraph<Solution_, ?>, Object>>> variableReferenceToAfterProcessor;
@@ -44,6 +45,7 @@ public abstract sealed class AbstractVariableReferenceGraph<Solution_, ChangeSet
4445
}
4546
graph = graphCreator.apply(instanceCount);
4647
graph.withNodeData(nodeList);
48+
nodeTopologicalOrders = buildNodeTopologicalOrderArray(graph, nodeList.size());
4749

4850
var visited = Collections.newSetFromMap(new IdentityHashMap<>());
4951
changeSet = createChangeSet(instanceCount);
@@ -62,6 +64,16 @@ public abstract sealed class AbstractVariableReferenceGraph<Solution_, ChangeSet
6264
}
6365
}
6466

67+
private BaseTopologicalOrderGraph.NodeTopologicalOrder[] buildNodeTopologicalOrderArray(BaseTopologicalOrderGraph graph,
68+
int graphSize) {
69+
var out = new BaseTopologicalOrderGraph.NodeTopologicalOrder[graphSize];
70+
71+
for (var i = 0; i < out.length; i++) {
72+
out[i] = new BaseTopologicalOrderGraph.NodeTopologicalOrder(i, graph);
73+
}
74+
return out;
75+
}
76+
6577
protected abstract ChangeSet_ createChangeSet(int instanceCount);
6678

6779
public @Nullable GraphNode<Solution_> lookupOrNull(VariableMetaModel<?, ?, ?> variableId, Object entity) {

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/AffectedEntitiesUpdater.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ final class AffectedEntitiesUpdater<Solution_>
1515
// From WorkingReferenceGraph.
1616
private final BaseTopologicalOrderGraph graph;
1717
private final List<GraphNode<Solution_>> nodeList; // Immutable.
18+
private final BaseTopologicalOrderGraph.NodeTopologicalOrder[] nodeTopologicalOrders; // Immutable
1819
private final ChangedVariableNotifier<Solution_> changedVariableNotifier;
1920

2021
// Internal state; expensive to create, therefore we reuse.
@@ -23,10 +24,12 @@ final class AffectedEntitiesUpdater<Solution_>
2324
private final PriorityQueue<BaseTopologicalOrderGraph.NodeTopologicalOrder> changeQueue;
2425

2526
AffectedEntitiesUpdater(BaseTopologicalOrderGraph graph, List<GraphNode<Solution_>> nodeList,
27+
BaseTopologicalOrderGraph.NodeTopologicalOrder[] nodeTopologicalOrders,
2628
Function<Object, List<GraphNode<Solution_>>> entityToContainingNode,
2729
int entityCount, ChangedVariableNotifier<Solution_> changedVariableNotifier) {
2830
this.graph = graph;
2931
this.nodeList = nodeList;
32+
this.nodeTopologicalOrders = nodeTopologicalOrders;
3033
this.changedVariableNotifier = changedVariableNotifier;
3134
var instanceCount = nodeList.size();
3235
this.loopedTracker = new LoopedTracker(instanceCount,
@@ -87,7 +90,7 @@ public void accept(BitSet changed) {
8790
while (iterator.hasNext()) {
8891
var nextNodeForwardEdge = iterator.nextInt();
8992
if (!visited.get(nextNodeForwardEdge)) {
90-
changeQueue.add(graph.getTopologicalOrder(nextNodeForwardEdge));
93+
changeQueue.add(nodeTopologicalOrders[nextNodeForwardEdge]);
9194
}
9295
}
9396
}
@@ -110,7 +113,7 @@ private void initializeChangeQueue(BitSet changed) {
110113
// This should never happen, since arrays in Java are limited
111114
// to slightly less than Integer.MAX_VALUE.
112115
for (var i = changed.nextSetBit(0); i >= 0; i = changed.nextSetBit(i + 1)) {
113-
changeQueue.add(graph.getTopologicalOrder(i));
116+
changeQueue.add(nodeTopologicalOrders[i]);
114117
if (i == Integer.MAX_VALUE) {
115118
break; // or (i+1) would overflow
116119
}

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/BaseTopologicalOrderGraph.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ public interface BaseTopologicalOrderGraph {
2828
boolean isLooped(LoopedTracker loopedTracker, int node);
2929

3030
/**
31-
* Returns a tuple containing node ID and a number corresponding to its topological order.
32-
* In particular, after {@link TopologicalOrderGraph#commitChanges(java.util.BitSet)} is called, the following
33-
* must be true for any pair of nodes A, B where:
31+
* Returns a tuple containing node ID and a number corresponding to its topological order. In particular, after
32+
* {@link TopologicalOrderGraph#commitChanges(java.util.BitSet)} is called, the following must be true for any pair of nodes
33+
* A, B where:
3434
* <ul>
3535
* <li>A is a predecessor of B</li>
3636
* <li>`isLooped(A) == isLooped(B) == false`</li>
@@ -39,23 +39,38 @@ public interface BaseTopologicalOrderGraph {
3939
* <p>
4040
* Said number may not be unique.
4141
*/
42-
NodeTopologicalOrder getTopologicalOrder(int node);
42+
int getTopologicalOrder(int node);
4343

4444
/**
45-
* Stores a graph node id along its topological order.
45+
* Stores a graph node id and dynamically calculates its topological order.
4646
* Comparisons ignore node id and only use the topological order.
4747
* For instance, for x = (0, 0) and y = (1, 5), x is before y, whereas for
4848
* x = (0, 5) and y = (1, 0), y is before x. Note {@link BaseTopologicalOrderGraph}
4949
* is not guaranteed to return every topological order index (i.e.
5050
* it might be the case no nodes has order 0).
5151
*/
52-
record NodeTopologicalOrder(int nodeId, int order)
52+
record NodeTopologicalOrder(int nodeId, BaseTopologicalOrderGraph graph)
5353
implements
5454
Comparable<NodeTopologicalOrder> {
5555

56+
/**
57+
* If the {@link BaseTopologicalOrderGraph#getTopologicalOrder(int)} of the node
58+
* changes while inside a {@link java.util.PriorityQueue}, that {@link java.util.PriorityQueue}
59+
* would be corrupted.
60+
* <p>
61+
* It is vital that all changes to a {@link TopologicalOrderGraph} happen
62+
* before nodes are inserted into a {@link java.util.PriorityQueue}, and that
63+
* the {@link java.util.PriorityQueue} is cleared before future changes to the
64+
* {@link TopologicalOrderGraph}.
65+
*
66+
* @param other The node this node is being topologically compared to.
67+
* @return a negative int if this node is topologically before the other node,
68+
* a positive int if this node is topologically after the other node,
69+
* 0 if both nodes have the same topological order.
70+
*/
5671
@Override
5772
public int compareTo(NodeTopologicalOrder other) {
58-
return order - other.order;
73+
return graph.getTopologicalOrder(nodeId) - graph.getTopologicalOrder(other.nodeId);
5974
}
6075

6176
@Override

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DefaultShadowVariableSessionFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ private static <Solution_> boolean hasNoNonDeclarativeSourcesFromParent(
253253
graph.commitChanges(new BitSet());
254254
var sortedDeclarativeVariables = new ArrayList<>(declarativeShadowVariables);
255255
sortedDeclarativeVariables.sort(Comparator.<DeclarativeShadowVariableDescriptor<Solution_>> comparingInt(
256-
variable -> graph.getTopologicalOrder(nameToIndex.get(variable.getVariableName())).order())
256+
variable -> graph.getTopologicalOrder(nameToIndex.get(variable.getVariableName())))
257257
.thenComparing(VariableDescriptor::getVariableName));
258258
return sortedDeclarativeVariables;
259259
}

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DefaultTopologicalOrderGraph.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414

1515
public class DefaultTopologicalOrderGraph implements TopologicalOrderGraph {
1616

17-
private final NodeTopologicalOrder[] nodeIdToTopologicalOrderMap;
17+
private final int[] nodeIdToTopologicalOrderMap;
1818
private final Map<Integer, List<Integer>> componentMap;
1919
private final Set<Integer>[] forwardEdges;
2020
private final Set<Integer>[] backEdges;
2121
private final boolean[] isNodeInLoopedComponent;
2222

2323
@SuppressWarnings({ "unchecked" })
2424
public DefaultTopologicalOrderGraph(final int size) {
25-
this.nodeIdToTopologicalOrderMap = new NodeTopologicalOrder[size];
25+
this.nodeIdToTopologicalOrderMap = new int[size];
2626
this.componentMap = CollectionUtils.newLinkedHashMap(size);
2727
this.forwardEdges = new Set[size];
2828
this.backEdges = new Set[size];
@@ -31,7 +31,7 @@ public DefaultTopologicalOrderGraph(final int size) {
3131
forwardEdges[i] = new HashSet<>();
3232
backEdges[i] = new HashSet<>();
3333
isNodeInLoopedComponent[i] = false;
34-
nodeIdToTopologicalOrderMap[i] = new NodeTopologicalOrder(i, i);
34+
nodeIdToTopologicalOrderMap[i] = i;
3535
}
3636
}
3737

@@ -112,7 +112,7 @@ public boolean isLooped(LoopedTracker loopedTracker, int node) {
112112
}
113113

114114
@Override
115-
public NodeTopologicalOrder getTopologicalOrder(int node) {
115+
public int getTopologicalOrder(int node) {
116116
return nodeIdToTopologicalOrderMap[node];
117117
}
118118

@@ -141,7 +141,7 @@ public void commitChanges(BitSet changed) {
141141
var isComponentLooped = componentSize != 1;
142142
var componentNodes = new ArrayList<Integer>(componentSize);
143143
for (var node = component.nextSetBit(0); node >= 0; node = component.nextSetBit(node + 1)) {
144-
nodeIdToTopologicalOrderMap[node] = new NodeTopologicalOrder(node, ordIndex);
144+
nodeIdToTopologicalOrderMap[node] = ordIndex;
145145
componentNodes.add(node);
146146
componentMap.put(node, componentNodes);
147147

@@ -206,7 +206,7 @@ public String toString() {
206206
var out = new StringBuilder();
207207
out.append("DefaultTopologicalOrderGraph{\n");
208208
for (var node = 0; node < forwardEdges.length; node++) {
209-
out.append(" ").append(node).append("(").append(nodeIdToTopologicalOrderMap[node].order()).append(") -> ")
209+
out.append(" ").append(node).append("(").append(nodeIdToTopologicalOrderMap[node]).append(") -> ")
210210
.append(forwardEdges[node].stream()
211211
.sorted()
212212
.map(Object::toString)

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DefaultVariableReferenceGraph.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ public DefaultVariableReferenceGraph(VariableReferenceGraphBuilder<Solution_> ou
3535
// Otherwise its internal collections were observed being re-created so often
3636
// that the allocation of arrays would become a major bottleneck.
3737
affectedEntitiesUpdater =
38-
new AffectedEntitiesUpdater<>(graph, nodeList, immutableEntityToVariableReferenceMap::get,
38+
new AffectedEntitiesUpdater<>(graph, nodeList, nodeTopologicalOrders,
39+
immutableEntityToVariableReferenceMap::get,
3940
outerGraph.entityToEntityId.size(), outerGraph.changedVariableNotifier);
4041
}
4142

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/FixedVariableReferenceGraph.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public FixedVariableReferenceGraph(VariableReferenceGraphBuilder<Solution_> oute
2525
// each node to changed.
2626
changedVariableNotifier = outerGraph.changedVariableNotifier;
2727
for (var node = 0; node < nodeList.size(); node++) {
28-
changeSet.add(graph.getTopologicalOrder(node));
28+
changeSet.add(nodeTopologicalOrders[node]);
2929
var variableReference = nodeList.get(node).variableReferences().get(0);
3030
var entityConsistencyState = variableReference.entityConsistencyState();
3131
if (variableReference.groupEntities() != null) {
@@ -55,7 +55,7 @@ public void markChanged(@NonNull GraphNode<Solution_> node) {
5555
// Before the graph is finalized, ignore changes, since
5656
// we don't know the topological order yet
5757
if (isFinalized) {
58-
changeSet.add(graph.getTopologicalOrder(node.graphNodeId()));
58+
changeSet.add(nodeTopologicalOrders[node.graphNodeId()]);
5959
}
6060
}
6161

@@ -85,7 +85,7 @@ public void updateChanged() {
8585
continue;
8686
}
8787
visited.set(nextNode);
88-
changeSet.add(graph.getTopologicalOrder(nextNode));
88+
changeSet.add(nodeTopologicalOrders[nextNode]);
8989
}
9090
}
9191
}

core/src/test/java/ai/timefold/solver/core/impl/domain/variable/declarative/AbstractTopologicalGraphTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ private static void assertTopologicalOrder(TopologicalOrderGraph graph, List<Int
4949
var lowerMembers = expectedTopologicalNodeOrder[j];
5050
for (var a : members) {
5151
for (var b : lowerMembers) {
52-
var aOrder = graph.getTopologicalOrder(a).order();
53-
var bOrder = graph.getTopologicalOrder(b).order();
52+
var aOrder = graph.getTopologicalOrder(a);
53+
var bOrder = graph.getTopologicalOrder(b);
5454
assertThat(aOrder)
5555
.withFailMessage(
5656
() -> "Expected topological order of %d (%d) to be greater than %d (%d) in graph %s"

0 commit comments

Comments
 (0)