diff --git a/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java b/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java index abb78182010..3f07f491dcb 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java @@ -40,6 +40,7 @@ public final class ScoreDirectorFactoryConfig extends AbstractConfig constraintProviderCustomProperties = null; + @Nullable private Boolean constraintStreamAutomaticNodeSharing; private Boolean constraintStreamProfilingEnabled; @@ -96,7 +97,8 @@ public void setConstraintProviderCustomProperties( return constraintStreamAutomaticNodeSharing; } - public void setConstraintStreamAutomaticNodeSharing(@Nullable Boolean constraintStreamAutomaticNodeSharing) { + public void + setConstraintStreamAutomaticNodeSharing(@Nullable Boolean constraintStreamAutomaticNodeSharing) { this.constraintStreamAutomaticNodeSharing = constraintStreamAutomaticNodeSharing; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java index df7939d3983..bd6239823f4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java @@ -47,10 +47,14 @@ public final class BavetConstraintStreamScoreDirectorFactory getConstraintProviderClass(ScoreDirectorFactoryConfig config, Class providedConstraintProviderClass) { - if (Boolean.TRUE.equals(config.getConstraintStreamAutomaticNodeSharing())) { - var enterpriseService = - TimefoldSolverEnterpriseService.loadOrFail(TimefoldSolverEnterpriseService.Feature.AUTOMATIC_NODE_SHARING); - return enterpriseService.createNodeSharer().buildNodeSharedConstraintProvider(providedConstraintProviderClass); + var automaticNodeSharing = Objects.requireNonNullElse(config.getConstraintStreamAutomaticNodeSharing(), + true); + + if (automaticNodeSharing) { + return TimefoldSolverEnterpriseService.loadOrDefault( + enterpriseService -> enterpriseService.createNodeSharer() + .buildNodeSharedConstraintProvider(providedConstraintProviderClass), + () -> providedConstraintProviderClass); } else { return providedConstraintProviderClass; } diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc index c53feac8300..90485bb6c26 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc @@ -9,7 +9,6 @@ which is called in its deepest loops. Faster score calculation will return the same solution in less time with the same algorithm, which normally means a better solution in equal time. - [#moveEvaluationSpeed] == Move evaluation and score calculation speed @@ -62,6 +61,11 @@ without forcing you to write a complicated incremental score calculation algorit Note that the speedup is relative to the size of your planning problem (your __n__), making incremental score calculation far more scalable. +[#enterprisePerformanceImprovements] +== Enterprise performance improvements + +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. +The Enterprise Edition also allows for <>, making it easy to identify constraint performance bottlenecks. [#avoidCallingRemoteServicesDuringScoreCalculation] == 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. * The second best option is to use a `HashMap` or `HashSet`. * Avoid using `java.util.Stream` or any other form of explicit iteration in constraints, as that is much slower. -[#enableAutomaticNodeSharing] -== Enable automatic node sharing -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. - [#benchmark] == Benchmark Whatever you do, benchmark on a large and diverse set of inputs. diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java index cb846924629..50f11a9294d 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java @@ -260,7 +260,6 @@ SolverConfigBuildItem recordAndRegisterBuildTimeBeans(CombinedIndexBuildItem com // Step 2 - validate all SolverConfig definitions assertNoMemberAnnotationWithoutClassAnnotation(indexView); - assertNodeSharingDisabled(solverConfigMap); assertSolverConfigSolutionClasses(indexView, solverConfigMap); assertSolverConfigEntityClasses(indexView); assertSolverConfigConstraintClasses(indexView, solverConfigMap); @@ -388,22 +387,6 @@ Some solver configs (%s) don't specify a %s class, yet there are multiple availa } } - private void assertNodeSharingDisabled(Map solverConfigMap) { - for (var entry : solverConfigMap.entrySet()) { - var solverConfig = entry.getValue(); - var scoreDirectorFactoryConfig = solverConfig.getScoreDirectorFactoryConfig(); - if (scoreDirectorFactoryConfig != null && - Boolean.TRUE.equals(scoreDirectorFactoryConfig.getConstraintStreamAutomaticNodeSharing())) { - throw new IllegalStateException(""" - SolverConfig %s enabled automatic node sharing via SolverConfig, which is not allowed. - Enable automatic node sharing with the property %s instead.""" - .formatted( - entry.getKey(), - "quarkus.timefold.solver.constraint-stream-automatic-node-sharing=true")); - } - } - } - private void assertSolverConfigEntityClasses(IndexView indexView) { // No entity classes assertEmptyInstances(indexView, DotNames.PLANNING_ENTITY); @@ -813,6 +796,11 @@ private void applySolverProperties(IndexView indexView, String solverName, Solve } solverConfig.withNearbyDistanceMeterClass((Class>) clazz); }); + + timefoldBuildTimeConfig.getSolverConfig(solverName) + .flatMap(SolverBuildTimeConfig::constraintStreamAutomaticNodeSharing) + .ifPresent(automaticNodeSharing -> solverConfig.getScoreDirectorFactoryConfig() + .withConstraintStreamAutomaticNodeSharing(automaticNodeSharing)); // Termination properties are set at runtime } diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java index 954a044cf7d..0779466bb16 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/config/SolverBuildTimeConfig.java @@ -31,7 +31,7 @@ public interface SolverBuildTimeConfig { * Enable the Nearby Selection quick configuration. *

* Note: this setting is only available in Timefold Solver - * Enterprise Edition. + * Enterprise Edition. */ // Build time - visited by SolverConfig.visitReferencedClasses // which generates the constructor used by Quarkus @@ -48,7 +48,7 @@ public interface SolverBuildTimeConfig { * If constraint profiling is enabled. Defaults to false. *

* Note: this setting is only available in Timefold Solver - * Enterprise Edition. + * Enterprise Edition. */ Optional constraintStreamProfilingEnabled(); @@ -57,10 +57,11 @@ public interface SolverBuildTimeConfig { * so nodes share lambdas when possible, improving performance. * When enabled, breakpoints placed in the {@link ConstraintProvider} * will no longer be triggered. - * Defaults to "false". + * Defaults to "true"; the solver may decide to disable it regardless, + * if it turns out the feature can not be supported in a given environment. *

* Note: this setting is only available in Timefold Solver - * Enterprise Edition. + * Enterprise Edition. */ // Build time - modifies the ConstraintProvider class if set Optional constraintStreamAutomaticNodeSharing(); diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorNodeSharingFailFastTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorNodeSharingTest.java similarity index 58% rename from quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorNodeSharingFailFastTest.java rename to quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorNodeSharingTest.java index f2c5d3f1d69..c9e6992ee34 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorNodeSharingFailFastTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorNodeSharingTest.java @@ -1,8 +1,10 @@ package ai.timefold.solver.quarkus; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import jakarta.inject.Inject; + +import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.quarkus.testdomain.normal.TestdataQuarkusConstraintProvider; import ai.timefold.solver.quarkus.testdomain.normal.TestdataQuarkusEntity; import ai.timefold.solver.quarkus.testdomain.normal.TestdataQuarkusSolution; @@ -14,7 +16,7 @@ import io.quarkus.test.QuarkusUnitTest; -public class TimefoldProcessorNodeSharingFailFastTest { +public class TimefoldProcessorNodeSharingTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() @@ -23,18 +25,15 @@ public class TimefoldProcessorNodeSharingFailFastTest { .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClasses(TestdataQuarkusEntity.class, TestdataQuarkusSolution.class, TestdataQuarkusConstraintProvider.class) - .addAsResource("ai/timefold/solver/quarkus/solverConfigWithNodeSharing.xml")) - .assertException(exception -> { - assertThat(exception).isInstanceOf(IllegalStateException.class) - .hasMessageContainingAll( - "enabled automatic node sharing via SolverConfig, which is not allowed.", - "Enable automatic node sharing with the property", - "quarkus.timefold.solver.constraint-stream-automatic-node-sharing=true"); - }); + .addAsResource("ai/timefold/solver/quarkus/solverConfigWithNodeSharing.xml")); + + @Inject + SolverConfig solverConfig; @Test - void test() { - fail("Should not call this method."); + void isEnabledInSolverConfig() { + assertEquals(true, + solverConfig.getScoreDirectorFactoryConfig().getConstraintStreamAutomaticNodeSharing()); } } diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java index 787f13d90bd..a7d9ced6bd5 100644 --- a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java @@ -30,7 +30,7 @@ public interface SolverRuntimeConfig { /** * Note: this setting is only available in Timefold Solver - * Enterprise Edition. + * Enterprise Edition. * Enable multithreaded solving for a single problem, which increases CPU consumption. * Defaults to {@value SolverConfig#MOVE_THREAD_COUNT_NONE}. * Other options include {@value SolverConfig#MOVE_THREAD_COUNT_AUTO}, a number diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java index 026efca83f9..1e425f04035 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java @@ -35,6 +35,7 @@ import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.core.config.solver.termination.DiminishedReturnsTerminationConfig; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; +import ai.timefold.solver.core.enterprise.TimefoldSolverEnterpriseService; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.io.jaxb.SolverConfigIO; import ai.timefold.solver.spring.boot.autoconfigure.config.DiminishedReturnsProperties; @@ -261,14 +262,33 @@ private void loadSolverConfig(IncludeAbstractClassesEntityScanner entityScanner, private void applyScoreDirectorFactoryProperties(IncludeAbstractClassesEntityScanner entityScanner, SolverConfig solverConfig, SolverProperties solverProperties) { applyScoreDirectorFactoryProperties(entityScanner, solverConfig); - if (solverProperties.getConstraintStreamAutomaticNodeSharing() != null - && solverProperties.getConstraintStreamAutomaticNodeSharing()) { + if (solverProperties.getConstraintStreamAutomaticNodeSharing() != null) { if (NativeDetector.inNativeImage()) { - throw new UnsupportedOperationException( - "Constraint stream automatic node sharing is unsupported in a Spring native image."); + if (solverProperties.getConstraintStreamAutomaticNodeSharing()) { + // We are in enterprise, so log a note + LOG.debug(""" + Constraint stream automatic node sharing was disabled because it \ + is unsupported in a Spring native image."""); + } + Objects.requireNonNull(solverConfig.getScoreDirectorFactoryConfig()) + .setConstraintStreamAutomaticNodeSharing(false); + } else { + Objects.requireNonNull(solverConfig.getScoreDirectorFactoryConfig()) + .setConstraintStreamAutomaticNodeSharing(solverProperties.getConstraintStreamAutomaticNodeSharing()); + } + } else if (NativeDetector.inNativeImage()) { + // Explicitly set it to disabled in a native image if unspecified so + // the solver does not try to node share when enterprise is used. + if (TimefoldSolverEnterpriseService.loadOrDefault( + ignored -> true, + () -> false)) { + // We are in enterprise, so log a note + LOG.debug(""" + Constraint stream automatic node sharing was disabled because it \ + is unsupported in a Spring native image."""); } Objects.requireNonNull(solverConfig.getScoreDirectorFactoryConfig()) - .setConstraintStreamAutomaticNodeSharing(true); + .setConstraintStreamAutomaticNodeSharing(false); } if (solverProperties.getConstraintStreamProfilingEnabled() != null) { Objects.requireNonNull(solverConfig.getScoreDirectorFactoryConfig()) diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java index 028006d5d5c..b45127fe40e 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperties.java @@ -33,7 +33,7 @@ public class SolverProperties { /** * Note: this setting is only available in Timefold Solver - * Enterprise Edition. + * Enterprise Edition. * Enable multithreaded solving for a single problem, which increases CPU consumption. * Defaults to "NONE". * Other options include "AUTO", a number or formula based on the available processor count. @@ -51,12 +51,13 @@ public class SolverProperties { /** * Note: this setting is only available in Timefold Solver - * Enterprise Edition. + * Enterprise Edition. * Enable rewriting the {@link ConstraintProvider} class * so nodes share lambdas when possible, improving performance. * When enabled, breakpoints placed in the {@link ConstraintProvider} * will no longer be triggered. - * Defaults to "false". + * Defaults to "true"; the solver may decide to disable it regardless, + * if it turns out the feature can not be supported in a given environment. */ private Boolean constraintStreamAutomaticNodeSharing; diff --git a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java index a5d6b4b53e6..d48ec18191b 100644 --- a/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java +++ b/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/config/SolverProperty.java @@ -44,7 +44,8 @@ public enum SolverProperty { SolverProperties::setConstraintStreamProfilingEnabled, value -> Boolean.valueOf(value.toString())), CONSTRAINT_STREAM_AUTOMATIC_NODE_SHARING("constraint-stream-automatic-node-sharing", - SolverProperties::setConstraintStreamAutomaticNodeSharing, value -> Boolean.valueOf(value.toString())), + SolverProperties::setConstraintStreamAutomaticNodeSharing, + value -> Boolean.valueOf(value.toString())), RANDOM_SEED("random-seed", SolverProperties::setRandomSeed, value -> Long.parseLong(value.toString())), TERMINATION("termination", SolverProperties::setTermination, value -> { if (value instanceof TerminationProperties terminationProperties) { diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverConstraintAutoConfigurationTest.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverConstraintAutoConfigurationTest.java index 0c84fb01e38..820b3796e93 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverConstraintAutoConfigurationTest.java +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverConstraintAutoConfigurationTest.java @@ -36,7 +36,6 @@ class TimefoldSolverConstraintAutoConfigurationTest { private final ApplicationContextRunner contextRunner; private final ApplicationContextRunner multiConstraintProviderRunner; - private final ApplicationContextRunner fakeNativeWithNodeSharingContextRunner; private final ApplicationContextRunner fakeNativeWithoutNodeSharingContextRunner; private final FilteredClassLoader testFilteredClassLoader; @@ -49,12 +48,6 @@ public TimefoldSolverConstraintAutoConfigurationTest() { .withConfiguration( AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class)) .withUserConfiguration(MultipleConstraintSpringTestConfiguration.class); - fakeNativeWithNodeSharingContextRunner = new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class)) - .withUserConfiguration(NormalSpringTestConfiguration.class) - .withPropertyValues("timefold.solver.%s=true" - .formatted(SolverProperty.CONSTRAINT_STREAM_AUTOMATIC_NODE_SHARING.getPropertyName())); fakeNativeWithoutNodeSharingContextRunner = new ApplicationContextRunner() .withConfiguration( AutoConfigurations.of(TimefoldSolverAutoConfiguration.class, TimefoldSolverBeanFactory.class)) @@ -133,21 +126,6 @@ void readOnlyConcreteProviderClass() { .doesNotThrowAnyException(); } - @Test - void nodeSharingFailFastInNativeImage() { - try (var nativeDetectorMock = Mockito.mockStatic(NativeDetector.class)) { - nativeDetectorMock.when(NativeDetector::inNativeImage).thenReturn(true); - fakeNativeWithNodeSharingContextRunner - .run(context -> { - Throwable startupFailure = context.getStartupFailure(); - assertThat(startupFailure) - .isInstanceOf(UnsupportedOperationException.class) - .hasMessageContainingAll("node sharing", "unsupported", "native"); - }); - } - - } - @Test void nodeSharingDisabledWorksInNativeImage() { try (var nativeDetectorMock = Mockito.mockStatic(NativeDetector.class)) {