diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java index dd3f5251491..b724c2e6390 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java @@ -304,6 +304,7 @@ protected static SubSingleBenchmarkResult createMerge( // Skip oldResult.usedMemoryAfterInputSolution newResult.succeeded = oldResult.succeeded; newResult.score = oldResult.score; + newResult.initialized = oldResult.initialized; newResult.timeMillisSpent = oldResult.timeMillisSpent; newResult.scoreCalculationCount = oldResult.scoreCalculationCount; newResult.moveEvaluationCount = oldResult.moveEvaluationCount; diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java index 119aa76e771..1057665a2e9 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java @@ -12,13 +12,14 @@ import java.util.function.ObjLongConsumer; import java.util.stream.Collectors; -import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListener; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; +import ai.timefold.solver.core.impl.score.director.InnerScore; +import ai.timefold.solver.core.impl.solver.monitoring.SolverMetricUtil; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import io.micrometer.core.instrument.Meter; @@ -88,21 +89,18 @@ public Set getMeterIds(SolverMetric metric, Tags runId) { .collect(Collectors.toSet()); } - public void extractScoreFromMeters(SolverMetric metric, Tags runId, Consumer> scoreConsumer) { - var labelNames = scoreDefinition.getLevelLabels(); - for (var i = 0; i < labelNames.length; i++) { - labelNames[i] = labelNames[i].replace(' ', '.'); - } - var levelNumbers = new Number[labelNames.length]; - for (var i = 0; i < labelNames.length; i++) { - var scoreLevelGauge = this.find(metric.getMeterId() + "." + labelNames[i]).tags(runId).gauge(); + public void extractScoreFromMeters(SolverMetric metric, Tags runId, Consumer> scoreConsumer) { + var score = SolverMetricUtil.extractScore(metric, scoreDefinition, id -> { + var scoreLevelGauge = this.find(id).tags(runId).gauge(); if (scoreLevelGauge != null && Double.isFinite(scoreLevelGauge.value())) { - levelNumbers[i] = scoreLevelNumberConverter.apply(scoreLevelGauge.value()); + return scoreLevelNumberConverter.apply(scoreLevelGauge.value()); } else { - return; + return null; } + }); + if (score != null) { + scoreConsumer.accept(score); } - scoreConsumer.accept(scoreDefinition.fromLevelNumbers(levelNumbers)); } @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -119,23 +117,17 @@ public void extractConstraintSummariesFromMeters(SolverMetric metric, Tags runId // Get the score from the corresponding constraint package and constraint name meters extractScoreFromMeters(metric, constraintMatchTotalRunId, // Get the count gauge (add constraint package and constraint name to the run tags) - score -> getGaugeValue(metric.getMeterId() + ".count", constraintMatchTotalRunId, - count -> constraintMatchTotalConsumer - .accept(new ConstraintSummary(constraintRef, score, count.intValue())))); + score -> { + var count = SolverMetricUtil.getGaugeValue(this, SolverMetricUtil.getGaugeName(metric, "count"), + constraintMatchTotalRunId); + if (count != null) { + constraintMatchTotalConsumer + .accept(new ConstraintSummary(constraintRef, score.raw(), count.intValue())); + } + }); }); } - public void getGaugeValue(SolverMetric metric, Tags runId, Consumer gaugeConsumer) { - getGaugeValue(metric.getMeterId(), runId, gaugeConsumer); - } - - public void getGaugeValue(String meterId, Tags runId, Consumer gaugeConsumer) { - var gauge = this.find(meterId).tags(runId).gauge(); - if (gauge != null && Double.isFinite(gauge.value())) { - gaugeConsumer.accept(gauge.value()); - } - } - public void extractMoveCountPerType(SolverScope solverScope, ObjLongConsumer gaugeConsumer) { solverScope.getMoveCountTypes().forEach(type -> { var gauge = this.find(SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + type) diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/bestscore/BestScoreSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/bestscore/BestScoreSubSingleStatistic.java index 5115b967007..1edee6ffe3c 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/bestscore/BestScoreSubSingleStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/bestscore/BestScoreSubSingleStatistic.java @@ -32,7 +32,7 @@ public void open(StatisticRegistry registry, Tags runTag) { registry.addListener(SolverMetric.BEST_SCORE, timestamp -> registry.extractScoreFromMeters(SolverMetric.BEST_SCORE, runTag, score -> pointList - .add(new BestScoreStatisticPoint(timestamp, score, subSingleBenchmarkResult.isInitialized())))); + .add(new BestScoreStatisticPoint(timestamp, score.raw(), score.isFullyAssigned())))); } // ************************************************************************ diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/bestsolutionmutation/BestSolutionMutationSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/bestsolutionmutation/BestSolutionMutationSubSingleStatistic.java index 3bdbe9072c0..7b2cb651dc9 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/bestsolutionmutation/BestSolutionMutationSubSingleStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/bestsolutionmutation/BestSolutionMutationSubSingleStatistic.java @@ -9,6 +9,7 @@ import ai.timefold.solver.benchmark.impl.statistic.StatisticRegistry; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; +import ai.timefold.solver.core.impl.solver.monitoring.SolverMetricUtil; import io.micrometer.core.instrument.Tags; @@ -30,9 +31,12 @@ public BestSolutionMutationSubSingleStatistic(SubSingleBenchmarkResult subSingle @Override public void open(StatisticRegistry registry, Tags runTag) { registry.addListener(SolverMetric.BEST_SOLUTION_MUTATION, - timestamp -> registry.getGaugeValue(SolverMetric.BEST_SOLUTION_MUTATION, runTag, - mutationCount -> pointList - .add(new BestSolutionMutationStatisticPoint(timestamp, mutationCount.intValue())))); + timestamp -> { + var mutationCount = SolverMetricUtil.getGaugeValue(registry, SolverMetric.BEST_SOLUTION_MUTATION, runTag); + if (mutationCount != null) { + pointList.add(new BestSolutionMutationStatisticPoint(timestamp, mutationCount.intValue())); + } + }); } // ************************************************************************ diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractCalculationSpeedSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractCalculationSpeedSubSingleStatistic.java index feffe454cd4..f400fc51df7 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractCalculationSpeedSubSingleStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractCalculationSpeedSubSingleStatistic.java @@ -10,6 +10,7 @@ import ai.timefold.solver.benchmark.impl.statistic.StatisticRegistry; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; +import ai.timefold.solver.core.impl.solver.monitoring.SolverMetricUtil; import io.micrometer.core.instrument.Tags; @@ -44,7 +45,8 @@ public void open(StatisticRegistry registry, Tags runTag) { @Override public void accept(Long timeMillisSpent) { if (timeMillisSpent >= nextTimeMillisThreshold) { - registry.getGaugeValue(solverMetric, runTag, countNumber -> { + var countNumber = SolverMetricUtil.getGaugeValue(registry, solverMetric, runTag); + if (countNumber != null) { var moveEvaluationCount = countNumber.longValue(); var countInterval = moveEvaluationCount - lastCalculationCount.get(); var timeMillisSpentInterval = timeMillisSpent - lastTimeMillisSpent; @@ -55,7 +57,7 @@ public void accept(Long timeMillisSpent) { var speed = countInterval * 1000L / timeMillisSpentInterval; pointList.add(new LongStatisticPoint(timeMillisSpent, speed)); lastCalculationCount.set(moveEvaluationCount); - }); + } lastTimeMillisSpent = timeMillisSpent; nextTimeMillisThreshold += timeMillisThresholdInterval; if (nextTimeMillisThreshold < timeMillisSpent) { diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/memoryuse/MemoryUseSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/memoryuse/MemoryUseSubSingleStatistic.java index ef600b9b737..0ef46a2170b 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/memoryuse/MemoryUseSubSingleStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/memoryuse/MemoryUseSubSingleStatistic.java @@ -10,6 +10,7 @@ import ai.timefold.solver.benchmark.impl.statistic.StatisticRegistry; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; +import ai.timefold.solver.core.impl.solver.monitoring.SolverMetricUtil; import io.micrometer.core.instrument.Tags; @@ -58,10 +59,11 @@ public MemoryUseSubSingleStatisticListener(StatisticRegistry registry, Tags t @Override public void accept(Long timeMillisSpent) { if (timeMillisSpent >= nextTimeMillisThreshold) { - registry.getGaugeValue(SolverMetric.MEMORY_USE, tags, - memoryUse -> pointList.add( - new MemoryUseStatisticPoint(timeMillisSpent, memoryUse.longValue(), - (long) registry.find("jvm.memory.max").tags(tags).gauge().value()))); + var memoryUse = SolverMetricUtil.getGaugeValue(registry, SolverMetric.MEMORY_USE, tags); + if (memoryUse != null) { + var max = SolverMetricUtil.getGaugeValue(registry, "jvm.memory.max", tags); + pointList.add(new MemoryUseStatisticPoint(timeMillisSpent, memoryUse.longValue(), max.longValue())); + } nextTimeMillisThreshold += timeMillisThresholdInterval; if (nextTimeMillisThreshold < timeMillisSpent) { diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountperstep/MoveCountPerStepSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountperstep/MoveCountPerStepSubSingleStatistic.java index 061977e84be..533d5288fa7 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountperstep/MoveCountPerStepSubSingleStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountperstep/MoveCountPerStepSubSingleStatistic.java @@ -9,6 +9,7 @@ import ai.timefold.solver.benchmark.impl.statistic.StatisticRegistry; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; +import ai.timefold.solver.core.impl.solver.monitoring.SolverMetricUtil; import io.micrometer.core.instrument.Tags; @@ -30,10 +31,18 @@ public MoveCountPerStepSubSingleStatistic(SubSingleBenchmarkResult subSingleBenc @Override public void open(StatisticRegistry registry, Tags runTag) { registry.addListener(SolverMetric.MOVE_COUNT_PER_STEP, - timeMillisSpent -> registry.getGaugeValue(SolverMetric.MOVE_COUNT_PER_STEP.getMeterId() + ".accepted", runTag, - accepted -> registry.getGaugeValue(SolverMetric.MOVE_COUNT_PER_STEP.getMeterId() + ".selected", runTag, - selected -> pointList.add(new MoveCountPerStepStatisticPoint(timeMillisSpent, - accepted.longValue(), selected.longValue()))))); + timeMillisSpent -> { + var accepted = SolverMetricUtil.getGaugeValue(registry, + SolverMetric.MOVE_COUNT_PER_STEP.getMeterId() + ".accepted", runTag); + if (accepted != null) { + var selected = SolverMetricUtil.getGaugeValue(registry, + SolverMetric.MOVE_COUNT_PER_STEP.getMeterId() + ".selected", runTag); + if (selected != null) { + pointList.add(new MoveCountPerStepStatisticPoint(timeMillisSpent, accepted.longValue(), + selected.longValue())); + } + } + }); } // ************************************************************************ diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/stepscore/StepScoreSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/stepscore/StepScoreSubSingleStatistic.java index 6a2a86f4801..efa63c407ad 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/stepscore/StepScoreSubSingleStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/stepscore/StepScoreSubSingleStatistic.java @@ -31,8 +31,8 @@ public StepScoreSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkRe public void open(StatisticRegistry registry, Tags runTag) { registry.addListener(SolverMetric.STEP_SCORE, timeMillisSpent -> registry.extractScoreFromMeters(SolverMetric.STEP_SCORE, runTag, - score -> pointList.add(new StepScoreStatisticPoint(timeMillisSpent, score, - subSingleBenchmarkResult.isInitialized())))); + score -> pointList + .add(new StepScoreStatisticPoint(timeMillisSpent, score.raw(), score.isFullyAssigned())))); } // ************************************************************************ diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/pickedmovetypebestscore/PickedMoveTypeBestScoreDiffSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/pickedmovetypebestscore/PickedMoveTypeBestScoreDiffSubSingleStatistic.java index 700a94bcc08..738e0592c64 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/pickedmovetypebestscore/PickedMoveTypeBestScoreDiffSubSingleStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/pickedmovetypebestscore/PickedMoveTypeBestScoreDiffSubSingleStatistic.java @@ -35,7 +35,7 @@ public void open(StatisticRegistry registry, Tags runTag) { registry.extractScoreFromMeters(SolverMetric.PICKED_MOVE_TYPE_BEST_SCORE_DIFF, runTag.and(Tag.of("move.type", moveType)), score -> pointList.add(new PickedMoveTypeBestScoreDiffStatisticPoint( - timeMillisSpent, moveType, score))); + timeMillisSpent, moveType, score.raw()))); } }); } diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/pickedmovetypestepscore/PickedMoveTypeStepScoreDiffSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/pickedmovetypestepscore/PickedMoveTypeStepScoreDiffSubSingleStatistic.java index 9657a08147b..4fb1a0d8e97 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/pickedmovetypestepscore/PickedMoveTypeStepScoreDiffSubSingleStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/subsingle/pickedmovetypestepscore/PickedMoveTypeStepScoreDiffSubSingleStatistic.java @@ -35,7 +35,7 @@ public void open(StatisticRegistry registry, Tags runTag) { registry.extractScoreFromMeters(SolverMetric.PICKED_MOVE_TYPE_STEP_SCORE_DIFF, runTag.and(Tag.of("move.type", moveType)), score -> pointList.add(new PickedMoveTypeStepScoreDiffStatisticPoint( - timeMillisSpent, moveType, score))); + timeMillisSpent, moveType, score.raw()))); } }); } diff --git a/benchmark/src/test/java/ai/timefold/solver/benchmark/api/PlannerBenchmarkTest.java b/benchmark/src/test/java/ai/timefold/solver/benchmark/api/PlannerBenchmarkTest.java new file mode 100644 index 00000000000..f4c906c6c25 --- /dev/null +++ b/benchmark/src/test/java/ai/timefold/solver/benchmark/api/PlannerBenchmarkTest.java @@ -0,0 +1,81 @@ +package ai.timefold.solver.benchmark.api; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; + +import ai.timefold.solver.benchmark.config.PlannerBenchmarkConfig; +import ai.timefold.solver.benchmark.config.SolverBenchmarkConfig; +import ai.timefold.solver.benchmark.impl.DefaultPlannerBenchmark; +import ai.timefold.solver.core.config.solver.SolverConfig; +import ai.timefold.solver.core.config.solver.termination.TerminationConfig; +import ai.timefold.solver.core.testdomain.TestdataConstraintProvider; +import ai.timefold.solver.core.testdomain.TestdataEntity; +import ai.timefold.solver.core.testdomain.TestdataSolution; +import ai.timefold.solver.core.testdomain.TestdataValue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class PlannerBenchmarkTest { + + @Test + void runPlannerBenchmark(@TempDir Path benchmarkTestDir) { + var benchmarkConfig = new PlannerBenchmarkConfig(); + benchmarkConfig.setBenchmarkDirectory(benchmarkTestDir.toFile()); + benchmarkConfig.setWarmUpMillisecondsSpentLimit(1L); // Minimize warmup. + var inheritedSolverConfig = new SolverBenchmarkConfig(); + inheritedSolverConfig.setSolverConfig(new SolverConfig() + .withSolutionClass(TestdataSolution.class) + .withEntityClasses(TestdataEntity.class) + .withConstraintProviderClass(TestdataConstraintProvider.class) + // Only run for a short amount of time. + .withTerminationConfig(new TerminationConfig().withUnimprovedMillisecondsSpentLimit(100L))); + benchmarkConfig.setInheritedSolverBenchmarkConfig(inheritedSolverConfig); + benchmarkConfig.setSolverBenchmarkConfigList(List.of(new SolverBenchmarkConfig())); + var benchmarkFactory = PlannerBenchmarkFactory.create(benchmarkConfig); + + var solution1 = new TestdataSolution("s1"); + solution1.setEntityList(Arrays.asList(new TestdataEntity("e1"), new TestdataEntity("e2"), new TestdataEntity("e3"))); + solution1.setValueList(Arrays.asList(new TestdataValue("v1"), new TestdataValue("v2"))); + + var plannerBenchmark = (DefaultPlannerBenchmark) benchmarkFactory.buildPlannerBenchmark(solution1); + plannerBenchmark.benchmark(); // Run the benchmark. + var folder = plannerBenchmark.getBenchmarkReport().getHtmlOverviewFile() + .toPath() + .getParent(); + var csv = folder.resolve(Path.of("Problem_0", "Config_0", "sub0", "BEST_SCORE.csv")); + assertThat(csv).exists(); + + try (var lines = Files.lines(csv)) { + var lineList = lines.toList(); + assertThat(lineList).hasSizeGreaterThan(1); + assertSoftly(softly -> { + // Proper header. + softly.assertThat(lineList) + .first() + .isEqualTo(""" + "timeMillisSpent","score","initialized" + """.trim()); + // Checks that best score was recorded at least once. + // Requires LS to have started, as CH does not store best score. + // We only check score+initialized, as "timeMillisSpent" can be anything. + softly.assertThat(lineList) + .last() + .asString() + .endsWith(""" + "-3","true" + """.trim()); + }); + } catch (IOException e) { + fail(e); + } + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/api/solver/event/BestSolutionChangedEvent.java b/core/src/main/java/ai/timefold/solver/core/api/solver/event/BestSolutionChangedEvent.java index fc5589e1097..4cc2f6a0056 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/solver/event/BestSolutionChangedEvent.java +++ b/core/src/main/java/ai/timefold/solver/core/api/solver/event/BestSolutionChangedEvent.java @@ -15,6 +15,7 @@ * * @param the solution type, the class with the {@link PlanningSolution} annotation */ +// TODO In Solver 2.0, maybe convert this to an interface. public class BestSolutionChangedEvent extends EventObject { private final Solver solver; @@ -25,7 +26,7 @@ public class BestSolutionChangedEvent extends EventObject { /** * @param timeMillisSpent {@code >= 0L} - * @deprecated Use {@link #BestSolutionChangedEvent(Solver, long, Object, Score, boolean)} instead. + * @deprecated Users should not manually construct instances of this event. */ @Deprecated(forRemoval = true, since = "1.22.0") public BestSolutionChangedEvent(@NonNull Solver solver, long timeMillisSpent, @@ -35,7 +36,9 @@ public BestSolutionChangedEvent(@NonNull Solver solver, long timeMill /** * @param timeMillisSpent {@code >= 0L} + * @deprecated Users should not manually construct instances of this event. */ + @Deprecated(forRemoval = true, since = "1.23.0") public BestSolutionChangedEvent(@NonNull Solver solver, long timeMillisSpent, @NonNull Solution_ newBestSolution, @NonNull Score newBestScore, boolean isNewBestSolutionInitialized) { diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java b/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java index 54e7c46e2b4..8b60f38ebb4 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java @@ -1,35 +1,26 @@ package ai.timefold.solver.core.config.solver.monitoring; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.ToDoubleFunction; import jakarta.xml.bind.annotation.XmlEnum; -import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.solver.Solver; -import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; +import ai.timefold.solver.core.impl.solver.monitoring.statistic.BestScoreStatistic; +import ai.timefold.solver.core.impl.solver.monitoring.statistic.BestSolutionMutationCountStatistic; +import ai.timefold.solver.core.impl.solver.monitoring.statistic.MemoryUseStatistic; +import ai.timefold.solver.core.impl.solver.monitoring.statistic.MoveCountPerTypeStatistic; +import ai.timefold.solver.core.impl.solver.monitoring.statistic.PickedMoveBestScoreDiffStatistic; +import ai.timefold.solver.core.impl.solver.monitoring.statistic.PickedMoveStepScoreDiffStatistic; +import ai.timefold.solver.core.impl.solver.monitoring.statistic.SolverScopeStatistic; +import ai.timefold.solver.core.impl.solver.monitoring.statistic.SolverStatistic; +import ai.timefold.solver.core.impl.solver.monitoring.statistic.StatelessSolverStatistic; import ai.timefold.solver.core.impl.solver.scope.SolverScope; -import ai.timefold.solver.core.impl.statistic.BestScoreStatistic; -import ai.timefold.solver.core.impl.statistic.BestSolutionMutationCountStatistic; -import ai.timefold.solver.core.impl.statistic.MemoryUseStatistic; -import ai.timefold.solver.core.impl.statistic.MoveCountPerTypeStatistic; -import ai.timefold.solver.core.impl.statistic.PickedMoveBestScoreDiffStatistic; -import ai.timefold.solver.core.impl.statistic.PickedMoveStepScoreDiffStatistic; -import ai.timefold.solver.core.impl.statistic.SolverScopeStatistic; -import ai.timefold.solver.core.impl.statistic.SolverStatistic; -import ai.timefold.solver.core.impl.statistic.StatelessSolverStatistic; import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.NullMarked; - -import io.micrometer.core.instrument.Metrics; -import io.micrometer.core.instrument.Tags; @XmlEnum public enum SolverMetric { + SOLVE_DURATION("timefold.solver.solve.duration", false), ERROR_COUNT("timefold.solver.errors", false), SCORE_CALCULATION_COUNT("timefold.solver.score.calculation.count", @@ -97,30 +88,6 @@ public enum SolverMetric { return meterId; } - @NullMarked - public static void registerScoreMetrics(SolverMetric metric, Tags tags, ScoreDefinition scoreDefinition, - Map>> tagToScoreLevels, Score innerScore) { - Number[] levelValues = innerScore.toLevelNumbers(); - if (tagToScoreLevels.containsKey(tags)) { - List> scoreLevels = tagToScoreLevels.get(tags); - for (int i = 0; i < levelValues.length; i++) { - scoreLevels.get(i).set(levelValues[i]); - } - } else { - String[] levelLabels = scoreDefinition.getLevelLabels(); - for (int i = 0; i < levelLabels.length; i++) { - levelLabels[i] = levelLabels[i].replace(' ', '.'); - } - List> scoreLevels = new ArrayList<>(levelValues.length); - for (int i = 0; i < levelValues.length; i++) { - scoreLevels.add(Metrics.gauge(metric.getMeterId() + "." + levelLabels[i], - tags, new AtomicReference<>(levelValues[i]), - ar -> ar.get().doubleValue())); - } - tagToScoreLevels.put(tags, scoreLevels); - } - } - public boolean isMetricBestSolutionBased() { return isBestSolutionBased; } @@ -130,7 +97,6 @@ public boolean isMetricConstraintMatchBased() { } @SuppressWarnings("unchecked") - // TODO: clarify @NonNull ok here public void register(@NonNull Solver solver) { registerFunction.register(solver); } @@ -139,4 +105,5 @@ public void register(@NonNull Solver solver) { public void unregister(@NonNull Solver solver) { registerFunction.unregister(solver); } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java index b5c97a015d2..bb3c691c183 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java @@ -1,12 +1,11 @@ package ai.timefold.solver.core.impl.localsearch; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; @@ -16,6 +15,9 @@ import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; import ai.timefold.solver.core.impl.phase.AbstractPhase; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; +import ai.timefold.solver.core.impl.score.director.InnerScore; +import ai.timefold.solver.core.impl.solver.monitoring.ScoreLevels; +import ai.timefold.solver.core.impl.solver.monitoring.SolverMetricUtil; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.impl.solver.termination.PhaseTermination; @@ -35,8 +37,8 @@ public class DefaultLocalSearchPhase extends AbstractPhase protected final AtomicLong selectedMoveCountPerStep = new AtomicLong(0); protected final Map constraintMatchTotalTagsToStepCount = new ConcurrentHashMap<>(); protected final Map constraintMatchTotalTagsToBestCount = new ConcurrentHashMap<>(); - protected final Map>> constraintMatchTotalStepScoreMap = new ConcurrentHashMap<>(); - protected final Map>> constraintMatchTotalBestScoreMap = new ConcurrentHashMap<>(); + protected final Map constraintMatchTotalStepScoreMap = new ConcurrentHashMap<>(); + protected final Map constraintMatchTotalBestScoreMap = new ConcurrentHashMap<>(); private DefaultLocalSearchPhase(Builder builder) { super(builder); @@ -181,9 +183,10 @@ private void collectMetrics(LocalSearchStepScope stepScope) { } } - private void collectConstraintMatchTotalMetrics(SolverMetric metric, Tags tags, Map countMap, - Map>> scoreMap, ConstraintMatchTotal constraintMatchTotal, - ScoreDefinition scoreDefinition, SolverScope solverScope) { + private > void collectConstraintMatchTotalMetrics(SolverMetric metric, Tags tags, + Map countMap, Map scoreMap, + ConstraintMatchTotal constraintMatchTotal, ScoreDefinition scoreDefinition, + SolverScope solverScope) { if (solverScope.isMetricEnabled(metric)) { if (countMap.containsKey(tags)) { countMap.get(tags).set(constraintMatchTotal.getConstraintMatchCount()); @@ -193,7 +196,8 @@ private void collectConstraintMatchTotalMetrics(SolverMetric metric, Tags tags, Metrics.gauge(metric.getMeterId() + ".count", tags, count); } - SolverMetric.registerScoreMetrics(metric, tags, scoreDefinition, scoreMap, constraintMatchTotal.getScore()); + SolverMetricUtil.registerScore(metric, tags, scoreDefinition, scoreMap, + InnerScore.fullyAssigned(constraintMatchTotal.getScore())); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java index a2f4915717c..fac2ae9e9b5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.phase; +import java.util.Map; + import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.config.solver.EnvironmentMode; @@ -9,8 +11,11 @@ import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleSupport; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; +import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; import ai.timefold.solver.core.impl.solver.exception.ScoreCorruptionException; import ai.timefold.solver.core.impl.solver.exception.VariableCorruptionException; +import ai.timefold.solver.core.impl.solver.monitoring.ScoreLevels; +import ai.timefold.solver.core.impl.solver.monitoring.SolverMetricUtil; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.impl.solver.termination.PhaseTermination; import ai.timefold.solver.core.impl.solver.termination.Termination; @@ -18,6 +23,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.micrometer.core.instrument.Tags; + /** * @param the solution type, the class with the {@link PlanningSolution} annotation * @see DefaultLocalSearchPhase @@ -192,12 +199,12 @@ public void stepEnded(AbstractStepScope stepScope) { private static void collectMetrics(AbstractStepScope stepScope) { var solverScope = stepScope.getPhaseScope().getSolverScope(); - if (solverScope.isMetricEnabled(SolverMetric.STEP_SCORE) && stepScope.getScore().fullyAssigned()) { - SolverMetric.registerScoreMetrics(SolverMetric.STEP_SCORE, - solverScope.getMonitoringTags(), - solverScope.getScoreDefinition(), - solverScope.getStepScoreMap(), - stepScope.getScore().raw()); + if (solverScope.isMetricEnabled(SolverMetric.STEP_SCORE) && stepScope.getScore().isFullyAssigned()) { + Tags tags = solverScope.getMonitoringTags(); + ScoreDefinition scoreDefinition = solverScope.getScoreDefinition(); + Map tagToScoreLevels = solverScope.getStepScoreMap(); + SolverMetricUtil.registerScore(SolverMetric.STEP_SCORE, tags, scoreDefinition, tagToScoreLevels, + stepScope.getScore()); } } @@ -216,7 +223,7 @@ public void removePhaseLifecycleListener(PhaseLifecycleListener phase // ************************************************************************ protected void assertWorkingSolutionInitialized(AbstractPhaseScope phaseScope) { - if (!phaseScope.getStartingScore().fullyAssigned()) { + if (!phaseScope.getStartingScore().isFullyAssigned()) { var scoreDirector = phaseScope.getScoreDirector(); var solutionDescriptor = scoreDirector.getSolutionDescriptor(); var workingSolution = scoreDirector.getWorkingSolution(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/DefaultScoreExplanation.java b/core/src/main/java/ai/timefold/solver/core/impl/score/DefaultScoreExplanation.java index c16d7d32102..3b5a09e2f63 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/DefaultScoreExplanation.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/DefaultScoreExplanation.java @@ -49,7 +49,7 @@ public static > String explainScore(InnerScore> constraintMatchTotalComparator = comparing(ConstraintMatchTotal::getScore); Comparator> constraintMatchComparator = comparing(ConstraintMatch::getScore); @@ -163,7 +163,7 @@ public DefaultScoreExplanation(Solution_ solution, InnerScore innerScore @Override public boolean isInitialized() { - return innerScore.fullyAssigned(); + return innerScore.isFullyAssigned(); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScore.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScore.java index 0a9c84cd3e4..3bf634e3902 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScore.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScore.java @@ -7,6 +7,8 @@ import ai.timefold.solver.core.api.domain.variable.PlanningVariable; import ai.timefold.solver.core.api.score.Score; +import org.jspecify.annotations.NullMarked; + /** * Carries information on if the {@link PlanningSolution} of this score was fully initialized when it was calculated. * This only works for solutions where: @@ -19,6 +21,7 @@ * * For solutions which do allow unassigning values, {@link #unassignedCount} is always zero. */ +@NullMarked public record InnerScore>(Score_ raw, int unassignedCount) implements Comparable> { @@ -39,7 +42,7 @@ public static > InnerScore withUnassignedCo } } - public boolean fullyAssigned() { + public boolean isFullyAssigned() { return unassignedCount == 0; } @@ -55,6 +58,6 @@ public int compareTo(InnerScore other) { @Override public String toString() { - return fullyAssigned() ? raw.toString() : "-%dinit/%s".formatted(unassignedCount, raw); + return isFullyAssigned() ? raw.toString() : "-%dinit/%s".formatted(unassignedCount, raw); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java index 4ca62d2dbb7..a7c3d7b033c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java @@ -367,7 +367,7 @@ default ScoreAnalysis buildScoreAnalysis(ScoreAnalysisFetchPolicy scoreA var constraintAnalysis = getConstraintAnalysis(constraintMatchTotal, scoreAnalysisFetchPolicy); constraintAnalysisMap.put(constraintMatchTotal.getConstraintRef(), constraintAnalysis); } - return new ScoreAnalysis<>(state.raw(), constraintAnalysisMap, state.fullyAssigned()); + return new ScoreAnalysis<>(state.raw(), constraintAnalysisMap, state.isFullyAssigned()); } /* diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/event/DefaultBestSolutionChangedEvent.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/event/DefaultBestSolutionChangedEvent.java new file mode 100644 index 00000000000..ecfa39ba6b7 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/event/DefaultBestSolutionChangedEvent.java @@ -0,0 +1,23 @@ +package ai.timefold.solver.core.impl.solver.event; + +import ai.timefold.solver.core.api.solver.Solver; +import ai.timefold.solver.core.api.solver.event.BestSolutionChangedEvent; +import ai.timefold.solver.core.impl.score.director.InnerScore; + +import org.jspecify.annotations.NonNull; + +public final class DefaultBestSolutionChangedEvent extends BestSolutionChangedEvent { + + private final int unassignedCount; + + public DefaultBestSolutionChangedEvent(@NonNull Solver solver, long timeMillisSpent, + @NonNull Solution_ newBestSolution, @NonNull InnerScore newBestScore) { + super(solver, timeMillisSpent, newBestSolution, newBestScore.raw(), newBestScore.isFullyAssigned()); + this.unassignedCount = newBestScore.unassignedCount(); + } + + public int getUnassignedCount() { + return unassignedCount; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/event/SolverEventSupport.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/event/SolverEventSupport.java index a1357b1504b..591933a1b51 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/event/SolverEventSupport.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/event/SolverEventSupport.java @@ -2,7 +2,6 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.solver.Solver; -import ai.timefold.solver.core.api.solver.event.BestSolutionChangedEvent; import ai.timefold.solver.core.api.solver.event.SolverEventListener; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -27,8 +26,7 @@ public void fireBestSolutionChanged(SolverScope solverScope, Solution // We need to clone the new best solution, or we may share the same instance with user consumers. // Reusing the instance can lead to inconsistent states if intermediary consumers change the solution. var newBestSolutionCloned = solverScope.getScoreDirector().cloneSolution(newBestSolution); - var event = new BestSolutionChangedEvent<>(solver, timeMillisSpent, newBestSolutionCloned, bestScore.raw(), - bestScore.fullyAssigned()); + var event = new DefaultBestSolutionChangedEvent<>(solver, timeMillisSpent, newBestSolutionCloned, bestScore); do { it.next().bestSolutionChanged(event); } while (it.hasNext()); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/ScoreLevels.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/ScoreLevels.java new file mode 100644 index 00000000000..0cee9dd5066 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/ScoreLevels.java @@ -0,0 +1,33 @@ +package ai.timefold.solver.core.impl.solver.monitoring; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +public final class ScoreLevels { + + final AtomicReference[] levelValues; + final AtomicInteger unassignedCount; + + @SuppressWarnings("unchecked") + ScoreLevels(int unassignedCount, Number[] levelValues) { + // We store the values inside a constant reference, + // so that the metric can always load the latest value. + // If we stored the value directly and just overwrote it, + // the metric would always hold a reference to the old value, + // effectively ignoring the update. + this.unassignedCount = new AtomicInteger(unassignedCount); + this.levelValues = Arrays.stream(levelValues) + .map(AtomicReference::new) + .toArray(AtomicReference[]::new); + } + + void setLevelValue(int level, Number value) { + levelValues[level].set(value); + } + + void setUnassignedCount(int unassignedCount) { + this.unassignedCount.set(unassignedCount); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/SolverMetricUtil.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/SolverMetricUtil.java new file mode 100644 index 00000000000..145a93589aa --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/SolverMetricUtil.java @@ -0,0 +1,100 @@ +package ai.timefold.solver.core.impl.solver.monitoring; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; +import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; +import ai.timefold.solver.core.impl.score.director.InnerScore; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; + +@NullMarked +public final class SolverMetricUtil { + + // Necessary for benchmarker, but otherwise undocumented and not considered public. + private static final String UNASSIGNED_COUNT_LABEL = "unassigned.count"; + + public static > void registerScore(SolverMetric metric, Tags tags, + ScoreDefinition scoreDefinition, Map tagToScoreLevels, InnerScore innerScore) { + var levelValues = innerScore.raw().toLevelNumbers(); + if (tagToScoreLevels.containsKey(tags)) { + // Set new score levels for the previously registered gauges to read. + var scoreLevels = tagToScoreLevels.get(tags); + scoreLevels.setUnassignedCount(innerScore.unassignedCount()); + for (var i = 0; i < levelValues.length; i++) { + scoreLevels.setLevelValue(i, levelValues[i]); + } + } else { + var levelLabels = getLevelLabels(scoreDefinition); + var scoreLevels = new Number[levelLabels.length]; + System.arraycopy(levelValues, 0, scoreLevels, 0, levelValues.length); + var result = new ScoreLevels(innerScore.unassignedCount(), scoreLevels); + tagToScoreLevels.put(tags, result); + + // Register the gauges to read the score levels. + Metrics.gauge(getGaugeName(metric, UNASSIGNED_COUNT_LABEL), tags, result.unassignedCount, + AtomicInteger::doubleValue); + for (var i = 0; i < levelValues.length; i++) { + Metrics.gauge(getGaugeName(metric, levelLabels[i]), tags, result.levelValues[i], + ref -> ref.get().doubleValue()); + } + } + } + + private static String[] getLevelLabels(ScoreDefinition scoreDefinition) { + var labelNames = scoreDefinition.getLevelLabels(); + for (var i = 0; i < labelNames.length; i++) { + labelNames[i] = labelNames[i].replace(' ', '.'); + } + return labelNames; + } + + public static String getGaugeName(SolverMetric metric, String label) { + return metric.getMeterId() + "." + label; + } + + public static @Nullable Double getGaugeValue(MeterRegistry registry, SolverMetric metric, Tags runId) { + return getGaugeValue(registry, metric.getMeterId(), runId); + } + + public static @Nullable Double getGaugeValue(MeterRegistry registry, String meterId, Tags runId) { + var gauge = registry.find(meterId).tags(runId).gauge(); + if (gauge != null && Double.isFinite(gauge.value())) { + return gauge.value(); + } else { + return null; + } + } + + public static > @Nullable InnerScore extractScore(SolverMetric metric, + ScoreDefinition scoreDefinition, Function scoreLevelFunction) { + var levelLabels = getLevelLabels(scoreDefinition); + var levelNumbers = new Number[levelLabels.length]; + for (var i = 0; i < levelLabels.length; i++) { + var levelNumber = scoreLevelFunction.apply(getGaugeName(metric, levelLabels[i])); + if (levelNumber == null) { + return null; + } + levelNumbers[i] = levelNumber; + } + var score = scoreDefinition.fromLevelNumbers(levelNumbers); + var unassignedCount = scoreLevelFunction.apply(getGaugeName(metric, UNASSIGNED_COUNT_LABEL)); + if (unassignedCount == null) { + return null; + } + return InnerScore.withUnassignedCount(score, unassignedCount.intValue()); + } + + private SolverMetricUtil() { + // No external instances. + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/statistic/BestScoreStatistic.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/BestScoreStatistic.java similarity index 65% rename from core/src/main/java/ai/timefold/solver/core/impl/statistic/BestScoreStatistic.java rename to core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/BestScoreStatistic.java index 3c126981242..6ecfe5322b7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/statistic/BestScoreStatistic.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/BestScoreStatistic.java @@ -1,21 +1,23 @@ -package ai.timefold.solver.core.impl.statistic; +package ai.timefold.solver.core.impl.solver.monitoring.statistic; -import java.util.List; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.api.solver.event.SolverEventListener; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; +import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.impl.solver.DefaultSolver; +import ai.timefold.solver.core.impl.solver.event.DefaultBestSolutionChangedEvent; +import ai.timefold.solver.core.impl.solver.monitoring.ScoreLevels; +import ai.timefold.solver.core.impl.solver.monitoring.SolverMetricUtil; import io.micrometer.core.instrument.Tags; public class BestScoreStatistic implements SolverStatistic { - private final Map>> tagsToBestScoreMap = new ConcurrentHashMap<>(); + private final Map tagsToBestScoreMap = new ConcurrentHashMap<>(); private final Map, SolverEventListener> solverToEventListenerMap = new WeakHashMap<>(); @Override @@ -38,8 +40,11 @@ public void register(Solver solver) { var scoreDefinition = defaultSolver.getSolverScope().getScoreDefinition(); var tags = extractTags(solver); SolverEventListener listener = - event -> SolverMetric.registerScoreMetrics(SolverMetric.BEST_SCORE, tags, scoreDefinition, tagsToBestScoreMap, - event.getNewBestScore()); + event -> { + var castEvent = (DefaultBestSolutionChangedEvent) event; + SolverMetricUtil.registerScore(SolverMetric.BEST_SCORE, tags, scoreDefinition, tagsToBestScoreMap, + InnerScore.withUnassignedCount(event.getNewBestScore(), castEvent.getUnassignedCount())); + }; solverToEventListenerMap.put(defaultSolver, listener); defaultSolver.addEventListener(listener); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/statistic/BestSolutionMutationCountStatistic.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/BestSolutionMutationCountStatistic.java similarity index 97% rename from core/src/main/java/ai/timefold/solver/core/impl/statistic/BestSolutionMutationCountStatistic.java rename to core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/BestSolutionMutationCountStatistic.java index 454390ca47d..7cd43c5bdaa 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/statistic/BestSolutionMutationCountStatistic.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/BestSolutionMutationCountStatistic.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.statistic; +package ai.timefold.solver.core.impl.solver.monitoring.statistic; import java.util.Map; import java.util.WeakHashMap; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/statistic/MemoryUseStatistic.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/MemoryUseStatistic.java similarity index 91% rename from core/src/main/java/ai/timefold/solver/core/impl/statistic/MemoryUseStatistic.java rename to core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/MemoryUseStatistic.java index 3adaf334996..5fbfdc9c21d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/statistic/MemoryUseStatistic.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/MemoryUseStatistic.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.statistic; +package ai.timefold.solver.core.impl.solver.monitoring.statistic; import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.impl.solver.DefaultSolver; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/statistic/MoveCountPerTypeStatistic.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/MoveCountPerTypeStatistic.java similarity index 98% rename from core/src/main/java/ai/timefold/solver/core/impl/statistic/MoveCountPerTypeStatistic.java rename to core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/MoveCountPerTypeStatistic.java index 5e1db3ed45a..b51f171fb04 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/statistic/MoveCountPerTypeStatistic.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/MoveCountPerTypeStatistic.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.statistic; +package ai.timefold.solver.core.impl.solver.monitoring.statistic; import java.util.HashMap; import java.util.Map; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/statistic/PickedMoveBestScoreDiffStatistic.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/PickedMoveBestScoreDiffStatistic.java similarity index 83% rename from core/src/main/java/ai/timefold/solver/core/impl/statistic/PickedMoveBestScoreDiffStatistic.java rename to core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/PickedMoveBestScoreDiffStatistic.java index 9022a0454da..2c0682f3957 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/statistic/PickedMoveBestScoreDiffStatistic.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/PickedMoveBestScoreDiffStatistic.java @@ -1,10 +1,8 @@ -package ai.timefold.solver.core.impl.statistic; +package ai.timefold.solver.core.impl.solver.monitoring.statistic; -import java.util.List; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.solver.Solver; @@ -15,7 +13,10 @@ import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; +import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.impl.solver.DefaultSolver; +import ai.timefold.solver.core.impl.solver.monitoring.ScoreLevels; +import ai.timefold.solver.core.impl.solver.monitoring.SolverMetricUtil; import io.micrometer.core.instrument.Tags; @@ -47,7 +48,7 @@ private static class PickedMoveBestScoreDiffStatisticListener scoreDefinition; - private final Map>> tagsToMoveScoreMap = new ConcurrentHashMap<>(); + private final Map tagsToMoveScoreMap = new ConcurrentHashMap<>(); public PickedMoveBestScoreDiffStatisticListener(ScoreDefinition scoreDefinition) { this.scoreDefinition = scoreDefinition; @@ -80,12 +81,10 @@ private void localSearchStepEnded(LocalSearchStepScope stepScope) { var newBestScore = stepScope. getScore().raw(); var bestScoreDiff = newBestScore.subtract(oldBestScore); oldBestScore = newBestScore; - SolverMetric.registerScoreMetrics(SolverMetric.PICKED_MOVE_TYPE_BEST_SCORE_DIFF, - stepScope.getPhaseScope().getSolverScope().getMonitoringTags() - .and("move.type", moveType), - scoreDefinition, - tagsToMoveScoreMap, - bestScoreDiff); + var tags = stepScope.getPhaseScope().getSolverScope().getMonitoringTags() + .and("move.type", moveType); + SolverMetricUtil.registerScore(SolverMetric.PICKED_MOVE_TYPE_BEST_SCORE_DIFF, tags, scoreDefinition, + tagsToMoveScoreMap, InnerScore.fullyAssigned(bestScoreDiff)); } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/statistic/PickedMoveStepScoreDiffStatistic.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/PickedMoveStepScoreDiffStatistic.java similarity index 83% rename from core/src/main/java/ai/timefold/solver/core/impl/statistic/PickedMoveStepScoreDiffStatistic.java rename to core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/PickedMoveStepScoreDiffStatistic.java index 9282820dd8c..abc61379a5f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/statistic/PickedMoveStepScoreDiffStatistic.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/PickedMoveStepScoreDiffStatistic.java @@ -1,10 +1,8 @@ -package ai.timefold.solver.core.impl.statistic; +package ai.timefold.solver.core.impl.solver.monitoring.statistic; -import java.util.List; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.solver.Solver; @@ -15,7 +13,10 @@ import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; +import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.impl.solver.DefaultSolver; +import ai.timefold.solver.core.impl.solver.monitoring.ScoreLevels; +import ai.timefold.solver.core.impl.solver.monitoring.SolverMetricUtil; import io.micrometer.core.instrument.Tags; @@ -48,7 +49,7 @@ private static class PickedMoveStepScoreDiffStatisticListener scoreDefinition; - private final Map>> tagsToMoveScoreMap = new ConcurrentHashMap<>(); + private final Map tagsToMoveScoreMap = new ConcurrentHashMap<>(); public PickedMoveStepScoreDiffStatisticListener(ScoreDefinition scoreDefinition) { this.scoreDefinition = scoreDefinition; @@ -82,12 +83,10 @@ private void localSearchStepEnded(LocalSearchStepScope stepScope) { var stepScoreDiff = newStepScore.subtract(oldStepScore); oldStepScore = newStepScore; - SolverMetric.registerScoreMetrics(SolverMetric.PICKED_MOVE_TYPE_STEP_SCORE_DIFF, - stepScope.getPhaseScope().getSolverScope().getMonitoringTags() - .and("move.type", moveType), - scoreDefinition, - tagsToMoveScoreMap, - stepScoreDiff); + var tags = stepScope.getPhaseScope().getSolverScope().getMonitoringTags() + .and("move.type", moveType); + SolverMetricUtil.registerScore(SolverMetric.PICKED_MOVE_TYPE_STEP_SCORE_DIFF, tags, scoreDefinition, + tagsToMoveScoreMap, InnerScore.fullyAssigned(stepScoreDiff)); } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/statistic/SolverScopeStatistic.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/SolverScopeStatistic.java similarity index 95% rename from core/src/main/java/ai/timefold/solver/core/impl/statistic/SolverScopeStatistic.java rename to core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/SolverScopeStatistic.java index 38a686b0fae..fdacd0e8294 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/statistic/SolverScopeStatistic.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/SolverScopeStatistic.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.statistic; +package ai.timefold.solver.core.impl.solver.monitoring.statistic; import java.util.function.ToDoubleFunction; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/statistic/SolverStatistic.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/SolverStatistic.java similarity index 74% rename from core/src/main/java/ai/timefold/solver/core/impl/statistic/SolverStatistic.java rename to core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/SolverStatistic.java index b39423beef8..49c72aa383e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/statistic/SolverStatistic.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/SolverStatistic.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.statistic; +package ai.timefold.solver.core.impl.solver.monitoring.statistic; import ai.timefold.solver.core.api.solver.Solver; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/statistic/StatelessSolverStatistic.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/StatelessSolverStatistic.java similarity index 86% rename from core/src/main/java/ai/timefold/solver/core/impl/statistic/StatelessSolverStatistic.java rename to core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/StatelessSolverStatistic.java index 3d06b44e208..c6643f2302a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/statistic/StatelessSolverStatistic.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/monitoring/statistic/StatelessSolverStatistic.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.statistic; +package ai.timefold.solver.core.impl.solver.monitoring.statistic; import ai.timefold.solver.core.api.solver.Solver; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java index ac7fe8b5808..401309d08c6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java @@ -54,7 +54,7 @@ public void solvingStarted(SolverScope solverScope) { solverScope.setBestSolutionTimeMillis(solverScope.getClock().millis()); // The original bestSolution might be the final bestSolution and should have an accurate Score solverScope.getSolutionDescriptor().setScore(solverScope.getBestSolution(), score); - if (innerScore.fullyAssigned()) { + if (innerScore.isFullyAssigned()) { solverScope.setStartingInitializedScore(innerScore.raw()); } else { solverScope.setStartingInitializedScore(null); @@ -145,7 +145,7 @@ private void updateBestSolutionWithoutFiring(SolverScope solverScope) private void updateBestSolutionWithoutFiring(SolverScope solverScope, InnerScore bestScore, Solution_ bestSolution) { - if (bestScore.fullyAssigned() && !solverScope.isBestSolutionInitialized()) { + if (bestScore.isFullyAssigned() && !solverScope.isBestSolutionInitialized()) { solverScope.setStartingInitializedScore(bestScore.raw()); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java index 20f9f83cf7b..ca578f2b720 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java @@ -5,7 +5,6 @@ import java.time.Clock; import java.util.Collections; import java.util.EnumSet; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Random; @@ -27,6 +26,7 @@ import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.solver.AbstractSolver; import ai.timefold.solver.core.impl.solver.change.DefaultProblemChangeDirector; +import ai.timefold.solver.core.impl.solver.monitoring.ScoreLevels; import ai.timefold.solver.core.impl.solver.termination.PhaseTermination; import ai.timefold.solver.core.impl.solver.thread.ChildThreadType; @@ -69,7 +69,7 @@ public class SolverScope { /** * Used for tracking step score */ - private final Map>> stepScoreMap = new ConcurrentHashMap<>(); + private final Map stepScoreMap = new ConcurrentHashMap<>(); /** * Used for tracking move count per move type @@ -122,7 +122,7 @@ public void setMonitoringTags(Tags monitoringTags) { this.monitoringTags = monitoringTags; } - public Map>> getStepScoreMap() { + public Map getStepScoreMap() { return stepScoreMap; } @@ -290,7 +290,7 @@ public void endingNow() { } public boolean isBestSolutionInitialized() { - return getBestScore().fullyAssigned(); + return getBestScore().isFullyAssigned(); } public long calculateTimeMillisSpentUpToNow() { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/BestScoreFeasibleTermination.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/BestScoreFeasibleTermination.java index a66eb29f4ac..b87259e83af 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/BestScoreFeasibleTermination.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/BestScoreFeasibleTermination.java @@ -40,7 +40,7 @@ public boolean isPhaseTerminated(AbstractPhaseScope phaseScope) { } private static boolean isTerminated(InnerScore innerScore) { - return innerScore.fullyAssigned() && innerScore.raw().isFeasible(); + return innerScore.isFullyAssigned() && innerScore.raw().isFeasible(); } @SuppressWarnings({ "unchecked", "rawtypes" }) @@ -57,7 +57,7 @@ public double calculatePhaseTimeGradient(AbstractPhaseScope phaseScop > double calculateFeasibilityTimeGradient(@Nullable InnerScore innerStartScore, Score_ score) { - if (innerStartScore == null || !innerStartScore.fullyAssigned()) { + if (innerStartScore == null || !innerStartScore.isFullyAssigned()) { return 0.0; } var startScore = innerStartScore.raw(); diff --git a/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java b/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java index 84fcaa38f08..9441f71dc2f 100644 --- a/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java @@ -289,7 +289,7 @@ public void changeWorkingSolution(ScoreDirector scoreDirector, (InnerScoreDirector) scoreDirector; var score = innerScoreDirector.calculateScore(); - if (!score.fullyAssigned()) { + if (!score.isFullyAssigned()) { throw new IllegalStateException("The solution (" + TestdataEntity.class.getSimpleName() + ") was not fully initialized by CustomSolverPhase: (" + this.getClass().getCanonicalName() + ")"); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorSemanticsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorSemanticsTest.java index a113a937744..42fcd2a2da5 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorSemanticsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorSemanticsTest.java @@ -145,7 +145,7 @@ void constraintPresentEvenIfNoMatches() { scoreDirector.setWorkingSolution(solution); var score1 = scoreDirector.calculateScore(); assertSoftly(softly -> { - softly.assertThat(score1.fullyAssigned()).isTrue(); + softly.assertThat(score1.isFullyAssigned()).isTrue(); softly.assertThat(score1.raw().score()).isEqualTo(1); softly.assertThat(scoreDirector.getConstraintMatchTotalMap()) .containsOnlyKeys("ai.timefold.solver.core.testdomain.constraintconfiguration/First weight"); @@ -158,7 +158,7 @@ void constraintPresentEvenIfNoMatches() { scoreDirector.afterVariableChanged(entity, "value"); var score2 = scoreDirector.calculateScore(); assertSoftly(softly -> { - softly.assertThat(score2.fullyAssigned()).isFalse(); + softly.assertThat(score2.isFullyAssigned()).isFalse(); softly.assertThat(score2.raw().score()).isZero(); softly.assertThat(scoreDirector.getConstraintMatchTotalMap()) .containsOnlyKeys("ai.timefold.solver.core.testdomain.constraintconfiguration/First weight");