From 491b14c80efab3cb06ebf9639b14d1029ad969fc Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 29 Oct 2025 14:37:49 -0400 Subject: [PATCH] feat: add constraint stream profiling Profiling constraints is notorishing difficult, since each component of a constraint are converted to nodes, some of which are shared. As such, a JVM method profile is basically unreadable and does not represent how much time is actually spent for each constraint. To aid in profiling, an optional constraintStreamProfilingMode configuration was added. If set to a value other than NONE, it wraps each tuple lifecycle node inside a ProfilingTupleLifecycle, which will measure how long each lifecycle executes. The ProfilingTupleLifecycle find out what constraint is responsible for creating that lifecycle by getting snapshot of the stack traces from its constraint stream's creator (when a constraint stream is shared, their stack traces are merged into the same set). At the end of solving, a profiling summary is then produced in the INFO log. The details differ depending on the profiling mode: - In the BY_METHOD profiling mode, (className, methodName) is used as the key - In the BY_LINE profiling mode, (className, methodName, lineNumber) is used as the key. The methods/lines are printed in descending order of time percentage spent. The sum of time percentage spent may be over 100%, since methods/lines can share time spent with other methods/lines. timefold.solver.constraint-stream-profiling-mode was added as a property to Quarkus and Spring Boot to configure profiling (defaults to NONE). --- benchmark/src/main/resources/benchmark.xsd | 24 ++++ core/src/build/revapi-differences.json | 11 ++ .../director/ConstraintProfilingMode.java | 22 ++++ .../director/ScoreDirectorFactoryConfig.java | 18 +++ .../core/impl/bavet/AbstractSession.java | 4 + .../solver/core/impl/bavet/NodeNetwork.java | 15 ++- .../core/impl/bavet/common/AbstractNode.java | 23 ++++ .../bavet/common/AbstractNodeBuildHelper.java | 31 ++++- .../common/BavetAbstractConstraintStream.java | 30 +++++ .../core/impl/bavet/common/BavetStream.java | 6 + .../bavet/common/ConstraintNodeLocation.java | 52 ++++++++ .../bavet/common/ConstraintNodeProfileId.java | 17 +++ .../impl/bavet/common/ConstraintProfiler.java | 112 ++++++++++++++++++ .../common/tuple/ProfilingTupleLifecycle.java | 31 +++++ .../bavet/common/tuple/TupleLifecycle.java | 11 ++ .../enumerating/DatasetSessionFactory.java | 2 +- .../common/DataNodeBuildHelper.java | 2 +- .../BavetConstraintStreamScoreDirector.java | 3 + ...tConstraintStreamScoreDirectorFactory.java | 11 +- .../stream/bavet/BavetConstraintFactory.java | 4 +- .../bavet/BavetConstraintSessionFactory.java | 15 ++- .../common/BavetPrecomputeBuildHelper.java | 5 +- .../common/ConstraintNodeBuildHelper.java | 8 +- core/src/main/resources/solver.xsd | 16 +++ .../bavet/common/ConstraintProfilerTest.java | 106 +++++++++++++++++ .../move/ChangeMoveDefinitionTest.java | 3 +- .../move/ListChangeMoveDefinitionTest.java | 3 +- .../move/ListSwapMoveDefinitionTest.java | 3 +- .../maybeapi/move/SwapMoveDefinitionTest.java | 3 +- .../BavetConstraintStreamImplSupport.java | 3 +- .../quarkus/deployment/TimefoldProcessor.java | 7 +- .../config/SolverBuildTimeConfig.java | 6 + ...TimefoldProcessorSolverPropertiesTest.java | 4 + .../TimefoldSolverAutoConfiguration.java | 4 + .../config/SolverProperties.java | 11 ++ .../autoconfigure/config/SolverProperty.java | 3 + ...lverSingleSolverAutoConfigurationTest.java | 15 +++ .../stream/ScoreDirectorFactoryCache.java | 4 +- 38 files changed, 621 insertions(+), 27 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/config/score/director/ConstraintProfilingMode.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ConstraintNodeLocation.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ConstraintNodeProfileId.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ConstraintProfiler.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/ProfilingTupleLifecycle.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/bavet/common/ConstraintProfilerTest.java diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 86ede21c93..3041c71ccb 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -458,6 +458,9 @@ + + + @@ -2819,6 +2822,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index 537f4e3860..37cbc3628e 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -450,6 +450,17 @@ "new": "class ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig", "annotation": "@org.jspecify.annotations.NullMarked", "justification": "Update config" + }, + { + "ignore": true, + "code": "java.annotation.attributeValueChanged", + "old": "class ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig", + "new": "class ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig", + "annotationType": "jakarta.xml.bind.annotation.XmlType", + "attribute": "propOrder", + "oldValue": "{\"easyScoreCalculatorClass\", \"easyScoreCalculatorCustomProperties\", \"constraintProviderClass\", \"constraintProviderCustomProperties\", \"constraintStreamImplType\", \"incrementalScoreCalculatorClass\", \"incrementalScoreCalculatorCustomProperties\", \"scoreDrlList\", \"initializingScoreTrend\", \"assertionScoreDirectorFactory\"}", + "newValue": "{\"easyScoreCalculatorClass\", \"easyScoreCalculatorCustomProperties\", \"constraintProviderClass\", \"constraintProviderCustomProperties\", \"constraintStreamImplType\", \"constraintStreamAutomaticNodeSharing\", \"constraintStreamProfilingMode\", \"incrementalScoreCalculatorClass\", \"incrementalScoreCalculatorCustomProperties\", \"scoreDrlList\", \"initializingScoreTrend\", \"assertionScoreDirectorFactory\"}", + "justification": "Add constraint profiling to config" } ] } diff --git a/core/src/main/java/ai/timefold/solver/core/config/score/director/ConstraintProfilingMode.java b/core/src/main/java/ai/timefold/solver/core/config/score/director/ConstraintProfilingMode.java new file mode 100644 index 0000000000..cbf2aa814a --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/config/score/director/ConstraintProfilingMode.java @@ -0,0 +1,22 @@ +package ai.timefold.solver.core.config.score.director; + +/** + * Controls how constraints are profiled. + * Enabling profiling have a minor performance impact. + */ +public enum ConstraintProfilingMode { + /** + * Disables profiling. + */ + NONE, + + /** + * Profile by the method an operation was defined in. + */ + BY_METHOD, + + /** + * Profile by the line an operation was defined on. + */ + BY_LINE +} diff --git a/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java b/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java index c402d3e6fc..6b88e40885 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java @@ -27,6 +27,7 @@ "constraintProviderCustomProperties", "constraintStreamImplType", "constraintStreamAutomaticNodeSharing", + "constraintStreamProfilingMode", "incrementalScoreCalculatorClass", "incrementalScoreCalculatorCustomProperties", "scoreDrlList", @@ -46,6 +47,7 @@ public class ScoreDirectorFactoryConfig extends AbstractConfig constraintProviderCustomProperties = null; protected ConstraintStreamImplType constraintStreamImplType; protected Boolean constraintStreamAutomaticNodeSharing; + protected ConstraintProfilingMode constraintStreamProfilingMode; protected Class incrementalScoreCalculatorClass = null; @@ -126,6 +128,14 @@ public void setConstraintStreamAutomaticNodeSharing(@Nullable Boolean constraint this.constraintStreamAutomaticNodeSharing = constraintStreamAutomaticNodeSharing; } + public ConstraintProfilingMode getConstraintStreamProfilingMode() { + return constraintStreamProfilingMode; + } + + public void setConstraintStreamProfilingMode(ConstraintProfilingMode constraintStreamProfilingMode) { + this.constraintStreamProfilingMode = constraintStreamProfilingMode; + } + public @Nullable Class getIncrementalScoreCalculatorClass() { return incrementalScoreCalculatorClass; } @@ -227,6 +237,12 @@ public void setAssertionScoreDirectorFactory(@Nullable ScoreDirectorFactoryConfi return this; } + public @NonNull ScoreDirectorFactoryConfig + withConstraintStreamProfiling(@NonNull ConstraintProfilingMode constraintStreamProfiling) { + this.constraintStreamProfilingMode = constraintStreamProfiling; + return this; + } + public @NonNull ScoreDirectorFactoryConfig withIncrementalScoreCalculatorClass( @NonNull Class incrementalScoreCalculatorClass) { @@ -288,6 +304,8 @@ public ScoreDirectorFactoryConfig withScoreDrls(String... scoreDrls) { constraintStreamImplType, inheritedConfig.getConstraintStreamImplType()); constraintStreamAutomaticNodeSharing = ConfigUtils.inheritOverwritableProperty(constraintStreamAutomaticNodeSharing, inheritedConfig.getConstraintStreamAutomaticNodeSharing()); + constraintStreamProfilingMode = ConfigUtils.inheritOverwritableProperty(constraintStreamProfilingMode, + inheritedConfig.getConstraintStreamProfilingMode()); incrementalScoreCalculatorClass = ConfigUtils.inheritOverwritableProperty( incrementalScoreCalculatorClass, inheritedConfig.getIncrementalScoreCalculatorClass()); incrementalScoreCalculatorCustomProperties = ConfigUtils.inheritMergeableMapProperty( diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/AbstractSession.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/AbstractSession.java index f4f1320ff8..262b78eaf2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/AbstractSession.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/AbstractSession.java @@ -63,4 +63,8 @@ public void settle() { nodeNetwork.settle(); } + public final void summarizeProfileIfPresent() { + nodeNetwork.summarizeProfileIfPresent(); + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/NodeNetwork.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/NodeNetwork.java index 07d21536ab..e31b99fa87 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/NodeNetwork.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/NodeNetwork.java @@ -8,8 +8,12 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.impl.bavet.common.BavetRootNode; +import ai.timefold.solver.core.impl.bavet.common.ConstraintProfiler; import ai.timefold.solver.core.impl.bavet.common.Propagator; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + /** * Represents Bavet's network of nodes, specific to a particular session. * Nodes only used by disabled constraints have already been removed. @@ -19,10 +23,11 @@ * @param layeredNodes nodes grouped first by their layer, then by their index within the layer; * propagation needs to happen in this order. */ +@NullMarked public record NodeNetwork(Map, List>> declaredClassToNodeMap, - Propagator[][] layeredNodes) { + Propagator[][] layeredNodes, @Nullable ConstraintProfiler constraintProfiler) { - public static final NodeNetwork EMPTY = new NodeNetwork(Map.of(), new Propagator[0][0]); + public static final NodeNetwork EMPTY = new NodeNetwork(Map.of(), new Propagator[0][0], null); public int forEachNodeCount() { return declaredClassToNodeMap.size(); @@ -70,6 +75,12 @@ private static void settleLayer(Propagator[] nodesInLayer) { } } + public void summarizeProfileIfPresent() { + if (constraintProfiler != null) { + constraintProfiler.summarize(); + } + } + @Override public boolean equals(Object o) { if (this == o) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNode.java index f66816b2eb..b0b4342b33 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNode.java @@ -1,14 +1,23 @@ package ai.timefold.solver.core.impl.bavet.common; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintSession; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + /** * @see PropagationQueue Description of the propagation mechanism. */ +@NullMarked public abstract class AbstractNode { private long id; private long layerIndex = -1; + private @Nullable Set locationSet; /** * Instead of calling the propagation directly from here, @@ -29,6 +38,20 @@ public final void setId(long id) { this.id = id; } + public Set getLocationSet() { + if (locationSet == null) { + return Collections.emptySet(); + } + return locationSet; + } + + public void addLocationSet(Set locationSet) { + if (this.locationSet == null) { + this.locationSet = new LinkedHashSet<>(); + } + this.locationSet.addAll(locationSet); + } + public final void setLayerIndex(long layerIndex) { if (layerIndex < 0) { throw new IllegalArgumentException("Impossible state: layer index (" + layerIndex + ") must be at least 0."); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java index 7b3f605910..15b47b9b6f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java @@ -17,6 +17,10 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.RightTupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked public abstract class AbstractNodeBuildHelper { private final Set activeStreamSet; @@ -24,15 +28,22 @@ public abstract class AbstractNodeBuildHelper { private final Map> tupleLifecycleMap; private final Map storeIndexMap; + @Nullable + private final ConstraintProfiler constraintProfiler; + + @Nullable private List reversedNodeList; + private long nextLifecycleProfilingId = 0; - protected AbstractNodeBuildHelper(Set activeStreamSet) { + protected AbstractNodeBuildHelper(Set activeStreamSet, + @Nullable ConstraintProfiler constraintProfiler) { this.activeStreamSet = activeStreamSet; int activeStreamSetSize = activeStreamSet.size(); this.nodeCreatorMap = new HashMap<>(Math.max(16, activeStreamSetSize)); this.tupleLifecycleMap = new HashMap<>(Math.max(16, activeStreamSetSize)); this.storeIndexMap = new HashMap<>(Math.max(16, activeStreamSetSize / 2)); this.reversedNodeList = new ArrayList<>(activeStreamSetSize); + this.constraintProfiler = constraintProfiler; } public boolean isStreamActive(Stream_ stream) { @@ -45,6 +56,7 @@ public void addNode(AbstractNode node, Stream_ creator) { public void addNode(AbstractNode node, Stream_ creator, Stream_ parent) { reversedNodeList.add(node); + node.addLocationSet(creator.getLocationSet()); nodeCreatorMap.put(node, creator); if (!(node instanceof BavetRootNode)) { if (parent == null) { @@ -57,6 +69,7 @@ public void addNode(AbstractNode node, Stream_ creator, Stream_ parent) { public void addNode(AbstractNode node, Stream_ creator, Stream_ leftParent, Stream_ rightParent) { reversedNodeList.add(node); + node.addLocationSet(creator.getLocationSet()); nodeCreatorMap.put(node, creator); putInsertUpdateRetract(leftParent, TupleLifecycle.ofLeft((LeftTupleLifecycle) node)); putInsertUpdateRetract(rightParent, TupleLifecycle.ofRight((RightTupleLifecycle) node)); @@ -64,6 +77,11 @@ public void addNode(AbstractNode node, Stream_ creator, Stream_ leftParent, Stre public void putInsertUpdateRetract(Stream_ stream, TupleLifecycle tupleLifecycle) { + if (constraintProfiler != null) { + tupleLifecycle = TupleLifecycle.profiling(constraintProfiler, nextLifecycleProfilingId, + stream, tupleLifecycle); + nextLifecycleProfilingId++; + } tupleLifecycleMap.put(stream, tupleLifecycle); } @@ -147,11 +165,14 @@ public AbstractNode findParentNode(Stream_ childNodeCreator) { } public static NodeNetwork buildNodeNetwork(List nodeList, - Map, List>> declaredClassToNodeMap) { + Map, List>> declaredClassToNodeMap, + AbstractNodeBuildHelper nodeBuildHelper) { var layerMap = new TreeMap>(); for (var node : nodeList) { - layerMap.computeIfAbsent(node.getLayerIndex(), k -> new ArrayList<>()) - .add(node.getPropagator()); + var layer = node.getLayerIndex(); + var propagator = node.getPropagator(); + layerMap.computeIfAbsent(layer, k -> new ArrayList<>()) + .add(propagator); } var layerCount = layerMap.size(); var layeredNodes = new Propagator[layerCount][]; @@ -159,7 +180,7 @@ public static NodeNetwork buildNodeNetwork(List nodeList, var layer = layerMap.get((long) i); layeredNodes[i] = layer.toArray(new Propagator[0]); } - return new NodeNetwork(declaredClassToNodeMap, layeredNodes); + return new NodeNetwork(declaredClassToNodeMap, layeredNodes, nodeBuildHelper.constraintProfiler); } public > List buildNodeList(Set streamSet, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/BavetAbstractConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/BavetAbstractConstraintStream.java index be636ae902..ad3fb48be3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/BavetAbstractConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/BavetAbstractConstraintStream.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.impl.bavet.common; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; @@ -22,15 +23,22 @@ public abstract class BavetAbstractConstraintStream extends AbstractConstraintStream implements BavetStream { + private static final String BAVET_IMPL_PACKAGE = "ai.timefold.solver.core.impl.bavet"; + private static final String BAVET_SCORE_IMPL_PACKAGE = "ai.timefold.solver.core.impl.score.stream"; + private static final String BAVET_SCORE_API_PACKAGE = "ai.timefold.solver.core.api.score.stream"; + protected final BavetConstraintFactory constraintFactory; protected final BavetAbstractConstraintStream parent; protected final List> childStreamList = new ArrayList<>(2); + protected final Set streamLocationSet; protected BavetAbstractConstraintStream(BavetConstraintFactory constraintFactory, BavetAbstractConstraintStream parent) { super(parent.getRetrievalSemantics()); this.constraintFactory = constraintFactory; this.parent = parent; + this.streamLocationSet = new LinkedHashSet<>(); + streamLocationSet.add(determineStreamLocation()); } protected BavetAbstractConstraintStream(BavetConstraintFactory constraintFactory, @@ -38,6 +46,28 @@ protected BavetAbstractConstraintStream(BavetConstraintFactory constr super(retrievalSemantics); this.constraintFactory = constraintFactory; this.parent = null; + this.streamLocationSet = new LinkedHashSet<>(); + streamLocationSet.add(determineStreamLocation()); + } + + private static ConstraintNodeLocation determineStreamLocation() { + return StackWalker.getInstance().walk(stack -> stack + .dropWhile(stackFrame -> stackFrame.getClassName().startsWith(BAVET_IMPL_PACKAGE) || + stackFrame.getClassName().startsWith(BAVET_SCORE_IMPL_PACKAGE) || + stackFrame.getClassName().startsWith(BAVET_SCORE_API_PACKAGE)) + .map(stackFrame -> new ConstraintNodeLocation(stackFrame.getClassName(), + stackFrame.getMethodName(), + stackFrame.getLineNumber())) + .findFirst() + .orElseGet(ConstraintNodeLocation::unknown)); + } + + public Set getLocationSet() { + return streamLocationSet; + } + + public void addLocationSet(Set locationSet) { + streamLocationSet.addAll(locationSet); } /** diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/BavetStream.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/BavetStream.java index 5866eefdb8..55009e5220 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/BavetStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/BavetStream.java @@ -1,7 +1,13 @@ package ai.timefold.solver.core.impl.bavet.common; +import java.util.Set; + public interface BavetStream { Stream_ getParent(); + default Set getLocationSet() { + return Set.of(ConstraintNodeLocation.unknown()); + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ConstraintNodeLocation.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ConstraintNodeLocation.java new file mode 100644 index 0000000000..ef671775b9 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ConstraintNodeLocation.java @@ -0,0 +1,52 @@ +package ai.timefold.solver.core.impl.bavet.common; + +import java.util.Objects; + +import ai.timefold.solver.core.impl.util.Pair; +import ai.timefold.solver.core.impl.util.Triple; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public record ConstraintNodeLocation(String className, + String methodName, + int lineNumber) { + + public static ConstraintNodeLocation unknown() { + return new ConstraintNodeLocation("", "", -1); + } + + public record LocationKeyAndDisplay(Object key, String display) implements Comparable { + @Override + public boolean equals(Object object) { + if (!(object instanceof LocationKeyAndDisplay that)) + return false; + return Objects.equals(key, that.key); + } + + @Override + public int hashCode() { + return Objects.hashCode(key); + } + + @Override + public String toString() { + return display; + } + + @Override + public int compareTo(LocationKeyAndDisplay o) { + return display.compareTo(o.display); + } + } + + public LocationKeyAndDisplay getMethodId() { + return new LocationKeyAndDisplay(new Pair<>(className, methodName), + "%s#%s".formatted(className, methodName)); + } + + public LocationKeyAndDisplay getLineId() { + return new LocationKeyAndDisplay(new Triple<>(className, methodName, lineNumber), + "%s#%s:%d".formatted(className, methodName, lineNumber)); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ConstraintNodeProfileId.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ConstraintNodeProfileId.java new file mode 100644 index 0000000000..2b0df8bb7e --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ConstraintNodeProfileId.java @@ -0,0 +1,17 @@ +package ai.timefold.solver.core.impl.bavet.common; + +import java.util.Set; + +public record ConstraintNodeProfileId(long key, Set locationSet) { + @Override + public boolean equals(Object object) { + if (!(object instanceof ConstraintNodeProfileId that)) + return false; + return key == that.key; + } + + @Override + public int hashCode() { + return Long.hashCode(key); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ConstraintProfiler.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ConstraintProfiler.java new file mode 100644 index 0000000000..ab0973ccf8 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ConstraintProfiler.java @@ -0,0 +1,112 @@ +package ai.timefold.solver.core.impl.bavet.common; + +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +import ai.timefold.solver.core.config.score.director.ConstraintProfilingMode; +import ai.timefold.solver.core.impl.util.MutableLong; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.micrometer.core.instrument.Clock; + +public final class ConstraintProfiler { + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Clock clock; + private final ConstraintProfilingMode profilingMode; + private final Map profileIdToRetractRuntime; + private final Map profileIdToUpdateRuntime; + private final Map profileIdToInsertRuntime; + + public ConstraintProfiler(ConstraintProfilingMode profilingMode) { + this(Clock.SYSTEM, profilingMode); + } + + public ConstraintProfiler(Clock clock, ConstraintProfilingMode profilingMode) { + this.clock = clock; + this.profilingMode = profilingMode; + this.profileIdToRetractRuntime = new LinkedHashMap<>(); + this.profileIdToUpdateRuntime = new LinkedHashMap<>(); + this.profileIdToInsertRuntime = new LinkedHashMap<>(); + } + + public void register(ConstraintNodeProfileId profileId) { + // When phase changes, the node network is recalculated, + // but the profiler is reused + profileIdToRetractRuntime.putIfAbsent(profileId, new MutableLong()); + profileIdToUpdateRuntime.putIfAbsent(profileId, new MutableLong()); + profileIdToInsertRuntime.putIfAbsent(profileId, new MutableLong()); + } + + public void measure(ConstraintNodeProfileId profileId, Operation operation, Runnable measurable) { + var start = clock.monotonicTime(); + measurable.run(); + var end = clock.monotonicTime(); + var duration = end - start; + switch (operation) { + case RETRACT -> profileIdToRetractRuntime.get(profileId).add(duration); + case UPDATE -> profileIdToUpdateRuntime.get(profileId).add(duration); + case INSERT -> profileIdToInsertRuntime.get(profileId).add(duration); + } + } + + String getSummary() { + var summary = new StringBuilder("Constraint Profiling Summary"); + Map methodIdToTotalRuntime = new LinkedHashMap<>(); + var totalDuration = 0L; + totalDuration += getTotalDuration(methodIdToTotalRuntime, profileIdToRetractRuntime); + totalDuration += getTotalDuration(methodIdToTotalRuntime, profileIdToUpdateRuntime); + totalDuration += getTotalDuration(methodIdToTotalRuntime, profileIdToInsertRuntime); + + long finalTotalDuration = totalDuration; + methodIdToTotalRuntime.entrySet() + .stream() + .sorted((Comparator>) Comparator + .comparing(entry -> (Comparable) ((Map.Entry) entry).getValue()) + .thenComparing(entry -> ((Map.Entry) entry).getKey()) + .reversed()) + .forEach(entry -> { + var percentage = entry.getValue().doubleValue() / finalTotalDuration; + summary.append('\n') + .append(entry.getKey().display()) + .append(' ') + .append(String.format("%.2f", percentage * 100)) + .append('%'); + }); + + return summary.toString(); + } + + public void summarize() { + logger.info(getSummary()); + + } + + private long getTotalDuration(Map methodIdToTotalRuntime, + Map profileIdToRuntime) { + var totalDuration = 0L; + Function profileIdToKey = switch (profilingMode) { + case BY_METHOD -> ConstraintNodeLocation::getMethodId; + case BY_LINE -> ConstraintNodeLocation::getLineId; + case NONE -> throw new IllegalStateException("Impossible state: profiling is disabled"); + }; + for (var entry : profileIdToRuntime.entrySet()) { + var duration = entry.getValue().longValue(); + for (var location : entry.getKey().locationSet()) { + methodIdToTotalRuntime.computeIfAbsent(profileIdToKey.apply(location), k -> new MutableLong()) + .add(duration); + } + totalDuration += duration; + } + return totalDuration; + } + + public enum Operation { + RETRACT, + UPDATE, + INSERT + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/ProfilingTupleLifecycle.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/ProfilingTupleLifecycle.java new file mode 100644 index 0000000000..e9928bab24 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/ProfilingTupleLifecycle.java @@ -0,0 +1,31 @@ +package ai.timefold.solver.core.impl.bavet.common.tuple; + +import ai.timefold.solver.core.impl.bavet.common.ConstraintNodeProfileId; +import ai.timefold.solver.core.impl.bavet.common.ConstraintProfiler; + +public record ProfilingTupleLifecycle( + ConstraintProfiler constraintProfiler, + ConstraintNodeProfileId profileId, + TupleLifecycle delegate) implements TupleLifecycle { + public ProfilingTupleLifecycle { + constraintProfiler.register(profileId); + } + + @Override + public void insert(Tuple_ tuple) { + constraintProfiler.measure(profileId, ConstraintProfiler.Operation.INSERT, + () -> delegate.insert(tuple)); + } + + @Override + public void update(Tuple_ tuple) { + constraintProfiler.measure(profileId, ConstraintProfiler.Operation.UPDATE, + () -> delegate.update(tuple)); + } + + @Override + public void retract(Tuple_ tuple) { + constraintProfiler.measure(profileId, ConstraintProfiler.Operation.RETRACT, + () -> delegate.retract(tuple)); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/TupleLifecycle.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/TupleLifecycle.java index 675e6f7164..4a987e9c09 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/TupleLifecycle.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/TupleLifecycle.java @@ -7,6 +7,9 @@ import ai.timefold.solver.core.api.function.QuadPredicate; import ai.timefold.solver.core.api.function.TriPredicate; +import ai.timefold.solver.core.impl.bavet.common.BavetStream; +import ai.timefold.solver.core.impl.bavet.common.ConstraintNodeProfileId; +import ai.timefold.solver.core.impl.bavet.common.ConstraintProfiler; public interface TupleLifecycle { @@ -55,6 +58,14 @@ static TupleLifecycle recording() { return new RecordingTupleLifecycle<>(); } + static TupleLifecycle profiling( + ConstraintProfiler constraintProfiler, long lifecycleId, Stream_ stream, + TupleLifecycle delegate) { + return new ProfilingTupleLifecycle<>(constraintProfiler, + new ConstraintNodeProfileId(lifecycleId, stream.getLocationSet()), + delegate); + } + void insert(Tuple_ tuple); void update(Tuple_ tuple); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/DatasetSessionFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/DatasetSessionFactory.java index 879f50cffc..4a6ac9720c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/DatasetSessionFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/DatasetSessionFactory.java @@ -60,7 +60,7 @@ private NodeNetwork buildNodeNetwork(Set> e // TODO implement node network visualization throw new UnsupportedOperationException("Not implemented yet"); } - return AbstractNodeBuildHelper.buildNodeNetwork(nodeList, declaredClassToNodeMap); + return AbstractNodeBuildHelper.buildNodeNetwork(nodeList, declaredClassToNodeMap, buildHelper); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/common/DataNodeBuildHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/common/DataNodeBuildHelper.java index 9e2c02f9fb..c09107197d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/common/DataNodeBuildHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/common/DataNodeBuildHelper.java @@ -21,7 +21,7 @@ public final class DataNodeBuildHelper extends AbstractNodeBuildHelpe public DataNodeBuildHelper(SessionContext sessionContext, Set> activeStreamSet) { - super(activeStreamSet); + super(activeStreamSet, null); this.sessionContext = Objects.requireNonNull(sessionContext); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java index 1072445bdf..9a969b0fc0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java @@ -125,6 +125,9 @@ public boolean requiresFlushing() { public void close() { super.close(); if (session != null) { + if (!derived) { + session.summarizeProfileIfPresent(); + } session = null; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java index 3815286b2c..41475428ec 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java @@ -1,11 +1,13 @@ package ai.timefold.solver.core.impl.score.director.stream; import java.util.Arrays; +import java.util.Objects; import java.util.function.Consumer; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.config.score.director.ConstraintProfilingMode; import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.util.ConfigUtils; @@ -36,9 +38,11 @@ public final class BavetConstraintStreamScoreDirectorFactory(solutionDescriptor, constraintProvider, environmentMode); + return new BavetConstraintStreamScoreDirectorFactory<>(solutionDescriptor, constraintProvider, environmentMode, + profilingMode); } private static Class getConstraintProviderClass(ScoreDirectorFactoryConfig config, @@ -56,11 +60,12 @@ private static Class getConstraintProviderClass(Sc private final ConstraintMetaModel constraintMetaModel; public BavetConstraintStreamScoreDirectorFactory(SolutionDescriptor solutionDescriptor, - ConstraintProvider constraintProvider, EnvironmentMode environmentMode) { + ConstraintProvider constraintProvider, EnvironmentMode environmentMode, ConstraintProfilingMode profilingMode) { super(solutionDescriptor); var constraintFactory = new BavetConstraintFactory<>(solutionDescriptor, environmentMode); constraintMetaModel = DefaultConstraintMetaModel.of(constraintFactory.buildConstraints(constraintProvider)); - constraintSessionFactory = new BavetConstraintSessionFactory<>(solutionDescriptor, constraintMetaModel); + constraintSessionFactory = + new BavetConstraintSessionFactory<>(solutionDescriptor, constraintMetaModel, profilingMode); } public BavetConstraintSession newSession(Solution_ workingSolution, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java index 6e4fd2652e..a1aa29cda4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java @@ -105,10 +105,12 @@ public > Stream_ share( */ public > Stream_ share(Stream_ stream, Consumer consumer) { - return (Stream_) sharingStreamMap.computeIfAbsent(stream, k -> { + var out = (Stream_) sharingStreamMap.computeIfAbsent(stream, k -> { consumer.accept(stream); return stream; }); + out.addLocationSet(stream.getLocationSet()); + return out; } // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSessionFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSessionFactory.java index c38a493cde..436518da04 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSessionFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSessionFactory.java @@ -12,10 +12,12 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel; +import ai.timefold.solver.core.config.score.director.ConstraintProfilingMode; import ai.timefold.solver.core.impl.bavet.NodeNetwork; import ai.timefold.solver.core.impl.bavet.common.AbstractNodeBuildHelper; import ai.timefold.solver.core.impl.bavet.common.BavetAbstractConstraintStream; import ai.timefold.solver.core.impl.bavet.common.BavetRootNode; +import ai.timefold.solver.core.impl.bavet.common.ConstraintProfiler; import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode; import ai.timefold.solver.core.impl.bavet.visual.NodeGraph; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; @@ -25,6 +27,7 @@ import ai.timefold.solver.core.impl.score.stream.common.inliner.AbstractScoreInliner; import ai.timefold.solver.core.impl.util.CollectionUtils; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; @@ -36,11 +39,14 @@ public final class BavetConstraintSessionFactory solutionDescriptor; private final ConstraintMetaModel constraintMetaModel; + private final @Nullable ConstraintProfiler constraintProfiler; public BavetConstraintSessionFactory(SolutionDescriptor solutionDescriptor, - ConstraintMetaModel constraintMetaModel) { + ConstraintMetaModel constraintMetaModel, ConstraintProfilingMode profilingMode) { this.solutionDescriptor = Objects.requireNonNull(solutionDescriptor); this.constraintMetaModel = Objects.requireNonNull(constraintMetaModel); + this.constraintProfiler = + (profilingMode != ConstraintProfilingMode.NONE) ? new ConstraintProfiler(profilingMode) : null; } // ************************************************************************ @@ -117,14 +123,17 @@ public BavetConstraintSession buildSession(Solution_ workingSolution, } return new BavetConstraintSession<>(scoreInliner, buildNodeNetwork(workingSolution, consistencyTracker, constraintStreamSet, scoreInliner, + constraintProfiler, nodeNetworkVisualizationConsumer)); } private static > NodeNetwork buildNodeNetwork(Solution_ workingSolution, ConsistencyTracker consistencyTracker, Set> constraintStreamSet, AbstractScoreInliner scoreInliner, + ConstraintProfiler profiler, Consumer nodeNetworkVisualizationConsumer) { - var buildHelper = new ConstraintNodeBuildHelper<>(consistencyTracker, constraintStreamSet, scoreInliner); + var buildHelper = + new ConstraintNodeBuildHelper<>(consistencyTracker, constraintStreamSet, scoreInliner, profiler); var declaredClassToNodeMap = new LinkedHashMap, List>>(); var nodeList = buildHelper.buildNodeList(constraintStreamSet, buildHelper, BavetAbstractConstraintStream::buildNode, @@ -161,7 +170,7 @@ private static > NodeNetwork buildNodeNe .buildGraphvizDOT(); nodeNetworkVisualizationConsumer.accept(visualisation); } - return AbstractNodeBuildHelper.buildNodeNetwork(nodeList, declaredClassToNodeMap); + return AbstractNodeBuildHelper.buildNodeNetwork(nodeList, declaredClassToNodeMap, buildHelper); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/BavetPrecomputeBuildHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/BavetPrecomputeBuildHelper.java index c61a018cf7..034645da51 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/BavetPrecomputeBuildHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/BavetPrecomputeBuildHelper.java @@ -63,7 +63,8 @@ public BavetPrecomputeBuildHelper( var buildHelper = new ConstraintNodeBuildHelper<>(new ConsistencyTracker<>(), streamSet, AbstractScoreInliner.buildScoreInliner(new SimpleScoreDefinition(), Collections.emptyMap(), - ConstraintMatchPolicy.DISABLED)); + ConstraintMatchPolicy.DISABLED), + null); var declaredClassToNodeMap = new LinkedHashMap, List>>(); var nodeList = buildHelper.buildNodeList(streamSet, buildHelper, @@ -79,7 +80,7 @@ public BavetPrecomputeBuildHelper( } }); - this.nodeNetwork = AbstractNodeBuildHelper.buildNodeNetwork(nodeList, declaredClassToNodeMap); + this.nodeNetwork = AbstractNodeBuildHelper.buildNodeNetwork(nodeList, declaredClassToNodeMap, buildHelper); this.recordingTupleLifecycle = (RecordingTupleLifecycle) buildHelper .getAggregatedTupleLifecycle(List.of(recordingPrecomputeConstraintStream)); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java index 383236c26e..a3b2512dc0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java @@ -8,11 +8,14 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.bavet.common.AbstractNodeBuildHelper; import ai.timefold.solver.core.impl.bavet.common.BavetAbstractConstraintStream; +import ai.timefold.solver.core.impl.bavet.common.ConstraintProfiler; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.variable.declarative.ConsistencyTracker; import ai.timefold.solver.core.impl.score.stream.common.ForEachFilteringCriteria; import ai.timefold.solver.core.impl.score.stream.common.inliner.AbstractScoreInliner; +import org.jspecify.annotations.Nullable; + public final class ConstraintNodeBuildHelper> extends AbstractNodeBuildHelper> { @@ -22,8 +25,9 @@ public final class ConstraintNodeBuildHelper consistencyTracker, Set> activeStreamSet, - AbstractScoreInliner scoreInliner) { - super(activeStreamSet); + AbstractScoreInliner scoreInliner, + @Nullable ConstraintProfiler profiler) { + super(activeStreamSet, profiler); this.consistencyTracker = consistencyTracker; this.scoreInliner = scoreInliner; this.entityDescriptorToForEachCriteriaToPredicateMap = new HashMap<>(); diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 8c6e02fc1c..97a2f4127f 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -111,6 +111,8 @@ + + @@ -1705,6 +1707,20 @@ + + + + + + + + + + + + + + diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/ConstraintProfilerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/ConstraintProfilerTest.java new file mode 100644 index 0000000000..66f21eba3b --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/ConstraintProfilerTest.java @@ -0,0 +1,106 @@ +package ai.timefold.solver.core.impl.bavet.common; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Set; + +import ai.timefold.solver.core.config.score.director.ConstraintProfilingMode; + +import org.junit.jupiter.api.Test; + +import io.micrometer.core.instrument.MockClock; + +class ConstraintProfilerTest { + final String constraintProviderClassName = "MyConstraintProvider"; + final ConstraintNodeProfileId A_1 = new ConstraintNodeProfileId(0, Set.of( + new ConstraintNodeLocation(constraintProviderClassName, "a", 1))); + final ConstraintNodeProfileId A_2 = new ConstraintNodeProfileId(1, Set.of( + new ConstraintNodeLocation(constraintProviderClassName, "a", 2))); + final ConstraintNodeProfileId B_1 = new ConstraintNodeProfileId(2, Set.of( + new ConstraintNodeLocation(constraintProviderClassName, "b", 1))); + final ConstraintNodeProfileId B_2 = new ConstraintNodeProfileId(3, Set.of( + new ConstraintNodeLocation(constraintProviderClassName, "b", 2))); + final ConstraintNodeProfileId AB_3 = new ConstraintNodeProfileId(4, Set.of( + new ConstraintNodeLocation(constraintProviderClassName, "a", 3), + new ConstraintNodeLocation(constraintProviderClassName, "b", 3))); + + ConstraintProfiler constraintProfiler; + MockClock clock; + + void setUp(ConstraintProfilingMode constraintProfilingMode) { + clock = new MockClock(); + constraintProfiler = new ConstraintProfiler(clock, constraintProfilingMode); + List.of(A_1, A_2, B_1, B_2, AB_3).forEach(constraintProfiler::register); + } + + Runnable advance(long seconds) { + return () -> clock.addSeconds(seconds); + } + + @Test + void getSummaryByMethod() { + setUp(ConstraintProfilingMode.BY_METHOD); + + constraintProfiler.measure(A_1, ConstraintProfiler.Operation.INSERT, + advance(1)); + constraintProfiler.measure(A_2, ConstraintProfiler.Operation.RETRACT, + advance(2)); + constraintProfiler.measure(B_1, ConstraintProfiler.Operation.UPDATE, + advance(3)); + constraintProfiler.measure(B_2, ConstraintProfiler.Operation.INSERT, + advance(4)); + constraintProfiler.measure(AB_3, ConstraintProfiler.Operation.INSERT, + advance(5)); + constraintProfiler.measure(A_1, ConstraintProfiler.Operation.INSERT, + advance(3)); + + // Total A = 1 + 2 + 5 + 3 = 11 + // Total B = 3 + 4 + 5 = 12 + // Subtract 5 because A3 and B3 share the same node + // Total = 11 + 12 - 5 = 18 + + assertThat(constraintProfiler.getSummary()) + .isEqualTo(""" + Constraint Profiling Summary + MyConstraintProvider#b 66.67% + MyConstraintProvider#a 61.11%"""); + } + + @Test + void getSummaryByLine() { + setUp(ConstraintProfilingMode.BY_LINE); + + constraintProfiler.measure(A_1, ConstraintProfiler.Operation.INSERT, + advance(1)); + constraintProfiler.measure(A_2, ConstraintProfiler.Operation.RETRACT, + advance(2)); + constraintProfiler.measure(B_1, ConstraintProfiler.Operation.UPDATE, + advance(3)); + constraintProfiler.measure(B_2, ConstraintProfiler.Operation.INSERT, + advance(4)); + constraintProfiler.measure(AB_3, ConstraintProfiler.Operation.INSERT, + advance(5)); + constraintProfiler.measure(A_1, ConstraintProfiler.Operation.INSERT, + advance(3)); + + // Total A1 = 1 + 3 = 4 + // Total A2 = 2 + // Total A3 = 5 + // Total B1 = 3 + // Total B2 = 4 + // Total B3 = 5 + // Subtract 5 because A3 and B3 share the same node + // Total = 4 + 2 + 3 + 4 + 5 + 5 - 5 = 18 + + assertThat(constraintProfiler.getSummary()) + .isEqualTo(""" + Constraint Profiling Summary + MyConstraintProvider#b:3 27.78% + MyConstraintProvider#a:3 27.78% + MyConstraintProvider#b:2 22.22% + MyConstraintProvider#a:1 22.22% + MyConstraintProvider#b:1 16.67% + MyConstraintProvider#a:2 11.11%"""); + } +} \ No newline at end of file diff --git a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ChangeMoveDefinitionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ChangeMoveDefinitionTest.java index cc47071583..cd6143b30c 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ChangeMoveDefinitionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ChangeMoveDefinitionTest.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.config.score.director.ConstraintProfilingMode; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveDefinition; @@ -339,7 +340,7 @@ private Iterable> createMoveIterable(MoveDefinition< var firstEntityClass = solutionDescriptor.getMetaModel().genuineEntities().get(0).type(); var constraintProvider = new TestingConstraintProvider(firstEntityClass); var scoreDirectorFactory = new BavetConstraintStreamScoreDirectorFactory<>(solutionDescriptor, constraintProvider, - EnvironmentMode.TRACKED_FULL_ASSERT); + EnvironmentMode.TRACKED_FULL_ASSERT, ConstraintProfilingMode.NONE); var scoreDirector = scoreDirectorFactory.buildScoreDirector(); scoreDirector.setWorkingSolution(solution); return scoreDirector; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ListChangeMoveDefinitionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ListChangeMoveDefinitionTest.java index 2fb657a726..179008664a 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ListChangeMoveDefinitionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ListChangeMoveDefinitionTest.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.config.score.director.ConstraintProfilingMode; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveDefinition; @@ -366,7 +367,7 @@ private Iterable> createMoveIterable(MoveDefinition< var firstEntityClass = solutionDescriptor.getMetaModel().genuineEntities().get(0).type(); var constraintProvider = new TestingConstraintProvider(firstEntityClass); var scoreDirectorFactory = new BavetConstraintStreamScoreDirectorFactory<>(solutionDescriptor, constraintProvider, - EnvironmentMode.TRACKED_FULL_ASSERT); + EnvironmentMode.TRACKED_FULL_ASSERT, ConstraintProfilingMode.NONE); var scoreDirector = scoreDirectorFactory.buildScoreDirector(); scoreDirector.setWorkingSolution(solution); return scoreDirector; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ListSwapMoveDefinitionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ListSwapMoveDefinitionTest.java index 6d4b1fb617..af3d12ddef 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ListSwapMoveDefinitionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ListSwapMoveDefinitionTest.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.config.score.director.ConstraintProfilingMode; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveDefinition; @@ -125,7 +126,7 @@ private Iterable> createMoveIterable(MoveDefinition< var firstEntityClass = solutionDescriptor.getMetaModel().genuineEntities().get(0).type(); var constraintProvider = new TestingConstraintProvider(firstEntityClass); var scoreDirectorFactory = new BavetConstraintStreamScoreDirectorFactory<>(solutionDescriptor, constraintProvider, - EnvironmentMode.TRACKED_FULL_ASSERT); + EnvironmentMode.TRACKED_FULL_ASSERT, ConstraintProfilingMode.NONE); var scoreDirector = scoreDirectorFactory.buildScoreDirector(); scoreDirector.setWorkingSolution(solution); return scoreDirector; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/SwapMoveDefinitionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/SwapMoveDefinitionTest.java index e4478d8a7b..daf9e326db 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/SwapMoveDefinitionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/SwapMoveDefinitionTest.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.config.score.director.ConstraintProfilingMode; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveDefinition; @@ -147,7 +148,7 @@ private Iterable> createMoveIterable(MoveDefinition< var firstEntityClass = solutionDescriptor.getMetaModel().genuineEntities().get(0).type(); var constraintProvider = new TestingConstraintProvider(firstEntityClass); var scoreDirectorFactory = new BavetConstraintStreamScoreDirectorFactory<>(solutionDescriptor, constraintProvider, - EnvironmentMode.TRACKED_FULL_ASSERT); + EnvironmentMode.TRACKED_FULL_ASSERT, ConstraintProfilingMode.NONE); var scoreDirector = scoreDirectorFactory.buildScoreDirector(); scoreDirector.setWorkingSolution(solution); return scoreDirector; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintStreamImplSupport.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintStreamImplSupport.java index 1ac2b50706..4b8c948712 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintStreamImplSupport.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintStreamImplSupport.java @@ -3,6 +3,7 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.config.score.director.ConstraintProfilingMode; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; @@ -18,7 +19,7 @@ public record BavetConstraintStreamImplSupport(ConstraintMatchPolicy constraintM public , Solution_> InnerScoreDirector buildScoreDirector( SolutionDescriptor solutionDescriptorSupplier, ConstraintProvider constraintProvider) { var scoreDirectorFactory = new BavetConstraintStreamScoreDirectorFactory(solutionDescriptorSupplier, - constraintProvider, EnvironmentMode.PHASE_ASSERT); + constraintProvider, EnvironmentMode.PHASE_ASSERT, ConstraintProfilingMode.NONE); return scoreDirectorFactory.createScoreDirectorBuilder() .withConstraintMatchPolicy(constraintMatchPolicy) .build(); diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java index 4391944538..c2b05ad7c3 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java @@ -789,6 +789,12 @@ private void applySolverProperties(IndexView indexView, String solverName, Solve .flatMap(SolverBuildTimeConfig::enabledPreviewFeatures) .ifPresent(solverConfig::setEnablePreviewFeatureSet); + timefoldBuildTimeConfig.getSolverConfig(solverName) + .flatMap(SolverBuildTimeConfig::constraintStreamProfilingMode) + .ifPresent(profilingMode -> { + solverConfig.getScoreDirectorFactoryConfig().withConstraintStreamProfiling(profilingMode); + }); + timefoldBuildTimeConfig.getSolverConfig(solverName) .flatMap(SolverBuildTimeConfig::nearbyDistanceMeterClass) .ifPresent(clazz -> { @@ -800,7 +806,6 @@ private void applySolverProperties(IndexView indexView, String solverName, Solve } solverConfig.withNearbyDistanceMeterClass((Class>) clazz); }); - // Termination properties are set at runtime } diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java index 8120de3ffa..9572859c5b 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.score.stream.ConstraintStreamImplType; +import ai.timefold.solver.core.config.score.director.ConstraintProfilingMode; import ai.timefold.solver.core.config.solver.PreviewFeature; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.quarkus.config.SolverRuntimeConfig; @@ -58,6 +59,11 @@ public interface SolverBuildTimeConfig { @Deprecated(forRemoval = true, since = "1.4.0") Optional constraintStreamImplType(); + /** + * What profiling mode to use. Defaults to {@link ConstraintProfilingMode#NONE}. + */ + Optional constraintStreamProfilingMode(); + /** * Note: this setting is only available * for Timefold Solver diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverPropertiesTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverPropertiesTest.java index 99b2c9b5ee..4b5792447b 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverPropertiesTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorSolverPropertiesTest.java @@ -11,6 +11,7 @@ import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.solver.SolverFactory; +import ai.timefold.solver.core.config.score.director.ConstraintProfilingMode; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.quarkus.testdomain.dummy.DummyDistanceMeter; @@ -35,6 +36,7 @@ class TimefoldProcessorSolverPropertiesTest { "ai.timefold.solver.quarkus.testdomain.dummy.DummyDistanceMeter") .overrideConfigKey("quarkus.timefold.solver.move-thread-count", "2") .overrideConfigKey("quarkus.timefold.solver.domain-access-type", "REFLECTION") + .overrideConfigKey("quarkus.timefold.solver.constraint-stream-profiling-mode", "BY_METHOD") .overrideConfigKey("quarkus.timefold.solver.termination.spent-limit", "4h") .overrideConfigKey("quarkus.timefold.solver.termination.unimproved-spent-limit", "5h") .overrideConfigKey("quarkus.timefold.solver.termination.best-score-limit", "0") @@ -58,6 +60,8 @@ void solverProperties() { assertEquals(DomainAccessType.REFLECTION, solverConfig.getDomainAccessType()); assertEquals(null, solverConfig.getScoreDirectorFactoryConfig().getConstraintStreamImplType()); + assertEquals(ConstraintProfilingMode.BY_METHOD, + solverConfig.getScoreDirectorFactoryConfig().getConstraintStreamProfilingMode()); assertNotNull(solverConfig.getNearbyDistanceMeterClass()); assertNotNull(solverFactory); } diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java index 1eb52152d6..f7f7adb3bd 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java @@ -266,6 +266,10 @@ private void applyScoreDirectorFactoryProperties(IncludeAbstractClassesEntitySca Objects.requireNonNull(solverConfig.getScoreDirectorFactoryConfig()) .setConstraintStreamAutomaticNodeSharing(true); } + if (solverProperties.getConstraintStreamProfilingMode() != null) { + Objects.requireNonNull(solverConfig.getScoreDirectorFactoryConfig()) + .setConstraintStreamProfilingMode(solverProperties.getConstraintStreamProfilingMode()); + } if (solverProperties.getEnvironmentMode() != null) { solverConfig.setEnvironmentMode(solverProperties.getEnvironmentMode()); } diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java index 619e77b063..041dab5bca 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java @@ -6,6 +6,7 @@ import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.score.stream.ConstraintStreamImplType; +import ai.timefold.solver.core.config.score.director.ConstraintProfilingMode; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.PreviewFeature; import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; @@ -66,6 +67,8 @@ public class SolverProperties { @Deprecated(forRemoval = true, since = "1.4.0") private ConstraintStreamImplType constraintStreamImplType; + private ConstraintProfilingMode constraintStreamProfilingMode; + /** * Note: this setting is only available * for Timefold Solver @@ -162,6 +165,14 @@ public void setConstraintStreamImplType(ConstraintStreamImplType constraintStrea this.constraintStreamImplType = constraintStreamImplType; } + public ConstraintProfilingMode getConstraintStreamProfilingMode() { + return constraintStreamProfilingMode; + } + + public void setConstraintStreamProfilingMode(ConstraintProfilingMode constraintStreamProfilingMode) { + this.constraintStreamProfilingMode = constraintStreamProfilingMode; + } + public Boolean getConstraintStreamAutomaticNodeSharing() { return constraintStreamAutomaticNodeSharing; } diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java index 2651c7f3e1..eb2205b303 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java @@ -12,6 +12,7 @@ import ai.timefold.solver.core.api.domain.common.DomainAccessType; import ai.timefold.solver.core.api.score.stream.ConstraintStreamImplType; +import ai.timefold.solver.core.config.score.director.ConstraintProfilingMode; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.PreviewFeature; import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; @@ -50,6 +51,8 @@ public enum SolverProperty { @Deprecated(forRemoval = true, since = "1.4.0") CONSTRAINT_STREAM_IMPL_TYPE("constraint-stream-impl-type", SolverProperties::setConstraintStreamImplType, value -> ConstraintStreamImplType.valueOf(value.toString())), + CONSTRAINT_STREAM_PROFILING_MODE("constraint-stream-profiling-mode", SolverProperties::setConstraintStreamProfilingMode, + value -> ConstraintProfilingMode.valueOf(value.toString())), CONSTRAINT_STREAM_AUTOMATIC_NODE_SHARING("constraint-stream-automatic-node-sharing", SolverProperties::setConstraintStreamAutomaticNodeSharing, value -> Boolean.valueOf(value.toString())), RANDOM_SEED("random-seed", SolverProperties::setRandomSeed, value -> Long.parseLong(value.toString())), diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverSingleSolverAutoConfigurationTest.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverSingleSolverAutoConfigurationTest.java index c9d45f7e94..df5bd8f646 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverSingleSolverAutoConfigurationTest.java +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverSingleSolverAutoConfigurationTest.java @@ -6,6 +6,7 @@ import java.time.Duration; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.stream.IntStream; import ai.timefold.solver.benchmark.api.PlannerBenchmarkFactory; @@ -13,6 +14,7 @@ import ai.timefold.solver.core.api.solver.SolverConfigOverride; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.api.solver.SolverManager; +import ai.timefold.solver.core.config.score.director.ConstraintProfilingMode; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.core.impl.solver.DefaultSolverJob; @@ -128,6 +130,19 @@ void solve() { }); } + @Test + void solveWithProfilingMode() { + contextRunner + .withClassLoader(allDefaultsFilteredClassLoader) + .withPropertyValues("timefold.solver.constraint-stream-profiling-mode=BY_METHOD") + .run(context -> { + var solverConfig = context.getBean(SolverConfig.class); + assertThat(Objects.requireNonNull(solverConfig.getScoreDirectorFactoryConfig()) + .getConstraintStreamProfilingMode()) + .isEqualTo(ConstraintProfilingMode.BY_METHOD); + }); + } + @Test void solveWithParallelSolverCount() { contextRunner diff --git a/test/src/main/java/ai/timefold/solver/test/impl/score/stream/ScoreDirectorFactoryCache.java b/test/src/main/java/ai/timefold/solver/test/impl/score/stream/ScoreDirectorFactoryCache.java index 6732f2d494..2f242f3327 100644 --- a/test/src/main/java/ai/timefold/solver/test/impl/score/stream/ScoreDirectorFactoryCache.java +++ b/test/src/main/java/ai/timefold/solver/test/impl/score/stream/ScoreDirectorFactoryCache.java @@ -11,6 +11,7 @@ import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.config.score.director.ConstraintProfilingMode; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirectorFactory; @@ -85,7 +86,8 @@ public ScoreDirectorFactoryCache(SolutionDescriptor solutionDescripto ConstraintRef constraintRef, ConstraintProvider constraintProvider, EnvironmentMode environmentMode) { return scoreDirectorFactoryMap.computeIfAbsent(constraintRef, - k -> new BavetConstraintStreamScoreDirectorFactory<>(solutionDescriptor, constraintProvider, environmentMode)); + k -> new BavetConstraintStreamScoreDirectorFactory<>(solutionDescriptor, constraintProvider, environmentMode, + ConstraintProfilingMode.NONE)); } }