@@ -1622,6 +1622,109 @@ This stream now contains all `Employee` instances, both with and without shifts
16221622and can be passed to the <<collectorsLoadBalance,load balancing constraint collector>>,
16231623which will compute the unfairness metric.
16241624
1625+ [#constraintStreamsPrecompute]
1626+ === Precomputing a stream
1627+
1628+ When a stream depends only on properties of problem facts or entities,
1629+ and not on any planning variables (genuine or shadow),
1630+ that stream can be precomputed to improve performance.
1631+ Consider the following `No overlapping shifts` constraint:
1632+
1633+ [tabs]
1634+ ====
1635+ Java::
1636+ +
1637+ [source,java,options="nowrap"]
1638+ ----
1639+ Constraint noOverlappingShifts(ConstraintFactory constraintFactory) {
1640+ return constraintFactory.forEachUniquePair(Shift.class,
1641+ Joiners.overlapping(Shift::getStart, Shift::getEnd),
1642+ Joiners.equal(Shift::getEmployee))
1643+ .penalize(HardSoftScore.ONE_HARD, (shift1, shift2) -> shift1.getOverlap(shift2))
1644+ .asConstraint("No overlapping shifts");
1645+ }
1646+ ----
1647+ ====
1648+
1649+ Whenever any variable changes, this constraint will recalculate what shifts overlap and their overlap amount.
1650+ Since `start` and `end` are not variables (genuine or shadow) and `getOverlap()` does not depend on any variables,
1651+ some parts of the constraint can be precomputed to improve performance:
1652+
1653+ [tabs]
1654+ ====
1655+ Java::
1656+ +
1657+ [source,java,options="nowrap"]
1658+ ----
1659+ TriConstraintStream<Shift, Shift, Integer> overlappingShifts(PrecomputeFactory precomputeFactory) {
1660+ return precomputeFactory.forEachUnfilteredUniquePair(Shift.class,
1661+ Joiners.overlapping(Shift::getStart, Shift::getEnd))
1662+ .expand((shift1, shift2) -> shift1.getOverlap(shift2));
1663+ }
1664+
1665+ Constraint noOverlappingShifts(ConstraintFactory constraintFactory) {
1666+ return constraintFactory.precompute(this::overlappingShifts)
1667+ .filter((shift1, shift2, overlap) ->
1668+ shift1.getEmployee() != null &&
1669+ shift1.getEmployee() == shift2.getEmployee())
1670+ .penalize(HardSoftScore.ONE_HARD, (shift1, shift2, overlap) -> overlap)
1671+ .asConstraint("No overlapping shifts");
1672+ }
1673+ ----
1674+ ====
1675+
1676+ This will precompute and store the overlapping shifts, and their amount of overlap.
1677+ When any of the overlapping shifts' variables change, only the filter will be re-evaluated.
1678+ That filter also won't be evaluated for non-overlapping shifts,
1679+ since they were already eliminated by the precomputed stream.
1680+
1681+ ==== Precomputing and planning variables
1682+
1683+ Precomputed streams are unfiltered and will include both unassigned and inconsistent planning entities.
1684+ If only assigned entities are desired,
1685+ add a filter after the `precompute` call to check if the entities' variables are assigned.
1686+
1687+ [IMPORTANT]
1688+ ====
1689+ A score corruption will occur if a precomputed stream depends on variables (genuine or shadow).
1690+ For example, if `employee` is a planning variable, then the following constraint will cause a score corruption:
1691+
1692+ [tabs]
1693+ =====
1694+ Java::
1695+ +
1696+ [source,java,options="nowrap"]
1697+ ----
1698+ TriConstraintStream<Shift, Shift, Integer> overlappingShifts(PrecomputeFactory precomputeFactory) {
1699+ // This references Shift::getEmployee, which depends
1700+ // on a planning variable and thus is not valid to
1701+ // use in a precomputed stream
1702+ return precomputeFactory.forEachUnfilteredUniquePair(Shift.class,
1703+ Joiners.overlapping(Shift::getStart, Shift::getEnd),
1704+ Joiners.equal(Shift::getEmployee))
1705+ .expand((shift1, shift2) -> shift1.getOverlap(shift2));
1706+ }
1707+
1708+ Constraint noOverlappingShifts(ConstraintFactory constraintFactory) {
1709+ return constraintFactory.precompute(this::overlappingShifts)
1710+ .penalize(HardSoftScore.ONE_HARD, (shift1, shift2, overlap) -> overlap)
1711+ .asConstraint("No overlapping shifts");
1712+ }
1713+ ----
1714+ =====
1715+ ====
1716+
1717+ ==== When to use precomputing
1718+
1719+ Constraint streams are already very efficient, so precomputing them may result in little change.
1720+ It can be useful in cases where the constraint stream needs to execute expensive operations,
1721+ such as filters which look up data in large collections or which compute complex calculations.
1722+ It also helps with large joins where the join condition doesn't depend on any variables.
1723+
1724+ However, only the biggest of datasets typically struggle with these issues.
1725+ Always measure move evaluation speed before and after implementing precomputing
1726+ to determine if using it brings benefits for your use case.
1727+
16251728
16261729[#constraintStreamsTesting]
16271730== Testing a constraint stream
0 commit comments