From 1c29f34666ca2e600e1ff0b7f778f4bae1b43912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Thu, 6 Nov 2025 16:20:10 +0100 Subject: [PATCH 1/4] chore: refactor CS index to support random access --- .../impl/bavet/bi/IndexedIfExistsBiNode.java | 20 +- .../core/impl/bavet/bi/IndexedJoinBiNode.java | 23 +- .../bavet/bi/UnindexedIfExistsBiNode.java | 18 +- .../impl/bavet/bi/UnindexedJoinBiNode.java | 23 +- .../bavet/common/AbstractIfExistsNode.java | 86 +++--- .../common/AbstractIndexedIfExistsNode.java | 110 ++++---- .../bavet/common/AbstractIndexedJoinNode.java | 93 ++++--- .../impl/bavet/common/AbstractJoinNode.java | 96 +++---- .../common/AbstractUnindexedIfExistsNode.java | 117 ++++----- .../common/AbstractUnindexedJoinNode.java | 98 ++++--- .../bavet/common/ExistsCounterHandle.java | 36 +++ .../ExistsCounterHandlePositionTracker.java | 88 +++++++ .../common/ExistsCounterPositionTracker.java | 34 +++ .../bavet/common/TuplePositionTracker.java | 32 +++ .../bavet/common/index/ComparisonIndexer.java | 19 +- .../common/index/ElementPositionTracker.java | 36 +++ .../bavet/common/index/EqualsIndexer.java | 20 +- .../impl/bavet/common/index/IndexedSet.java | 176 +++++++++++++ .../core/impl/bavet/common/index/Indexer.java | 5 +- .../bavet/common/index/IndexerFactory.java | 20 +- .../impl/bavet/common/index/NoneIndexer.java | 25 +- .../common/tuple/OutputStoreSizeTracker.java | 46 ++++ .../tuple/TupleStorePositionTracker.java | 7 + .../bavet/quad/IndexedIfExistsQuadNode.java | 20 +- .../impl/bavet/quad/IndexedJoinQuadNode.java | 23 +- .../bavet/quad/UnindexedIfExistsQuadNode.java | 15 +- .../bavet/quad/UnindexedJoinQuadNode.java | 23 +- .../bavet/tri/IndexedIfExistsTriNode.java | 20 +- .../impl/bavet/tri/IndexedJoinTriNode.java | 24 +- .../bavet/tri/UnindexedIfExistsTriNode.java | 18 +- .../impl/bavet/tri/UnindexedJoinTriNode.java | 24 +- .../bavet/uni/IndexedIfExistsUniNode.java | 20 +- .../bavet/uni/UnindexedIfExistsUniNode.java | 18 +- .../bi/JoinBiEnumeratingStream.java | 28 +- .../uni/IfExistsUniEnumeratingStream.java | 38 +-- .../bi/BavetIfExistsBiConstraintStream.java | 32 +-- .../bavet/bi/BavetJoinBiConstraintStream.java | 28 +- .../BavetIfExistsQuadConstraintStream.java | 32 +-- .../quad/BavetJoinQuadConstraintStream.java | 28 +- .../tri/BavetIfExistsTriConstraintStream.java | 32 +-- .../tri/BavetJoinTriConstraintStream.java | 28 +- .../uni/BavetIfExistsUniConstraintStream.java | 32 +-- .../index/EqualsAndComparisonIndexerTest.java | 18 +- .../bavet/common/index/EqualsIndexerTest.java | 18 +- .../bavet/common/index/IndexedSetTest.java | 245 ++++++++++++++++++ .../bavet/common/index/NoneIndexerTest.java | 19 +- 46 files changed, 1230 insertions(+), 781 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandle.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandlePositionTracker.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterPositionTracker.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TuplePositionTracker.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ElementPositionTracker.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/OutputStoreSizeTracker.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/TupleStorePositionTracker.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedIfExistsBiNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedIfExistsBiNode.java index 50b0239e2b..bb83700435 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedIfExistsBiNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedIfExistsBiNode.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class IndexedIfExistsBiNode extends AbstractIndexedIfExistsNode, C> { @@ -12,23 +13,10 @@ public final class IndexedIfExistsBiNode extends AbstractIndexedIfExist private final TriPredicate filtering; public IndexedIfExistsBiNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, -1, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public IndexedIfExistsBiNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, TriPredicate filtering) { - super(shouldExist, indexerFactory.buildBiLeftKeysExtractor(), indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, filtering != null); + super(shouldExist, indexerFactory.buildBiLeftKeysExtractor(), indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedJoinBiNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedJoinBiNode.java index 86eddba25f..09d077fd11 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedJoinBiNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedJoinBiNode.java @@ -5,31 +5,26 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractIndexedJoinNode; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class IndexedJoinBiNode extends AbstractIndexedJoinNode, B, BiTuple> { private final BiPredicate filtering; - private final int outputStoreSize; - - public IndexedJoinBiNode(IndexerFactory indexerFactory, - int inputStoreIndexA, int inputStoreIndexEntryA, int inputStoreIndexOutTupleListA, - int inputStoreIndexB, int inputStoreIndexEntryB, int inputStoreIndexOutTupleListB, - TupleLifecycle> nextNodesTupleLifecycle, BiPredicate filtering, - int outputStoreSize, int outputStoreIndexOutEntryA, int outputStoreIndexOutEntryB) { - super(indexerFactory.buildUniLeftKeysExtractor(), indexerFactory, - inputStoreIndexA, inputStoreIndexEntryA, inputStoreIndexOutTupleListA, - inputStoreIndexB, inputStoreIndexEntryB, inputStoreIndexOutTupleListB, - nextNodesTupleLifecycle, filtering != null, - outputStoreIndexOutEntryA, outputStoreIndexOutEntryB); + + public IndexedJoinBiNode(IndexerFactory indexerFactory, TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle> nextNodesTupleLifecycle, BiPredicate filtering) { + super(indexerFactory.buildUniLeftKeysExtractor(), indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; - this.outputStoreSize = outputStoreSize; } @Override protected BiTuple createOutTuple(UniTuple leftTuple, UniTuple rightTuple) { - return new BiTuple<>(leftTuple.factA, rightTuple.factA, outputStoreSize); + return new BiTuple<>(leftTuple.factA, rightTuple.factA, outputStoreSizeTracker.computeOutputStoreSize()); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedIfExistsBiNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedIfExistsBiNode.java index 7316766738..a54170598a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedIfExistsBiNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedIfExistsBiNode.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractUnindexedIfExistsNode; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class UnindexedIfExistsBiNode extends AbstractUnindexedIfExistsNode, C> { @@ -11,22 +12,11 @@ public final class UnindexedIfExistsBiNode extends AbstractUnindexedIfE private final TriPredicate filtering; public UnindexedIfExistsBiNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, - inputStoreIndexLeftCounterEntry, -1, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public UnindexedIfExistsBiNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, int inputStoreIndexRightEntry, - int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, TriPredicate filtering) { - super(shouldExist, - inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, inputStoreIndexRightEntry, - inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, filtering != null); + super(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, nextNodesTupleLifecycle, + filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedJoinBiNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedJoinBiNode.java index ebd0cd9c72..74e3e55588 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedJoinBiNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedJoinBiNode.java @@ -4,32 +4,27 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractUnindexedJoinNode; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class UnindexedJoinBiNode extends AbstractUnindexedJoinNode, B, BiTuple> { private final BiPredicate filtering; - private final int outputStoreSize; - - public UnindexedJoinBiNode( - int inputStoreIndexLeftEntry, int inputStoreIndexLeftOutTupleList, - int inputStoreIndexRightEntry, int inputStoreIndexRightOutTupleList, - TupleLifecycle> nextNodesTupleLifecycle, BiPredicate filtering, - int outputStoreSize, - int outputStoreIndexLeftOutEntry, int outputStoreIndexRightOutEntry) { - super(inputStoreIndexLeftEntry, inputStoreIndexLeftOutTupleList, - inputStoreIndexRightEntry, inputStoreIndexRightOutTupleList, - nextNodesTupleLifecycle, filtering != null, - outputStoreIndexLeftOutEntry, outputStoreIndexRightOutEntry); + + public UnindexedJoinBiNode(TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle> nextNodesTupleLifecycle, BiPredicate filtering) { + super(leftTupleStorePositionTracker, rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, + filtering != null); this.filtering = filtering; - this.outputStoreSize = outputStoreSize; } @Override protected BiTuple createOutTuple(UniTuple leftTuple, UniTuple rightTuple) { - return new BiTuple<>(leftTuple.factA, rightTuple.factA, outputStoreSize); + return new BiTuple<>(leftTuple.factA, rightTuple.factA, outputStoreSizeTracker.computeOutputStoreSize()); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index 26aecf2ab1..b73360c97a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -1,11 +1,11 @@ package ai.timefold.solver.core.impl.bavet.common; +import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.util.ElementAwareList; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; /** * This class has two direct children: {@link AbstractIndexedIfExistsNode} and {@link AbstractUnindexedIfExistsNode}. @@ -20,20 +20,17 @@ public abstract class AbstractIfExistsNode> { protected final boolean shouldExist; - - protected final int inputStoreIndexLeftTrackerList; // -1 if !isFiltering - protected final int inputStoreIndexRightTrackerList; // -1 if !isFiltering - + protected final int inputStoreIndexLeftTrackerSet; // -1 if !isFiltering + protected final int inputStoreIndexRightTrackerSet; // -1 if !isFiltering protected final boolean isFiltering; private final DynamicPropagationQueue> propagationQueue; - protected AbstractIfExistsNode(boolean shouldExist, - int inputStoreIndexLeftTrackerList, int inputStoreIndexRightTrackerList, - TupleLifecycle nextNodesTupleLifecycle, + protected AbstractIfExistsNode(boolean shouldExist, TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering) { this.shouldExist = shouldExist; - this.inputStoreIndexLeftTrackerList = inputStoreIndexLeftTrackerList; - this.inputStoreIndexRightTrackerList = inputStoreIndexRightTrackerList; + this.inputStoreIndexLeftTrackerSet = isFiltering ? leftTupleStorePositionTracker.reserveNextAvailablePosition() : -1; + this.inputStoreIndexRightTrackerSet = isFiltering ? rightTupleStorePositionTracker.reserveNextAvailablePosition() : -1; this.isFiltering = isFiltering; this.propagationQueue = new DynamicPropagationQueue<>(nextNodesTupleLifecycle); } @@ -66,8 +63,9 @@ protected void updateCounterLeft(ExistsCounter counter) { } case OK, DYING -> propagationQueue.update(counter); case DEAD, ABORTING -> propagationQueue.insert(counter); - default -> throw new IllegalStateException("Impossible state: the counter (" + counter - + ") has an impossible insert state (" + state + ")."); + default -> + throw new IllegalStateException("Impossible state: the counter (%s) has an impossible insert state (%s)." + .formatted(counter, state)); } } else { // Retract or remain dead @@ -80,8 +78,9 @@ protected void updateCounterLeft(ExistsCounter counter) { propagationQueue.retract(counter, TupleState.ABORTING); case OK, UPDATING -> // Kill the original propagation. propagationQueue.retract(counter, TupleState.DYING); - default -> throw new IllegalStateException("Impossible state: The counter (" + counter - + ") has an impossible retract state (" + state + ")."); + default -> + throw new IllegalStateException("Impossible state: The counter (%s) has an impossible retract state (%s)." + .formatted(counter, state)); } } @@ -115,27 +114,26 @@ protected void decrementCounterRight(ExistsCounter counter) { } // Else do not even propagate an update } - protected ElementAwareList> updateRightTrackerList(UniTuple rightTuple) { - ElementAwareList> rightTrackerList = rightTuple.getStore(inputStoreIndexRightTrackerList); - for (FilteringTracker tuple : rightTrackerList) { + IndexedSet> updateRightTrackerSet(UniTuple rightTuple) { + IndexedSet> rightTrackerSet = rightTuple.getStore(inputStoreIndexRightTrackerSet); + rightTrackerSet.forEach(tuple -> { decrementCounterRight(tuple.counter); tuple.remove(); - } - return rightTrackerList; + }); + return rightTrackerSet; } - protected void updateCounterFromLeft(LeftTuple_ leftTuple, UniTuple rightTuple, ExistsCounter counter, - ElementAwareList> leftTrackerList) { + void updateCounterFromLeft(LeftTuple_ leftTuple, UniTuple rightTuple, ExistsCounter counter, + IndexedSet> leftTrackerSet) { if (testFiltering(leftTuple, rightTuple)) { counter.countRight++; - ElementAwareList> rightTrackerList = - rightTuple.getStore(inputStoreIndexRightTrackerList); - new FilteringTracker<>(counter, leftTrackerList, rightTrackerList); + IndexedSet> rightTrackerSet = rightTuple.getStore(inputStoreIndexRightTrackerSet); + new ExistsCounterHandle<>(counter, leftTrackerSet, rightTrackerSet); } } - protected void updateCounterFromRight(UniTuple rightTuple, ExistsCounter counter, - ElementAwareList> rightTrackerList) { + void updateCounterFromRight(UniTuple rightTuple, ExistsCounter counter, + IndexedSet> rightTrackerSet) { var leftTuple = counter.leftTuple; if (!leftTuple.state.isActive()) { // Assume the following scenario: @@ -156,9 +154,9 @@ protected void updateCounterFromRight(UniTuple rightTuple, ExistsCounter } if (testFiltering(counter.leftTuple, rightTuple)) { incrementCounterRight(counter); - ElementAwareList> leftTrackerList = - counter.leftTuple.getStore(inputStoreIndexLeftTrackerList); - new FilteringTracker<>(counter, leftTrackerList, rightTrackerList); + IndexedSet> leftTrackerSet = + counter.leftTuple.getStore(inputStoreIndexLeftTrackerSet); + new ExistsCounterHandle<>(counter, leftTrackerSet, rightTrackerSet); } } @@ -166,8 +164,8 @@ private void doInsertCounter(ExistsCounter counter) { switch (counter.state) { case DYING -> propagationQueue.update(counter); case DEAD, ABORTING -> propagationQueue.insert(counter); - default -> throw new IllegalStateException("Impossible state: the counter (" + counter - + ") has an impossible insert state (" + counter.state + ")."); + default -> throw new IllegalStateException("Impossible state: the counter (%s) has an impossible insert state (%s)." + .formatted(counter, counter.state)); } } @@ -177,8 +175,9 @@ private void doRetractCounter(ExistsCounter counter) { propagationQueue.retract(counter, TupleState.ABORTING); case OK, UPDATING -> // Kill the original propagation. propagationQueue.retract(counter, TupleState.DYING); - default -> throw new IllegalStateException("Impossible state: The counter (" + counter - + ") has an impossible retract state (" + counter.state + ")."); + default -> + throw new IllegalStateException("Impossible state: The counter (%s) has an impossible retract state (%s)." + .formatted(counter, counter.state)); } } @@ -187,23 +186,4 @@ public Propagator getPropagator() { return propagationQueue; } - protected static final class FilteringTracker { - final ExistsCounter counter; - private final ElementAwareListEntry> leftTrackerEntry; - private final ElementAwareListEntry> rightTrackerEntry; - - FilteringTracker(ExistsCounter counter, ElementAwareList> leftTrackerList, - ElementAwareList> rightTrackerList) { - this.counter = counter; - leftTrackerEntry = leftTrackerList.add(this); - rightTrackerEntry = rightTrackerList.add(this); - } - - public void remove() { - leftTrackerEntry.remove(); - rightTrackerEntry.remove(); - } - - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java index 1f12232e6a..bd174958b4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.impl.bavet.common; +import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; import ai.timefold.solver.core.impl.bavet.common.index.Indexer; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory.KeysExtractor; @@ -8,9 +9,8 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.LeftTupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.RightTupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.util.ElementAwareList; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; /** * There is a strong likelihood that any change to this class, which is not related to indexing, @@ -26,56 +26,53 @@ public abstract class AbstractIndexedIfExistsNode keysExtractorLeft; private final UniKeysExtractor keysExtractorRight; private final int inputStoreIndexLeftKeys; - private final int inputStoreIndexLeftCounterEntry; + private final int inputStoreIndexLeftCounter; private final int inputStoreIndexRightKeys; - private final int inputStoreIndexRightEntry; private final Indexer> indexerLeft; private final Indexer> indexerRight; protected AbstractIndexedIfExistsNode(boolean shouldExist, KeysExtractor keysExtractorLeft, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering) { - super(shouldExist, - inputStoreIndexLeftTrackerList, inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, isFiltering); + super(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, nextNodesTupleLifecycle, isFiltering); this.keysExtractorLeft = keysExtractorLeft; this.keysExtractorRight = indexerFactory.buildRightKeysExtractor(); - this.inputStoreIndexLeftKeys = inputStoreIndexLeftKeys; - this.inputStoreIndexLeftCounterEntry = inputStoreIndexLeftCounterEntry; - this.inputStoreIndexRightKeys = inputStoreIndexRightKeys; - this.inputStoreIndexRightEntry = inputStoreIndexRightEntry; - this.indexerLeft = indexerFactory.buildIndexer(true); - this.indexerRight = indexerFactory.buildIndexer(false); + this.inputStoreIndexLeftKeys = leftTupleStorePositionTracker.reserveNextAvailablePosition(); + this.inputStoreIndexLeftCounter = leftTupleStorePositionTracker.reserveNextAvailablePosition(); + this.inputStoreIndexRightKeys = rightTupleStorePositionTracker.reserveNextAvailablePosition(); + this.indexerLeft = indexerFactory.buildIndexer(true, + new ExistsCounterPositionTracker<>(leftTupleStorePositionTracker.reserveNextAvailablePosition())); + this.indexerRight = indexerFactory.buildIndexer(false, + new TuplePositionTracker<>(rightTupleStorePositionTracker.reserveNextAvailablePosition())); } @Override public final void insertLeft(LeftTuple_ leftTuple) { if (leftTuple.getStore(inputStoreIndexLeftKeys) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + leftTuple - + ") was already added in the tupleStore."); + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(leftTuple)); } var indexKeys = keysExtractorLeft.apply(leftTuple); leftTuple.setStore(inputStoreIndexLeftKeys, indexKeys); var counter = new ExistsCounter<>(leftTuple); - var counterEntry = indexerLeft.put(indexKeys, counter); - updateCounterRight(leftTuple, indexKeys, counter, counterEntry); + indexerLeft.put(indexKeys, counter); + updateCounterRight(leftTuple, indexKeys, counter); initCounterLeft(counter); } - private void updateCounterRight(LeftTuple_ leftTuple, Object indexKeys, ExistsCounter counter, - ElementAwareListEntry> counterEntry) { - leftTuple.setStore(inputStoreIndexLeftCounterEntry, counterEntry); + private void updateCounterRight(LeftTuple_ leftTuple, Object indexKeys, ExistsCounter counter) { + leftTuple.setStore(inputStoreIndexLeftCounter, counter); if (!isFiltering) { counter.countRight = indexerRight.size(indexKeys); } else { - var leftTrackerList = new ElementAwareList>(); + var leftTrackerSet = new IndexedSet>(ExistsCounterHandlePositionTracker.left()); indexerRight.forEach(indexKeys, - rightTuple -> updateCounterFromLeft(leftTuple, rightTuple, counter, leftTrackerList)); - leftTuple.setStore(inputStoreIndexLeftTrackerList, leftTrackerList); + rightTuple -> updateCounterFromLeft(leftTuple, rightTuple, counter, leftTrackerSet)); + leftTuple.setStore(inputStoreIndexLeftTrackerSet, leftTrackerSet); } } @@ -88,8 +85,7 @@ public final void updateLeft(LeftTuple_ leftTuple) { return; } var newIndexKeys = keysExtractorLeft.apply(leftTuple); - ElementAwareListEntry> counterEntry = leftTuple.getStore(inputStoreIndexLeftCounterEntry); - var counter = counterEntry.getElement(); + ExistsCounter counter = leftTuple.getStore(inputStoreIndexLeftCounter); if (oldIndexKeys.equals(newIndexKeys)) { // No need for re-indexing because the index keys didn't change @@ -98,19 +94,20 @@ public final void updateLeft(LeftTuple_ leftTuple) { updateUnchangedCounterLeft(counter); } else { // Call filtering for the leftTuple and rightTuple combinations again - ElementAwareList> leftTrackerList = - leftTuple.getStore(inputStoreIndexLeftTrackerList); - leftTrackerList.forEach(FilteringTracker::remove); + IndexedSet> leftTrackerSet = + leftTuple.getStore(inputStoreIndexLeftTrackerSet); + leftTrackerSet.forEach(ExistsCounterHandle::remove); counter.countRight = 0; indexerRight.forEach(oldIndexKeys, - rightTuple -> updateCounterFromLeft(leftTuple, rightTuple, counter, leftTrackerList)); + rightTuple -> updateCounterFromLeft(leftTuple, rightTuple, counter, leftTrackerSet)); updateCounterLeft(counter); } } else { - updateIndexerLeft(oldIndexKeys, counterEntry, leftTuple); + updateIndexerLeft(oldIndexKeys, counter, leftTuple); counter.countRight = 0; leftTuple.setStore(inputStoreIndexLeftKeys, newIndexKeys); - updateCounterRight(leftTuple, newIndexKeys, counter, indexerLeft.put(newIndexKeys, counter)); + indexerLeft.put(newIndexKeys, counter); + updateCounterRight(leftTuple, newIndexKeys, counter); updateCounterLeft(counter); } } @@ -122,32 +119,31 @@ public final void retractLeft(LeftTuple_ leftTuple) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - ElementAwareListEntry> counterEntry = leftTuple.getStore(inputStoreIndexLeftCounterEntry); - var counter = counterEntry.getElement(); - updateIndexerLeft(indexKeys, counterEntry, leftTuple); + ExistsCounter counter = leftTuple.getStore(inputStoreIndexLeftCounter); + updateIndexerLeft(indexKeys, counter, leftTuple); killCounterLeft(counter); } - private void updateIndexerLeft(Object indexKeys, ElementAwareListEntry> counterEntry, - LeftTuple_ leftTuple) { - indexerLeft.remove(indexKeys, counterEntry); + private void updateIndexerLeft(Object indexKeys, ExistsCounter counter, LeftTuple_ leftTuple) { + indexerLeft.remove(indexKeys, counter); if (isFiltering) { - ElementAwareList> leftTrackerList = leftTuple.getStore(inputStoreIndexLeftTrackerList); - leftTrackerList.forEach(FilteringTracker::remove); + IndexedSet> leftTrackerSet = + leftTuple.getStore(inputStoreIndexLeftTrackerSet); + leftTrackerSet.forEach(ExistsCounterHandle::remove); } } @Override public final void insertRight(UniTuple rightTuple) { if (rightTuple.getStore(inputStoreIndexRightKeys) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + rightTuple - + ") was already added in the tupleStore."); + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(rightTuple)); } var indexKeys = keysExtractorRight.apply(rightTuple); rightTuple.setStore(inputStoreIndexRightKeys, indexKeys); - var rightEntry = indexerRight.put(indexKeys, rightTuple); - rightTuple.setStore(inputStoreIndexRightEntry, rightEntry); + indexerRight.put(indexKeys, rightTuple); updateCounterLeft(rightTuple, indexKeys); } @@ -155,9 +151,10 @@ private void updateCounterLeft(UniTuple rightTuple, Object indexKeys) { if (!isFiltering) { indexerLeft.forEach(indexKeys, this::incrementCounterRight); } else { - var rightTrackerList = new ElementAwareList>(); - indexerLeft.forEach(indexKeys, counter -> updateCounterFromRight(rightTuple, counter, rightTrackerList)); - rightTuple.setStore(inputStoreIndexRightTrackerList, rightTrackerList); + var rightTrackerSet = + new IndexedSet>(ExistsCounterHandlePositionTracker.right()); + indexerLeft.forEach(indexKeys, counter -> updateCounterFromRight(rightTuple, counter, rightTrackerSet)); + rightTuple.setStore(inputStoreIndexRightTrackerSet, rightTrackerSet); } } @@ -173,21 +170,19 @@ public final void updateRight(UniTuple rightTuple) { if (oldIndexKeys.equals(newIndexKeys)) { // No need for re-indexing because the index keys didn't change if (isFiltering) { - var rightTrackerList = updateRightTrackerList(rightTuple); + var rightTrackerSet = updateRightTrackerSet(rightTuple); indexerLeft.forEach(oldIndexKeys, - counter -> updateCounterFromRight(rightTuple, counter, rightTrackerList)); + counter -> updateCounterFromRight(rightTuple, counter, rightTrackerSet)); } } else { - ElementAwareListEntry> rightEntry = rightTuple.getStore(inputStoreIndexRightEntry); - indexerRight.remove(oldIndexKeys, rightEntry); + indexerRight.remove(oldIndexKeys, rightTuple); if (!isFiltering) { indexerLeft.forEach(oldIndexKeys, this::decrementCounterRight); } else { - updateRightTrackerList(rightTuple); + updateRightTrackerSet(rightTuple); } rightTuple.setStore(inputStoreIndexRightKeys, newIndexKeys); - rightEntry = indexerRight.put(newIndexKeys, rightTuple); - rightTuple.setStore(inputStoreIndexRightEntry, rightEntry); + indexerRight.put(newIndexKeys, rightTuple); updateCounterLeft(rightTuple, newIndexKeys); } } @@ -199,12 +194,11 @@ public final void retractRight(UniTuple rightTuple) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - ElementAwareListEntry> rightEntry = rightTuple.removeStore(inputStoreIndexRightEntry); - indexerRight.remove(indexKeys, rightEntry); + indexerRight.remove(indexKeys, rightTuple); if (!isFiltering) { indexerLeft.forEach(indexKeys, this::decrementCounterRight); } else { - updateRightTrackerList(rightTuple); + updateRightTrackerSet(rightTuple); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedJoinNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedJoinNode.java index 2e050483cf..398abae78c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedJoinNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedJoinNode.java @@ -1,16 +1,17 @@ package ai.timefold.solver.core.impl.bavet.common; +import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; import ai.timefold.solver.core.impl.bavet.common.index.Indexer; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory.KeysExtractor; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory.UniKeysExtractor; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.LeftTupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.RightTupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.util.ElementAwareList; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; /** * There is a strong likelihood that any change to this class, which is not related to indexing, @@ -26,9 +27,10 @@ public abstract class AbstractIndexedJoinNode keysExtractorLeft; private final UniKeysExtractor keysExtractorRight; private final int inputStoreIndexLeftKeys; - private final int inputStoreIndexLeftEntry; private final int inputStoreIndexRightKeys; - private final int inputStoreIndexRightEntry; + private final int outputStoreIndexLeftPosition; + private final int outputStoreIndexRightPosition; + /** * Calls for example {@link AbstractScorer#insert(AbstractTuple)} and/or ... */ @@ -36,31 +38,33 @@ public abstract class AbstractIndexedJoinNode> indexerRight; protected AbstractIndexedJoinNode(KeysExtractor keysExtractorLeft, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftEntry, int inputStoreIndexLeftOutTupleList, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, int inputStoreIndexRightOutTupleList, - TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering, int outputStoreIndexLeftOutEntry, - int outputStoreIndexRightOutEntry) { - super(inputStoreIndexLeftOutTupleList, inputStoreIndexRightOutTupleList, nextNodesTupleLifecycle, isFiltering, - outputStoreIndexLeftOutEntry, outputStoreIndexRightOutEntry); + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, + OutputStoreSizeTracker outputStoreSizeTracker, TupleLifecycle nextNodesTupleLifecycle, + boolean isFiltering) { + super(leftTupleStorePositionTracker, rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, + isFiltering); this.keysExtractorLeft = keysExtractorLeft; this.keysExtractorRight = indexerFactory.buildRightKeysExtractor(); - this.inputStoreIndexLeftKeys = inputStoreIndexLeftKeys; - this.inputStoreIndexLeftEntry = inputStoreIndexLeftEntry; - this.inputStoreIndexRightKeys = inputStoreIndexRightKeys; - this.inputStoreIndexRightEntry = inputStoreIndexRightEntry; - this.indexerLeft = indexerFactory.buildIndexer(true); - this.indexerRight = indexerFactory.buildIndexer(false); + this.inputStoreIndexLeftKeys = leftTupleStorePositionTracker.reserveNextAvailablePosition(); + this.inputStoreIndexRightKeys = rightTupleStorePositionTracker.reserveNextAvailablePosition(); + this.outputStoreIndexLeftPosition = outputStoreSizeTracker.reserveNextAvailablePosition(); + this.outputStoreIndexRightPosition = outputStoreSizeTracker.reserveNextAvailablePosition(); + this.indexerLeft = indexerFactory.buildIndexer(true, + new TuplePositionTracker<>(leftTupleStorePositionTracker.reserveNextAvailablePosition())); + this.indexerRight = indexerFactory.buildIndexer(false, + new TuplePositionTracker<>(rightTupleStorePositionTracker.reserveNextAvailablePosition())); } @Override public final void insertLeft(LeftTuple_ leftTuple) { if (leftTuple.getStore(inputStoreIndexLeftKeys) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + leftTuple - + ") was already added in the tupleStore."); + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(leftTuple)); } var indexKeys = keysExtractorLeft.apply(leftTuple); - var outTupleListLeft = new ElementAwareList(); - leftTuple.setStore(inputStoreIndexLeftOutTupleList, outTupleListLeft); + var outTupleSetLeft = new IndexedSet<>(new TuplePositionTracker<>(outputStoreIndexLeftPosition)); + leftTuple.setStore(inputStoreIndexLeftOutTupleSet, outTupleSetLeft); indexAndPropagateLeft(leftTuple, indexKeys); } @@ -78,20 +82,17 @@ public final void updateLeft(LeftTuple_ leftTuple) { // Prefer an update over retract-insert if possible innerUpdateLeft(leftTuple, consumer -> indexerRight.forEach(oldIndexKeys, consumer)); } else { - ElementAwareListEntry leftEntry = leftTuple.getStore(inputStoreIndexLeftEntry); - ElementAwareList outTupleListLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleList); - indexerLeft.remove(oldIndexKeys, leftEntry); - outTupleListLeft.forEach(this::retractOutTuple); - // outTupleListLeft is now empty - // No need for leftTuple.setStore(inputStoreIndexLeftOutTupleList, outTupleListLeft); + IndexedSet outTupleSetLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleSet); + indexerLeft.remove(oldIndexKeys, leftTuple); + outTupleSetLeft.forEach(this::retractOutTuple); + // outTupleSetLeft is now empty, no need for leftTuple.setStore(...); indexAndPropagateLeft(leftTuple, newIndexKeys); } } private void indexAndPropagateLeft(LeftTuple_ leftTuple, Object indexKeys) { leftTuple.setStore(inputStoreIndexLeftKeys, indexKeys); - var leftEntry = indexerLeft.put(indexKeys, leftTuple); - leftTuple.setStore(inputStoreIndexLeftEntry, leftEntry); + indexerLeft.put(indexKeys, leftTuple); indexerRight.forEach(indexKeys, rightTuple -> insertOutTupleFiltered(leftTuple, rightTuple)); } @@ -102,21 +103,21 @@ public final void retractLeft(LeftTuple_ leftTuple) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - ElementAwareListEntry leftEntry = leftTuple.removeStore(inputStoreIndexLeftEntry); - ElementAwareList outTupleListLeft = leftTuple.removeStore(inputStoreIndexLeftOutTupleList); - indexerLeft.remove(indexKeys, leftEntry); - outTupleListLeft.forEach(this::retractOutTuple); + IndexedSet outTupleSetLeft = leftTuple.removeStore(inputStoreIndexLeftOutTupleSet); + indexerLeft.remove(indexKeys, leftTuple); + outTupleSetLeft.forEach(this::retractOutTuple); } @Override public final void insertRight(UniTuple rightTuple) { if (rightTuple.getStore(inputStoreIndexRightKeys) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + rightTuple - + ") was already added in the tupleStore."); + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(rightTuple)); } var indexKeys = keysExtractorRight.apply(rightTuple); - var outTupleListRight = new ElementAwareList(); - rightTuple.setStore(inputStoreIndexRightOutTupleList, outTupleListRight); + var outTupleSetRight = new IndexedSet<>(new TuplePositionTracker<>(outputStoreIndexRightPosition)); + rightTuple.setStore(inputStoreIndexRightOutTupleSet, outTupleSetRight); indexAndPropagateRight(rightTuple, indexKeys); } @@ -134,20 +135,17 @@ public final void updateRight(UniTuple rightTuple) { // Prefer an update over retract-insert if possible innerUpdateRight(rightTuple, consumer -> indexerLeft.forEach(oldIndexKeys, consumer)); } else { - ElementAwareListEntry> rightEntry = rightTuple.getStore(inputStoreIndexRightEntry); - ElementAwareList outTupleListRight = rightTuple.getStore(inputStoreIndexRightOutTupleList); - indexerRight.remove(oldIndexKeys, rightEntry); - outTupleListRight.forEach(this::retractOutTuple); - // outTupleListRight is now empty - // No need for rightTuple.setStore(inputStoreIndexRightOutTupleList, outTupleListRight); + IndexedSet outTupleSetRight = rightTuple.getStore(inputStoreIndexRightOutTupleSet); + indexerRight.remove(oldIndexKeys, rightTuple); + outTupleSetRight.forEach(this::retractOutTuple); + // outTupleSetRight is now empty, no need for rightTuple.setStore(...); indexAndPropagateRight(rightTuple, newIndexKeys); } } private void indexAndPropagateRight(UniTuple rightTuple, Object indexKeys) { rightTuple.setStore(inputStoreIndexRightKeys, indexKeys); - var rightEntry = indexerRight.put(indexKeys, rightTuple); - rightTuple.setStore(inputStoreIndexRightEntry, rightEntry); + indexerRight.put(indexKeys, rightTuple); indexerLeft.forEach(indexKeys, leftTuple -> insertOutTupleFiltered(leftTuple, rightTuple)); } @@ -158,10 +156,9 @@ public final void retractRight(UniTuple rightTuple) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - ElementAwareListEntry> rightEntry = rightTuple.removeStore(inputStoreIndexRightEntry); - ElementAwareList outTupleListRight = rightTuple.removeStore(inputStoreIndexRightOutTupleList); - indexerRight.remove(indexKeys, rightEntry); - outTupleListRight.forEach(this::retractOutTuple); + IndexedSet outTupleSetRight = rightTuple.removeStore(inputStoreIndexRightOutTupleSet); + indexerRight.remove(indexKeys, rightTuple); + outTupleSetRight.forEach(this::retractOutTuple); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java index 5303ce47e0..cfc660e8cb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java @@ -2,12 +2,15 @@ import java.util.function.Consumer; +import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.util.ElementAwareList; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; + +import org.jspecify.annotations.Nullable; /** * This class has two direct children: {@link AbstractIndexedJoinNode} and {@link AbstractUnindexedJoinNode}. @@ -21,21 +24,23 @@ public abstract class AbstractJoinNode extends AbstractTwoInputNode> { - protected final int inputStoreIndexLeftOutTupleList; - protected final int inputStoreIndexRightOutTupleList; + protected final int inputStoreIndexLeftOutTupleSet; + protected final int inputStoreIndexRightOutTupleSet; private final boolean isFiltering; - private final int outputStoreIndexLeftOutEntry; - private final int outputStoreIndexRightOutEntry; + private final int outputStoreIndexLeftOutSet; + private final int outputStoreIndexRightOutSet; + protected final OutputStoreSizeTracker outputStoreSizeTracker; private final StaticPropagationQueue propagationQueue; - protected AbstractJoinNode(int inputStoreIndexLeftOutTupleList, int inputStoreIndexRightOutTupleList, - TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering, - int outputStoreIndexLeftOutEntry, int outputStoreIndexRightOutEntry) { - this.inputStoreIndexLeftOutTupleList = inputStoreIndexLeftOutTupleList; - this.inputStoreIndexRightOutTupleList = inputStoreIndexRightOutTupleList; + protected AbstractJoinNode(TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering) { + this.inputStoreIndexLeftOutTupleSet = leftTupleStorePositionTracker.reserveNextAvailablePosition(); + this.inputStoreIndexRightOutTupleSet = rightTupleStorePositionTracker.reserveNextAvailablePosition(); this.isFiltering = isFiltering; - this.outputStoreIndexLeftOutEntry = outputStoreIndexLeftOutEntry; - this.outputStoreIndexRightOutEntry = outputStoreIndexRightOutEntry; + this.outputStoreIndexLeftOutSet = outputStoreSizeTracker.reserveNextAvailablePosition(); + this.outputStoreIndexRightOutSet = outputStoreSizeTracker.reserveNextAvailablePosition(); + this.outputStoreSizeTracker = outputStoreSizeTracker; this.propagationQueue = new StaticPropagationQueue<>(nextNodesTupleLifecycle); } @@ -49,12 +54,12 @@ protected AbstractJoinNode(int inputStoreIndexLeftOutTupleList, int inputStoreIn protected final void insertOutTuple(LeftTuple_ leftTuple, UniTuple rightTuple) { var outTuple = createOutTuple(leftTuple, rightTuple); - ElementAwareList outTupleListLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleList); - var outEntryLeft = outTupleListLeft.add(outTuple); - outTuple.setStore(outputStoreIndexLeftOutEntry, outEntryLeft); - ElementAwareList outTupleListRight = rightTuple.getStore(inputStoreIndexRightOutTupleList); - var outEntryRight = outTupleListRight.add(outTuple); - outTuple.setStore(outputStoreIndexRightOutEntry, outEntryRight); + IndexedSet outTupleSetLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleSet); + outTupleSetLeft.add(outTuple); + outTuple.setStore(outputStoreIndexLeftOutSet, outTupleSetLeft); + IndexedSet outTupleSetRight = rightTuple.getStore(inputStoreIndexRightOutTupleSet); + outTupleSetRight.add(outTuple); + outTuple.setStore(outputStoreIndexRightOutSet, outTupleSetRight); propagationQueue.insert(outTuple); } @@ -82,16 +87,14 @@ protected final void insertOutTupleFiltered(LeftTuple_ leftTuple, UniTuple>> rightTupleConsumer) { // Prefer an update over retract-insert if possible - ElementAwareList outTupleListLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleList); + IndexedSet outTupleSetLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleSet); // Propagate the update for downstream filters, matchWeighers, ... if (!isFiltering) { - for (var outTuple : outTupleListLeft) { - updateOutTupleLeft(outTuple, leftTuple); - } + outTupleSetLeft.forEach(outTuple -> updateOutTupleLeft(outTuple, leftTuple)); } else { rightTupleConsumer.accept(rightTuple -> { - ElementAwareList rightOutList = rightTuple.getStore(inputStoreIndexRightOutTupleList); - processOutTupleUpdate(leftTuple, rightTuple, rightOutList, outTupleListLeft, outputStoreIndexRightOutEntry); + IndexedSet outTupleSetRight = rightTuple.getStore(inputStoreIndexRightOutTupleSet); + processOutTupleUpdate(leftTuple, rightTuple, outTupleSetRight, outTupleSetLeft, outputStoreIndexRightOutSet); }); } } @@ -114,23 +117,24 @@ private void doUpdateOutTuple(OutTuple_ outTuple) { protected final void innerUpdateRight(UniTuple rightTuple, Consumer> leftTupleConsumer) { // Prefer an update over retract-insert if possible - ElementAwareList outTupleListRight = rightTuple.getStore(inputStoreIndexRightOutTupleList); + IndexedSet outTupleSetRight = rightTuple.getStore(inputStoreIndexRightOutTupleSet); if (!isFiltering) { // Propagate the update for downstream filters, matchWeighers, ... - for (var outTuple : outTupleListRight) { + outTupleSetRight.forEach(outTuple -> { setOutTupleRightFact(outTuple, rightTuple); doUpdateOutTuple(outTuple); - } + }); } else { leftTupleConsumer.accept(leftTuple -> { - ElementAwareList leftOutList = leftTuple.getStore(inputStoreIndexLeftOutTupleList); - processOutTupleUpdate(leftTuple, rightTuple, leftOutList, outTupleListRight, outputStoreIndexLeftOutEntry); + IndexedSet outTupleSetLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleSet); + processOutTupleUpdate(leftTuple, rightTuple, outTupleSetLeft, outTupleSetRight, outputStoreIndexLeftOutSet); }); } } - private void processOutTupleUpdate(LeftTuple_ leftTuple, UniTuple rightTuple, ElementAwareList outList, - ElementAwareList outTupleList, int outputStoreIndexOutEntry) { + private void processOutTupleUpdate(LeftTuple_ leftTuple, UniTuple rightTuple, + IndexedSet referenceOutTupleSet, IndexedSet outTupleSet, + int outputStoreIndexOutSet) { if (!leftTuple.state.isActive()) { // Assume the following scenario: // - The join is of two entities of the same type, both filtering out unassigned. @@ -148,7 +152,7 @@ private void processOutTupleUpdate(LeftTuple_ leftTuple, UniTuple rightT // However, no such issue could have been reproduced; when in doubt, leave it out. return; } - var outTuple = findOutTuple(outTupleList, outList, outputStoreIndexOutEntry); + var outTuple = findOutTuple(outTupleSet, referenceOutTupleSet, outputStoreIndexOutSet); if (testFiltering(leftTuple, rightTuple)) { if (outTuple == null) { insertOutTuple(leftTuple, rightTuple); @@ -162,28 +166,24 @@ private void processOutTupleUpdate(LeftTuple_ leftTuple, UniTuple rightT } } - private static Tuple_ findOutTuple(ElementAwareList outTupleList, - ElementAwareList outList, int outputStoreIndexOutEntry) { - // Hack: the outTuple has no left/right input tuple reference, use the left/right outList reference instead. - var item = outTupleList.first(); - while (item != null) { - // Creating list iterators here caused major GC pressure; therefore, we iterate over the entries directly. - var outTuple = item.getElement(); - ElementAwareListEntry outEntry = outTuple.getStore(outputStoreIndexOutEntry); - var outEntryList = outEntry.getList(); - if (outList == outEntryList) { + private static @Nullable Tuple_ findOutTuple(IndexedSet outTupleSet, + IndexedSet referenceOutTupleSet, int outputStoreIndexOutSet) { + // Hack: the outTuple has no left/right input tuple reference, use the left/right outSet reference instead. + var list = outTupleSet.asList(); + for (var i = 0; i < list.size(); i++) { // Avoid allocating iterators. + var outTuple = list.get(i); + if (referenceOutTupleSet == outTuple.getStore(outputStoreIndexOutSet)) { return outTuple; } - item = item.next(); } return null; } protected final void retractOutTuple(OutTuple_ outTuple) { - ElementAwareListEntry outEntryLeft = outTuple.removeStore(outputStoreIndexLeftOutEntry); - outEntryLeft.remove(); - ElementAwareListEntry outEntryRight = outTuple.removeStore(outputStoreIndexRightOutEntry); - outEntryRight.remove(); + IndexedSet outSetLeft = outTuple.removeStore(outputStoreIndexLeftOutSet); + outSetLeft.remove(outTuple); + IndexedSet outSetRight = outTuple.removeStore(outputStoreIndexRightOutSet); + outSetRight.remove(outTuple); var state = outTuple.state; if (!state.isActive()) { // Impossible because they shouldn't linger in the indexes. throw new IllegalStateException("Impossible state: The tuple (%s) in node (%s) is in an unexpected state (%s)." diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java index 9709dd62a1..2ade3386dc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java @@ -1,12 +1,12 @@ package ai.timefold.solver.core.impl.bavet.common; +import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.LeftTupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.RightTupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.util.ElementAwareList; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; /** * There is a strong likelihood that any change made to this class @@ -19,134 +19,123 @@ public abstract class AbstractUnindexedIfExistsNode implements LeftTupleLifecycle, RightTupleLifecycle> { - private final int inputStoreIndexLeftCounterEntry; + private final int inputStoreIndexLeftCounter; + private final int inputStoreIndexRightTuple; - private final int inputStoreIndexRightEntry; + private final IndexedSet> leftCounterSet; + private final IndexedSet> rightTupleSet; - // Acts as a leftTupleList too - private final ElementAwareList> leftCounterList = new ElementAwareList<>(); - private final ElementAwareList> rightTupleList = new ElementAwareList<>(); - - protected AbstractUnindexedIfExistsNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, int inputStoreIndexRightEntry, - int inputStoreIndexRightTrackerList, - TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering) { - super(shouldExist, - inputStoreIndexLeftTrackerList, inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, isFiltering); - this.inputStoreIndexLeftCounterEntry = inputStoreIndexLeftCounterEntry; - this.inputStoreIndexRightEntry = inputStoreIndexRightEntry; + protected AbstractUnindexedIfExistsNode(boolean shouldExist, TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle nextNodesTupleLifecycle, + boolean isFiltering) { + super(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, nextNodesTupleLifecycle, isFiltering); + this.inputStoreIndexLeftCounter = leftTupleStorePositionTracker.reserveNextAvailablePosition(); + this.inputStoreIndexRightTuple = rightTupleStorePositionTracker.reserveNextAvailablePosition(); + this.leftCounterSet = new IndexedSet<>( + new ExistsCounterPositionTracker<>(leftTupleStorePositionTracker.reserveNextAvailablePosition())); + this.rightTupleSet = new IndexedSet<>(new TuplePositionTracker<>(inputStoreIndexRightTuple)); } @Override public final void insertLeft(LeftTuple_ leftTuple) { - if (leftTuple.getStore(inputStoreIndexLeftCounterEntry) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + leftTuple - + ") was already added in the tupleStore."); + if (leftTuple.getStore(inputStoreIndexLeftCounter) != null) { + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(leftTuple)); } var counter = new ExistsCounter<>(leftTuple); - var counterEntry = leftCounterList.add(counter); - leftTuple.setStore(inputStoreIndexLeftCounterEntry, counterEntry); + leftCounterSet.add(counter); + leftTuple.setStore(inputStoreIndexLeftCounter, counter); if (!isFiltering) { - counter.countRight = rightTupleList.size(); + counter.countRight = rightTupleSet.size(); } else { - var leftTrackerList = new ElementAwareList>(); - for (var tuple : rightTupleList) { - updateCounterFromLeft(leftTuple, tuple, counter, leftTrackerList); - } - leftTuple.setStore(inputStoreIndexLeftTrackerList, leftTrackerList); + var leftTrackerSet = + new IndexedSet>(ExistsCounterHandlePositionTracker.left()); + rightTupleSet.forEach(tuple -> updateCounterFromLeft(leftTuple, tuple, counter, leftTrackerSet)); + leftTuple.setStore(inputStoreIndexLeftTrackerSet, leftTrackerSet); } initCounterLeft(counter); } @Override public final void updateLeft(LeftTuple_ leftTuple) { - ElementAwareListEntry> counterEntry = leftTuple.getStore(inputStoreIndexLeftCounterEntry); - if (counterEntry == null) { + ExistsCounter counter = leftTuple.getStore(inputStoreIndexLeftCounter); + if (counter == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) insertLeft(leftTuple); return; } - var counter = counterEntry.getElement(); // The indexers contain counters in the DEAD state, to track the rightCount. if (!isFiltering) { updateUnchangedCounterLeft(counter); } else { // Call filtering for the leftTuple and rightTuple combinations again - ElementAwareList> leftTrackerList = leftTuple.getStore(inputStoreIndexLeftTrackerList); - leftTrackerList.forEach(FilteringTracker::remove); + IndexedSet> leftTrackerSet = + leftTuple.getStore(inputStoreIndexLeftTrackerSet); + leftTrackerSet.forEach(ExistsCounterHandle::remove); counter.countRight = 0; - for (var tuple : rightTupleList) { - updateCounterFromLeft(leftTuple, tuple, counter, leftTrackerList); - } + rightTupleSet.forEach(tuple -> updateCounterFromLeft(leftTuple, tuple, counter, leftTrackerSet)); updateCounterLeft(counter); } } @Override public final void retractLeft(LeftTuple_ leftTuple) { - ElementAwareListEntry> counterEntry = leftTuple.removeStore(inputStoreIndexLeftCounterEntry); - if (counterEntry == null) { + ExistsCounter counter = leftTuple.removeStore(inputStoreIndexLeftCounter); + if (counter == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - var counter = counterEntry.getElement(); - counterEntry.remove(); + leftCounterSet.remove(counter); if (isFiltering) { - ElementAwareList> leftTrackerList = leftTuple.getStore(inputStoreIndexLeftTrackerList); - leftTrackerList.forEach(FilteringTracker::remove); + IndexedSet> leftTrackerSet = leftTuple.getStore(inputStoreIndexLeftTrackerSet); + leftTrackerSet.forEach(ExistsCounterHandle::remove); } killCounterLeft(counter); } @Override public final void insertRight(UniTuple rightTuple) { - if (rightTuple.getStore(inputStoreIndexRightEntry) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + rightTuple - + ") was already added in the tupleStore."); + if (rightTuple.getStore(inputStoreIndexRightTuple) != null) { + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(rightTuple)); } - var rightEntry = rightTupleList.add(rightTuple); - rightTuple.setStore(inputStoreIndexRightEntry, rightEntry); + rightTupleSet.add(rightTuple); if (!isFiltering) { - leftCounterList.forEach(this::incrementCounterRight); + leftCounterSet.forEach(this::incrementCounterRight); } else { - var rightTrackerList = new ElementAwareList>(); - for (var tuple : leftCounterList) { - updateCounterFromRight(rightTuple, tuple, rightTrackerList); - } - rightTuple.setStore(inputStoreIndexRightTrackerList, rightTrackerList); + var rightTrackerSet = new IndexedSet>(ExistsCounterHandlePositionTracker.right()); + leftCounterSet.forEach(tuple -> updateCounterFromRight(rightTuple, tuple, rightTrackerSet)); + rightTuple.setStore(inputStoreIndexRightTrackerSet, rightTrackerSet); } } @Override public final void updateRight(UniTuple rightTuple) { - ElementAwareListEntry> rightEntry = rightTuple.getStore(inputStoreIndexRightEntry); - if (rightEntry == null) { + if (rightTuple.getStore(inputStoreIndexRightTuple) == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) insertRight(rightTuple); return; } if (isFiltering) { - var rightTrackerList = updateRightTrackerList(rightTuple); - for (var tuple : leftCounterList) { - updateCounterFromRight(rightTuple, tuple, rightTrackerList); - } + var rightTrackerSet = updateRightTrackerSet(rightTuple); + leftCounterSet.forEach(tuple -> updateCounterFromRight(rightTuple, tuple, rightTrackerSet)); } } @Override public final void retractRight(UniTuple rightTuple) { - ElementAwareListEntry> rightEntry = rightTuple.removeStore(inputStoreIndexRightEntry); - if (rightEntry == null) { + if (rightTuple.getStore(inputStoreIndexRightTuple) == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - rightEntry.remove(); + rightTupleSet.remove(rightTuple); if (!isFiltering) { - leftCounterList.forEach(this::decrementCounterRight); + leftCounterSet.forEach(this::decrementCounterRight); } else { - updateRightTrackerList(rightTuple); + updateRightTrackerSet(rightTuple); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedJoinNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedJoinNode.java index 971b569288..22cba406ac 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedJoinNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedJoinNode.java @@ -1,12 +1,13 @@ package ai.timefold.solver.core.impl.bavet.common; +import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.LeftTupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.RightTupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.util.ElementAwareList; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; /** * There is a strong likelihood that any change made to this class @@ -19,95 +20,92 @@ public abstract class AbstractUnindexedJoinNode implements LeftTupleLifecycle, RightTupleLifecycle> { - private final int inputStoreIndexLeftEntry; - private final int inputStoreIndexRightEntry; - private final ElementAwareList leftTupleList = new ElementAwareList<>(); - private final ElementAwareList> rightTupleList = new ElementAwareList<>(); + private final int inputStoreIndexLeftPosition; + private final int inputStoreIndexRightPosition; + private final int outputStoreIndexLeftPosition; + private final int outputStoreIndexRightPosition; + private final IndexedSet leftTupleSet; + private final IndexedSet> rightTupleSet; - protected AbstractUnindexedJoinNode(int inputStoreIndexLeftEntry, int inputStoreIndexLeftOutTupleList, - int inputStoreIndexRightEntry, int inputStoreIndexRightOutTupleList, - TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering, int outputStoreIndexLeftOutEntry, - int outputStoreIndexRightOutEntry) { - super(inputStoreIndexLeftOutTupleList, inputStoreIndexRightOutTupleList, nextNodesTupleLifecycle, isFiltering, - outputStoreIndexLeftOutEntry, outputStoreIndexRightOutEntry); - this.inputStoreIndexLeftEntry = inputStoreIndexLeftEntry; - this.inputStoreIndexRightEntry = inputStoreIndexRightEntry; + protected AbstractUnindexedJoinNode(TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering) { + super(leftTupleStorePositionTracker, rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, + isFiltering); + this.inputStoreIndexLeftPosition = leftTupleStorePositionTracker.reserveNextAvailablePosition(); + this.inputStoreIndexRightPosition = rightTupleStorePositionTracker.reserveNextAvailablePosition(); + this.outputStoreIndexLeftPosition = outputStoreSizeTracker.reserveNextAvailablePosition(); + this.outputStoreIndexRightPosition = outputStoreSizeTracker.reserveNextAvailablePosition(); + this.leftTupleSet = new IndexedSet<>(new TuplePositionTracker<>(inputStoreIndexLeftPosition)); + this.rightTupleSet = new IndexedSet<>(new TuplePositionTracker<>(inputStoreIndexRightPosition)); } @Override public final void insertLeft(LeftTuple_ leftTuple) { - if (leftTuple.getStore(inputStoreIndexLeftEntry) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + leftTuple - + ") was already added in the tupleStore."); - } - var leftEntry = leftTupleList.add(leftTuple); - leftTuple.setStore(inputStoreIndexLeftEntry, leftEntry); - var outTupleListLeft = new ElementAwareList(); - leftTuple.setStore(inputStoreIndexLeftOutTupleList, outTupleListLeft); - for (var tuple : rightTupleList) { - insertOutTupleFiltered(leftTuple, tuple); + if (leftTuple.getStore(inputStoreIndexLeftPosition) != null) { + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(leftTuple)); } + leftTupleSet.add(leftTuple); + var outTupleSetLeft = new IndexedSet<>(new TuplePositionTracker<>(outputStoreIndexLeftPosition)); + leftTuple.setStore(inputStoreIndexLeftOutTupleSet, outTupleSetLeft); + rightTupleSet.forEach(tuple -> insertOutTupleFiltered(leftTuple, tuple)); } @Override public final void updateLeft(LeftTuple_ leftTuple) { - ElementAwareListEntry leftEntry = leftTuple.getStore(inputStoreIndexLeftEntry); - if (leftEntry == null) { + if (leftTuple.getStore(inputStoreIndexLeftPosition) == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) insertLeft(leftTuple); return; } - innerUpdateLeft(leftTuple, rightTupleList::forEach); + innerUpdateLeft(leftTuple, rightTupleSet::forEach); } @Override public final void retractLeft(LeftTuple_ leftTuple) { - ElementAwareListEntry leftEntry = leftTuple.removeStore(inputStoreIndexLeftEntry); - if (leftEntry == null) { + if (leftTuple.getStore(inputStoreIndexLeftPosition) == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - ElementAwareList outTupleListLeft = leftTuple.removeStore(inputStoreIndexLeftOutTupleList); - leftEntry.remove(); - outTupleListLeft.forEach(this::retractOutTuple); + IndexedSet outTupleSetLeft = leftTuple.removeStore(inputStoreIndexLeftOutTupleSet); + leftTupleSet.remove(leftTuple); + outTupleSetLeft.forEach(this::retractOutTuple); } @Override public final void insertRight(UniTuple rightTuple) { - if (rightTuple.getStore(inputStoreIndexRightEntry) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + rightTuple - + ") was already added in the tupleStore."); - } - var rightEntry = rightTupleList.add(rightTuple); - rightTuple.setStore(inputStoreIndexRightEntry, rightEntry); - var outTupleListRight = new ElementAwareList(); - rightTuple.setStore(inputStoreIndexRightOutTupleList, outTupleListRight); - for (var tuple : leftTupleList) { - insertOutTupleFiltered(tuple, rightTuple); + if (rightTuple.getStore(inputStoreIndexRightPosition) != null) { + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(rightTuple)); } + rightTupleSet.add(rightTuple); + var outTupleSetRight = new IndexedSet(new TuplePositionTracker<>(outputStoreIndexRightPosition)); + rightTuple.setStore(inputStoreIndexRightOutTupleSet, outTupleSetRight); + leftTupleSet.forEach(tuple -> insertOutTupleFiltered(tuple, rightTuple)); } @Override public final void updateRight(UniTuple rightTuple) { - ElementAwareListEntry> rightEntry = rightTuple.getStore(inputStoreIndexRightEntry); - if (rightEntry == null) { + if (rightTuple.getStore(inputStoreIndexRightPosition) == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) insertRight(rightTuple); return; } - innerUpdateRight(rightTuple, leftTupleList::forEach); + innerUpdateRight(rightTuple, leftTupleSet::forEach); } @Override public final void retractRight(UniTuple rightTuple) { - ElementAwareListEntry> rightEntry = rightTuple.removeStore(inputStoreIndexRightEntry); - if (rightEntry == null) { + if (rightTuple.getStore(inputStoreIndexRightPosition) == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - ElementAwareList outTupleListRight = rightTuple.removeStore(inputStoreIndexRightOutTupleList); - rightEntry.remove(); - outTupleListRight.forEach(this::retractOutTuple); + IndexedSet outTupleSetRight = rightTuple.removeStore(inputStoreIndexRightOutTupleSet); + rightTupleSet.remove(rightTuple); + outTupleSetRight.forEach(this::retractOutTuple); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandle.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandle.java new file mode 100644 index 0000000000..f6e40c734c --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandle.java @@ -0,0 +1,36 @@ +package ai.timefold.solver.core.impl.bavet.common; + +import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; +import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; + +/** + * Used for filtering in {@link AbstractIfExistsNode}. + * There is no place where both left and right sets for each counter would be kept together, + * therefore we create this handle to avoid expensive iteration. + * (The alternative would be to look up things on the left when we have the right, and vice versa.) + * + * @param + */ +final class ExistsCounterHandle { + + final ExistsCounter counter; + private final IndexedSet> leftSet; + private final IndexedSet> rightSet; + int leftPosition = -1; + int rightPosition = -1; + + ExistsCounterHandle(ExistsCounter counter, IndexedSet> leftSet, + IndexedSet> rightSet) { + this.counter = counter; + this.leftSet = leftSet; + leftSet.add(this); + this.rightSet = rightSet; + rightSet.add(this); + } + + public void remove() { + leftSet.remove(this); + rightSet.remove(this); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandlePositionTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandlePositionTracker.java new file mode 100644 index 0000000000..d204fed047 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandlePositionTracker.java @@ -0,0 +1,88 @@ +package ai.timefold.solver.core.impl.bavet.common; + +import ai.timefold.solver.core.impl.bavet.common.index.ElementPositionTracker; +import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; + +import org.jspecify.annotations.NullMarked; + +@SuppressWarnings({ "rawtypes", "unchecked" }) +@NullMarked +record ExistsCounterHandlePositionTracker(PositionGetter positionGetter, + PositionClearer positionClearer, + PositionSetter positionSetter) + implements + ElementPositionTracker> { + + private static final ExistsCounterHandlePositionTracker LEFT = new ExistsCounterHandlePositionTracker( + tracker -> tracker.leftPosition, + tracker -> { + var result = tracker.leftPosition; + tracker.leftPosition = -1; + return result; + }, + (tracker, position) -> { + var oldValue = tracker.leftPosition; + tracker.leftPosition = position; + return oldValue; + }); + private static final ExistsCounterHandlePositionTracker RIGHT = new ExistsCounterHandlePositionTracker( + tracker -> tracker.rightPosition, + tracker -> { + var result = tracker.rightPosition; + tracker.rightPosition = -1; + return result; + }, + (tracker, position) -> { + var oldValue = tracker.rightPosition; + tracker.rightPosition = position; + return oldValue; + }); + + public static ExistsCounterHandlePositionTracker left() { + return LEFT; + } + + public static ExistsCounterHandlePositionTracker right() { + return RIGHT; + } + + @Override + public int getPosition(ExistsCounterHandle element) { + return positionGetter.apply(element); + } + + @Override + public int setPosition(ExistsCounterHandle element, int position) { + return positionSetter.apply(element, position); + } + + @Override + public int clearPosition(ExistsCounterHandle element) { + return positionClearer.apply(element); + } + + @FunctionalInterface + @NullMarked + interface PositionGetter { + + int apply(ExistsCounterHandle tracker); + + } + + @FunctionalInterface + @NullMarked + interface PositionClearer { + + int apply(ExistsCounterHandle tracker); + + } + + @FunctionalInterface + @NullMarked + interface PositionSetter { + + int apply(ExistsCounterHandle tracker, int position); + + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterPositionTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterPositionTracker.java new file mode 100644 index 0000000000..0d52a39c73 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterPositionTracker.java @@ -0,0 +1,34 @@ +package ai.timefold.solver.core.impl.bavet.common; + +import ai.timefold.solver.core.impl.bavet.common.index.ElementPositionTracker; +import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; + +record ExistsCounterPositionTracker(int inputStorePosition) + implements + ElementPositionTracker> { + + @Override + public int getPosition(ExistsCounter element) { + var tuple = element.getTuple(); + var value = tuple.getStore(inputStorePosition); + return value == null ? -1 : (int) value; + } + + @Override + public int setPosition(ExistsCounter element, int position) { + var tuple = element.getTuple(); + var oldValue = getPosition(element); + tuple.setStore(inputStorePosition, position); + return oldValue; + } + + @Override + public int clearPosition(ExistsCounter element) { + try { + return element.getTuple().removeStore(inputStorePosition); + } catch (NullPointerException e) { + return -1; + } + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TuplePositionTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TuplePositionTracker.java new file mode 100644 index 0000000000..a6e801db41 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TuplePositionTracker.java @@ -0,0 +1,32 @@ +package ai.timefold.solver.core.impl.bavet.common; + +import ai.timefold.solver.core.impl.bavet.common.index.ElementPositionTracker; +import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; + +public record TuplePositionTracker(int inputStorePosition) + implements + ElementPositionTracker { + + @Override + public int getPosition(Tuple_ element) { + var value = element.getStore(inputStorePosition); + return value == null ? -1 : (int) value; + } + + @Override + public int setPosition(Tuple_ element, int position) { + var oldValue = getPosition(element); + element.setStore(inputStorePosition, position); + return oldValue; + } + + @Override + public int clearPosition(Tuple_ element) { + try { + return element.removeStore(inputStorePosition); + } catch (NullPointerException e) { + return -1; + } + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ComparisonIndexer.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ComparisonIndexer.java index 09c5102468..a783a10d31 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ComparisonIndexer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ComparisonIndexer.java @@ -9,7 +9,6 @@ import java.util.function.Supplier; import ai.timefold.solver.core.impl.bavet.common.joiner.JoinerType; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; final class ComparisonIndexer> implements Indexer { @@ -26,8 +25,8 @@ final class ComparisonIndexer> * * @param comparisonJoinerType the type of comparison to use */ - public ComparisonIndexer(JoinerType comparisonJoinerType) { - this(comparisonJoinerType, new SingleKeyRetriever<>(), NoneIndexer::new); + public ComparisonIndexer(JoinerType comparisonJoinerType, ElementPositionTracker elementPositionTracker) { + this(comparisonJoinerType, new SingleKeyRetriever<>(), () -> new NoneIndexer<>(elementPositionTracker)); } /** @@ -61,7 +60,7 @@ private ComparisonIndexer(JoinerType comparisonJoinerType, KeyRetriever ke } @Override - public ElementAwareListEntry put(Object indexKeys, T tuple) { + public void put(Object indexKeys, T element) { Key_ indexKey = keyRetriever.apply(indexKeys); // Avoids computeIfAbsent in order to not create lambdas on the hot path. var downstreamIndexer = comparisonMap.get(indexKey); @@ -69,25 +68,25 @@ public ElementAwareListEntry put(Object indexKeys, T tuple) { downstreamIndexer = downstreamIndexerSupplier.get(); comparisonMap.put(indexKey, downstreamIndexer); } - return downstreamIndexer.put(indexKeys, tuple); + downstreamIndexer.put(indexKeys, element); } @Override - public void remove(Object indexKeys, ElementAwareListEntry entry) { + public void remove(Object indexKeys, T element) { Key_ indexKey = keyRetriever.apply(indexKeys); - var downstreamIndexer = getDownstreamIndexer(indexKeys, indexKey, entry); - downstreamIndexer.remove(indexKeys, entry); + var downstreamIndexer = getDownstreamIndexer(indexKeys, indexKey, element); + downstreamIndexer.remove(indexKeys, element); if (downstreamIndexer.isEmpty()) { comparisonMap.remove(indexKey); } } - private Indexer getDownstreamIndexer(Object indexKeys, Key_ indexerKey, ElementAwareListEntry entry) { + private Indexer getDownstreamIndexer(Object indexKeys, Key_ indexerKey, T entry) { var downstreamIndexer = comparisonMap.get(indexerKey); if (downstreamIndexer == null) { throw new IllegalStateException( "Impossible state: the tuple (%s) with indexKeys (%s) doesn't exist in the indexer %s." - .formatted(entry.getElement(), indexKeys, this)); + .formatted(entry, indexKeys, this)); } return downstreamIndexer; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ElementPositionTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ElementPositionTracker.java new file mode 100644 index 0000000000..f0c50f848b --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ElementPositionTracker.java @@ -0,0 +1,36 @@ +package ai.timefold.solver.core.impl.bavet.common.index; + +/** + * Allows to read and modify the position of an element in an {@link IndexedSet}. + * Typically points to a field in the element itself. + * + * @param + */ +public interface ElementPositionTracker { + + /** + * Gets the position of the given element. + * + * @param element never null + * @return >= 0 if the element is tracked, or -1 if it is not tracked + */ + int getPosition(T element); + + /** + * Sets the position of the given element. + * + * @param element never null + * @param position >= 0 + * @return the previous position of the element, or -1 if it was not tracked before + */ + int setPosition(T element, int position); + + /** + * Clears the position of the given element. + * + * @param element never null + * @return the previous position of the element, or -1 if it was not tracked before + */ + int clearPosition(T element); + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexer.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexer.java index f81d90c82a..365b4faf95 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexer.java @@ -6,8 +6,6 @@ import java.util.function.Consumer; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; - final class EqualsIndexer implements Indexer { private final KeyRetriever keyRetriever; @@ -18,9 +16,9 @@ final class EqualsIndexer implements Indexer { * Construct an {@link EqualsIndexer} which immediately ends in a {@link NoneIndexer}. * This means {@code indexKeys} must be a single key. */ - public EqualsIndexer() { + public EqualsIndexer(ElementPositionTracker elementPositionTracker) { this.keyRetriever = new SingleKeyRetriever<>(); - this.downstreamIndexerSupplier = NoneIndexer::new; + this.downstreamIndexerSupplier = () -> new NoneIndexer<>(elementPositionTracker); } /** @@ -36,7 +34,7 @@ public EqualsIndexer(int keyIndex, Supplier> downstreamIndexerSupplie } @Override - public ElementAwareListEntry put(Object indexKeys, T tuple) { + public void put(Object indexKeys, T element) { Key_ indexKey = keyRetriever.apply(indexKeys); // Avoids computeIfAbsent in order to not create lambdas on the hot path. Indexer downstreamIndexer = downstreamIndexerMap.get(indexKey); @@ -44,25 +42,25 @@ public ElementAwareListEntry put(Object indexKeys, T tuple) { downstreamIndexer = downstreamIndexerSupplier.get(); downstreamIndexerMap.put(indexKey, downstreamIndexer); } - return downstreamIndexer.put(indexKeys, tuple); + downstreamIndexer.put(indexKeys, element); } @Override - public void remove(Object indexKeys, ElementAwareListEntry entry) { + public void remove(Object indexKeys, T element) { Key_ indexKey = keyRetriever.apply(indexKeys); - Indexer downstreamIndexer = getDownstreamIndexer(indexKeys, indexKey, entry); - downstreamIndexer.remove(indexKeys, entry); + Indexer downstreamIndexer = getDownstreamIndexer(indexKeys, indexKey, element); + downstreamIndexer.remove(indexKeys, element); if (downstreamIndexer.isEmpty()) { downstreamIndexerMap.remove(indexKey); } } - private Indexer getDownstreamIndexer(Object indexKeys, Key_ indexerKey, ElementAwareListEntry entry) { + private Indexer getDownstreamIndexer(Object indexKeys, Key_ indexerKey, T entry) { Indexer downstreamIndexer = downstreamIndexerMap.get(indexerKey); if (downstreamIndexer == null) { throw new IllegalStateException( "Impossible state: the tuple (%s) with indexKey (%s) doesn't exist in the indexer %s." - .formatted(entry.getElement(), indexKeys, this)); + .formatted(entry, indexKeys, this)); } return downstreamIndexer; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java new file mode 100644 index 0000000000..e2bab41547 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java @@ -0,0 +1,176 @@ +package ai.timefold.solver.core.impl.bavet.common.index; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +import ai.timefold.solver.core.impl.util.ElementAwareList; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * {@link ArrayList}-backed set which allows to {@link #remove(Object)} an element without knowing its position + * and without an expensive lookup. + *

+ * It uses an {@link ElementPositionTracker} to track the insertion position of each element. + * When an element is removed, the insertion position of later elements is not changed. + * Instead, when the next element is removed, the search starts from its last known insertion position, + * iterating backwards until the element is found. + * We also keep a counter of deleted elements to avoid excessive iteration; + * we can guarantee that the current position of an element will not be further away + * than the number of earlier deletions. + *

+ * Together with the fact that removals are relatively rare, + * this keeps the average removal cost low while giving us all benefits of {@link ArrayList}, + * such as memory efficiency, random access, and fast iteration. + * Random access is not required for Constraint Streams, but Neighborhoods make heavy use of it; + * if we used the {@link ElementAwareList} implementation instead, + * we would have to copy the elements to an array every time we need to access them randomly during move generation. + * + * + * @param + */ +@NullMarked +public final class IndexedSet { + + private final ElementPositionTracker elementPositionTracker; + private @Nullable ArrayList elementList; // Lazily initialized, so that empty indexes use no memory. + private int removalCount = 0; + + public IndexedSet(ElementPositionTracker elementPositionTracker) { + this.elementPositionTracker = Objects.requireNonNull(elementPositionTracker); + } + + private List getElementList() { + if (elementList == null) { + elementList = new ArrayList<>(); + } + return elementList; + } + + /** + * Appends the specified element to the end of this collection, if not already present. + * Will use identity comparison to check for presence; + * two different instances which {@link Object#equals(Object) equal} are considered different elements. + * + * @param element element to be appended to this collection + * @throws IllegalStateException if the element was already present in this collection + */ + public void add(T element) { + var actualElementList = getElementList(); + actualElementList.add(element); + if (elementPositionTracker.setPosition(element, actualElementList.size() - 1) >= 0) { + throw new IllegalStateException("Impossible state: the element (%s) was already added to the IndexedSet." + .formatted(element)); + } + } + + /** + * Removes the first occurrence of the specified element from this collection, if it is present. + * Will use identity comparison to check for presence; + * two different instances which {@link Object#equals(Object) equal} are considered different elements. + * + * @param element element to be removed from this collection + * @throws IllegalStateException if the element was not found in this collection + */ + public void remove(T element) { + if (!innerRemove(element)) { + throw new IllegalStateException("Impossible state: the element (%s) was not found in the IndexedSet." + .formatted(element)); + } + } + + private boolean innerRemove(T element) { + if (isEmpty()) { + return false; + } + var insertionPosition = elementPositionTracker.clearPosition(element); + if (insertionPosition < 0) { + return false; + } + var actualElementList = getElementList(); + var upperBound = Math.min(insertionPosition, actualElementList.size() - 1); + var lowerBound = Math.max(0, insertionPosition - removalCount); + var actualPosition = findElement(actualElementList, element, lowerBound, upperBound); + if (actualPosition < 0) { + return false; + } + actualElementList.remove(actualPosition); + if (isEmpty()) { + removalCount = 0; + } else if (actualElementList.size() > actualPosition) { + // We only mark removals that actually affect later elements. + // Removing the last element does not affect any other element. + removalCount++; + } + return true; + } + + /** + * Search for the element in the given range. + * + * @param actualElementList the list to search in + * @param element the element to search for + * @param startIndex start of the range we are currently considering (inclusive) + * @param endIndex end of the range we are currently considering (inclusive) + * @return the index of the element if found, -1 otherwise + */ + private static int findElement(List actualElementList, T element, int startIndex, int endIndex) { + for (var i = endIndex; i >= startIndex; i--) { + // Iterating backwards as the element is more likely to be closer to the end of the range, + // which is where it was originally inserted. + var maybeElement = actualElementList.get(i); + if (maybeElement == element) { + return i; + } + } + return -1; + } + + public int size() { + return elementList == null ? 0 : elementList.size(); + } + + /** + * Performs the given action for each element of the collection + * until all elements have been processed. + * + * @param tupleConsumer the action to be performed for each element + */ + public void forEach(Consumer tupleConsumer) { + if (elementList == null) { + return; + } + var i = 0; + while (i < elementList.size()) { + var oldRemovalCount = removalCount; // The consumer may remove some elements, shifting others forward. + tupleConsumer.accept(elementList.get(i)); + var elementDrift = removalCount - oldRemovalCount; + // Move to the next element, adjusting for any shifts due to removals. + // If no elements were removed by the consumer, we simply move to the next index. + i -= elementDrift - 1; + } + } + + public boolean isEmpty() { + return elementList == null || elementList.isEmpty(); + } + + /** + * Returns a standard {@link List} view of this collection. + * Users must not modify the returned list, as that would also change the underlying data structure. + * + * @return a standard list view of this element-aware list + */ + public List asList() { + return elementList == null ? Collections.emptyList() : elementList; + } + + public String toString() { + return elementList == null ? "[]" : elementList.toString(); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java index add7191cf1..b1375e5d78 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java @@ -3,7 +3,6 @@ import java.util.function.Consumer; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; /** * An indexer for entity or fact {@code X}, @@ -22,9 +21,9 @@ */ public sealed interface Indexer permits ComparisonIndexer, EqualsIndexer, NoneIndexer { - ElementAwareListEntry put(Object indexKeys, T tuple); + void put(Object indexKeys, T element); - void remove(Object indexKeys, ElementAwareListEntry entry); + void remove(Object indexKeys, T element); int size(Object indexKeys); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java index ce4eec540e..09f53fb401 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java @@ -473,34 +473,32 @@ public UniKeysExtractor buildRightKeysExtractor() { return buildUniKeysExtractor(joiner::getRightMapping); } - public Indexer buildIndexer(boolean isLeftBridge) { - /* - * Note that if creating indexer for a right bridge node, the joiner type has to be flipped. - * ( becomes .) - */ + public Indexer buildIndexer(boolean isLeftBridge, ElementPositionTracker elementPositionTracker) { + // Note that if creating indexer for a right bridge node, the joiner type has to be flipped. + // ( becomes .) if (!hasJoiners()) { // NoneJoiner results in NoneIndexer. - return new NoneIndexer<>(); + return new NoneIndexer<>(elementPositionTracker); } else if (joiner.getJoinerCount() == 1) { // Single joiner maps directly to EqualsIndexer or ComparisonIndexer. var joinerType = joiner.getJoinerType(0); if (joinerType == JoinerType.EQUAL) { - return new EqualsIndexer<>(); + return new EqualsIndexer<>(elementPositionTracker); } else { - return new ComparisonIndexer<>(isLeftBridge ? joinerType : joinerType.flip()); + return new ComparisonIndexer<>(isLeftBridge ? joinerType : joinerType.flip(), elementPositionTracker); } } // The following code builds the children first, so it needs to iterate over the joiners in reverse order. var descendingJoinerTypeMap = joinerTypeMap.descendingMap(); - Supplier> noneIndexerSupplier = NoneIndexer::new; + Supplier> noneIndexerSupplier = () -> new NoneIndexer<>(elementPositionTracker); Supplier> downstreamIndexerSupplier = noneIndexerSupplier; var indexPropertyId = descendingJoinerTypeMap.size() - 1; for (var entry : descendingJoinerTypeMap.entrySet()) { var joinerType = entry.getValue(); if (downstreamIndexerSupplier == noneIndexerSupplier && indexPropertyId == 0) { if (joinerType == JoinerType.EQUAL) { - downstreamIndexerSupplier = EqualsIndexer::new; + downstreamIndexerSupplier = () -> new EqualsIndexer<>(elementPositionTracker); } else { var actualJoinerType = isLeftBridge ? joinerType : joinerType.flip(); - downstreamIndexerSupplier = () -> new ComparisonIndexer<>(actualJoinerType); + downstreamIndexerSupplier = () -> new ComparisonIndexer<>(actualJoinerType, elementPositionTracker); } } else { var actualDownstreamIndexerSupplier = downstreamIndexerSupplier; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexer.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexer.java index 88d060df76..277c2095ed 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexer.java @@ -2,41 +2,42 @@ import java.util.function.Consumer; -import ai.timefold.solver.core.impl.util.ElementAwareList; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; - public final class NoneIndexer implements Indexer { - private final ElementAwareList tupleList = new ElementAwareList<>(); + private final IndexedSet store; + + public NoneIndexer(ElementPositionTracker elementPositionTracker) { + this.store = new IndexedSet<>(elementPositionTracker); + } @Override - public ElementAwareListEntry put(Object indexKeys, T tuple) { - return tupleList.add(tuple); + public void put(Object indexKeys, T element) { + store.add(element); } @Override - public void remove(Object indexKeys, ElementAwareListEntry entry) { - entry.remove(); + public void remove(Object indexKeys, T element) { + store.remove(element); } @Override public int size(Object indexKeys) { - return tupleList.size(); + return store.size(); } @Override public void forEach(Object indexKeys, Consumer tupleConsumer) { - tupleList.forEach(tupleConsumer); + store.forEach(tupleConsumer); } @Override public boolean isEmpty() { - return tupleList.size() == 0; + return store.isEmpty(); } @Override public String toString() { - return "size = " + tupleList.size(); + return "size = " + store.size(); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/OutputStoreSizeTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/OutputStoreSizeTracker.java new file mode 100644 index 0000000000..b9e9a3e66d --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/OutputStoreSizeTracker.java @@ -0,0 +1,46 @@ +package ai.timefold.solver.core.impl.bavet.common.tuple; + +/** + * Tracks the size of the output store by allowing reservations of positions. + * Once the final size is computed, no further reservations can be made + * and output tuples will be created using this size of their {@link AbstractTuple tuple store}. + */ +public final class OutputStoreSizeTracker implements TupleStorePositionTracker { + + private int effectiveOutputStoreSize; + private int finalOutputStoreSize = -1; + + public OutputStoreSizeTracker(int initialSize) { + if (initialSize < 0) { + throw new IllegalArgumentException( + "Impossible state: The initialSize (%d) must be non-negative.".formatted(initialSize)); + } + this.effectiveOutputStoreSize = initialSize; + } + + /** + * @return the next available position in the output store, reserved exclusively for use by the caller + * @throws IllegalStateException if {@link #computeOutputStoreSize()} has already been called. + */ + @Override + public int reserveNextAvailablePosition() { + if (finalOutputStoreSize >= 0) { + throw new IllegalStateException("Impossible state: The finalOutputStoreSize (%s) has already been computed." + .formatted(finalOutputStoreSize)); + } + return effectiveOutputStoreSize++; + } + + /** + * Finalizes the output store size and prevents further reservations. + * + * @return the final output store size + */ + public int computeOutputStoreSize() { + if (finalOutputStoreSize < 0) { + finalOutputStoreSize = effectiveOutputStoreSize; + } + return finalOutputStoreSize; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/TupleStorePositionTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/TupleStorePositionTracker.java new file mode 100644 index 0000000000..87a34cf448 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/TupleStorePositionTracker.java @@ -0,0 +1,7 @@ +package ai.timefold.solver.core.impl.bavet.common.tuple; + +public interface TupleStorePositionTracker { + + int reserveNextAvailablePosition(); + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedIfExistsQuadNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedIfExistsQuadNode.java index 3503e7fd0d..7f282d844d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedIfExistsQuadNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedIfExistsQuadNode.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.QuadTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class IndexedIfExistsQuadNode extends AbstractIndexedIfExistsNode, E> { @@ -12,23 +13,10 @@ public final class IndexedIfExistsQuadNode extends AbstractIndexe private final PentaPredicate filtering; public IndexedIfExistsQuadNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, -1, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public IndexedIfExistsQuadNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, PentaPredicate filtering) { - super(shouldExist, indexerFactory.buildQuadLeftKeysExtractor(), indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, filtering != null); + super(shouldExist, indexerFactory.buildQuadLeftKeysExtractor(), indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedJoinQuadNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedJoinQuadNode.java index adc92c74d5..2e583a0657 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedJoinQuadNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedJoinQuadNode.java @@ -3,35 +3,30 @@ import ai.timefold.solver.core.api.function.QuadPredicate; import ai.timefold.solver.core.impl.bavet.common.AbstractIndexedJoinNode; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.QuadTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class IndexedJoinQuadNode extends AbstractIndexedJoinNode, D, QuadTuple> { private final QuadPredicate filtering; - private final int outputStoreSize; - - public IndexedJoinQuadNode(IndexerFactory indexerFactory, - int inputStoreIndexABC, int inputStoreIndexEntryABC, int inputStoreIndexOutTupleListABC, - int inputStoreIndexD, int inputStoreIndexEntryD, int inputStoreIndexOutTupleListD, - TupleLifecycle> nextNodesTupleLifecycle, QuadPredicate filtering, - int outputStoreSize, int outputStoreIndexOutEntryABC, int outputStoreIndexOutEntryD) { - super(indexerFactory.buildTriLeftKeysExtractor(), indexerFactory, - inputStoreIndexABC, inputStoreIndexEntryABC, inputStoreIndexOutTupleListABC, - inputStoreIndexD, inputStoreIndexEntryD, inputStoreIndexOutTupleListD, - nextNodesTupleLifecycle, filtering != null, - outputStoreIndexOutEntryABC, outputStoreIndexOutEntryD); + + public IndexedJoinQuadNode(IndexerFactory indexerFactory, TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle> nextNodesTupleLifecycle, QuadPredicate filtering) { + super(indexerFactory.buildTriLeftKeysExtractor(), indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; - this.outputStoreSize = outputStoreSize; } @Override protected QuadTuple createOutTuple(TriTuple leftTuple, UniTuple rightTuple) { return new QuadTuple<>(leftTuple.factA, leftTuple.factB, leftTuple.factC, rightTuple.factA, - outputStoreSize); + outputStoreSizeTracker.computeOutputStoreSize()); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedIfExistsQuadNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedIfExistsQuadNode.java index 88de613585..4de6ab2ec3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedIfExistsQuadNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedIfExistsQuadNode.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractUnindexedIfExistsNode; import ai.timefold.solver.core.impl.bavet.common.tuple.QuadTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class UnindexedIfExistsQuadNode extends AbstractUnindexedIfExistsNode, E> { @@ -11,21 +12,11 @@ public final class UnindexedIfExistsQuadNode extends AbstractUnin private final PentaPredicate filtering; public UnindexedIfExistsQuadNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, - inputStoreIndexLeftCounterEntry, -1, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public UnindexedIfExistsQuadNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, int inputStoreIndexRightEntry, - int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, PentaPredicate filtering) { super(shouldExist, - inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, inputStoreIndexRightEntry, - inputStoreIndexRightTrackerList, + leftTupleStorePositionTracker, rightTupleStorePositionTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedJoinQuadNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedJoinQuadNode.java index 96111c2bc3..8aeb285ced 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedJoinQuadNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedJoinQuadNode.java @@ -2,35 +2,30 @@ import ai.timefold.solver.core.api.function.QuadPredicate; import ai.timefold.solver.core.impl.bavet.common.AbstractUnindexedJoinNode; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.QuadTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class UnindexedJoinQuadNode extends AbstractUnindexedJoinNode, D, QuadTuple> { private final QuadPredicate filtering; - private final int outputStoreSize; - - public UnindexedJoinQuadNode( - int inputStoreIndexLeftEntry, int inputStoreIndexLeftOutTupleList, - int inputStoreIndexRightEntry, int inputStoreIndexRightOutTupleList, - TupleLifecycle> nextNodesTupleLifecycle, QuadPredicate filtering, - int outputStoreSize, - int outputStoreIndexLeftOutEntry, int outputStoreIndexRightOutEntry) { - super(inputStoreIndexLeftEntry, inputStoreIndexLeftOutTupleList, - inputStoreIndexRightEntry, inputStoreIndexRightOutTupleList, - nextNodesTupleLifecycle, filtering != null, - outputStoreIndexLeftOutEntry, outputStoreIndexRightOutEntry); + + public UnindexedJoinQuadNode(TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle> nextNodesTupleLifecycle, QuadPredicate filtering) { + super(leftTupleStorePositionTracker, rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, + filtering != null); this.filtering = filtering; - this.outputStoreSize = outputStoreSize; } @Override protected QuadTuple createOutTuple(TriTuple leftTuple, UniTuple rightTuple) { return new QuadTuple<>(leftTuple.factA, leftTuple.factB, leftTuple.factC, rightTuple.factA, - outputStoreSize); + outputStoreSizeTracker.computeOutputStoreSize()); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedIfExistsTriNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedIfExistsTriNode.java index e061769494..65e942d9a5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedIfExistsTriNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedIfExistsTriNode.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class IndexedIfExistsTriNode extends AbstractIndexedIfExistsNode, D> { @@ -12,23 +13,10 @@ public final class IndexedIfExistsTriNode extends AbstractIndexedIfE private final QuadPredicate filtering; public IndexedIfExistsTriNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, -1, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public IndexedIfExistsTriNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, QuadPredicate filtering) { - super(shouldExist, indexerFactory.buildTriLeftKeysExtractor(), indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, filtering != null); + super(shouldExist, indexerFactory.buildTriLeftKeysExtractor(), indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedJoinTriNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedJoinTriNode.java index 3751f3bd4f..d14b25f8a0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedJoinTriNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedJoinTriNode.java @@ -4,33 +4,29 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractIndexedJoinNode; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class IndexedJoinTriNode extends AbstractIndexedJoinNode, C, TriTuple> { private final TriPredicate filtering; - private final int outputStoreSize; - - public IndexedJoinTriNode(IndexerFactory indexerFactory, - int inputStoreIndexAB, int inputStoreIndexEntryAB, int inputStoreIndexOutTupleListAB, - int inputStoreIndexC, int inputStoreIndexEntryC, int inputStoreIndexOutTupleListC, - TupleLifecycle> nextNodesTupleLifecycle, TriPredicate filtering, - int outputStoreSize, int outputStoreIndexOutEntryAB, int outputStoreIndexOutEntryC) { - super(indexerFactory.buildBiLeftKeysExtractor(), indexerFactory, - inputStoreIndexAB, inputStoreIndexEntryAB, inputStoreIndexOutTupleListAB, - inputStoreIndexC, inputStoreIndexEntryC, inputStoreIndexOutTupleListC, - nextNodesTupleLifecycle, filtering != null, - outputStoreIndexOutEntryAB, outputStoreIndexOutEntryC); + + public IndexedJoinTriNode(IndexerFactory indexerFactory, TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle> nextNodesTupleLifecycle, TriPredicate filtering) { + super(indexerFactory.buildBiLeftKeysExtractor(), indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; - this.outputStoreSize = outputStoreSize; } @Override protected TriTuple createOutTuple(BiTuple leftTuple, UniTuple rightTuple) { - return new TriTuple<>(leftTuple.factA, leftTuple.factB, rightTuple.factA, outputStoreSize); + return new TriTuple<>(leftTuple.factA, leftTuple.factB, rightTuple.factA, + outputStoreSizeTracker.computeOutputStoreSize()); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedIfExistsTriNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedIfExistsTriNode.java index b83d7623f1..99d2607339 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedIfExistsTriNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedIfExistsTriNode.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractUnindexedIfExistsNode; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class UnindexedIfExistsTriNode extends AbstractUnindexedIfExistsNode, D> { @@ -11,22 +12,11 @@ public final class UnindexedIfExistsTriNode extends AbstractUnindexe private final QuadPredicate filtering; public UnindexedIfExistsTriNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, - inputStoreIndexLeftCounterEntry, -1, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public UnindexedIfExistsTriNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, int inputStoreIndexRightEntry, - int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, QuadPredicate filtering) { - super(shouldExist, - inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, inputStoreIndexRightEntry, - inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, filtering != null); + super(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, nextNodesTupleLifecycle, + filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedJoinTriNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedJoinTriNode.java index 0755e70dc5..dfcbc68b95 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedJoinTriNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedJoinTriNode.java @@ -3,33 +3,29 @@ import ai.timefold.solver.core.api.function.TriPredicate; import ai.timefold.solver.core.impl.bavet.common.AbstractUnindexedJoinNode; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class UnindexedJoinTriNode extends AbstractUnindexedJoinNode, C, TriTuple> { private final TriPredicate filtering; - private final int outputStoreSize; - - public UnindexedJoinTriNode( - int inputStoreIndexLeftEntry, int inputStoreIndexLeftOutTupleList, - int inputStoreIndexRightEntry, int inputStoreIndexRightOutTupleList, - TupleLifecycle> nextNodesTupleLifecycle, TriPredicate filtering, - int outputStoreSize, - int outputStoreIndexLeftOutEntry, int outputStoreIndexRightOutEntry) { - super(inputStoreIndexLeftEntry, inputStoreIndexLeftOutTupleList, - inputStoreIndexRightEntry, inputStoreIndexRightOutTupleList, - nextNodesTupleLifecycle, filtering != null, - outputStoreIndexLeftOutEntry, outputStoreIndexRightOutEntry); + + public UnindexedJoinTriNode(TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle> nextNodesTupleLifecycle, TriPredicate filtering) { + super(leftTupleStorePositionTracker, rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, + filtering != null); this.filtering = filtering; - this.outputStoreSize = outputStoreSize; } @Override protected TriTuple createOutTuple(BiTuple leftTuple, UniTuple rightTuple) { - return new TriTuple<>(leftTuple.factA, leftTuple.factB, rightTuple.factA, outputStoreSize); + return new TriTuple<>(leftTuple.factA, leftTuple.factB, rightTuple.factA, + outputStoreSizeTracker.computeOutputStoreSize()); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/IndexedIfExistsUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/IndexedIfExistsUniNode.java index 0f7860615f..2b589deff3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/IndexedIfExistsUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/IndexedIfExistsUniNode.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractIndexedIfExistsNode; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class IndexedIfExistsUniNode extends AbstractIndexedIfExistsNode, B> { @@ -12,23 +13,10 @@ public final class IndexedIfExistsUniNode extends AbstractIndexedIfExistsN private final BiPredicate filtering; public IndexedIfExistsUniNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, -1, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public IndexedIfExistsUniNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, BiPredicate filtering) { - super(shouldExist, indexerFactory.buildUniLeftKeysExtractor(), indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, filtering != null); + super(shouldExist, indexerFactory.buildUniLeftKeysExtractor(), indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/UnindexedIfExistsUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/UnindexedIfExistsUniNode.java index 3d8709b282..04721aba36 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/UnindexedIfExistsUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/UnindexedIfExistsUniNode.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractUnindexedIfExistsNode; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class UnindexedIfExistsUniNode extends AbstractUnindexedIfExistsNode, B> { @@ -11,22 +12,11 @@ public final class UnindexedIfExistsUniNode extends AbstractUnindexedIfExi private final BiPredicate filtering; public UnindexedIfExistsUniNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, - inputStoreIndexLeftCounterEntry, -1, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public UnindexedIfExistsUniNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, int inputStoreIndexRightEntry, - int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, BiPredicate filtering) { - super(shouldExist, - inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, inputStoreIndexRightEntry, - inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, filtering != null); + super(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, nextNodesTupleLifecycle, + filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/bi/JoinBiEnumeratingStream.java b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/bi/JoinBiEnumeratingStream.java index e70b1afdc2..4a527ef433 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/bi/JoinBiEnumeratingStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/bi/JoinBiEnumeratingStream.java @@ -7,7 +7,9 @@ import ai.timefold.solver.core.impl.bavet.bi.UnindexedJoinBiNode; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.function.BiEnumeratingFilter; import ai.timefold.solver.core.impl.neighborhood.stream.enumerating.EnumeratingStreamFactory; import ai.timefold.solver.core.impl.neighborhood.stream.enumerating.common.AbstractEnumeratingStream; @@ -45,26 +47,18 @@ public void collectActiveEnumeratingStreams(Set buildHelper) { var solutionView = buildHelper.getSessionContext().solutionView(); var filteringDataJoiner = this.filtering == null ? null : this.filtering.toBiPredicate(solutionView); - var outputStoreSize = buildHelper.extractTupleStoreSize(this); TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); var indexerFactory = new IndexerFactory<>(joiner.toBiJoiner()); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()); + OutputStoreSizeTracker outputStoreSizeTracker = new OutputStoreSizeTracker(buildHelper.extractTupleStoreSize(this)); var node = indexerFactory.hasJoiners() - ? new IndexedJoinBiNode<>(indexerFactory, - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filteringDataJoiner, - outputStoreSize + 2, outputStoreSize, outputStoreSize + 1) - : new UnindexedJoinBiNode<>( - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filteringDataJoiner, - outputStoreSize + 2, outputStoreSize, outputStoreSize + 1); + ? new IndexedJoinBiNode<>(indexerFactory, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filteringDataJoiner) + : new UnindexedJoinBiNode<>(leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filteringDataJoiner); buildHelper.addNode(node, this, leftParent, rightParent); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/IfExistsUniEnumeratingStream.java b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/IfExistsUniEnumeratingStream.java index 0664f4523c..b0ab4f1b07 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/IfExistsUniEnumeratingStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/IfExistsUniEnumeratingStream.java @@ -6,6 +6,7 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractIfExistsNode; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.bavet.uni.IndexedIfExistsUniNode; import ai.timefold.solver.core.impl.bavet.uni.UnindexedIfExistsUniNode; @@ -63,35 +64,18 @@ private AbstractIfExistsNode, B> getNode(IndexerFactory indexerFa DataNodeBuildHelper buildHelper, TupleLifecycle> downstream) { var sessionContext = buildHelper.getSessionContext(); var isFiltering = filtering != null; + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()); if (indexerFactory.hasJoiners()) { - if (isFiltering) { - return new IndexedIfExistsUniNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - downstream, filtering.toBiPredicate(sessionContext.solutionView())); - } else { - return new IndexedIfExistsUniNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - downstream); - } - } else if (isFiltering) { - return new UnindexedIfExistsUniNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - downstream, filtering.toBiPredicate(sessionContext.solutionView())); + return new IndexedIfExistsUniNode<>(shouldExist, indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, downstream, + isFiltering ? filtering.toBiPredicate(sessionContext.solutionView()) : null); } else { - return new UnindexedIfExistsUniNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), downstream); + return new UnindexedIfExistsUniNode<>(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + downstream, + isFiltering ? filtering.toBiPredicate(sessionContext.solutionView()) : null); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetIfExistsBiConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetIfExistsBiConstraintStream.java index 1291db1aa7..7e0e3bcbd2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetIfExistsBiConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetIfExistsBiConstraintStream.java @@ -11,6 +11,7 @@ import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.tri.joiner.DefaultTriJoiner; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintFactory; import ai.timefold.solver.core.impl.score.stream.bavet.common.BavetIfExistsConstraintStream; @@ -66,30 +67,15 @@ public BavetAbstractConstraintStream getTupleSource() { public > void buildNode(ConstraintNodeBuildHelper buildHelper) { TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); IndexerFactory indexerFactory = new IndexerFactory<>(joiner); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()); var node = indexerFactory.hasJoiners() - ? (filtering == null ? new IndexedIfExistsBiNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), - downstream) - : new IndexedIfExistsBiNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), - downstream, filtering)) - : (filtering == null ? new UnindexedIfExistsBiNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), downstream) - : new UnindexedIfExistsBiNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), - downstream, filtering)); + ? new IndexedIfExistsBiNode<>(shouldExist, indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, downstream, filtering) + : new UnindexedIfExistsBiNode<>(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + downstream, filtering); buildHelper.addNode(node, this, this, parentBridgeC); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetJoinBiConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetJoinBiConstraintStream.java index 8df0b6a407..c04dfd736e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetJoinBiConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetJoinBiConstraintStream.java @@ -11,7 +11,9 @@ import ai.timefold.solver.core.impl.bavet.common.BavetAbstractConstraintStream; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintFactory; import ai.timefold.solver.core.impl.score.stream.bavet.common.BavetJoinConstraintStream; import ai.timefold.solver.core.impl.score.stream.bavet.common.ConstraintNodeBuildHelper; @@ -54,26 +56,18 @@ public void collectActiveConstraintStreams(Set> void buildNode(ConstraintNodeBuildHelper buildHelper) { - int outputStoreSize = buildHelper.extractTupleStoreSize(this); TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); IndexerFactory indexerFactory = new IndexerFactory<>(joiner); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()); + OutputStoreSizeTracker outputStoreSizeTracker = new OutputStoreSizeTracker(buildHelper.extractTupleStoreSize(this)); var node = indexerFactory.hasJoiners() - ? new IndexedJoinBiNode<>(indexerFactory, - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filtering, outputStoreSize + 2, - outputStoreSize, outputStoreSize + 1) - : new UnindexedJoinBiNode<>( - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filtering, outputStoreSize + 2, - outputStoreSize, outputStoreSize + 1); + ? new IndexedJoinBiNode<>(indexerFactory, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filtering) + : new UnindexedJoinBiNode<>(leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filtering); buildHelper.addNode(node, this, leftParent, rightParent); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetIfExistsQuadConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetIfExistsQuadConstraintStream.java index 0225a7f1d4..e06ce7500d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetIfExistsQuadConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetIfExistsQuadConstraintStream.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.QuadTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.penta.joiner.DefaultPentaJoiner; import ai.timefold.solver.core.impl.bavet.quad.IndexedIfExistsQuadNode; import ai.timefold.solver.core.impl.bavet.quad.UnindexedIfExistsQuadNode; @@ -66,30 +67,15 @@ public BavetAbstractConstraintStream getTupleSource() { public > void buildNode(ConstraintNodeBuildHelper buildHelper) { TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); IndexerFactory indexerFactory = new IndexerFactory<>(joiner); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()); var node = indexerFactory.hasJoiners() - ? (filtering == null ? new IndexedIfExistsQuadNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), - downstream) - : new IndexedIfExistsQuadNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), - downstream, filtering)) - : (filtering == null ? new UnindexedIfExistsQuadNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), downstream) - : new UnindexedIfExistsQuadNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), - downstream, filtering)); + ? new IndexedIfExistsQuadNode<>(shouldExist, indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, downstream, filtering) + : new UnindexedIfExistsQuadNode<>(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + downstream, filtering); buildHelper.addNode(node, this, this, parentBridgeE); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetJoinQuadConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetJoinQuadConstraintStream.java index 69e39b4e54..cf0f9d873c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetJoinQuadConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetJoinQuadConstraintStream.java @@ -7,8 +7,10 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.bavet.common.BavetAbstractConstraintStream; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.QuadTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.quad.IndexedJoinQuadNode; import ai.timefold.solver.core.impl.bavet.quad.UnindexedJoinQuadNode; import ai.timefold.solver.core.impl.bavet.quad.joiner.DefaultQuadJoiner; @@ -57,26 +59,18 @@ public void collectActiveConstraintStreams(Set> void buildNode(ConstraintNodeBuildHelper buildHelper) { - int outputStoreSize = buildHelper.extractTupleStoreSize(this); TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); IndexerFactory indexerFactory = new IndexerFactory<>(joiner); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()); + OutputStoreSizeTracker outputStoreSizeTracker = new OutputStoreSizeTracker(buildHelper.extractTupleStoreSize(this)); var node = indexerFactory.hasJoiners() - ? new IndexedJoinQuadNode<>(indexerFactory, - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filtering, outputStoreSize + 2, - outputStoreSize, outputStoreSize + 1) - : new UnindexedJoinQuadNode<>( - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filtering, outputStoreSize + 2, - outputStoreSize, outputStoreSize + 1); + ? new IndexedJoinQuadNode<>(indexerFactory, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filtering) + : new UnindexedJoinQuadNode<>(leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filtering); buildHelper.addNode(node, this, leftParent, rightParent); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetIfExistsTriConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetIfExistsTriConstraintStream.java index 83ff4355f4..4fb8128b4f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetIfExistsTriConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetIfExistsTriConstraintStream.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.quad.joiner.DefaultQuadJoiner; import ai.timefold.solver.core.impl.bavet.tri.IndexedIfExistsTriNode; import ai.timefold.solver.core.impl.bavet.tri.UnindexedIfExistsTriNode; @@ -66,30 +67,15 @@ public BavetAbstractConstraintStream getTupleSource() { public > void buildNode(ConstraintNodeBuildHelper buildHelper) { TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); IndexerFactory indexerFactory = new IndexerFactory<>(joiner); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()); var node = indexerFactory.hasJoiners() - ? (filtering == null ? new IndexedIfExistsTriNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), - downstream) - : new IndexedIfExistsTriNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), - downstream, filtering)) - : (filtering == null ? new UnindexedIfExistsTriNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), downstream) - : new UnindexedIfExistsTriNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), - downstream, filtering)); + ? new IndexedIfExistsTriNode<>(shouldExist, indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, downstream, filtering) + : new UnindexedIfExistsTriNode<>(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + downstream, filtering); buildHelper.addNode(node, this, this, parentBridgeD); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetJoinTriConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetJoinTriConstraintStream.java index 8303c34a00..090467c6f8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetJoinTriConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetJoinTriConstraintStream.java @@ -7,8 +7,10 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.bavet.common.BavetAbstractConstraintStream; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.tri.IndexedJoinTriNode; import ai.timefold.solver.core.impl.bavet.tri.UnindexedJoinTriNode; import ai.timefold.solver.core.impl.bavet.tri.joiner.DefaultTriJoiner; @@ -57,26 +59,18 @@ public void collectActiveConstraintStreams(Set> void buildNode(ConstraintNodeBuildHelper buildHelper) { - int outputStoreSize = buildHelper.extractTupleStoreSize(this); TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); IndexerFactory indexerFactory = new IndexerFactory<>(joiner); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()); + OutputStoreSizeTracker outputStoreSizeTracker = new OutputStoreSizeTracker(buildHelper.extractTupleStoreSize(this)); var node = indexerFactory.hasJoiners() - ? new IndexedJoinTriNode<>(indexerFactory, - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filtering, outputStoreSize + 2, - outputStoreSize, outputStoreSize + 1) - : new UnindexedJoinTriNode<>( - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filtering, outputStoreSize + 2, - outputStoreSize, outputStoreSize + 1); + ? new IndexedJoinTriNode<>(indexerFactory, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filtering) + : new UnindexedJoinTriNode<>(leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filtering); buildHelper.addNode(node, this, leftParent, rightParent); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetIfExistsUniConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetIfExistsUniConstraintStream.java index f74a234858..91fb548b36 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetIfExistsUniConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetIfExistsUniConstraintStream.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.impl.bavet.common.BavetAbstractConstraintStream; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.bavet.uni.IndexedIfExistsUniNode; import ai.timefold.solver.core.impl.bavet.uni.UnindexedIfExistsUniNode; @@ -65,30 +66,15 @@ public BavetAbstractConstraintStream getTupleSource() { public > void buildNode(ConstraintNodeBuildHelper buildHelper) { TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); IndexerFactory indexerFactory = new IndexerFactory<>(joiner); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()); var node = indexerFactory.hasJoiners() - ? (filtering == null ? new IndexedIfExistsUniNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - downstream) - : new IndexedIfExistsUniNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - downstream, filtering)) - : (filtering == null ? new UnindexedIfExistsUniNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), downstream) - : new UnindexedIfExistsUniNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - downstream, filtering)); + ? new IndexedIfExistsUniNode<>(shouldExist, indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, downstream, filtering) + : new UnindexedIfExistsUniNode<>(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + downstream, filtering); buildHelper.addNode(node, this, this, parentBridgeB); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsAndComparisonIndexerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsAndComparisonIndexerTest.java index 63330f8193..789e6eb605 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsAndComparisonIndexerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsAndComparisonIndexerTest.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.api.score.stream.Joiners; import ai.timefold.solver.core.impl.bavet.bi.joiner.DefaultBiJoiner; +import ai.timefold.solver.core.impl.bavet.common.TuplePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import org.junit.jupiter.api.Test; @@ -14,16 +15,17 @@ class EqualsAndComparisonIndexerTest extends AbstractIndexerTest { private final DefaultBiJoiner joiner = (DefaultBiJoiner) Joiners.equal(Person::gender) .and(Joiners.lessThanOrEqual(Person::age)); + private final ElementPositionTracker> positionFunction = new TuplePositionTracker<>(0); @Test void iEmpty() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); assertThat(getTuples(indexer, "F", 40)).isEmpty(); } @Test void put() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); var annTuple = newTuple("Ann-F-40"); assertThat(indexer.size(IndexKeys.ofMany("F", 40))).isEqualTo(0); indexer.put(IndexKeys.ofMany("F", 40), annTuple); @@ -32,18 +34,18 @@ void put() { @Test void removeTwice() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); var annTuple = newTuple("Ann-F-40"); - var annEntry = indexer.put(IndexKeys.ofMany("F", 40), annTuple); + indexer.put(IndexKeys.ofMany("F", 40), annTuple); - indexer.remove(IndexKeys.ofMany("F", 40), annEntry); - assertThatThrownBy(() -> indexer.remove(IndexKeys.ofMany("F", 40), annEntry)) + indexer.remove(IndexKeys.ofMany("F", 40), annTuple); + assertThatThrownBy(() -> indexer.remove(IndexKeys.ofMany("F", 40), annTuple)) .isInstanceOf(IllegalStateException.class); } @Test void visit() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); var annTuple = newTuple("Ann-F-40"); indexer.put(IndexKeys.ofMany("F", 40), annTuple); @@ -61,7 +63,7 @@ void visit() { } private static UniTuple newTuple(String factA) { - return new UniTuple<>(factA, 0); + return new UniTuple<>(factA, 1); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexerTest.java index 54f9bc0dd0..ef40f86d11 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexerTest.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.api.score.stream.Joiners; import ai.timefold.solver.core.impl.bavet.bi.joiner.DefaultBiJoiner; +import ai.timefold.solver.core.impl.bavet.common.TuplePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.util.Pair; @@ -15,16 +16,17 @@ class EqualsIndexerTest extends AbstractIndexerTest { private final DefaultBiJoiner joiner = (DefaultBiJoiner) Joiners.equal(Person::gender) .and(Joiners.equal(Person::age)); + private final ElementPositionTracker> positionFunction = new TuplePositionTracker<>(0); @Test void isEmpty() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); assertThat(getTuples(indexer, "F", 40)).isEmpty(); } @Test void put() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); var annTuple = newTuple("Ann-F-40"); assertThat(indexer.size(IndexKeys.ofMany("F", 40))).isEqualTo(0); indexer.put(IndexKeys.ofMany("F", 40), annTuple); @@ -33,18 +35,18 @@ void put() { @Test void removeTwice() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); var annTuple = newTuple("Ann-F-40"); - var annEntry = indexer.put(IndexKeys.ofMany("F", 40), annTuple); + indexer.put(IndexKeys.ofMany("F", 40), annTuple); - indexer.remove(IndexKeys.ofMany("F", 40), annEntry); - assertThatThrownBy(() -> indexer.remove(IndexKeys.ofMany("F", 40), annEntry)) + indexer.remove(IndexKeys.ofMany("F", 40), annTuple); + assertThatThrownBy(() -> indexer.remove(IndexKeys.ofMany("F", 40), annTuple)) .isInstanceOf(IllegalStateException.class); } @Test void visit() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); var annTuple = newTuple("Ann-F-40"); indexer.put(IndexKeys.of(new Pair<>("F", 40)), annTuple); @@ -61,7 +63,7 @@ void visit() { } private static UniTuple newTuple(String factA) { - return new UniTuple<>(factA, 0); + return new UniTuple<>(factA, 1); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java new file mode 100644 index 0000000000..8c75058fbc --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java @@ -0,0 +1,245 @@ +package ai.timefold.solver.core.impl.bavet.common.index; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +class IndexedSetTest { + + private final ElementPositionTracker stringTracker = new SimpleTracker<>(); + + @Test + void addMultipleElements() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + + assertThat(set.size()).isEqualTo(3); + assertThat(set.asList()).containsExactly("A", "B", "C"); + } + + @Test + void addDuplicateElement() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + assertThatThrownBy(() -> set.add("A")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("was already added"); + } + + @Test + void removeElement() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.remove("A"); + + assertThat(set.isEmpty()).isTrue(); + assertThat(set.size()).isZero(); + } + + @Test + void removeNonExistentElement() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + assertThatThrownBy(() -> set.remove("B")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("was not found"); + } + + @Test + void removeFromEmptySet() { + var set = new IndexedSet<>(stringTracker); + + assertThatThrownBy(() -> set.remove("A")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("was not found"); + } + + @Test + void removeMiddleElement() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.remove("B"); + + assertThat(set.size()).isEqualTo(2); + assertThat(set.asList()).containsExactly("A", "C"); + } + + @Test + void removeLastElement() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.remove("C"); + + assertThat(set.size()).isEqualTo(2); + assertThat(set.asList()).containsExactly("A", "B"); + } + + @Test + void removeFirstElement() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.remove("A"); + + assertThat(set.size()).isEqualTo(2); + assertThat(set.asList()).containsExactly("B", "C"); + } + + @Test + void multipleRemovalsAndAdditions() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.remove("B"); + set.add("D"); + set.remove("A"); + set.add("E"); + + assertThat(set.size()).isEqualTo(3); + assertThat(set.asList()).containsExactly("C", "D", "E"); + } + + @Test + void forEach() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + + var result = new ArrayList(); + set.forEach(result::add); + + assertThat(result).containsExactly("A", "B", "C"); + } + + @Test + void forEachOnEmptySet() { + var set = new IndexedSet<>(stringTracker); + + var result = new ArrayList(); + set.forEach(result::add); + + assertThat(result).isEmpty(); + } + + @Test + void forEachWithRemoval() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.add("D"); + + var result = new ArrayList(); + set.forEach(element -> { + result.add(element); + if (element.equals("B")) { + set.remove("B"); + } + }); + + assertThat(result).containsExactly("A", "B", "C", "D"); + assertThat(set.asList()).containsExactly("A", "C", "D"); + } + + @Test + void isEmpty() { + var set = new IndexedSet<>(stringTracker); + + assertThat(set.isEmpty()).isTrue(); + + set.add("A"); + assertThat(set.isEmpty()).isFalse(); + + set.remove("A"); + assertThat(set.isEmpty()).isTrue(); + } + + @Test + void asListOnEmptySet() { + var set = new IndexedSet<>(stringTracker); + + assertThat(set.asList()).isEmpty(); + } + + @Test + void toStringOnEmptySet() { + var set = new IndexedSet<>(stringTracker); + + assertThat(set.toString()).isEqualTo("[]"); + } + + @Test + void toStringWithElements() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + + assertThat(set.toString()).isEqualTo("[A, B]"); + } + + @Test + void largeSetWithManyRemovals() { + var intTracker = new SimpleTracker(); + var set = new IndexedSet<>(intTracker); + + for (int i = 0; i < 100; i++) { + set.add(i); + } + + for (int i = 0; i < 50; i++) { + set.remove(i * 2); + } + + assertThat(set.size()).isEqualTo(50); + for (int i = 0; i < 50; i++) { + assertThat(set.asList()).contains(i * 2 + 1); + } + } + + private static final class SimpleTracker implements ElementPositionTracker { + + private final Map positions = new HashMap<>(); + + @Override + public int setPosition(T element, int position) { + return positions.put(element, position) == null ? -1 : position; + } + + @Override + public int getPosition(T element) { + return positions.getOrDefault(element, -1); + } + + @Override + public int clearPosition(T element) { + var position = positions.remove(element); + return position == null ? -1 : position; + } + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexerTest.java index 2f1ea45a70..8d0aaf3863 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexerTest.java @@ -4,15 +4,18 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import ai.timefold.solver.core.impl.bavet.common.TuplePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import org.junit.jupiter.api.Test; class NoneIndexerTest extends AbstractIndexerTest { + private final ElementPositionTracker> elementPositionTracker = new TuplePositionTracker<>(0); + @Test void isEmpty() { - var indexer = new NoneIndexer<>(); + var indexer = new NoneIndexer<>(elementPositionTracker); assertSoftly(softly -> { softly.assertThat(getTuples(indexer)).isEmpty(); softly.assertThat(indexer.isEmpty()).isTrue(); @@ -21,7 +24,7 @@ void isEmpty() { @Test void put() { - var indexer = new NoneIndexer<>(); + var indexer = new NoneIndexer<>(elementPositionTracker); var annTuple = newTuple("Ann-F-40"); assertThat(indexer.size(IndexKeys.none())).isEqualTo(0); indexer.put(IndexKeys.none(), annTuple); @@ -34,26 +37,26 @@ void put() { @Test void removeTwice() { - var indexer = new NoneIndexer<>(); + var indexer = new NoneIndexer<>(elementPositionTracker); var annTuple = newTuple("Ann-F-40"); - var annEntry = indexer.put(IndexKeys.none(), annTuple); + indexer.put(IndexKeys.none(), annTuple); assertSoftly(softly -> { softly.assertThat(indexer.isEmpty()).isFalse(); softly.assertThat(getTuples(indexer)).containsExactly(annTuple); }); - indexer.remove(IndexKeys.none(), annEntry); + indexer.remove(IndexKeys.none(), annTuple); assertSoftly(softly -> { softly.assertThat(indexer.isEmpty()).isTrue(); softly.assertThat(getTuples(indexer)).isEmpty(); }); - assertThatThrownBy(() -> indexer.remove(IndexKeys.none(), annEntry)) + assertThatThrownBy(() -> indexer.remove(IndexKeys.none(), annTuple)) .isInstanceOf(IllegalStateException.class); } @Test void visit() { - var indexer = new NoneIndexer<>(); + var indexer = new NoneIndexer<>(elementPositionTracker); var annTuple = newTuple("Ann-F-40"); indexer.put(IndexKeys.none(), annTuple); @@ -64,7 +67,7 @@ void visit() { } private static UniTuple newTuple(String factA) { - return new UniTuple<>(factA, 0); + return new UniTuple<>(factA, 1); } } From a6e619e2ff883632b938ad6d3e952b17f91c308d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Sat, 8 Nov 2025 13:27:33 +0100 Subject: [PATCH 2/4] Optimizations around the counters --- .../common/AbstractIndexedIfExistsNode.java | 3 +- .../common/AbstractUnindexedIfExistsNode.java | 2 +- .../core/impl/bavet/common/ExistsCounter.java | 4 ++ .../bavet/common/ExistsCounterHandle.java | 3 ++ .../ExistsCounterHandlePositionTracker.java | 49 +++++-------------- .../common/ExistsCounterPositionTracker.java | 30 ++++++------ .../bavet/common/TuplePositionTracker.java | 13 ++--- .../common/index/ElementPositionTracker.java | 14 ++---- .../impl/bavet/common/index/IndexedSet.java | 26 +++++----- .../bavet/common/index/IndexedSetTest.java | 19 +------ 10 files changed, 58 insertions(+), 105 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java index bd174958b4..8bcc9170d9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java @@ -42,8 +42,7 @@ protected AbstractIndexedIfExistsNode(boolean shouldExist, this.inputStoreIndexLeftKeys = leftTupleStorePositionTracker.reserveNextAvailablePosition(); this.inputStoreIndexLeftCounter = leftTupleStorePositionTracker.reserveNextAvailablePosition(); this.inputStoreIndexRightKeys = rightTupleStorePositionTracker.reserveNextAvailablePosition(); - this.indexerLeft = indexerFactory.buildIndexer(true, - new ExistsCounterPositionTracker<>(leftTupleStorePositionTracker.reserveNextAvailablePosition())); + this.indexerLeft = indexerFactory.buildIndexer(true, ExistsCounterPositionTracker.instance()); this.indexerRight = indexerFactory.buildIndexer(false, new TuplePositionTracker<>(rightTupleStorePositionTracker.reserveNextAvailablePosition())); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java index 2ade3386dc..52626016fa 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java @@ -32,7 +32,7 @@ protected AbstractUnindexedIfExistsNode(boolean shouldExist, TupleStorePositionT this.inputStoreIndexLeftCounter = leftTupleStorePositionTracker.reserveNextAvailablePosition(); this.inputStoreIndexRightTuple = rightTupleStorePositionTracker.reserveNextAvailablePosition(); this.leftCounterSet = new IndexedSet<>( - new ExistsCounterPositionTracker<>(leftTupleStorePositionTracker.reserveNextAvailablePosition())); + new ExistsCounterPositionTracker<>()); this.rightTupleSet = new IndexedSet<>(new TuplePositionTracker<>(inputStoreIndexRightTuple)); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java index a47bdd2910..623f12c159 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java @@ -3,12 +3,16 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; +import org.jspecify.annotations.NullMarked; + +@NullMarked public final class ExistsCounter extends AbstractPropagationMetadataCarrier { final Tuple_ leftTuple; TupleState state = TupleState.DEAD; // It's the node's job to mark a new instance as CREATING. int countRight = 0; + int indexedSetPositon = -1; ExistsCounter(Tuple_ leftTuple) { this.leftTuple = leftTuple; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandle.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandle.java index f6e40c734c..ac10f4f385 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandle.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandle.java @@ -3,6 +3,8 @@ import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; +import org.jspecify.annotations.NullMarked; + /** * Used for filtering in {@link AbstractIfExistsNode}. * There is no place where both left and right sets for each counter would be kept together, @@ -11,6 +13,7 @@ * * @param */ +@NullMarked final class ExistsCounterHandle { final ExistsCounter counter; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandlePositionTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandlePositionTracker.java index d204fed047..b19efed2a5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandlePositionTracker.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandlePositionTracker.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.bavet.common; +import java.util.function.ToIntFunction; + import ai.timefold.solver.core.impl.bavet.common.index.ElementPositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; @@ -7,31 +9,21 @@ @SuppressWarnings({ "rawtypes", "unchecked" }) @NullMarked -record ExistsCounterHandlePositionTracker(PositionGetter positionGetter, - PositionClearer positionClearer, +record ExistsCounterHandlePositionTracker( + ToIntFunction> positionGetter, PositionSetter positionSetter) implements ElementPositionTracker> { private static final ExistsCounterHandlePositionTracker LEFT = new ExistsCounterHandlePositionTracker( - tracker -> tracker.leftPosition, - tracker -> { - var result = tracker.leftPosition; - tracker.leftPosition = -1; - return result; - }, + (ToIntFunction) tracker -> tracker.leftPosition, (tracker, position) -> { var oldValue = tracker.leftPosition; tracker.leftPosition = position; return oldValue; }); private static final ExistsCounterHandlePositionTracker RIGHT = new ExistsCounterHandlePositionTracker( - tracker -> tracker.rightPosition, - tracker -> { - var result = tracker.rightPosition; - tracker.rightPosition = -1; - return result; - }, + (ToIntFunction) tracker -> tracker.rightPosition, (tracker, position) -> { var oldValue = tracker.rightPosition; tracker.rightPosition = position; @@ -47,34 +39,15 @@ public static ExistsCounterHandlePositionTracker< } @Override - public int getPosition(ExistsCounterHandle element) { - return positionGetter.apply(element); - } - - @Override - public int setPosition(ExistsCounterHandle element, int position) { - return positionSetter.apply(element, position); + public void setPosition(ExistsCounterHandle element, int position) { + positionSetter.apply(element, position); } @Override public int clearPosition(ExistsCounterHandle element) { - return positionClearer.apply(element); - } - - @FunctionalInterface - @NullMarked - interface PositionGetter { - - int apply(ExistsCounterHandle tracker); - - } - - @FunctionalInterface - @NullMarked - interface PositionClearer { - - int apply(ExistsCounterHandle tracker); - + var oldPosition = positionGetter.applyAsInt(element); + positionSetter.apply(element, -1); + return oldPosition; } @FunctionalInterface diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterPositionTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterPositionTracker.java index 0d52a39c73..d6c3645300 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterPositionTracker.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterPositionTracker.java @@ -3,32 +3,30 @@ import ai.timefold.solver.core.impl.bavet.common.index.ElementPositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; -record ExistsCounterPositionTracker(int inputStorePosition) +import org.jspecify.annotations.NullMarked; + +@SuppressWarnings({ "unchecked", "rawtypes" }) +@NullMarked +record ExistsCounterPositionTracker() implements ElementPositionTracker> { - @Override - public int getPosition(ExistsCounter element) { - var tuple = element.getTuple(); - var value = tuple.getStore(inputStorePosition); - return value == null ? -1 : (int) value; + private static final ExistsCounterPositionTracker INSTANCE = new ExistsCounterPositionTracker(); + + public static ExistsCounterPositionTracker instance() { + return INSTANCE; } @Override - public int setPosition(ExistsCounter element, int position) { - var tuple = element.getTuple(); - var oldValue = getPosition(element); - tuple.setStore(inputStorePosition, position); - return oldValue; + public void setPosition(ExistsCounter element, int position) { + element.indexedSetPositon = position; } @Override public int clearPosition(ExistsCounter element) { - try { - return element.getTuple().removeStore(inputStorePosition); - } catch (NullPointerException e) { - return -1; - } + var oldPosition = element.indexedSetPositon; + element.indexedSetPositon = -1; + return oldPosition; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TuplePositionTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TuplePositionTracker.java index a6e801db41..96bc069bfd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TuplePositionTracker.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TuplePositionTracker.java @@ -3,21 +3,16 @@ import ai.timefold.solver.core.impl.bavet.common.index.ElementPositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; +import org.jspecify.annotations.NullMarked; + +@NullMarked public record TuplePositionTracker(int inputStorePosition) implements ElementPositionTracker { @Override - public int getPosition(Tuple_ element) { - var value = element.getStore(inputStorePosition); - return value == null ? -1 : (int) value; - } - - @Override - public int setPosition(Tuple_ element, int position) { - var oldValue = getPosition(element); + public void setPosition(Tuple_ element, int position) { element.setStore(inputStorePosition, position); - return oldValue; } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ElementPositionTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ElementPositionTracker.java index f0c50f848b..c3460ef140 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ElementPositionTracker.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ElementPositionTracker.java @@ -1,29 +1,23 @@ package ai.timefold.solver.core.impl.bavet.common.index; +import org.jspecify.annotations.NullMarked; + /** * Allows to read and modify the position of an element in an {@link IndexedSet}. * Typically points to a field in the element itself. * * @param */ +@NullMarked public interface ElementPositionTracker { - /** - * Gets the position of the given element. - * - * @param element never null - * @return >= 0 if the element is tracked, or -1 if it is not tracked - */ - int getPosition(T element); - /** * Sets the position of the given element. * * @param element never null * @param position >= 0 - * @return the previous position of the element, or -1 if it was not tracked before */ - int setPosition(T element, int position); + void setPosition(T element, int position); /** * Clears the position of the given element. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java index e2bab41547..31a9d3a38c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java @@ -12,8 +12,9 @@ import org.jspecify.annotations.Nullable; /** - * {@link ArrayList}-backed set which allows to {@link #remove(Object)} an element without knowing its position - * and without an expensive lookup. + * {@link ArrayList}-backed set which allows to {@link #remove(Object)} an element + * without knowing its position and without an expensive lookup. + * It also allows for direct random access like a list. *

* It uses an {@link ElementPositionTracker} to track the insertion position of each element. * When an element is removed, the insertion position of later elements is not changed. @@ -29,8 +30,10 @@ * Random access is not required for Constraint Streams, but Neighborhoods make heavy use of it; * if we used the {@link ElementAwareList} implementation instead, * we would have to copy the elements to an array every time we need to access them randomly during move generation. - * - * + *

+ * For performance reasons, this class does not check if an element was already added; + * duplicates must be avoided by the caller and will cause undefined behavior. + * * @param */ @NullMarked @@ -52,20 +55,19 @@ private List getElementList() { } /** - * Appends the specified element to the end of this collection, if not already present. - * Will use identity comparison to check for presence; - * two different instances which {@link Object#equals(Object) equal} are considered different elements. + * Appends the specified element to the end of this collection. + * If the element is already present, + * undefined, unexpected, and incorrect behavior should be expected. + *

+ * Presence of the element can be checked using the associated {@link ElementPositionTracker}. + * For performance reasons, this method avoids that check. * * @param element element to be appended to this collection - * @throws IllegalStateException if the element was already present in this collection */ public void add(T element) { var actualElementList = getElementList(); actualElementList.add(element); - if (elementPositionTracker.setPosition(element, actualElementList.size() - 1) >= 0) { - throw new IllegalStateException("Impossible state: the element (%s) was already added to the IndexedSet." - .formatted(element)); - } + elementPositionTracker.setPosition(element, actualElementList.size() - 1); } /** diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java index 8c75058fbc..e893eabe96 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java @@ -25,16 +25,6 @@ void addMultipleElements() { assertThat(set.asList()).containsExactly("A", "B", "C"); } - @Test - void addDuplicateElement() { - var set = new IndexedSet<>(stringTracker); - - set.add("A"); - assertThatThrownBy(() -> set.add("A")) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("was already added"); - } - @Test void removeElement() { var set = new IndexedSet<>(stringTracker); @@ -226,13 +216,8 @@ private static final class SimpleTracker implements ElementPositionTracker private final Map positions = new HashMap<>(); @Override - public int setPosition(T element, int position) { - return positions.put(element, position) == null ? -1 : position; - } - - @Override - public int getPosition(T element) { - return positions.getOrDefault(element, -1); + public void setPosition(T element, int position) { + positions.put(element, position); } @Override From 29e6a54b891f4815c141087a723d2d76462cc0ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Sat, 8 Nov 2025 17:32:13 +0100 Subject: [PATCH 3/4] Experiment with a different approach --- .../impl/bavet/common/index/IndexedSet.java | 121 ++++++++++-------- .../bavet/common/index/IndexedSetTest.java | 39 ++---- 2 files changed, 77 insertions(+), 83 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java index 31a9d3a38c..4c6e59f3d6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java @@ -1,16 +1,16 @@ package ai.timefold.solver.core.impl.bavet.common.index; +import ai.timefold.solver.core.impl.util.ElementAwareList; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + import java.util.ArrayList; +import java.util.BitSet; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.Consumer; -import ai.timefold.solver.core.impl.util.ElementAwareList; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - /** * {@link ArrayList}-backed set which allows to {@link #remove(Object)} an element * without knowing its position and without an expensive lookup. @@ -33,6 +33,9 @@ *

* For performance reasons, this class does not check if an element was already added; * duplicates must be avoided by the caller and will cause undefined behavior. + *

+ * This class is not thread-safe. + * It is in fact very thread-unsafe. * * @param */ @@ -41,7 +44,8 @@ public final class IndexedSet { private final ElementPositionTracker elementPositionTracker; private @Nullable ArrayList elementList; // Lazily initialized, so that empty indexes use no memory. - private int removalCount = 0; + private final BitSet gaps = new BitSet(0); + private int gapCount = 0; public IndexedSet(ElementPositionTracker elementPositionTracker) { this.elementPositionTracker = Objects.requireNonNull(elementPositionTracker); @@ -66,8 +70,16 @@ private List getElementList() { */ public void add(T element) { var actualElementList = getElementList(); - actualElementList.add(element); - elementPositionTracker.setPosition(element, actualElementList.size() - 1); + if (gapCount > 0) { + var gapIndex = gaps.nextSetBit(0); + actualElementList.set(gapIndex, element); + elementPositionTracker.setPosition(element, gapIndex); + gaps.clear(gapIndex); + gapCount--; + } else { + actualElementList.add(element); + elementPositionTracker.setPosition(element, actualElementList.size() - 1); + } } /** @@ -94,46 +106,19 @@ private boolean innerRemove(T element) { return false; } var actualElementList = getElementList(); - var upperBound = Math.min(insertionPosition, actualElementList.size() - 1); - var lowerBound = Math.max(0, insertionPosition - removalCount); - var actualPosition = findElement(actualElementList, element, lowerBound, upperBound); - if (actualPosition < 0) { - return false; - } - actualElementList.remove(actualPosition); - if (isEmpty()) { - removalCount = 0; - } else if (actualElementList.size() > actualPosition) { - // We only mark removals that actually affect later elements. - // Removing the last element does not affect any other element. - removalCount++; + if (insertionPosition == actualElementList.size() - 1) { + // The element was the last one added; we can simply remove it. + actualElementList.remove(insertionPosition); + } else { + actualElementList.set(insertionPosition, null); + gaps.set(insertionPosition); + gapCount++; } return true; } - /** - * Search for the element in the given range. - * - * @param actualElementList the list to search in - * @param element the element to search for - * @param startIndex start of the range we are currently considering (inclusive) - * @param endIndex end of the range we are currently considering (inclusive) - * @return the index of the element if found, -1 otherwise - */ - private static int findElement(List actualElementList, T element, int startIndex, int endIndex) { - for (var i = endIndex; i >= startIndex; i--) { - // Iterating backwards as the element is more likely to be closer to the end of the range, - // which is where it was originally inserted. - var maybeElement = actualElementList.get(i); - if (maybeElement == element) { - return i; - } - } - return -1; - } - public int size() { - return elementList == null ? 0 : elementList.size(); + return elementList == null ? 0 : elementList.size() - gapCount; } /** @@ -146,19 +131,16 @@ public void forEach(Consumer tupleConsumer) { if (elementList == null) { return; } - var i = 0; - while (i < elementList.size()) { - var oldRemovalCount = removalCount; // The consumer may remove some elements, shifting others forward. - tupleConsumer.accept(elementList.get(i)); - var elementDrift = removalCount - oldRemovalCount; - // Move to the next element, adjusting for any shifts due to removals. - // If no elements were removed by the consumer, we simply move to the next index. - i -= elementDrift - 1; + for (var i = 0; i < elementList.size(); i++) { + var element = elementList.get(i); + if (element != null) { + tupleConsumer.accept(element); + } } } public boolean isEmpty() { - return elementList == null || elementList.isEmpty(); + return size() == 0; } /** @@ -168,11 +150,40 @@ public boolean isEmpty() { * @return a standard list view of this element-aware list */ public List asList() { - return elementList == null ? Collections.emptyList() : elementList; + if (elementList == null) { + return Collections.emptyList(); + } + var actualElementList = getElementList(); + defrag(actualElementList); + return actualElementList; + } + + private void defrag(List actualElementList) { + if (gapCount == 0) { + return; + } + var gap = gaps.nextSetBit(0); + while (gap >= 0) { + var lastNonGapIndex = findNonGapFromEnd(actualElementList); + if (lastNonGapIndex < 0 || gap >= lastNonGapIndex) { + break; + } + var lastElement = actualElementList.remove(lastNonGapIndex); + actualElementList.set(gap, lastElement); + elementPositionTracker.setPosition(lastElement, gap); + gap = gaps.nextSetBit(gap + 1); + } + gaps.clear(); + gapCount = 0; } - public String toString() { - return elementList == null ? "[]" : elementList.toString(); + private int findNonGapFromEnd(List actualElementList) { + var end = actualElementList.size() - 1; + var lastNonGap = gaps.previousClearBit(end); + for (var i = end; i > lastNonGap; i--) { + actualElementList.remove(i); + } + return lastNonGap; } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java index e893eabe96..32d019a845 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java @@ -1,13 +1,13 @@ package ai.timefold.solver.core.impl.bavet.common.index; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class IndexedSetTest { @@ -22,7 +22,7 @@ void addMultipleElements() { set.add("C"); assertThat(set.size()).isEqualTo(3); - assertThat(set.asList()).containsExactly("A", "B", "C"); + assertThat(set.asList()).containsExactlyInAnyOrder("A", "B", "C"); } @Test @@ -65,7 +65,7 @@ void removeMiddleElement() { set.remove("B"); assertThat(set.size()).isEqualTo(2); - assertThat(set.asList()).containsExactly("A", "C"); + assertThat(set.asList()).containsExactlyInAnyOrder("A", "C"); } @Test @@ -78,7 +78,7 @@ void removeLastElement() { set.remove("C"); assertThat(set.size()).isEqualTo(2); - assertThat(set.asList()).containsExactly("A", "B"); + assertThat(set.asList()).containsExactlyInAnyOrder("A", "B"); } @Test @@ -91,7 +91,7 @@ void removeFirstElement() { set.remove("A"); assertThat(set.size()).isEqualTo(2); - assertThat(set.asList()).containsExactly("B", "C"); + assertThat(set.asList()).containsExactlyInAnyOrder("B", "C"); } @Test @@ -107,7 +107,7 @@ void multipleRemovalsAndAdditions() { set.add("E"); assertThat(set.size()).isEqualTo(3); - assertThat(set.asList()).containsExactly("C", "D", "E"); + assertThat(set.asList()).containsExactlyInAnyOrder("C", "D", "E"); } @Test @@ -121,7 +121,7 @@ void forEach() { var result = new ArrayList(); set.forEach(result::add); - assertThat(result).containsExactly("A", "B", "C"); + assertThat(result).containsExactlyInAnyOrder("A", "B", "C"); } @Test @@ -151,8 +151,8 @@ void forEachWithRemoval() { } }); - assertThat(result).containsExactly("A", "B", "C", "D"); - assertThat(set.asList()).containsExactly("A", "C", "D"); + assertThat(result).containsExactlyInAnyOrder("A", "B", "C", "D"); + assertThat(set.asList()).containsExactlyInAnyOrder("A", "C", "D"); } @Test @@ -175,23 +175,6 @@ void asListOnEmptySet() { assertThat(set.asList()).isEmpty(); } - @Test - void toStringOnEmptySet() { - var set = new IndexedSet<>(stringTracker); - - assertThat(set.toString()).isEqualTo("[]"); - } - - @Test - void toStringWithElements() { - var set = new IndexedSet<>(stringTracker); - - set.add("A"); - set.add("B"); - - assertThat(set.toString()).isEqualTo("[A, B]"); - } - @Test void largeSetWithManyRemovals() { var intTracker = new SimpleTracker(); From 492ad9df3c87e8502ac660a357d3f3d066e4d3f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Sat, 8 Nov 2025 17:32:13 +0100 Subject: [PATCH 4/4] Experiment with a different approach --- .../impl/bavet/common/AbstractJoinNode.java | 9 +- .../impl/bavet/common/index/IndexedSet.java | 86 +++++-- .../bavet/common/index/IndexedSetTest.java | 223 +++++++++++++++++- 3 files changed, 284 insertions(+), 34 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java index cfc660e8cb..27f08d26e5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java @@ -169,14 +169,7 @@ private void processOutTupleUpdate(LeftTuple_ leftTuple, UniTuple rightT private static @Nullable Tuple_ findOutTuple(IndexedSet outTupleSet, IndexedSet referenceOutTupleSet, int outputStoreIndexOutSet) { // Hack: the outTuple has no left/right input tuple reference, use the left/right outSet reference instead. - var list = outTupleSet.asList(); - for (var i = 0; i < list.size(); i++) { // Avoid allocating iterators. - var outTuple = list.get(i); - if (referenceOutTupleSet == outTuple.getStore(outputStoreIndexOutSet)) { - return outTuple; - } - } - return null; + return outTupleSet.findFirst(outTuple -> referenceOutTupleSet == outTuple.getStore(outputStoreIndexOutSet)); } protected final void retractOutTuple(OutTuple_ outTuple) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java index 4c6e59f3d6..f3784f3465 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java @@ -1,15 +1,17 @@ package ai.timefold.solver.core.impl.bavet.common.index; -import ai.timefold.solver.core.impl.util.ElementAwareList; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - import java.util.ArrayList; import java.util.BitSet; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.Consumer; +import java.util.function.Predicate; + +import ai.timefold.solver.core.impl.util.ElementAwareList; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * {@link ArrayList}-backed set which allows to {@link #remove(Object)} an element @@ -72,16 +74,24 @@ public void add(T element) { var actualElementList = getElementList(); if (gapCount > 0) { var gapIndex = gaps.nextSetBit(0); - actualElementList.set(gapIndex, element); - elementPositionTracker.setPosition(element, gapIndex); - gaps.clear(gapIndex); - gapCount--; + putElementIntoGap(actualElementList, element, gapIndex); } else { actualElementList.add(element); elementPositionTracker.setPosition(element, actualElementList.size() - 1); } } + private void putElementIntoGap(List elementList, T element, int gap) { + setElementList(elementList, element, gap); + gaps.clear(gap); + gapCount--; + } + + private void setElementList(List elementList, T element, int position) { + elementList.set(position, element); + elementPositionTracker.setPosition(element, position); + } + /** * Removes the first occurrence of the specified element from this collection, if it is present. * Will use identity comparison to check for presence; @@ -109,6 +119,7 @@ private boolean innerRemove(T element) { if (insertionPosition == actualElementList.size() - 1) { // The element was the last one added; we can simply remove it. actualElementList.remove(insertionPosition); + removeTailGap(actualElementList); } else { actualElementList.set(insertionPosition, null); gaps.set(insertionPosition); @@ -125,18 +136,37 @@ public int size() { * Performs the given action for each element of the collection * until all elements have been processed. * - * @param tupleConsumer the action to be performed for each element + * @param elementConsumer the action to be performed for each element */ - public void forEach(Consumer tupleConsumer) { - if (elementList == null) { + public void forEach(Consumer elementConsumer) { + if (isEmpty()) { return; } + findFirst(element -> { + elementConsumer.accept(element); + return false; // Iterate until the end. + }); + } + + public @Nullable T findFirst(Predicate elementPredicate) { + if (isEmpty()) { + return null; + } for (var i = 0; i < elementList.size(); i++) { var element = elementList.get(i); - if (element != null) { - tupleConsumer.accept(element); + if (element == null) { + var nonGap = removeTailGap(elementList); + if (i >= nonGap) { + return null; + } + element = elementList.remove(nonGap); + putElementIntoGap(elementList, element, i); + } + if (elementPredicate.test(element)) { + return element; } } + return null; } public boolean isEmpty() { @@ -153,9 +183,8 @@ public List asList() { if (elementList == null) { return Collections.emptyList(); } - var actualElementList = getElementList(); - defrag(actualElementList); - return actualElementList; + defrag(elementList); + return elementList; } private void defrag(List actualElementList) { @@ -164,26 +193,37 @@ private void defrag(List actualElementList) { } var gap = gaps.nextSetBit(0); while (gap >= 0) { - var lastNonGapIndex = findNonGapFromEnd(actualElementList); + var lastNonGapIndex = removeTailGap(actualElementList); if (lastNonGapIndex < 0 || gap >= lastNonGapIndex) { break; } var lastElement = actualElementList.remove(lastNonGapIndex); - actualElementList.set(gap, lastElement); - elementPositionTracker.setPosition(lastElement, gap); + setElementList(actualElementList, lastElement, gap); gap = gaps.nextSetBit(gap + 1); } + resetGaps(); + } + + private void resetGaps() { gaps.clear(); gapCount = 0; } - private int findNonGapFromEnd(List actualElementList) { + private int removeTailGap(List actualElementList) { var end = actualElementList.size() - 1; var lastNonGap = gaps.previousClearBit(end); - for (var i = end; i > lastNonGap; i--) { - actualElementList.remove(i); + if (lastNonGap < 0) { + actualElementList.clear(); + resetGaps(); + return -1; + } else { + for (var i = end; i > lastNonGap; i--) { + actualElementList.remove(i); + } + gaps.clear(lastNonGap, end + 1); + gapCount = gaps.cardinality(); + return lastNonGap; } - return lastNonGap; } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java index 32d019a845..7d087e7741 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java @@ -1,13 +1,14 @@ package ai.timefold.solver.core.impl.bavet.common.index; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.Test; class IndexedSetTest { @@ -194,6 +195,222 @@ void largeSetWithManyRemovals() { } } + @Test + void addToGapAfterRemoval() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.remove("B"); + set.add("D"); + + assertThat(set.size()).isEqualTo(3); + assertThat(set.asList()).containsExactlyInAnyOrder("A", "C", "D"); + } + + @Test + void multipleGapsFilledInOrder() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.add("D"); + set.remove("B"); + set.remove("D"); + set.add("E"); + set.add("F"); + + assertThat(set.size()).isEqualTo(4); + assertThat(set.asList()).containsExactlyInAnyOrder("A", "C", "E", "F"); + } + + @Test + void findFirstWithPredicate() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + + var result = set.findFirst(element -> element.equals("B")); + + assertThat(result).isEqualTo("B"); + } + + @Test + void findFirstOnEmptySet() { + var set = new IndexedSet<>(stringTracker); + + var result = set.findFirst(element -> true); + + assertThat(result).isNull(); + } + + @Test + void findFirstNoMatch() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + + var result = set.findFirst(element -> element.equals("Z")); + + assertThat(result).isNull(); + } + + @Test + void findFirstWithGaps() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.add("D"); + set.remove("B"); + + var result = set.findFirst(element -> element.equals("C")); + + assertThat(result).isEqualTo("C"); + assertThat(set.size()).isEqualTo(3); + } + + @Test + void defragmentationDuringAsList() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.add("D"); + set.add("E"); + set.remove("B"); + set.remove("D"); + + var list = set.asList(); + + assertThat(list).containsExactlyInAnyOrder("A", "C", "E"); + assertThat(set.size()).isEqualTo(3); + } + + @Test + void removeElementWithNegativeInsertionPosition() { + var tracker = new SimpleTracker(); + var set = new IndexedSet<>(tracker); + + set.add("A"); + + tracker.positions.put("B", -1); + + assertThatThrownBy(() -> set.remove("B")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("was not found"); + } + + @Test + void sizeWithMultipleGaps() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.add("D"); + set.add("E"); + set.remove("B"); + set.remove("D"); + + assertThat(set.size()).isEqualTo(3); + } + + @Test + void addAfterMultipleRemovals() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.add("D"); + set.remove("A"); + set.remove("C"); + set.add("E"); + set.add("F"); + + assertThat(set.size()).isEqualTo(4); + assertThat(set.asList()).containsExactlyInAnyOrder("B", "D", "E", "F"); + } + + @Test + void consecutiveAdditionsAfterClearingGaps() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.remove("A"); + set.add("C"); + set.add("D"); + + assertThat(set.size()).isEqualTo(3); + assertThat(set.asList()).containsExactlyInAnyOrder("B", "C", "D"); + } + + @Test + void removeAllElementsOneByOne() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + + set.remove("A"); + set.remove("B"); + set.remove("C"); + + assertThat(set.isEmpty()).isTrue(); + assertThat(set.size()).isZero(); + assertThat(set.asList()).isEmpty(); + } + + @Test + void forEachWithMultipleGaps() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.add("D"); + set.add("E"); + set.remove("B"); + set.remove("D"); + + var result = new ArrayList(); + set.forEach(result::add); + + assertThat(result).containsExactlyInAnyOrder("A", "C", "E"); + } + + @Test + void findFirstDefragsInternalList() { + var set = new IndexedSet<>(stringTracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.add("D"); + set.remove("A"); + set.remove("C"); + + var result = new ArrayList(); + set.findFirst(element -> { + result.add(element); + return false; + }); + + assertThat(result).containsExactlyInAnyOrder("B", "D"); + } + + @NullMarked private static final class SimpleTracker implements ElementPositionTracker { private final Map positions = new HashMap<>();