Skip to content

Commit 88db14c

Browse files
feat: automatically enable automatic node sharing when possible (#2241)
Co-authored-by: Lukáš Petrovický <lukas@petrovicky.net>
1 parent 0a2e4ff commit 88db14c

11 files changed

Lines changed: 70 additions & 76 deletions

File tree

core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public final class ScoreDirectorFactoryConfig extends AbstractConfig<ScoreDirect
4040

4141
@XmlJavaTypeAdapter(JaxbCustomPropertiesAdapter.class)
4242
private Map<String, String> constraintProviderCustomProperties = null;
43+
@Nullable
4344
private Boolean constraintStreamAutomaticNodeSharing;
4445
private Boolean constraintStreamProfilingEnabled;
4546

@@ -96,7 +97,8 @@ public void setConstraintProviderCustomProperties(
9697
return constraintStreamAutomaticNodeSharing;
9798
}
9899

99-
public void setConstraintStreamAutomaticNodeSharing(@Nullable Boolean constraintStreamAutomaticNodeSharing) {
100+
public void
101+
setConstraintStreamAutomaticNodeSharing(@Nullable Boolean constraintStreamAutomaticNodeSharing) {
100102
this.constraintStreamAutomaticNodeSharing = constraintStreamAutomaticNodeSharing;
101103
}
102104

core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,14 @@ public final class BavetConstraintStreamScoreDirectorFactory<Solution_, Score_ e
4747

4848
private static Class<? extends ConstraintProvider> getConstraintProviderClass(ScoreDirectorFactoryConfig config,
4949
Class<? extends ConstraintProvider> providedConstraintProviderClass) {
50-
if (Boolean.TRUE.equals(config.getConstraintStreamAutomaticNodeSharing())) {
51-
var enterpriseService =
52-
TimefoldSolverEnterpriseService.loadOrFail(TimefoldSolverEnterpriseService.Feature.AUTOMATIC_NODE_SHARING);
53-
return enterpriseService.createNodeSharer().buildNodeSharedConstraintProvider(providedConstraintProviderClass);
50+
var automaticNodeSharing = Objects.requireNonNullElse(config.getConstraintStreamAutomaticNodeSharing(),
51+
true);
52+
53+
if (automaticNodeSharing) {
54+
return TimefoldSolverEnterpriseService.loadOrDefault(
55+
enterpriseService -> enterpriseService.createNodeSharer()
56+
.buildNodeSharedConstraintProvider(providedConstraintProviderClass),
57+
() -> providedConstraintProviderClass);
5458
} else {
5559
return providedConstraintProviderClass;
5660
}

docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ which is called in its deepest loops.
99
Faster score calculation will return the same solution in less time with the same algorithm,
1010
which normally means a better solution in equal time.
1111

12-
1312
[#moveEvaluationSpeed]
1413
== Move evaluation and score calculation speed
1514

@@ -62,6 +61,11 @@ without forcing you to write a complicated incremental score calculation algorit
6261
Note that the speedup is relative to the size of your planning problem (your __n__),
6362
making incremental score calculation far more scalable.
6463

64+
[#enterprisePerformanceImprovements]
65+
== Enterprise performance improvements
66+
67+
The xref:commercial-editions/commercial-editions.adoc[Enterprise Edition] of Timefold Solver has a number of xref:commercial-editions/performance-improvements.adoc[performance improvements] that can further speed up score calculation.
68+
The Enterprise Edition also allows for <<constraintProfiling,Constraint Profiling>>, making it easy to identify constraint performance bottlenecks.
6569

6670
[#avoidCallingRemoteServicesDuringScoreCalculation]
6771
== Avoid calling remote services during score calculation
@@ -305,10 +309,6 @@ The code on the hot path of your application needs to be as fast as possible.
305309
* The second best option is to use a `HashMap` or `HashSet`.
306310
* Avoid using `java.util.Stream` or any other form of explicit iteration in constraints, as that is much slower.
307311

308-
[#enableAutomaticNodeSharing]
309-
== Enable automatic node sharing
310-
If you are using the xref:commercial-editions/commercial-editions.adoc[Enterprise Edition], you should xref:commercial-editions/performance-improvements.adoc#automaticNodeSharing[enable automatic node sharing] as it can significantly speed up score calculation.
311-
312312
[#benchmark]
313313
== Benchmark
314314
Whatever you do, benchmark on a large and diverse set of inputs.

quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,6 @@ SolverConfigBuildItem recordAndRegisterBuildTimeBeans(CombinedIndexBuildItem com
260260

261261
// Step 2 - validate all SolverConfig definitions
262262
assertNoMemberAnnotationWithoutClassAnnotation(indexView);
263-
assertNodeSharingDisabled(solverConfigMap);
264263
assertSolverConfigSolutionClasses(indexView, solverConfigMap);
265264
assertSolverConfigEntityClasses(indexView);
266265
assertSolverConfigConstraintClasses(indexView, solverConfigMap);
@@ -388,22 +387,6 @@ Some solver configs (%s) don't specify a %s class, yet there are multiple availa
388387
}
389388
}
390389

391-
private void assertNodeSharingDisabled(Map<String, SolverConfig> solverConfigMap) {
392-
for (var entry : solverConfigMap.entrySet()) {
393-
var solverConfig = entry.getValue();
394-
var scoreDirectorFactoryConfig = solverConfig.getScoreDirectorFactoryConfig();
395-
if (scoreDirectorFactoryConfig != null &&
396-
Boolean.TRUE.equals(scoreDirectorFactoryConfig.getConstraintStreamAutomaticNodeSharing())) {
397-
throw new IllegalStateException("""
398-
SolverConfig %s enabled automatic node sharing via SolverConfig, which is not allowed.
399-
Enable automatic node sharing with the property %s instead."""
400-
.formatted(
401-
entry.getKey(),
402-
"quarkus.timefold.solver.constraint-stream-automatic-node-sharing=true"));
403-
}
404-
}
405-
}
406-
407390
private void assertSolverConfigEntityClasses(IndexView indexView) {
408391
// No entity classes
409392
assertEmptyInstances(indexView, DotNames.PLANNING_ENTITY);
@@ -813,6 +796,11 @@ private void applySolverProperties(IndexView indexView, String solverName, Solve
813796
}
814797
solverConfig.withNearbyDistanceMeterClass((Class<? extends NearbyDistanceMeter<?, ?>>) clazz);
815798
});
799+
800+
timefoldBuildTimeConfig.getSolverConfig(solverName)
801+
.flatMap(SolverBuildTimeConfig::constraintStreamAutomaticNodeSharing)
802+
.ifPresent(automaticNodeSharing -> solverConfig.getScoreDirectorFactoryConfig()
803+
.withConstraintStreamAutomaticNodeSharing(automaticNodeSharing));
816804
// Termination properties are set at runtime
817805
}
818806

quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public interface SolverBuildTimeConfig {
3131
* Enable the Nearby Selection quick configuration.
3232
* <p>
3333
* Note: this setting is only available in Timefold Solver
34-
* <a href="https://timefold.ai/docs/timefold-solver/latest/enterprise-edition/enterprise-edition">Enterprise Edition</a>.
34+
* <a href="https://timefold.ai/docs/timefold-solver/latest/commercial-editions/commercial-editions">Enterprise Edition</a>.
3535
*/
3636
// Build time - visited by SolverConfig.visitReferencedClasses
3737
// which generates the constructor used by Quarkus
@@ -48,7 +48,7 @@ public interface SolverBuildTimeConfig {
4848
* If constraint profiling is enabled. Defaults to false.
4949
* <p>
5050
* Note: this setting is only available in Timefold Solver
51-
* <a href="https://timefold.ai/docs/timefold-solver/latest/enterprise-edition/enterprise-edition">Enterprise Edition</a>.
51+
* <a href="https://timefold.ai/docs/timefold-solver/latest/commercial-editions/commercial-editions">Enterprise Edition</a>.
5252
*/
5353
Optional<Boolean> constraintStreamProfilingEnabled();
5454

@@ -57,10 +57,11 @@ public interface SolverBuildTimeConfig {
5757
* so nodes share lambdas when possible, improving performance.
5858
* When enabled, breakpoints placed in the {@link ConstraintProvider}
5959
* will no longer be triggered.
60-
* Defaults to "false".
60+
* Defaults to "true"; the solver may decide to disable it regardless,
61+
* if it turns out the feature can not be supported in a given environment.
6162
* <p>
6263
* Note: this setting is only available in Timefold Solver
63-
* <a href="https://timefold.ai/docs/timefold-solver/latest/enterprise-edition/enterprise-edition">Enterprise Edition</a>.
64+
* <a href="https://timefold.ai/docs/timefold-solver/latest/commercial-editions/commercial-editions">Enterprise Edition</a>.
6465
*/
6566
// Build time - modifies the ConstraintProvider class if set
6667
Optional<Boolean> constraintStreamAutomaticNodeSharing();

quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorNodeSharingFailFastTest.java renamed to quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorNodeSharingTest.java

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package ai.timefold.solver.quarkus;
22

3-
import static org.assertj.core.api.Assertions.assertThat;
4-
import static org.junit.jupiter.api.Assertions.fail;
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
54

5+
import jakarta.inject.Inject;
6+
7+
import ai.timefold.solver.core.config.solver.SolverConfig;
68
import ai.timefold.solver.quarkus.testdomain.normal.TestdataQuarkusConstraintProvider;
79
import ai.timefold.solver.quarkus.testdomain.normal.TestdataQuarkusEntity;
810
import ai.timefold.solver.quarkus.testdomain.normal.TestdataQuarkusSolution;
@@ -14,7 +16,7 @@
1416

1517
import io.quarkus.test.QuarkusUnitTest;
1618

17-
public class TimefoldProcessorNodeSharingFailFastTest {
19+
public class TimefoldProcessorNodeSharingTest {
1820

1921
@RegisterExtension
2022
static final QuarkusUnitTest config = new QuarkusUnitTest()
@@ -23,18 +25,15 @@ public class TimefoldProcessorNodeSharingFailFastTest {
2325
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
2426
.addClasses(TestdataQuarkusEntity.class,
2527
TestdataQuarkusSolution.class, TestdataQuarkusConstraintProvider.class)
26-
.addAsResource("ai/timefold/solver/quarkus/solverConfigWithNodeSharing.xml"))
27-
.assertException(exception -> {
28-
assertThat(exception).isInstanceOf(IllegalStateException.class)
29-
.hasMessageContainingAll(
30-
"enabled automatic node sharing via SolverConfig, which is not allowed.",
31-
"Enable automatic node sharing with the property",
32-
"quarkus.timefold.solver.constraint-stream-automatic-node-sharing=true");
33-
});
28+
.addAsResource("ai/timefold/solver/quarkus/solverConfigWithNodeSharing.xml"));
29+
30+
@Inject
31+
SolverConfig solverConfig;
3432

3533
@Test
36-
void test() {
37-
fail("Should not call this method.");
34+
void isEnabledInSolverConfig() {
35+
assertEquals(true,
36+
solverConfig.getScoreDirectorFactoryConfig().getConstraintStreamAutomaticNodeSharing());
3837
}
3938

4039
}

quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public interface SolverRuntimeConfig {
3030

3131
/**
3232
* Note: this setting is only available in Timefold Solver
33-
* <a href="https://timefold.ai/docs/timefold-solver/latest/enterprise-edition/enterprise-edition">Enterprise Edition</a>.
33+
* <a href="https://timefold.ai/docs/timefold-solver/latest/commercial-editions/commercial-editions">Enterprise Edition</a>.
3434
* Enable multithreaded solving for a single problem, which increases CPU consumption.
3535
* Defaults to {@value SolverConfig#MOVE_THREAD_COUNT_NONE}.
3636
* Other options include {@value SolverConfig#MOVE_THREAD_COUNT_AUTO}, a number

spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import ai.timefold.solver.core.config.solver.SolverConfig;
3636
import ai.timefold.solver.core.config.solver.termination.DiminishedReturnsTerminationConfig;
3737
import ai.timefold.solver.core.config.solver.termination.TerminationConfig;
38+
import ai.timefold.solver.core.enterprise.TimefoldSolverEnterpriseService;
3839
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
3940
import ai.timefold.solver.core.impl.io.jaxb.SolverConfigIO;
4041
import ai.timefold.solver.spring.boot.autoconfigure.config.DiminishedReturnsProperties;
@@ -261,14 +262,33 @@ private void loadSolverConfig(IncludeAbstractClassesEntityScanner entityScanner,
261262
private void applyScoreDirectorFactoryProperties(IncludeAbstractClassesEntityScanner entityScanner,
262263
SolverConfig solverConfig, SolverProperties solverProperties) {
263264
applyScoreDirectorFactoryProperties(entityScanner, solverConfig);
264-
if (solverProperties.getConstraintStreamAutomaticNodeSharing() != null
265-
&& solverProperties.getConstraintStreamAutomaticNodeSharing()) {
265+
if (solverProperties.getConstraintStreamAutomaticNodeSharing() != null) {
266266
if (NativeDetector.inNativeImage()) {
267-
throw new UnsupportedOperationException(
268-
"Constraint stream automatic node sharing is unsupported in a Spring native image.");
267+
if (solverProperties.getConstraintStreamAutomaticNodeSharing()) {
268+
// We are in enterprise, so log a note
269+
LOG.debug("""
270+
Constraint stream automatic node sharing was disabled because it \
271+
is unsupported in a Spring native image.""");
272+
}
273+
Objects.requireNonNull(solverConfig.getScoreDirectorFactoryConfig())
274+
.setConstraintStreamAutomaticNodeSharing(false);
275+
} else {
276+
Objects.requireNonNull(solverConfig.getScoreDirectorFactoryConfig())
277+
.setConstraintStreamAutomaticNodeSharing(solverProperties.getConstraintStreamAutomaticNodeSharing());
278+
}
279+
} else if (NativeDetector.inNativeImage()) {
280+
// Explicitly set it to disabled in a native image if unspecified so
281+
// the solver does not try to node share when enterprise is used.
282+
if (TimefoldSolverEnterpriseService.loadOrDefault(
283+
ignored -> true,
284+
() -> false)) {
285+
// We are in enterprise, so log a note
286+
LOG.debug("""
287+
Constraint stream automatic node sharing was disabled because it \
288+
is unsupported in a Spring native image.""");
269289
}
270290
Objects.requireNonNull(solverConfig.getScoreDirectorFactoryConfig())
271-
.setConstraintStreamAutomaticNodeSharing(true);
291+
.setConstraintStreamAutomaticNodeSharing(false);
272292
}
273293
if (solverProperties.getConstraintStreamProfilingEnabled() != null) {
274294
Objects.requireNonNull(solverConfig.getScoreDirectorFactoryConfig())

spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public class SolverProperties {
3333

3434
/**
3535
* Note: this setting is only available in Timefold Solver
36-
* <a href="https://timefold.ai/docs/timefold-solver/latest/enterprise-edition/enterprise-edition">Enterprise Edition</a>.
36+
* <a href="https://timefold.ai/docs/timefold-solver/latest/commercial-editions/commercial-editions">Enterprise Edition</a>.
3737
* Enable multithreaded solving for a single problem, which increases CPU consumption.
3838
* Defaults to "NONE".
3939
* Other options include "AUTO", a number or formula based on the available processor count.
@@ -51,12 +51,13 @@ public class SolverProperties {
5151

5252
/**
5353
* Note: this setting is only available in Timefold Solver
54-
* <a href="https://timefold.ai/docs/timefold-solver/latest/enterprise-edition/enterprise-edition">Enterprise Edition</a>.
54+
* <a href="https://timefold.ai/docs/timefold-solver/latest/commercial-editions/commercial-editions">Enterprise Edition</a>.
5555
* Enable rewriting the {@link ConstraintProvider} class
5656
* so nodes share lambdas when possible, improving performance.
5757
* When enabled, breakpoints placed in the {@link ConstraintProvider}
5858
* will no longer be triggered.
59-
* Defaults to "false".
59+
* Defaults to "true"; the solver may decide to disable it regardless,
60+
* if it turns out the feature can not be supported in a given environment.
6061
*/
6162
private Boolean constraintStreamAutomaticNodeSharing;
6263

spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public enum SolverProperty {
4444
SolverProperties::setConstraintStreamProfilingEnabled,
4545
value -> Boolean.valueOf(value.toString())),
4646
CONSTRAINT_STREAM_AUTOMATIC_NODE_SHARING("constraint-stream-automatic-node-sharing",
47-
SolverProperties::setConstraintStreamAutomaticNodeSharing, value -> Boolean.valueOf(value.toString())),
47+
SolverProperties::setConstraintStreamAutomaticNodeSharing,
48+
value -> Boolean.valueOf(value.toString())),
4849
RANDOM_SEED("random-seed", SolverProperties::setRandomSeed, value -> Long.parseLong(value.toString())),
4950
TERMINATION("termination", SolverProperties::setTermination, value -> {
5051
if (value instanceof TerminationProperties terminationProperties) {

0 commit comments

Comments
 (0)