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)); } }