Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
cead4c1
perf: optimize array creation (20 % of run time, untold memory)
triceo May 1, 2025
56e3079
perf: don't create list iterators
triceo May 1, 2025
8122fc5
perf: only create affected entities when necessary
triceo May 2, 2025
700d287
perf: remove duplicate check
triceo May 2, 2025
747495d
perf: zero out the LoopedTracker array
triceo May 2, 2025
73d5df0
perf: minimize map access
triceo May 2, 2025
d3740a1
perf: improve map performance
triceo May 2, 2025
18f482a
Split mutable and immutable parts of the graph
triceo May 2, 2025
8014db0
More immutability
triceo May 2, 2025
39f78c3
perf: reduce allocations in PriorityQueue
triceo May 2, 2025
eec3028
Further separation of logic into distinct boxes
triceo May 6, 2025
8203cb5
perf: don't compute the graph when no instances
triceo May 6, 2025
760f379
perf: improve performance by further reducing GC pressure
triceo May 17, 2025
e9a2f96
perf: further decrease allocation during processing affected entities
triceo May 18, 2025
abeb812
perf: allocate less Edges
triceo May 18, 2025
a7e2569
perf: don't incur hashing overhead during batches
triceo May 18, 2025
e826219
sonar
triceo May 18, 2025
e036938
Update core/src/main/java/ai/timefold/solver/core/impl/domain/variabl…
triceo May 23, 2025
4188bbd
Mutate edge count externally
triceo May 24, 2025
4f1905d
Remove TODOs
triceo May 24, 2025
b4cdea0
Builder
triceo May 24, 2025
1c4d539
No need to start batch changes
triceo May 24, 2025
d0932a5
Replace a large array with a smaller BitSet
triceo May 24, 2025
92e7832
Replace another large array with a smaller BitSet
triceo May 24, 2025
824d457
Remove the use of a massive array
triceo May 24, 2025
60028b6
Fix the issue
triceo May 25, 2025
e623314
Massive memory savings by introducing an array that does not index fr…
triceo May 25, 2025
1933d7b
Simplify Affected Entity update
triceo May 25, 2025
291955c
Simplify processor logic
triceo May 26, 2025
29a30a0
Remove unused field
triceo May 26, 2025
b654e7e
Better resize strategy for the dynamic array
triceo May 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.variable.CascadingUpdateShadowVariable;
Expand All @@ -34,6 +33,7 @@
import ai.timefold.solver.core.impl.domain.variable.declarative.DefaultShadowVariableSessionFactory;
import ai.timefold.solver.core.impl.domain.variable.declarative.DefaultTopologicalOrderGraph;
import ai.timefold.solver.core.impl.domain.variable.declarative.VariableReferenceGraph;
import ai.timefold.solver.core.impl.domain.variable.declarative.VariableReferenceGraphBuilder;
import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor;
import ai.timefold.solver.core.impl.domain.variable.descriptor.ShadowVariableDescriptor;
import ai.timefold.solver.core.impl.domain.variable.index.IndexShadowVariableDescriptor;
Expand Down Expand Up @@ -72,18 +72,19 @@ private ShadowVariableUpdateHelper(EnumSet<ShadowVariableType> supportedShadowVa
this.supportedShadowVariableTypes = supportedShadowVariableTypes;
}

@SuppressWarnings("unchecked")
public void updateShadowVariables(Solution_ solution) {
var initialSolutionDescriptor = (SolutionDescriptor<Solution_>) SolutionDescriptor.buildSolutionDescriptor(
Set.of(PreviewFeature.DECLARATIVE_SHADOW_VARIABLES),
solution.getClass());
var entityClassList = initialSolutionDescriptor.getAllEntitiesAndProblemFacts(solution)
var enabledPreviewFeatures = EnumSet.of(PreviewFeature.DECLARATIVE_SHADOW_VARIABLES);
var solutionClass = (Class<Solution_>) solution.getClass();
var initialSolutionDescriptor = SolutionDescriptor.buildSolutionDescriptor(
enabledPreviewFeatures, solutionClass);
var entityClassArray = initialSolutionDescriptor.getAllEntitiesAndProblemFacts(solution)
.stream()
.map(Object::getClass)
.distinct()
.toList();
var solutionDescriptor = (SolutionDescriptor<Solution_>) SolutionDescriptor.buildSolutionDescriptor(
Set.of(PreviewFeature.DECLARATIVE_SHADOW_VARIABLES),
solution.getClass(), entityClassList.toArray(Class[]::new));
.toArray(Class[]::new);
var solutionDescriptor = SolutionDescriptor.buildSolutionDescriptor(enabledPreviewFeatures, solutionClass,
entityClassArray);
try (var scoreDirector = new InternalScoreDirector<>(solutionDescriptor)) {
// When we have a solution, we can reuse the logic from VariableListenerSupport to update all variable types
scoreDirector.setWorkingSolution(solution);
Expand Down Expand Up @@ -117,9 +118,8 @@ public void updateShadowVariables(Class<Solution_> solutionClass,
.formatted(missingShadowVariableTypeList));
}
// No solution, we trigger all supported events manually
var session = new InternalShadowVariableSession<>(solutionDescriptor,
new VariableReferenceGraph<>(ChangedVariableNotifier.empty()));
session.init(entities);
var session = InternalShadowVariableSession.build(solutionDescriptor,
new VariableReferenceGraphBuilder<>(ChangedVariableNotifier.empty()), entities);
// Update all built-in shadow variables
var listVariableDescriptor = solutionDescriptor.getListVariableDescriptor();
if (listVariableDescriptor == null) {
Expand All @@ -135,11 +135,12 @@ public void updateShadowVariables(Class<Solution_> solutionClass,
private record InternalShadowVariableSession<Solution_>(SolutionDescriptor<Solution_> solutionDescriptor,
VariableReferenceGraph<Solution_> graph) {

public void init(Object... entities) {
if (!solutionDescriptor.getDeclarativeShadowVariableDescriptors().isEmpty()) {
DefaultShadowVariableSessionFactory.visitGraph(solutionDescriptor, graph, entities,
DefaultTopologicalOrderGraph::new);
}
public static <Solution_> InternalShadowVariableSession<Solution_> build(
SolutionDescriptor<Solution_> solutionDescriptor, VariableReferenceGraphBuilder<Solution_> graph,
Object... entities) {
return new InternalShadowVariableSession<>(solutionDescriptor,
DefaultShadowVariableSessionFactory.buildGraph(solutionDescriptor, graph, entities,
DefaultTopologicalOrderGraph::new));
}

/**
Expand Down Expand Up @@ -249,6 +250,7 @@ public void processListVariable(Object... entities) {
*
* @param entities the entities to be analyzed
*/
@SuppressWarnings("unchecked")
public void processCascadingVariable(Object... entities) {
var listVariableDescriptor = solutionDescriptor.getListVariableDescriptor();
if (listVariableDescriptor != null) {
Expand Down Expand Up @@ -336,8 +338,8 @@ private List<BasicVariableDescriptor<Solution_>> fetchBasicDescriptors(EntityDes
}
}

private static class InternalScoreDirectorFactory<Solution_, Score_ extends Score<Score_>, Factory_ extends AbstractScoreDirectorFactory<Solution_, Score_, Factory_>>
extends AbstractScoreDirectorFactory<Solution_, Score_, Factory_> {
private static class InternalScoreDirectorFactory<Solution_, Score_ extends Score<Score_>>
extends AbstractScoreDirectorFactory<Solution_, Score_, InternalScoreDirectorFactory<Solution_, Score_>> {

public InternalScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor) {
super(solutionDescriptor);
Expand All @@ -349,12 +351,11 @@ public InternalScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescri
}
}

private static class InternalScoreDirector<Solution_, Score_ extends Score<Score_>, Factory_ extends AbstractScoreDirectorFactory<Solution_, Score_, Factory_>>
extends AbstractScoreDirector<Solution_, Score_, Factory_> {
private static class InternalScoreDirector<Solution_, Score_ extends Score<Score_>>
extends AbstractScoreDirector<Solution_, Score_, InternalScoreDirectorFactory<Solution_, Score_>> {

public InternalScoreDirector(SolutionDescriptor<Solution_> solutionDescriptor) {
super((Factory_) new InternalScoreDirectorFactory<Solution_, Score_, Factory_>(solutionDescriptor), false, DISABLED,
false);
super(new InternalScoreDirectorFactory<>(solutionDescriptor), false, DISABLED, false);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package ai.timefold.solver.core.impl.domain.variable.declarative;

import java.util.BitSet;
import java.util.List;
import java.util.Objects;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;

import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor;
import ai.timefold.solver.core.impl.util.LinkedIdentityHashSet;

final class AffectedEntitiesUpdater<Solution_>
implements Consumer<BitSet> {

// From WorkingReferenceGraph.
private final BaseTopologicalOrderGraph graph;
private final List<EntityVariablePair<Solution_>> instanceList; // Immutable.
private final Function<Object, List<EntityVariablePair<Solution_>>> entityVariablePairFunction;
private final ChangedVariableNotifier<Solution_> changedVariableNotifier;

// Internal state; expensive to create, therefore we reuse.
private final AffectedEntities<Solution_> affectedEntities;
private final LoopedTracker loopedTracker;
private final BitSet visited;
private final PriorityQueue<BaseTopologicalOrderGraph.NodeTopologicalOrder> changeQueue;

AffectedEntitiesUpdater(BaseTopologicalOrderGraph graph, List<EntityVariablePair<Solution_>> instanceList,
Function<Object, List<EntityVariablePair<Solution_>>> entityVariablePairFunction,
ChangedVariableNotifier<Solution_> changedVariableNotifier) {
this.graph = graph;
this.instanceList = instanceList;
this.entityVariablePairFunction = entityVariablePairFunction;
this.changedVariableNotifier = changedVariableNotifier;
var instanceCount = instanceList.size();
this.affectedEntities = new AffectedEntities<>(this::updateLoopedStatusOfAffectedEntity);
this.loopedTracker = new LoopedTracker(instanceCount);
this.visited = new BitSet(instanceCount);
this.changeQueue = new PriorityQueue<>(instanceCount);
}

@Override
public void accept(BitSet changed) {
initializeChangeQueue(changed);

while (!changeQueue.isEmpty()) {
var nextNode = changeQueue.poll().nodeId();
if (visited.get(nextNode)) {
continue;
}
visited.set(nextNode);
var shadowVariable = instanceList.get(nextNode);
var isChanged = updateShadowVariable(shadowVariable, graph.isLooped(loopedTracker, nextNode));

if (isChanged) {
var iterator = graph.nodeForwardEdges(nextNode);
while (iterator.hasNext()) {
var nextNodeForwardEdge = iterator.nextInt();
if (!visited.get(nextNodeForwardEdge)) {
changeQueue.add(graph.getTopologicalOrder(nextNodeForwardEdge));
}
}
}
}

affectedEntities.processAndClear();
// Prepare for the next time updateChanged() is called.
// No need to clear changeQueue, as that already finishes empty.
loopedTracker.clear();
visited.clear();
}

private void initializeChangeQueue(BitSet changed) {
// BitSet iteration: get the first set bit at or after 0,
// then get the first set bit after that bit.
// Iteration ends when nextSetBit returns -1.
// This has the potential to overflow, since to do the
// test, we necessarily need to do nextSetBit(i + 1),
// and i + 1 can be negative if Integer.MAX_VALUE is set
// in the BitSet.
// This should never happen, since arrays in Java are limited
// to slightly less than Integer.MAX_VALUE.
for (var i = changed.nextSetBit(0); i >= 0; i = changed.nextSetBit(i + 1)) {
changeQueue.add(graph.getTopologicalOrder(i));
if (i == Integer.MAX_VALUE) {
break; // or (i+1) would overflow
}
}
changed.clear();
}

private void updateLoopedStatusOfAffectedEntity(Object affectedEntity) {
ShadowVariableLoopedVariableDescriptor<Solution_> shadowVariableLoopedDescriptor = null;
var isEntityLooped = false;
for (var node : entityVariablePairFunction.apply(affectedEntity)) {
// All variables come from the same entity,
// therefore all have the same looped marker.
shadowVariableLoopedDescriptor = node.variableReference().shadowVariableLoopedDescriptor();
if (graph.isLooped(loopedTracker, node.graphNodeId())) {
isEntityLooped = true;
break;
}
}
if (shadowVariableLoopedDescriptor == null) {
// At this point, affectedEntity is guaranteed to have looped marker.
// Otherwise AffectedEntities would not have sent it here.
throw new IllegalStateException("Impossible state: loop marker descriptor does not exist.");
}
var oldValue = shadowVariableLoopedDescriptor.getValue(affectedEntity);
if (!Objects.equals(oldValue, isEntityLooped)) {
changeShadowVariableAndNotify(shadowVariableLoopedDescriptor, affectedEntity, isEntityLooped);
}

}

private boolean updateShadowVariable(EntityVariablePair<Solution_> entityVariable, boolean isLooped) {
var entity = entityVariable.entity();
var shadowVariableReference = entityVariable.variableReference();
var oldValue = shadowVariableReference.memberAccessor().executeGetter(entity);

if (isLooped) {
// null might be a valid value, and thus it could be the case
// that is was not looped and null, then turned to looped and null,
// which is still considered a change.
affectedEntities.add(entityVariable);
if (oldValue != null) {
changeShadowVariableAndNotify(shadowVariableReference, entity, null);
}
return true;
} else {
var newValue = shadowVariableReference.calculator().apply(entity);
if (!Objects.equals(oldValue, newValue)) {
affectedEntities.add(entityVariable);
changeShadowVariableAndNotify(shadowVariableReference, entity, newValue);
return true;
}
}
return false;
}

private void changeShadowVariableAndNotify(VariableUpdaterInfo<Solution_> shadowVariableReference, Object entity,
Object newValue) {
var variableDescriptor = shadowVariableReference.variableDescriptor();
changeShadowVariableAndNotify(variableDescriptor, entity, newValue);
}

private void changeShadowVariableAndNotify(VariableDescriptor<Solution_> variableDescriptor, Object entity,
Object newValue) {
changedVariableNotifier.beforeVariableChanged().accept(variableDescriptor, entity);
variableDescriptor.setValue(entity, newValue);
changedVariableNotifier.afterVariableChanged().accept(variableDescriptor, entity);
}

private static final class AffectedEntities<Solution_> {

private final Consumer<Object> consumer;
private final Set<Object> entitiesForLoopedVarUpdateSet;

public AffectedEntities(Consumer<Object> consumer) {
this.consumer = consumer;
this.entitiesForLoopedVarUpdateSet = new LinkedIdentityHashSet<>();
}

public void add(EntityVariablePair<Solution_> shadowVariable) {
var shadowVariableLoopedDescriptor = shadowVariable.variableReference().shadowVariableLoopedDescriptor();
if (shadowVariableLoopedDescriptor == null) {
return;
}
entitiesForLoopedVarUpdateSet.add(shadowVariable.entity());
}

public void processAndClear() {
for (var entity : entitiesForLoopedVarUpdateSet) {
consumer.accept(entity);
}
entitiesForLoopedVarUpdateSet.clear();
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package ai.timefold.solver.core.impl.domain.variable.declarative;

import java.util.PrimitiveIterator;

/**
* Exists to expose read-only view of {@link TopologicalOrderGraph}.
*/
public interface BaseTopologicalOrderGraph {

/**
* Return an iterator of the nodes that have the `from` node as a predecessor.
*
* @param from The predecessor node.
* @return an iterator of nodes with from as a predecessor.
*/
PrimitiveIterator.OfInt nodeForwardEdges(int from);

/**
* Returns true if a given node is in a strongly connected component with a size
* greater than 1 (i.e. is in a loop) or is a transitive successor of a
* node with the above property.
*
* @param loopedTracker a tracker that can be used to record looped state to avoid
* recomputation.
* @param node The node being queried
* @return true if `node` is in a loop, false otherwise.
*/
boolean isLooped(LoopedTracker loopedTracker, int node);

/**
* Returns a tuple containing node ID and a number corresponding to its topological order.
* In particular, after {@link TopologicalOrderGraph#commitChanges()} is called, the following
* must be true for any pair of nodes A, B where:
* <ul>
* <li>A is a predecessor of B</li>
* <li>`isLooped(A) == isLooped(B) == false`</li>
* </ul>
* getTopologicalOrder(A) &lt; getTopologicalOrder(B)
* <p>
* Said number may not be unique.
*/
NodeTopologicalOrder getTopologicalOrder(int node);

/**
* Stores a graph node id along its topological order.
* Comparisons ignore node id and only use the topological order.
* For instance, for x = (0, 0) and y = (1, 5), x is before y, whereas for
* x = (0, 5) and y = (1, 0), y is before x. Note {@link BaseTopologicalOrderGraph}
* is not guaranteed to return every topological order index (i.e.
* it might be the case no nodes has order 0).
*/
record NodeTopologicalOrder(int nodeId, int order)
implements
Comparable<NodeTopologicalOrder> {

@Override
public int compareTo(NodeTopologicalOrder other) {
return order - other.order;
}

@Override
public boolean equals(Object o) {
if (o instanceof NodeTopologicalOrder other) {
return nodeId == other.nodeId;
}
return false;
}

@Override
public int hashCode() {
return nodeId;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

@NullMarked
public class DefaultShadowVariableSession<Solution_> implements Supply {

final VariableReferenceGraph<Solution_> graph;

public DefaultShadowVariableSession(VariableReferenceGraph<Solution_> graph) {
Expand Down
Loading
Loading