refactor: introduce incremental constraint collectors for improved performance#2270
refactor: introduce incremental constraint collectors for improved performance#2270triceo wants to merge 14 commits intoTimefoldAI:mainfrom
Conversation
3e5d3e0 to
a69c7c2
Compare
# Conflicts: # core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ListUndoableActionable.java
|
@Christopher-Chianelli Ready for review now. I wasn't able to keep the uni changes separate from the bi/tri/quad, so this PR is large. But if you review the uni parts, you can just skim the rest; they are mechanical copies with updates for the higher arities. |
|
It would be great if we had |
|
|
@triceo GitHub still not allowing me to create any review comments, so I putting my review comments directly on the the PR. UniConstraintCollector - Does a user need to implement both |
/**
* <li>{@link #add(Object)} will be called externally exactly once, when the value enters the group.
* An instance of {@link UniConstraintCollectorAccumulatedValue} will only be created if there is a value to add.</li>
* /
private final UniConstraintCollectorAccumulatedValue<A> innerValue;
private boolean active = false;
AccumulatedValue(ResultContainer_ container) {
this.innerValue = innerIncremental.intoGroup(container);
}
@Override
public void add(A a) {
if (!predicate.test(a)) {
return;
}
active = true;
innerValue.add(a);
}
@Override
public void update(A a) {
boolean nowActive = predicate.test(a);
if (active && nowActive) {
innerValue.update(a);
} else if (active) {
active = false;
innerValue.remove();
} else if (nowActive) {
active = true;
innerValue.add(a);
}
}i.e. |
They can, but they will not be. That is a contract that we guarantee. |
Something like this would cause constraintFactory.forEach(Employee.class)
.join(Shift.class, equal(id(), Shift::getEmployee)
// Employee has an InverseCollectionShadowVariable to get shift count
.groupBy((employee, shift) -> employee, conditionally((employee, shift) -> employee.getShiftCount() > 5, sum((employee, shift) -> shift.getDuration())))
.filter((employee, hours) -> hours != null && hours > 24)
.penalize(HardSoftScore.ONE_HARD, (employee, hours) -> hours - 24)
.asConstraint("Overworked employee")In particular, the
i.e. this is explictly allowed: |
|
|
||
| public abstract class AbstractLongSumSlot { | ||
|
|
||
| public static final class State { |
|
|
||
| public abstract class AbstractLongAverageSlot { | ||
|
|
||
| public static final class State { |
There was a problem hiding this comment.
Why seperate state into its own class? It a private field, so it not like subclasses can access the state field directly.


Still a lot of work to do, but early PoCs look promising.
This benchmark run proves there are no regressions from adding the incremental support to the GroupNode:
https://github.com/TimefoldAI/timefold-solver-benchmarks/actions/runs/25095715799/job/73533079940
This benchmark proves no regressions after the new collector implementations were integrated:
https://github.com/TimefoldAI/timefold-solver-benchmarks/actions/runs/25427319378
There are small performance improvements, but the most expensive collectors are not actually part of those benchmarks, so the biggest benefit of this PR does not show up there.