refactor: introduce incremental constraint collectors for improved performance#2270
refactor: introduce incremental constraint collectors for improved performance#2270triceo wants to merge 21 commits into
Conversation
3e5d3e0 to
a69c7c2
Compare
|
@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: |
Good point. For collectors which contain other collectors (and only for them), the contract as specified doesn't actually hold. Two ways out of this:
Since we want to prevent unnecessary garbage from being collected, the second option is probably the way to go. |
# Conflicts: # core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ListUndoableActionable.java
…nstraintCollector *ConstraintCollectorAccumulator now extends the corresponding function type (BiFunction/TriFunction/QuadFunction/PentaFunction) and throws on apply(). Bavet nodes detect incremental via instanceof in toIncremental() instead of isIncremental(). fromIncremental() deleted; all built-in impls return the accumulator type directly as a covariant override of accumulator(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@Christopher-Chianelli This is the finalized code, with the incremental switches completely removed. Please review if you find some time between Team Days and your PTO, as I'd like to merge this week. Do not work on this during Team Days, please - if you don't review before your PTO, I will handle this with Fred instead. |
|


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
The most expensive collectors are not actually part of those benchmarks, so the biggest benefit of this PR does not show up there.