Skip to content

Commit 2456d9d

Browse files
authored
fix: AbstractTimeLineChartProblemStatistic to not throw NPE when unmarshalled by JAXB (#1551)
When using the BenchmarkAggregatorFrame to unmarshall PlannerBenchmarkResult.xml with the problemStatisticType SCORE_CALCULATION_SPEED or MOVE_EVALUATION_SPEED the would result in null pointer exception due to empty JaxB noargs constructors calling the parameterized constructor with null as the PlannerBenchmarkResult. --------- Co-authored-by: Magnus Raagaard Kjeldsen <makje@systematic.com> Co-authored-by: Lukáš Petrovický <lukas@petrovicky.net>
1 parent f3502b9 commit 2456d9d

File tree

6 files changed

+214
-9
lines changed

6 files changed

+214
-9
lines changed

benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractTimeLineChartProblemStatistic.java

+23-3
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,20 @@
1212

1313
public abstract class AbstractTimeLineChartProblemStatistic extends ProblemStatistic<LineChart<Long, Long>> {
1414

15-
private final String reportFileName;
16-
private final String reportTitle;
17-
private final String yLabel;
15+
private String reportFileName;
16+
private String reportTitle;
17+
private String yLabel;
18+
private ProblemStatisticType statisticType;
19+
20+
protected AbstractTimeLineChartProblemStatistic(ProblemStatisticType statisticType) {
21+
super(null, statisticType);
22+
this.statisticType = statisticType;
23+
}
1824

1925
protected AbstractTimeLineChartProblemStatistic(ProblemStatisticType statisticType,
2026
ProblemBenchmarkResult<?> problemBenchmarkResult, String reportFileName, String reportTitle, String yLabel) {
2127
super(problemBenchmarkResult, statisticType);
28+
this.statisticType = statisticType;
2229
this.reportFileName = reportFileName;
2330
this.reportTitle = reportTitle;
2431
this.yLabel = yLabel;
@@ -47,4 +54,17 @@ protected List<LineChart<Long, Long>> generateCharts(BenchmarkReport benchmarkRe
4754
}
4855
return singletonList(builder.build(reportFileName, reportTitle, "Time spent", yLabel, false, true, false));
4956
}
57+
58+
public void setReportFileName(String reportFileName) {
59+
this.reportFileName = reportFileName;
60+
}
61+
62+
public void setReportTitle(String reportTitle) {
63+
this.reportTitle = reportTitle;
64+
}
65+
66+
public void setyLabel(String yLabel) {
67+
this.yLabel = yLabel;
68+
}
69+
5070
}

benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedProblemStatisticTime.java

+10-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88

99
public class MoveEvaluationSpeedProblemStatisticTime extends AbstractTimeLineChartProblemStatistic {
1010

11-
private MoveEvaluationSpeedProblemStatisticTime() {
12-
// For JAXB.
13-
this(null);
11+
protected MoveEvaluationSpeedProblemStatisticTime() {
12+
super(ProblemStatisticType.MOVE_EVALUATION_SPEED);
1413
}
1514

1615
public MoveEvaluationSpeedProblemStatisticTime(ProblemBenchmarkResult<?> problemBenchmarkResult) {
@@ -22,4 +21,12 @@ public MoveEvaluationSpeedProblemStatisticTime(ProblemBenchmarkResult<?> problem
2221
public SubSingleStatistic<?, ?> createSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkResult) {
2322
return new MoveEvaluationSpeedSubSingleStatistic<>(subSingleBenchmarkResult);
2423
}
24+
25+
@Override
26+
public void setProblemBenchmarkResult(ProblemBenchmarkResult problemBenchmarkResult) {
27+
super.setProblemBenchmarkResult(problemBenchmarkResult);
28+
this.setyLabel("Move evaluation speed per second");
29+
this.setReportTitle(problemBenchmarkResult.getName() + " move evaluation speed statistic");
30+
this.setReportFileName("moveEvaluationSpeedProblemStatisticChart");
31+
}
2532
}

benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedProblemStatistic.java

+10-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88

99
public class ScoreCalculationSpeedProblemStatistic extends AbstractTimeLineChartProblemStatistic {
1010

11-
private ScoreCalculationSpeedProblemStatistic() {
12-
// For JAXB.
13-
this(null);
11+
protected ScoreCalculationSpeedProblemStatistic() {
12+
super(ProblemStatisticType.SCORE_CALCULATION_SPEED);
1413
}
1514

1615
public ScoreCalculationSpeedProblemStatistic(ProblemBenchmarkResult problemBenchmarkResult) {
@@ -23,4 +22,12 @@ public ScoreCalculationSpeedProblemStatistic(ProblemBenchmarkResult problemBench
2322
public SubSingleStatistic createSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkResult) {
2423
return new ScoreCalculationSpeedSubSingleStatistic(subSingleBenchmarkResult);
2524
}
25+
26+
@Override
27+
public void setProblemBenchmarkResult(ProblemBenchmarkResult problemBenchmarkResult) {
28+
super.setProblemBenchmarkResult(problemBenchmarkResult);
29+
this.setyLabel("Score calculation speed per second");
30+
this.setReportTitle(problemBenchmarkResult.getName() + " score calculation speed statistic");
31+
this.setReportFileName("scoreCalculationSpeedProblemStatisticChart");
32+
}
2633
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package ai.timefold.solver.benchmark.util;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.io.File;
6+
import java.net.URL;
7+
import java.nio.charset.StandardCharsets;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.nio.file.Paths;
11+
import java.util.List;
12+
13+
import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType;
14+
import ai.timefold.solver.benchmark.impl.result.PlannerBenchmarkResult;
15+
import ai.timefold.solver.benchmark.impl.statistic.ProblemStatistic;
16+
17+
import org.junit.jupiter.api.BeforeAll;
18+
import org.junit.jupiter.api.Test;
19+
20+
class PlannerBenchmarkResultXmlTest {
21+
22+
private static final String RESOURCE = "/dummyPlannerBenchmarkResult.xml";
23+
private final TestableBenchmarkResultIO io = new TestableBenchmarkResultIO();
24+
private static URL url;
25+
26+
@BeforeAll
27+
static void setUp() {
28+
url = PlannerBenchmarkResultXmlTest.class.getResource(RESOURCE);
29+
assertThat(url).withFailMessage("Resource not found").isNotNull();
30+
}
31+
32+
@Test
33+
void shouldDeserializeXml() throws Exception {
34+
File xmlFile = Paths.get(url.toURI()).toFile();
35+
PlannerBenchmarkResult result = io.read(xmlFile);
36+
37+
assertThat(result.getUnifiedProblemBenchmarkResultList()).hasSize(1);
38+
39+
@SuppressWarnings("unchecked")
40+
List<ProblemStatistic<?>> stats =
41+
(List<ProblemStatistic<?>>) result.getUnifiedProblemBenchmarkResultList()
42+
.get(0)
43+
.getProblemStatisticList();
44+
45+
assertThat(stats)
46+
.extracting(ProblemStatistic::getProblemStatisticType)
47+
.containsExactlyInAnyOrder(
48+
ProblemStatisticType.BEST_SCORE,
49+
ProblemStatisticType.STEP_SCORE,
50+
ProblemStatisticType.MEMORY_USE,
51+
ProblemStatisticType.BEST_SOLUTION_MUTATION,
52+
ProblemStatisticType.MOVE_COUNT_PER_STEP,
53+
ProblemStatisticType.MOVE_COUNT_PER_TYPE,
54+
ProblemStatisticType.SCORE_CALCULATION_SPEED,
55+
ProblemStatisticType.MOVE_EVALUATION_SPEED);
56+
}
57+
58+
@Test
59+
void shouldSerializeBackToIdenticalXml() throws Exception {
60+
// 1. load original XML text from resource
61+
String originalXml = Files.readString(Paths.get(url.toURI()), StandardCharsets.UTF_8);
62+
63+
// 2. deserialize + serialize
64+
PlannerBenchmarkResult bench = io.read(Paths.get(url.toURI()).toFile());
65+
Path tmp = Files.createTempFile("roundtrip", ".xml");
66+
io.write(tmp.toFile(), bench);
67+
68+
String roundTripXml = Files.readString(tmp).trim();
69+
70+
// 3. compare, ignoring insignificant whitespace
71+
assertThat(roundTripXml).isEqualToIgnoringWhitespace(originalXml.trim());
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package ai.timefold.solver.benchmark.util;
2+
3+
import java.io.File;
4+
import java.io.IOException;
5+
import java.io.Writer;
6+
import java.nio.charset.StandardCharsets;
7+
import java.nio.file.Files;
8+
9+
import ai.timefold.solver.benchmark.impl.result.BenchmarkResultIO;
10+
import ai.timefold.solver.benchmark.impl.result.PlannerBenchmarkResult;
11+
12+
final class TestableBenchmarkResultIO extends BenchmarkResultIO {
13+
14+
public PlannerBenchmarkResult read(File file) {
15+
return super.readPlannerBenchmarkResult(file);
16+
}
17+
18+
public void write(File file, PlannerBenchmarkResult result) throws IOException {
19+
try (Writer w = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) {
20+
super.write(result, w);
21+
}
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<plannerBenchmarkResult>
3+
<name>unit-test</name>
4+
<aggregation>false</aggregation>
5+
<availableProcessors>4</availableProcessors>
6+
<solverBenchmarkResult>
7+
<name>Config_0</name>
8+
<subSingleCount>1</subSingleCount>
9+
<solverConfig>
10+
<solutionClass>ai.timefold.solver.core.impl.testdata.domain.TestdataSolution</solutionClass>
11+
<entityClass>ai.timefold.solver.core.impl.testdata.domain.TestdataEntity</entityClass>
12+
<scoreDirectorFactory>
13+
<constraintProviderClass>ai.timefold.solver.core.impl.testdata.domain.TestdataConstraintProvider</constraintProviderClass>
14+
</scoreDirectorFactory>
15+
<termination>
16+
<secondsSpentLimit>1</secondsSpentLimit>
17+
</termination>
18+
</solverConfig>
19+
20+
<singleBenchmarkResult id="1">
21+
<subSingleBenchmarkResult>
22+
<subSingleBenchmarkIndex>0</subSingleBenchmarkIndex>
23+
<succeeded>true</succeeded>
24+
<initialized>false</initialized>
25+
<timeMillisSpent>-1</timeMillisSpent>
26+
<scoreCalculationCount>0</scoreCalculationCount>
27+
<moveEvaluationCount>0</moveEvaluationCount>
28+
</subSingleBenchmarkResult>
29+
<allScoresInitialized>false</allScoresInitialized>
30+
<timeMillisSpent>-1</timeMillisSpent>
31+
<scoreCalculationCount>-1</scoreCalculationCount>
32+
<moveEvaluationCount>-1</moveEvaluationCount>
33+
</singleBenchmarkResult>
34+
35+
</solverBenchmarkResult>
36+
37+
<unifiedProblemBenchmarkResult>
38+
<name>P-1</name>
39+
<writeOutputSolutionEnabled>false</writeOutputSolutionEnabled>
40+
<bestScoreProblemStatistic>
41+
<problemStatisticType>BEST_SCORE</problemStatisticType>
42+
</bestScoreProblemStatistic>
43+
<stepScoreProblemStatistic>
44+
<problemStatisticType>STEP_SCORE</problemStatisticType>
45+
</stepScoreProblemStatistic>
46+
<memoryUseProblemStatistic>
47+
<problemStatisticType>MEMORY_USE</problemStatisticType>
48+
</memoryUseProblemStatistic>
49+
<bestSolutionMutationProblemStatistic>
50+
<problemStatisticType>BEST_SOLUTION_MUTATION</problemStatisticType>
51+
</bestSolutionMutationProblemStatistic>
52+
<moveCountPerStepProblemStatistic>
53+
<problemStatisticType>MOVE_COUNT_PER_STEP</problemStatisticType>
54+
</moveCountPerStepProblemStatistic>
55+
<bestScoreProblemStatistic>
56+
<problemStatisticType>MOVE_COUNT_PER_TYPE</problemStatisticType>
57+
</bestScoreProblemStatistic>
58+
<moveEvaluationSpeedProblemStatistic>
59+
<problemStatisticType>MOVE_EVALUATION_SPEED</problemStatisticType>
60+
<reportFileName>moveEvaluationSpeedProblemStatisticChart</reportFileName>
61+
<reportTitle>P-1 move evaluation speed statistic</reportTitle>
62+
<yLabel>Move evaluation speed per second</yLabel>
63+
<statisticType>MOVE_EVALUATION_SPEED</statisticType>
64+
</moveEvaluationSpeedProblemStatistic>
65+
<scoreCalculationSpeedProblemStatistic>
66+
<problemStatisticType>SCORE_CALCULATION_SPEED</problemStatisticType>
67+
<reportFileName>scoreCalculationSpeedProblemStatisticChart</reportFileName>
68+
<reportTitle>P-1 score calculation speed statistic</reportTitle>
69+
<yLabel>Score calculation speed per second</yLabel>
70+
<statisticType>SCORE_CALCULATION_SPEED</statisticType>
71+
</scoreCalculationSpeedProblemStatistic>
72+
<singleBenchmarkResult>1</singleBenchmarkResult>
73+
</unifiedProblemBenchmarkResult>
74+
<startingTimestamp>2025-04-28T09:50:14+02:00</startingTimestamp>
75+
</plannerBenchmarkResult>

0 commit comments

Comments
 (0)