Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public final class ScoreDirectorFactoryConfig extends AbstractConfig<ScoreDirect

@XmlJavaTypeAdapter(JaxbCustomPropertiesAdapter.class)
private Map<String, String> constraintProviderCustomProperties = null;
@Nullable
private Boolean constraintStreamAutomaticNodeSharing;
private Boolean constraintStreamProfilingEnabled;

Expand Down Expand Up @@ -96,7 +97,8 @@ public void setConstraintProviderCustomProperties(
return constraintStreamAutomaticNodeSharing;
}

public void setConstraintStreamAutomaticNodeSharing(@Nullable Boolean constraintStreamAutomaticNodeSharing) {
public void
setConstraintStreamAutomaticNodeSharing(@Nullable Boolean constraintStreamAutomaticNodeSharing) {
this.constraintStreamAutomaticNodeSharing = constraintStreamAutomaticNodeSharing;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@

private static Class<? extends ConstraintProvider> getConstraintProviderClass(ScoreDirectorFactoryConfig config,
Class<? extends ConstraintProvider> 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) {

Check warning on line 53 in core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a primitive boolean expression here.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AZ2Sh-p59Mm1IXzUW5ox&open=AZ2Sh-p59Mm1IXzUW5ox&pullRequest=2241
return TimefoldSolverEnterpriseService.loadOrDefault(
enterpriseService -> enterpriseService.createNodeSharer()
.buildNodeSharedConstraintProvider(providedConstraintProviderClass),
() -> providedConstraintProviderClass);
} else {
return providedConstraintProviderClass;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <<constraintProfiling,Constraint Profiling>>, making it easy to identify constraint performance bottlenecks.

[#avoidCallingRemoteServicesDuringScoreCalculation]
== Avoid calling remote services during score calculation
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -388,22 +387,6 @@ Some solver configs (%s) don't specify a %s class, yet there are multiple availa
}
}

private void assertNodeSharingDisabled(Map<String, SolverConfig> 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);
Expand Down Expand Up @@ -813,6 +796,11 @@ private void applySolverProperties(IndexView indexView, String solverName, Solve
}
solverConfig.withNearbyDistanceMeterClass((Class<? extends NearbyDistanceMeter<?, ?>>) clazz);
});

timefoldBuildTimeConfig.getSolverConfig(solverName)
.flatMap(SolverBuildTimeConfig::constraintStreamAutomaticNodeSharing)
.ifPresent(automaticNodeSharing -> solverConfig.getScoreDirectorFactoryConfig()
.withConstraintStreamAutomaticNodeSharing(automaticNodeSharing));
// Termination properties are set at runtime
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public interface SolverBuildTimeConfig {
* Enable the Nearby Selection quick configuration.
* <p>
* Note: this setting is only available in Timefold Solver
* <a href="https://timefold.ai/docs/timefold-solver/latest/enterprise-edition/enterprise-edition">Enterprise Edition</a>.
* <a href="https://timefold.ai/docs/timefold-solver/latest/commercial-editions/commercial-editions">Enterprise Edition</a>.
*/
// Build time - visited by SolverConfig.visitReferencedClasses
// which generates the constructor used by Quarkus
Expand All @@ -48,7 +48,7 @@ public interface SolverBuildTimeConfig {
* If constraint profiling is enabled. Defaults to false.
* <p>
* Note: this setting is only available in Timefold Solver
* <a href="https://timefold.ai/docs/timefold-solver/latest/enterprise-edition/enterprise-edition">Enterprise Edition</a>.
* <a href="https://timefold.ai/docs/timefold-solver/latest/commercial-editions/commercial-editions">Enterprise Edition</a>.
*/
Optional<Boolean> constraintStreamProfilingEnabled();

Expand All @@ -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.
* <p>
* Note: this setting is only available in Timefold Solver
* <a href="https://timefold.ai/docs/timefold-solver/latest/enterprise-edition/enterprise-edition">Enterprise Edition</a>.
* <a href="https://timefold.ai/docs/timefold-solver/latest/commercial-editions/commercial-editions">Enterprise Edition</a>.
*/
// Build time - modifies the ConstraintProvider class if set
Optional<Boolean> constraintStreamAutomaticNodeSharing();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,7 +16,7 @@

import io.quarkus.test.QuarkusUnitTest;

public class TimefoldProcessorNodeSharingFailFastTest {
public class TimefoldProcessorNodeSharingTest {

Check warning on line 19 in quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorNodeSharingTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this 'public' modifier.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AZ2U87Xl2EVQqD_abFHj&open=AZ2U87Xl2EVQqD_abFHj&pullRequest=2241

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
Expand All @@ -23,18 +25,15 @@
.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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public interface SolverRuntimeConfig {

/**
* Note: this setting is only available in Timefold Solver
* <a href="https://timefold.ai/docs/timefold-solver/latest/enterprise-edition/enterprise-edition">Enterprise Edition</a>.
* <a href="https://timefold.ai/docs/timefold-solver/latest/commercial-editions/commercial-editions">Enterprise Edition</a>.
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -261,14 +262,33 @@
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()) {

Check warning on line 267 in spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a primitive boolean expression here.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AZ2Sh-wA9Mm1IXzUW5oz&open=AZ2Sh-wA9Mm1IXzUW5oz&pullRequest=2241
// 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
Comment thread
Christopher-Chianelli marked this conversation as resolved.
// the solver does not try to node share when enterprise is used.
if (TimefoldSolverEnterpriseService.loadOrDefault(
ignored -> true,
() -> false)) {

Check warning on line 284 in spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldSolverAutoConfiguration.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a primitive boolean expression here.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AZ2RzQ1FLkRFsv27J_3F&open=AZ2RzQ1FLkRFsv27J_3F&pullRequest=2241
// 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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class SolverProperties {

/**
* Note: this setting is only available in Timefold Solver
* <a href="https://timefold.ai/docs/timefold-solver/latest/enterprise-edition/enterprise-edition">Enterprise Edition</a>.
* <a href="https://timefold.ai/docs/timefold-solver/latest/commercial-editions/commercial-editions">Enterprise Edition</a>.
* 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.
Expand All @@ -51,12 +51,13 @@ public class SolverProperties {

/**
* Note: this setting is only available in Timefold Solver
* <a href="https://timefold.ai/docs/timefold-solver/latest/enterprise-edition/enterprise-edition">Enterprise Edition</a>.
* <a href="https://timefold.ai/docs/timefold-solver/latest/commercial-editions/commercial-editions">Enterprise Edition</a>.
* 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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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))
Expand Down Expand Up @@ -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)) {
Expand Down
Loading