From 5dc8d03aec41d572e711427da0aa8fb3a776a7c8 Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Tue, 14 Apr 2026 10:56:24 +0200 Subject: [PATCH 1/3] docs: rename to "commercial editions" Also move Nearby selection to it's new spot --- docs/src/modules/ROOT/nav.adoc | 12 +- docs/src/modules/ROOT/pages/.index.adoc | 2 +- .../commercial-editions.adoc} | 271 +----------------- .../constraints-and-score/performance.adoc | 4 +- .../pages/frequently-asked-questions.adoc | 4 +- .../pages/integration/_config-properties.adoc | 6 +- .../ROOT/pages/integration/integration.adoc | 2 +- .../construction-heuristics.adoc | 2 +- .../move-selector-reference.adoc | 205 ++++++++++++- .../neighborhoods.adoc | 6 +- .../responding-to-change.adoc | 2 +- .../upgrade-from-v1.adoc | 4 +- .../benchmarking-and-tweaking.adoc | 2 +- .../modeling-planning-problems.adoc | 2 +- .../running-the-solver.adoc | 16 +- 15 files changed, 245 insertions(+), 295 deletions(-) rename docs/src/modules/ROOT/pages/{enterprise-edition/enterprise-edition.adoc => commercial-editions/commercial-editions.adoc} (76%) diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index a88bf546705..d4b53547e46 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -38,4 +38,14 @@ ** Migration Guides *** xref:upgrading-timefold-solver/migration-guides/variable-listeners-to-custom-shadow-variables.adoc[leveloffset=+1] *** xref:upgrading-timefold-solver/migration-guides/chained-variables-to-planning-list-variable.adoc[leveloffset=+1] -* xref:enterprise-edition/enterprise-edition.adoc[leveloffset=+1] +* xref:commercial-editions/commercial-editions.adoc[leveloffset=+1] +** xref:commercial-editions/installation.adoc[leveloffset=+1] +** Automatic Performance Improvements (Node sharing, DSVs...) +** xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[Nearby selection] +** Multi-threaded Solving +** Partitioned Search +** Constraint Profiling +** Throttling Events +** Multistage Moves + + diff --git a/docs/src/modules/ROOT/pages/.index.adoc b/docs/src/modules/ROOT/pages/.index.adoc index 3f7582a3525..b7934587e4c 100644 --- a/docs/src/modules/ROOT/pages/.index.adoc +++ b/docs/src/modules/ROOT/pages/.index.adoc @@ -32,4 +32,4 @@ include::integration/integration.adoc[leveloffset=+1] include::design-patterns/design-patterns.adoc[leveloffset=+1] include::frequently-asked-questions.adoc[leveloffset=+1] include::upgrading-timefold-solver/.upgrading-timefold-solver.adoc[leveloffset=+1] -include::enterprise-edition/enterprise-edition.adoc[leveloffset=+1] +include::commercial-editions/commercial-editions.adoc[leveloffset=+1] diff --git a/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc b/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc similarity index 76% rename from docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc rename to docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc index 465b3095a19..f0a3c170d65 100644 --- a/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc +++ b/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc @@ -1,71 +1,22 @@ -= Timefold Solver Enterprise Edition -:page-aliases: partitioned-search/partitioned-search.adoc += Plus/Enterprise Editions +:page-aliases: partitioned-search/partitioned-search.adoc, \ + enterprise-edition/enterprise-edition.adoc :doctype: book :sectnums: :icons: font -_Timefold Solver Enterprise Edition_ is a commercial product that offers <>, -such as <> and <>. -These features are essential to scale out to large datasets. +_Timefold Solver Plus_ and _Timefold Solver Enterprise_ are commercial products which offer <>, +such as <>, <> and <>. +These features are essential to scale out to large datasets and build trust with your users. -Unlike _Timefold Solver Community Edition_, the _Timefold Solver Enterprise Edition_ is not open-source. -We offer free licenses to non-profit organizations and for academic research. -Others may be entitled to a trial license to evaluate the product before purchasing. -Please https://timefold.ai/contact[contact Timefold] to get your Enterprise license. +Unlike _Timefold Solver Community Edition_, these commercial editions are not open-source. +We offer free trials as well as free licenses to non-profit organizations, start-ups and for academic research. +Please check out LINK TODO to get your commercial licenses. TIP: Looking for quicker time-to-value? Timefold offers https://docs.timefold.ai/[pre-built, fully tuned optimization models], no constraint building required. Just plug into our API and start optimizing immediately. -For a high-level overview of the differences between Timefold offerings, -see https://timefold.ai/pricing[Timefold Pricing]. - -[#switchToEnterpriseEdition] -== Switch to Enterprise Edition - -To switch from the open source _Timefold Solver Community Edition_ to _Enterprise Edition_, -https://timefold.ai/contact[contact us first] to get your Enterprise License. -This license is represented by a file named `timefold-enterprise-license.pem`, -which you can introduce to your project by one of the following methods: - -* Store the license's PEM string in the `TIMEFOLD_ENTERPRISE_LICENSE` environment variable. -* Store the absolute path to the file in `TIMEFOLD_ENTERPRISE_LICENSE_PATH` environment variable. -* Place the file in the user's home directory. -* Place the file in the root of your application classpath. - -Take care not to leak the license file, for example by committing it to a public repository, -logging the license file content in logs, or sharing it with unauthorized parties. - -Once your license is in place, -replace references to open-source artifacts by their enterprise counterparts -as shown in the following table: - -|=== -|Open source|Enterprise - -|`ai.timefold.solver:timefold-solver-bom` -|`ai.timefold.solver.enterprise:timefold-solver-enterprise-bom` - -|`ai.timefold.solver:timefold-solver-core` -|`ai.timefold.solver.enterprise:timefold-solver-enterprise-core` - -|`ai.timefold.solver:timefold-solver-jackson` -|`ai.timefold.solver.enterprise:timefold-solver-enterprise-jackson` - -|`ai.timefold.solver:timefold-solver-quarkus` -|`ai.timefold.solver.enterprise:timefold-solver-enterprise-quarkus` - -|`ai.timefold.solver:timefold-solver-quarkus-jackson` -|`ai.timefold.solver.enterprise:timefold-solver-enterprise-quarkus-jackson` - -|`ai.timefold.solver:timefold-solver-spring-boot-starter` -|`ai.timefold.solver.enterprise:timefold-solver-enterprise-spring-boot-starter` -|=== - -If you were not using some of these open source artifacts until now, -you do not need to add the enterprise counterparts to your project either. - - [#enterpriseEditionFeatures] == Features included in Enterprise Edition @@ -83,208 +34,6 @@ leading to significantly improved performance for some use cases, such as: - Heavy use of xref:using-timefold-solver/modeling-planning-problems.adoc#customShadowVariable[custom shadow variables]. -[#nearbySelection] -=== Nearby selection - -NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. -It is not available in the open source version. - -In some use cases (such as TSP and VRP, but also in other cases), -changing entities to nearby values or swapping nearby entities leads to better results faster. - -image::enterprise-edition/nearbySelectionMotivation.png[align="center"] - -This can *heavily increase scalability* and improve solution quality: - -image::enterprise-edition/nearbySelectionValueProposition.png[align="center"] - -Nearby selection increases the probability of selecting an entity or value which is nearby to the first entity being moved in that move. - -image::enterprise-edition/nearbySelectionRandomDistribution.png[align="center"] - -The distance between two entities or values is domain specific. -Therefore, implement the `NearbyDistanceMeter` interface: - -[tabs] -==== -Java:: -+ -[source,java,options="nowrap"] ----- -public interface NearbyDistanceMeter { - - double getNearbyDistance(Origin_ origin, Destination_ destination); - -} ----- -==== - -In a nutshell, when nearby selection is used in a list move selector, -`Origin_` is always a planning value (for example `Customer`) -but `Destination_` can be either a planning value or a planning entity. -That means that in VRP the distance meter must be able to handle both `Customer` and `Vehicle` as the `Destination_` argument: - -[tabs] -==== -Java:: -+ -[source,java,options="nowrap"] ----- -public class CustomerNearbyDistanceMeter implements NearbyDistanceMeter { - - public double getNearbyDistance(Customer origin, LocationAware destination) { - return origin.getDistanceTo(destination); - } - -} ----- -==== - -[NOTE] -==== -`NearbyDistanceMeter` implementations are expected to be stateless. -The solver may choose to reuse them in different contexts. - -The Nearby configuration is not enabled for the Construction Heuristics -because the method will analyze all possible moves. -Adding Nearby in this situation would only result in unnecessary costs -involving the generation of the distance matrix and sorting operations without taking advantage of the feature. -==== - -==== Nearby selection with a list variable - -To quickly configure nearby selection with a planning list variable, -add `nearbyDistanceMeterClass` element to your configuration file. -The following enables nearby selection with a list variable -for the local search: - -[source,xml,options="nowrap"] ----- - - - ... - org.acme.vehiclerouting.domain.solver.nearby.CustomerNearbyDistanceMeter - ... - ----- - -By default, the following move selectors are included: -xref:optimization-algorithms/move-selector-reference.adoc#changeMoveSelector[Change], -xref:optimization-algorithms/move-selector-reference.adoc#swapMoveSelector[Swap], -Change with Nearby, -Swap with Nearby, -and xref:optimization-algorithms/move-selector-reference.adoc#kOptListMoveSelector[2-OPT] with Nearby. - -===== Advanced configuration for local search - -To customize the move selectors, -add a `nearbySelection` element in the `destinationSelector`, `valueSelector` or `subListSelector` -and use xref:optimization-algorithms/move-selector-reference.adoc#mimicSelection[mimic selection] -to specify which destination, value, or subList should be nearby the selection. - -[source,xml,options="nowrap"] ----- - - - - - - - org.acme.vehiclerouting.domain.solver.nearby.CustomerNearbyDistanceMeter - - - - - - - - - org.acme.vehiclerouting.domain.solver.nearby.CustomerNearbyDistanceMeter - - - - - true - - - - - org.acme.vehiclerouting.domain.solver.nearby.CustomerNearbyDistanceMeter - - - - - true - - - - - org.acme.vehiclerouting.domain.solver.nearby.CustomerNearbyDistanceMeter - - - - ----- - -==== Power-tweaking distribution type - -The solver allows you to tweak the distribution type of the nearby selection, -or how likely are the nearest elements to be selected based on their distance from the current. - -[NOTE] -==== -Only tweak the default settings if you are prepared -to back your choices by extensive xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[benchmarking]. -==== - -The following ``NearbySelectionDistributionType``s are supported: - -* `PARABOLIC_DISTRIBUTION` (default): Nearest elements are selected with a higher probability. -+ -[source,xml,options="nowrap"] ----- - - 80 - ----- -+ -A `distributionSizeMaximum` parameter should not be 1 because if the nearest is already the planning value of the current entity, -then the only move that is selectable is not doable. -To allow every element to be selected regardless of the number of entities, -only set the distribution type (so without a `distributionSizeMaximum` parameter): -+ -[source,xml,options="nowrap"] ----- - - PARABOLIC_DISTRIBUTION - ----- -* ``BLOCK_DISTRIBUTION``: Only the n nearest are selected, with an equal probability. For example, select the 20 nearest: -+ -[source,xml,options="nowrap"] ----- - - 20 - ----- -* ``LINEAR_DISTRIBUTION``: Nearest elements are selected with a higher probability. The probability decreases linearly. -+ -[source,xml,options="nowrap"] ----- - - 40 - ----- -* ``BETA_DISTRIBUTION``: Selection according to a beta distribution. Slows down the solver significantly. -+ -[source,xml,options="nowrap"] ----- - - 1 - 5 - ----- - [#multithreadedSolving] [#enterpriseMultithreadedSolving] @@ -958,7 +707,7 @@ INFO:: How much time spent in score calculation is spent by each constraint (tot | 2: Teacher time efficiency | 6.19 s | 23.15 % | ---- + -xref:enterprise-edition/enterprise-edition.adoc#nodeSharing[When node sharing] is used, the time spent will be reported with "Work shared by", with the numbers identifying constraints that shared the work. +xref:commercial-editions/commercial-editions.adoc#nodeSharing[When node sharing] is used, the time spent will be reported with "Work shared by", with the numbers identifying constraints that shared the work. In the example below, work was shared by constraints 1 to 6: + [source,text] 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 e9d3fa73831..fea198a4db5 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc @@ -307,7 +307,7 @@ The code on the hot path of your application needs to be as fast as possible. [#enableAutomaticNodeSharing] == Enable automatic node sharing -If you are using the xref:enterprise-edition/enterprise-edition.adoc[Enterprise Edition], you should xref:enterprise-edition/enterprise-edition.adoc#automaticNodeSharing[enable automatic node sharing] as it can significantly speed up score calculation. +If you are using the xref:commercial-editions/commercial-editions.adoc[Enterprise Edition], you should xref:commercial-editions/commercial-editions.adoc#automaticNodeSharing[enable automatic node sharing] as it can significantly speed up score calculation. [#benchmark] == Benchmark @@ -320,7 +320,7 @@ JVM performance may differ by as much as 20% between runs. To decide whether you NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. It is not available in the open source version. -xref:constraints-and-score/score-calculation.adoc#constraintStreams[Constraint streams] have builtin support for xref:enterprise-edition/enterprise-edition.adoc#constraintProfiling[profiling constraints]. +xref:constraints-and-score/score-calculation.adoc#constraintStreams[Constraint streams] have builtin support for xref:commercial-editions/commercial-editions.adoc#constraintProfiling[profiling constraints]. Profiling constraints allows you to identify which constraints are taking the majority of the time spent in score calculation and thus are worth optimizing. NOTE: Traditional profiling tools such as Java Mission Control will report the internal classes used to calculate constraints instead of the constraints themselves. diff --git a/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc b/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc index 72fa7fbf08c..6319de67f83 100644 --- a/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc +++ b/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc @@ -10,9 +10,9 @@ This license is very liberal and allows reuse for commercial purposes. Read http://www.apache.org/foundation/licence-FAQ.html#WhatDoesItMEAN[the layman's explanation]. _Timefold Solver Enterprise Edition_ is a commercial product -that offers xref:enterprise-edition/enterprise-edition.adoc#features[additional features] +that offers xref:commercial-editions/commercial-editions.adoc#features[additional features] to scale out to very large datasets. -To find out more, see xref:enterprise-edition/enterprise-edition.adoc[Enterprise Edition section] of this documentation. +To find out more, see xref:commercial-editions/commercial-editions.adoc[Enterprise Edition section] of this documentation. == Does Timefold offer pre-built models? diff --git a/docs/src/modules/ROOT/pages/integration/_config-properties.adoc b/docs/src/modules/ROOT/pages/integration/_config-properties.adoc index 772873ccfce..932728a99d9 100644 --- a/docs/src/modules/ROOT/pages/integration/_config-properties.adoc +++ b/docs/src/modules/ROOT/pages/integration/_config-properties.adoc @@ -29,7 +29,7 @@ Enable xref:using-timefold-solver/running-the-solver.adoc#environmentMode[runtim implementation during development. {property_prefix}timefold.solver.{solver_name_prefix}constraint-stream-profiling-enabled:: -Enable xref:enterprise-edition/enterprise-edition.adoc#constraintProfiling[constraint profiling] to identify the constraints taking the most time in score calculation and thus might be worth optimizing. +Enable xref:commercial-editions/commercial-editions.adoc#constraintProfiling[constraint profiling] to identify the constraints taking the most time in score calculation and thus might be worth optimizing. {property_prefix}timefold.solver.{solver_name_prefix}daemon:: Enable xref:responding-to-change/responding-to-change.adoc#daemon[daemon mode]. @@ -38,11 +38,11 @@ This is often useful for xref:responding-to-change/responding-to-change.adoc#rea Defaults to `false`. {property_prefix}timefold.solver.{solver_name_prefix}move-thread-count:: -Enable xref:enterprise-edition/enterprise-edition.adoc#multithreadedIncrementalSolving[multithreaded incremental solving] for a single problem, which increases CPU consumption. +Enable xref:commercial-editions/commercial-editions.adoc#multithreadedIncrementalSolving[multithreaded incremental solving] for a single problem, which increases CPU consumption. Defaults to `NONE`. {property_prefix}timefold.solver.{solver_name_prefix}nearby-distance-meter-class:: -Enable the xref:enterprise-edition/enterprise-edition.adoc#nearbySelection[Nearby Selection] quick configuration. +Enable the xref:m#nearbySelection[Nearby Selection] quick configuration. If the Nearby Selection distance meter class is specified, the solver evaluates the available move selectors and automatically enables Nearby Selection for the compatible move selectors. diff --git a/docs/src/modules/ROOT/pages/integration/integration.adoc b/docs/src/modules/ROOT/pages/integration/integration.adoc index 1bbba1cceac..72c2a6221fb 100644 --- a/docs/src/modules/ROOT/pages/integration/integration.adoc +++ b/docs/src/modules/ROOT/pages/integration/integration.adoc @@ -1082,7 +1082,7 @@ Understand these guidelines to decide the hardware for a Timefold Solver service *** **Plenty**: There is enough heap space. The Garbage Collector is active, but its CPU usage is low. **** Adding more heap space does _not_ increase performance. **** Usually, this is around 300 to 500MB above the dataset size, _regardless of the problem scale_, -except with xref:enterprise-edition/enterprise-edition.adoc#nearbySelection[nearby selection] and caching move selector, +except with xref:commercial-editions/commercial-editions.adoc#nearbySelection[nearby selection] and caching move selector, neither of which are used by default. * **CPU power**: More is better. ** Improving CPU speed directly increases the xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed]. diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc index c876191db76..cb2366b6bff 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc @@ -899,7 +899,7 @@ With three or more variables, it's possible to combine the cartesian product and [#otherScalingTechniquesInConstructionHeuristics] === Other scaling techniques in construction heuristics -xref:enterprise-edition/enterprise-edition.adoc#partitionedSearch[Partitioned Search] reduces the number of moves per step. +xref:commercial-editions/commercial-editions.adoc#partitionedSearch[Partitioned Search] reduces the number of moves per step. On top of that, it runs the Construction Heuristic on the partitions in parallel. It is supported to only partition the Construction Heuristic phase. diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc index e9fe382b40d..35f4a5994ac 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc @@ -799,16 +799,207 @@ A replaying selector must reference a recorder's id with a ``mimicSelectorRef``: Mimic selection is useful to create <> from two moves that affect the same entity. - -[#nearbySelectionTeaser] -==== Nearby selection +[#nearbySelection] +=== Nearby selection NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. It is not available in the open source version. -Read about nearby selection in the xref:enterprise-edition/enterprise-edition.adoc#nearbySelection[Nearby selection section] -of xref:enterprise-edition/enterprise-edition.adoc[Timefold Solver Enterprise Edition]. +In some use cases (such as TSP and VRP, but also in other cases), +changing entities to nearby values or swapping nearby entities leads to better results faster. + +image::enterprise-edition/nearbySelectionMotivation.png[align="center"] + +This can *heavily increase scalability* and improve solution quality: + +image::enterprise-edition/nearbySelectionValueProposition.png[align="center"] + +Nearby selection increases the probability of selecting an entity or value which is nearby to the first entity being moved in that move. + +image::enterprise-edition/nearbySelectionRandomDistribution.png[align="center"] + +The distance between two entities or values is domain specific. +Therefore, implement the `NearbyDistanceMeter` interface: + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +public interface NearbyDistanceMeter { + + double getNearbyDistance(Origin_ origin, Destination_ destination); + +} +---- +==== + +In a nutshell, when nearby selection is used in a list move selector, +`Origin_` is always a planning value (for example `Customer`) +but `Destination_` can be either a planning value or a planning entity. +That means that in VRP the distance meter must be able to handle both `Customer` and `Vehicle` as the `Destination_` argument: + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +public class CustomerNearbyDistanceMeter implements NearbyDistanceMeter { + + public double getNearbyDistance(Customer origin, LocationAware destination) { + return origin.getDistanceTo(destination); + } + +} +---- +==== + +[NOTE] +==== +`NearbyDistanceMeter` implementations are expected to be stateless. +The solver may choose to reuse them in different contexts. + +The Nearby configuration is not enabled for the Construction Heuristics +because the method will analyze all possible moves. +Adding Nearby in this situation would only result in unnecessary costs +involving the generation of the distance matrix and sorting operations without taking advantage of the feature. +==== + +==== Nearby selection with a list variable + +To quickly configure nearby selection with a planning list variable, +add `nearbyDistanceMeterClass` element to your configuration file. +The following enables nearby selection with a list variable +for the local search: + +[source,xml,options="nowrap"] +---- + + + ... + org.acme.vehiclerouting.domain.solver.nearby.CustomerNearbyDistanceMeter + ... + +---- + +By default, the following move selectors are included: +xref:optimization-algorithms/move-selector-reference.adoc#changeMoveSelector[Change], +xref:optimization-algorithms/move-selector-reference.adoc#swapMoveSelector[Swap], +Change with Nearby, +Swap with Nearby, +and xref:optimization-algorithms/move-selector-reference.adoc#kOptListMoveSelector[2-OPT] with Nearby. + +===== Advanced configuration for local search + +To customize the move selectors, +add a `nearbySelection` element in the `destinationSelector`, `valueSelector` or `subListSelector` +and use xref:optimization-algorithms/move-selector-reference.adoc#mimicSelection[mimic selection] +to specify which destination, value, or subList should be nearby the selection. + +[source,xml,options="nowrap"] +---- + + + + + + + org.acme.vehiclerouting.domain.solver.nearby.CustomerNearbyDistanceMeter + + + + + + + + + org.acme.vehiclerouting.domain.solver.nearby.CustomerNearbyDistanceMeter + + + + + true + + + + + org.acme.vehiclerouting.domain.solver.nearby.CustomerNearbyDistanceMeter + + + + + true + + + + + org.acme.vehiclerouting.domain.solver.nearby.CustomerNearbyDistanceMeter + + + + +---- + +==== Power-tweaking distribution type + +The solver allows you to tweak the distribution type of the nearby selection, +or how likely are the nearest elements to be selected based on their distance from the current. + +[NOTE] +==== +Only tweak the default settings if you are prepared +to back your choices by extensive xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[benchmarking]. +==== +The following ``NearbySelectionDistributionType``s are supported: + +* `PARABOLIC_DISTRIBUTION` (default): Nearest elements are selected with a higher probability. ++ +[source,xml,options="nowrap"] +---- + + 80 + +---- ++ +A `distributionSizeMaximum` parameter should not be 1 because if the nearest is already the planning value of the current entity, +then the only move that is selectable is not doable. +To allow every element to be selected regardless of the number of entities, +only set the distribution type (so without a `distributionSizeMaximum` parameter): ++ +[source,xml,options="nowrap"] +---- + + PARABOLIC_DISTRIBUTION + +---- +* ``BLOCK_DISTRIBUTION``: Only the n nearest are selected, with an equal probability. For example, select the 20 nearest: ++ +[source,xml,options="nowrap"] +---- + + 20 + +---- +* ``LINEAR_DISTRIBUTION``: Nearest elements are selected with a higher probability. The probability decreases linearly. ++ +[source,xml,options="nowrap"] +---- + + 40 + +---- +* ``BETA_DISTRIBUTION``: Selection according to a beta distribution. Slows down the solver significantly. ++ +[source,xml,options="nowrap"] +---- + + 1 + 5 + +---- [#basicMoveSelectors] == Move selectors for basic variables @@ -1088,7 +1279,7 @@ opening up a new part of the solution space. [NOTE] ==== -If xref:enterprise-edition/enterprise-edition.adoc#nearbySelection[nearby selection] is enabled, +If xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[nearby selection] is enabled, the `RuinRecreateMove` is likely to underperform as it won't be able to rebuild the solution using nearby selection. This almost always results in worse solutions than those that were originally ruined, @@ -1376,7 +1567,7 @@ opening up a new part of the solution space. [NOTE] ==== -If xref:enterprise-edition/enterprise-edition.adoc#nearbySelection[nearby selection] is enabled, +If xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[nearby selection] is enabled, the `ListRuinRecreateMove` is likely to underperform as it won't be able to rebuild the solution using nearby selection. This almost always results in worse solutions than those that were originally ruined, diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/neighborhoods.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/neighborhoods.adoc index eb90da443fa..1e7a8e809d5 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/neighborhoods.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/neighborhoods.adoc @@ -96,7 +96,7 @@ that the user can optionally override to gain access to additional solver featur `Move rebase(Lookup lookup)`:: This method creates a copy of the move that is applicable to a different working solution. -This is only necessary when the solver is configured to use xref:enterprise-edition/enterprise-edition.adoc#multithreadedSolving[multi-threaded solving]. +This is only necessary when the solver is configured to use xref:commercial-editions/commercial-editions.adoc#multithreadedSolving[multi-threaded solving]. `String describe()`:: This method returns an identifier for the move type, which is used by parts of xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[the benchmarker] to separate results by move type. @@ -586,7 +586,7 @@ and the process of sampling is repeated each time the solver requests a new move The sampling phase currently only supports applying filtering and a limited selection of joiners, found in the `ai.timefold.solver.core.preview.api.neighborhood.stream.joiner.NeighborhoodsJoiners` class. More advanced selection strategies, -such as xref:enterprise-edition/enterprise-edition.adoc#nearbySelection[nearby selection], +such as xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[nearby selection], are likely to materialize here in the future as well. [#neighborhoodsMoveGeneration] @@ -790,6 +790,6 @@ and only then use that particular method to generate the move. The coexistence of Move Selectors and Neighborhoods is an issue we are actively working on. We will also work on adding more advanced moves in the default neighborhood, -such as 2-opt, 3-opt and xref:enterprise-edition/enterprise-edition.adoc#nearbySelection[nearby selection]. +such as 2-opt, 3-opt and xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[nearby selection]. The Neighborhoods API is still far from being a full replacement of Move Selectors. diff --git a/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc b/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc index 6ca8b5bda2c..58055f71477 100644 --- a/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc +++ b/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc @@ -748,7 +748,7 @@ But problems with the same publication deadline, solved by different organizatio are also initially better off with multi-stage planning, because of Conway's law and the high risk associated with unifying such groups. -Similarly to xref:enterprise-edition/enterprise-edition.adoc#partitionedSearch[Partitioned Search], multi-stage planning leads to suboptimal results. +Similarly to xref:commercial-editions/commercial-editions.adoc#partitionedSearch[Partitioned Search], multi-stage planning leads to suboptimal results. Nevertheless, it might be beneficial in order to simplify the maintenance, ownership, and help to start a project. Do not confuse multi-stage planning with xref:optimization-algorithms/optimization-algorithms.adoc#solverPhase[multi-phase solving]. diff --git a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-v1.adoc b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-v1.adoc index c427d65b91b..88764cd7a73 100644 --- a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-v1.adoc +++ b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-v1.adoc @@ -602,7 +602,7 @@ The `lookUpStrategyType` attribute has been removed from `@PlanningSolution`. Remove `lookUpStrategyType` from your `@PlanningSolution` annotations and ensure that your planning entities and problem facts have a `@PlanningId`-annotated field. -`LookupStrategyType` was used in xref:enterprise-edition/enterprise-edition.adoc#multithreadedIncrementalSolving[multi-threaded incremental solving] +`LookupStrategyType` was used in xref:commercial-editions/commercial-editions.adoc#multithreadedIncrementalSolving[multi-threaded incremental solving] to specify how the solver should match entities and facts between parent and child score directors. The default value was `PLANNING_ID_OR_NONE`, which meant that the solver would look up entities by their xref:using-timefold-solver/modeling-planning-problems.adoc#planningId[planning ID]. @@ -836,7 +836,7 @@ void onFinalBestSolution(FinalBestSolutionEvent event) { } ---- -Users of xref:enterprise-edition/enterprise-edition.adoc#throttlingBestSolutionEvents[solution throttling] +Users of xref:commercial-editions/commercial-editions.adoc#throttlingBestSolutionEvents[solution throttling] should also update their code to use `ThrottlingBestSolutionEventConsumer` instead of the now removed `ThrottlingBestSolutionConsumer`. ==== diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc index 17f86601029..f2f08ea5446 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc @@ -1067,7 +1067,7 @@ for example when running benchmarks on an application server or a cloud platform ---- -NOTE: This feature is independent of xref:enterprise-edition/enterprise-edition.adoc#multithreadedIncrementalSolving[multi-threaded incremental solving] +NOTE: This feature is independent of xref:commercial-editions/commercial-editions.adoc#multithreadedIncrementalSolving[multi-threaded incremental solving] and can be used in the open source version of Timefold Solver. diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc index 15213918e1f..20c190ca5fe 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc @@ -222,7 +222,7 @@ Alternatively, you can sometimes also introduce < Date: Tue, 14 Apr 2026 10:56:34 +0200 Subject: [PATCH 2/3] docs: add installation doc --- .../commercial-editions/installation.adoc | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/src/modules/ROOT/pages/commercial-editions/installation.adoc diff --git a/docs/src/modules/ROOT/pages/commercial-editions/installation.adoc b/docs/src/modules/ROOT/pages/commercial-editions/installation.adoc new file mode 100644 index 00000000000..abd7d6d5bc6 --- /dev/null +++ b/docs/src/modules/ROOT/pages/commercial-editions/installation.adoc @@ -0,0 +1,78 @@ += Installing Timefold Solver Plus & Enterprise + +In order to work with the commercial editions of Timefold Solver, 2 actions need to be taken: + +- <>; +- <>; + +[#solverLicenseKey] +== License Key + +A correctly configured and active license key is required in order for our commercial versions to function. + +[#solverObtainLicenseKey] +=== Obtaining a license key + + + +[NOTE] +==== +Take care not to leak the license file, for example by committing it to a public repository, +logging the license file content in logs, or sharing it with unauthorized parties. +==== + +[#solverSetupLicenseKey] +=== Set up your license key + +After obtaining your license file (.pem) you can introduce it to your project by one of the following methods: + +* Store the license's PEM string in the `TIMEFOLD_ENTERPRISE_LICENSE` environment variable. +* Store the absolute path to the file in `TIMEFOLD_ENTERPRISE_LICENSE_PATH` environment variable. +* Place the file in the user's home directory. +* Place the file in the root of your application classpath. + +[#enterpriseArtifacts] +== Use the enterprise artifacts + +The commercial edition uses different artifacts than our open-source version. +These artifacts are available from Maven Central, so no additional repositories need to be configured. + +[NOTE] +==== +To avoid operational complexity, all _commercial_ versions of Timefold Solver use the `enterprise` artifacts. +Features for your licensed version are enabled based on the provided license key. +==== + +Replace references to open-source artifacts by their `enterprise` counterparts +as shown in the following table: + +|=== +|Open source|Enterprise + +|`ai.timefold.solver:timefold-solver-bom` +|`ai.timefold.solver.enterprise:timefold-solver-enterprise-bom` + +|`ai.timefold.solver:timefold-solver-core` +|`ai.timefold.solver.enterprise:timefold-solver-enterprise-core` + +|`ai.timefold.solver:timefold-solver-jackson` +|`ai.timefold.solver.enterprise:timefold-solver-enterprise-jackson` + +|`ai.timefold.solver:timefold-solver-quarkus` +|`ai.timefold.solver.enterprise:timefold-solver-enterprise-quarkus` + +|`ai.timefold.solver:timefold-solver-quarkus-jackson` +|`ai.timefold.solver.enterprise:timefold-solver-enterprise-quarkus-jackson` + +|`ai.timefold.solver:timefold-solver-spring-boot-starter` +|`ai.timefold.solver.enterprise:timefold-solver-enterprise-spring-boot-starter` +|=== + +If you were not using some of these open source artifacts until now, +you do not need to add the enterprise counterparts to your project either. + +[#enterpriseLimitations] +== Limitations + +Blabla, Automatic Node Sharing puts some limitations. (Disable for now) + From a5bb22d12b35c3ffca24d370cc5d589a4d066136 Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Tue, 14 Apr 2026 14:57:00 +0200 Subject: [PATCH 3/3] docs: paid feature rework --- docs/src/modules/ROOT/nav.adoc | 18 +- .../commercial-editions/.only-enterprise.adoc | 1 + .../.only-plus-and-enterprise.adoc | 1 + .../commercial-editions.adoc | 843 +----------------- .../commercial-editions/multistage-moves.adoc | 15 + .../performance-improvements.adoc | 93 ++ .../constraints-and-score/performance.adoc | 168 +++- .../understanding-the-score.adoc | 5 +- .../move-selector-reference.adoc | 3 +- .../responding-to-change.adoc | 2 + .../running-the-solver.adoc | 414 ++++++++- 11 files changed, 725 insertions(+), 838 deletions(-) create mode 100644 docs/src/modules/ROOT/pages/commercial-editions/.only-enterprise.adoc create mode 100644 docs/src/modules/ROOT/pages/commercial-editions/.only-plus-and-enterprise.adoc create mode 100644 docs/src/modules/ROOT/pages/commercial-editions/multistage-moves.adoc create mode 100644 docs/src/modules/ROOT/pages/commercial-editions/performance-improvements.adoc diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index d4b53547e46..3ce6829d175 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -15,7 +15,7 @@ * Constraints and Score ** xref:constraints-and-score/overview.adoc[leveloffset=+1] ** xref:constraints-and-score/score-calculation.adoc[leveloffset=+1] -** xref:constraints-and-score/understanding-the-score.adoc[leveloffset=+1] +** xref:constraints-and-score/understanding-the-score.adoc[Understanding the score] ** xref:constraints-and-score/constraint-configuration.adoc[leveloffset=+1] ** xref:constraints-and-score/load-balancing-and-fairness.adoc[leveloffset=+1] ** xref:constraints-and-score/performance.adoc[leveloffset=+1] @@ -40,12 +40,12 @@ *** xref:upgrading-timefold-solver/migration-guides/chained-variables-to-planning-list-variable.adoc[leveloffset=+1] * xref:commercial-editions/commercial-editions.adoc[leveloffset=+1] ** xref:commercial-editions/installation.adoc[leveloffset=+1] -** Automatic Performance Improvements (Node sharing, DSVs...) +** xref:commercial-editions/performance-improvements.adoc[leveloffset=+1] +** xref:constraints-and-score/understanding-the-score.adoc[Score Analysis] +** xref:responding-to-change/responding-to-change.adoc#assignmentRecommendationAPI[Recommendation API] ** xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[Nearby selection] -** Multi-threaded Solving -** Partitioned Search -** Constraint Profiling -** Throttling Events -** Multistage Moves - - +** xref:using-timefold-solver/running-the-solver.adoc#multithreadedIncrementalSolving[Multithreaded solving] +** xref:using-timefold-solver/running-the-solver.adoc#partitionedSearch[Partitioned search] +** xref:constraints-and-score/performance.adoc#constraintProfiling[Constraint profiling] +** xref:commercial-editions/multistage-moves.adoc[leveloffset=+1] +** xref:using-timefold-solver/running-the-solver.adoc#throttlingBestSolutionEvents[Throttling best solution events] diff --git a/docs/src/modules/ROOT/pages/commercial-editions/.only-enterprise.adoc b/docs/src/modules/ROOT/pages/commercial-editions/.only-enterprise.adoc new file mode 100644 index 00000000000..c7a812736d0 --- /dev/null +++ b/docs/src/modules/ROOT/pages/commercial-editions/.only-enterprise.adoc @@ -0,0 +1 @@ +NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. \ No newline at end of file diff --git a/docs/src/modules/ROOT/pages/commercial-editions/.only-plus-and-enterprise.adoc b/docs/src/modules/ROOT/pages/commercial-editions/.only-plus-and-enterprise.adoc new file mode 100644 index 00000000000..619ee188d8f --- /dev/null +++ b/docs/src/modules/ROOT/pages/commercial-editions/.only-plus-and-enterprise.adoc @@ -0,0 +1 @@ +NOTE: This feature is exclusive to Timefold Solver Plus and Enterprise Editions. \ No newline at end of file diff --git a/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc b/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc index f0a3c170d65..efa193f1289 100644 --- a/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc +++ b/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc @@ -2,11 +2,11 @@ :page-aliases: partitioned-search/partitioned-search.adoc, \ enterprise-edition/enterprise-edition.adoc :doctype: book -:sectnums: :icons: font -_Timefold Solver Plus_ and _Timefold Solver Enterprise_ are commercial products which offer <>, -such as <>, <> and <>. +_Timefold Solver Plus_ and _Timefold Solver Enterprise_ are commercial products which offer additional features, +such as xref:constraints-and-score/understanding-the-score.adoc[score analysis], xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[nearby selection] and xref:using-timefold-solver/running-the-solver.adoc#multithreadedIncrementalSolving[multithreaded solving] +. These features are essential to scale out to large datasets and build trust with your users. Unlike _Timefold Solver Community Edition_, these commercial editions are not open-source. @@ -17,828 +17,37 @@ TIP: Looking for quicker time-to-value? Timefold offers https://docs.timefold.ai/[pre-built, fully tuned optimization models], no constraint building required. Just plug into our API and start optimizing immediately. -[#enterpriseEditionFeatures] -== Features included in Enterprise Edition +== Feature Comparison -The following features are only available in _Timefold Solver Enterprise Edition_: +[cols="4,^1,^1,^1", options="header"] +|=== +| Feature | Community | Plus | Enterprise -* xref:constraints-and-score/understanding-the-score.adoc[Explainability], -* <> for faster and better results, -* <> and <>, -* <> for speeding up Constraint Streams, -* xref:constraints-and-score/score-calculation.adoc#incrementalScoreCalculation[incremental score calculator] for the fastest possible move evaluation, -* and <> to not overload the consumers such as messaging queues. +| xref:constraints-and-score/understanding-the-score.adoc[Score Analysis] +| | ✓ | ✓ -We also automatically enable performance optimizations not available in the open-source version, -leading to significantly improved performance for some use cases, such as: +| xref:responding-to-change/responding-to-change.adoc#assignmentRecommendationAPI[Recommendation API] +| | ✓ | ✓ -- Heavy use of xref:using-timefold-solver/modeling-planning-problems.adoc#customShadowVariable[custom shadow variables]. +| xref:commercial-editions/performance-improvements.adoc[Performance improvements] +| | | ✓ +| xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[Nearby selection] +| | | ✓ -[#multithreadedSolving] -[#enterpriseMultithreadedSolving] -=== Multi-threaded solving +| xref:using-timefold-solver/running-the-solver.adoc#multithreadedIncrementalSolving[Multithreaded solving] +| | | ✓ -Multi-threaded solving is a term -which encapsulates several features that allow Timefold Solver to run on multi-core machines, -such as -<> and -<>. +| xref:using-timefold-solver/running-the-solver.adoc#partitionedSearch[Partitioned search] +| | | ✓ -For a primer on multi-threaded solving in general, see xref:using-timefold-solver/running-the-solver.adoc#multithreadedSolving[Multi-threaded solving]. +| xref:constraints-and-score/performance.adoc#constraintProfiling[Constraint profiling] +| | | ✓ -[#multithreadedIncrementalSolving] -==== Multi-threaded incremental solving +| xref:commercial-editions/multistage-moves.adoc[Multistage moves] +| | | ✓ -NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. -It is not available in the open source version. +| xref:using-timefold-solver/running-the-solver.adoc#throttlingBestSolutionEvents[Throttling best solution events] +| | | ✓ -With this feature, the solver can run significantly faster, -getting you the right solution earlier. -It has been designed to speed up the solver in cases where move evaluation is the bottleneck. -This typically happens when the constraints are computationally expensive, -or when the dataset is large. - -- The sweet spot for this feature is when the move evaluation speed is up to 10 thousand per second. -In this case, we have observed the algorithm to scale linearly with the number of move threads. -Every additional move thread will bring a speedup, -albeit with diminishing returns. -- For move evaluation speeds on the order of 100 thousand per second, -the algorithm no longer scales linearly, -but using 4 to 8 move threads may still be beneficial. -- For even higher move evaluation speeds, -the feature does not bring any benefit. -At these speeds, move evaluation is no longer the bottleneck. -If the solver continues to underperform, -perhaps you're suffering from xref:constraints-and-score/performance.adoc#scoreTrap[score traps] -or you may benefit from xref:optimization-algorithms/overview.adoc#customMoves[custom moves] -to help the solver escape local optima. - -[NOTE] -==== -These guidelines are strongly dependent on move selector configuration, -size of the dataset and performance of individual constraints. -We recommend you benchmark your use case -to determine the optimal number of move threads for your problem. -==== - -===== Enabling multi-threaded incremental solving - -Enable multi-threaded incremental solving -by xref:using-timefold-solver/modeling-planning-problems.adoc#planningId[adding a `@PlanningId` annotation] -on every planning entity class and planning value class. -Then configure a `moveThreadCount`: - -[tabs] -==== -Quarkus:: -+ --- -Add the following to your `application.properties`: - -[source,properties] ----- -quarkus.timefold.solver.move-thread-count=AUTO ----- --- -Spring:: -+ --- -Add the following to your `application.properties`: - -[source,properties] ----- -timefold.solver.move-thread-count=AUTO ----- --- -Java:: -+ --- -Use the `SolverConfig` class: - -[source,java,options="nowrap"] ----- -SolverConfig solverConfig = new SolverConfig() - ... - .withMoveThreadCount("AUTO"); ----- --- -XML:: -+ --- -Add the following to your `solverConfig.xml`: -[source,xml,options="nowrap"] ----- - - - ... - AUTO - ... - - ----- --- -==== - -Setting `moveThreadCount` to `AUTO` allows Timefold Solver to decide how many move threads to run in parallel. -This formula is based on experience and does not hog all CPU cores on a multi-core machine. - -A `moveThreadCount` of `4` xref:integration/integration.adoc#sizingHardwareAndSoftware[saturates almost 5 CPU cores]. -the 4 move threads fill up 4 CPU cores completely -and the solver thread uses most of another CPU core. - -The following ``moveThreadCount``s are supported: - -* `NONE` (default): Don't run any move threads. Use the single threaded code. -* ``AUTO``: Let Timefold Solver decide how many move threads to run in parallel. -On machines or containers with little or no CPUs, this falls back to the single threaded code. -* Static number: The number of move threads to run in parallel. - -It is counter-effective to set a `moveThreadCount` -that is higher than the number of available CPU cores, -as that will slow down the move evaluation speed. - -[IMPORTANT] -==== -In cloud environments where resource use is billed by the hour, -consider the trade-off between cost of the extra CPU cores needed and the time saved. -Compute nodes with higher CPU core counts are typically more expensive to run -and therefore you may end up paying more for the same result, -even though the actual compute time needed will be less. -==== - -[NOTE] -==== -Multi-threaded solving is _still reproducible_, as long as the resolved `moveThreadCount` is stable. -A run of the same solver configuration on 2 machines with a different number of CPUs, -is still reproducible, unless the `moveThreadCount` is set to `AUTO` or a function of `availableProcessorCount`. -==== - -===== Advanced configuration - -There are additional parameters you can supply to your `solverConfig.xml`: - -[source,xml,options="nowrap"] ----- - - 4 - ...MyAppServerThreadFactory - ... - ----- - -To run in an environment that doesn't like arbitrary thread creation, -use `threadFactoryClass` to plug in a <>. - - -[#partitionedSearch] -==== Partitioned search - -NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. -It is not available in the open source version. - -[#partitionedSearchAlgorithm] -===== Algorithm description - -It is often more efficient to partition large data sets (usually above 5000 planning entities) -into smaller pieces and solve them separately. -Partition Search is <>, -so it provides a performance boost on multi-core machines due to higher CPU utilization. -Additionally, even when only using one CPU, it finds an initial solution faster, -because the search space sum of a partitioned Construction Heuristic is far less than its non-partitioned variant. - -However, **partitioning does lead to suboptimal results**, even if the pieces are solved optimally, as shown below: - -image::enterprise-edition/mapReduceIsTerribleForTsp.png[align="center"] - -It effectively trades a short term gain in solution quality for long term loss. -One way to compensate for this loss, -is to run a non-partitioned Local Search after the Partitioned Search phase. - -[NOTE] -==== -Not all use cases can be partitioned. -Partitioning only works for use cases where the planning entities and value ranges can be split into n partitions, -without any of the constraints crossing boundaries between partitions. -==== - -[#partitionedSearchConfiguration] -===== Configuration - -Simplest configuration: - -[source,xml,options="nowrap"] ----- - - ...MyPartitioner - ----- - -Also xref:using-timefold-solver/modeling-planning-problems.adoc#planningId[add a `@PlanningId` annotation] -on every planning entity class and planning value class. -There are several ways to <>. - -Advanced configuration: - -[source,xml,options="nowrap"] ----- - - ... - ...MyPartitioner - 4 - - ... - ... - ----- - -The `runnablePartThreadLimit` allows limiting CPU usage to avoid hanging your machine, see below. - -To run in an environment that doesn't like arbitrary thread creation, -plug in a <>. - -[IMPORTANT] -==== -A xref:using-timefold-solver/running-the-solver.adoc#logging[logging level] of `debug` or `trace` causes congestion in multi-threaded Partitioned Search -and slows down the xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed]. -==== - -Just like a `` element, -the `` element can contain one or more xref:optimization-algorithms/overview.adoc#solverPhase[phases]. -Each of those phases will be run on each partition. - -A common configuration is to first run a Partitioned Search phase -(which includes a Construction Heuristic and a Local Search) -followed by a non-partitioned Local Search phase: - -[source,xml,options="nowrap"] ----- - - ...MyPartitioner - - - - - - - - - ----- - - -[#partitioningASolution] -===== Partitioning a solution - - -[#customSolutionPartitioner] -====== Custom `SolutionPartitioner` - -To use a custom `SolutionPartitioner`, configure one on the Partitioned Search phase: - -[source,xml,options="nowrap"] ----- - - ...MyPartitioner - ----- - -Implement the `SolutionPartitioner` interface: - -[source,java,options="nowrap"] ----- -public interface SolutionPartitioner { - - List splitWorkingSolution(Solution_ workingSolution, Integer runnablePartThreadLimit); - -} ----- - -The `size()` of the returned `List` is the `partCount` (the number of partitions). -This can be decided dynamically, for example, based on the size of the non-partitioned solution. -The `partCount` is unrelated to the `runnablePartThreadLimit`. - -To configure values of a `SolutionPartitioner` dynamically in the solver configuration -(so the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] can tweak those parameters), -add the `solutionPartitionerCustomProperties` element and use xref:using-timefold-solver/configuration.adoc#customPropertiesConfiguration[custom properties]: - -[source,xml,options="nowrap"] ----- - - ...MyPartitioner - - - - - ----- - -[#runnablePartThreadLimit] -===== Runnable part thread limit - -When running a multi-threaded solver, such as Partitioned Search, CPU power can quickly become a scarce resource, -which can cause other processes or threads to hang or freeze. -However, Timefold Solver has a system to prevent CPU starving of -other processes (such as an SSH connection in production or your IDE in development) -or other threads (such as the servlet threads that handle REST requests). - -As explained in xref:integration/integration.adoc#sizingHardwareAndSoftware[sizing hardware and software], -each solver (including each child solver) does no IO during `solve()` and therefore saturates one CPU core completely. -In Partitioned Search, every partition always has its own thread, called a part thread. -It is impossible for two partitions to share a thread, -because of xref:optimization-algorithms/overview.adoc#asynchronousTermination[asynchronous termination]: -the second thread would never run. -Every part thread will try to consume one CPU core entirely, so if there are more partitions than CPU cores, -this will probably hang the system. -`Thread.setPriority()` is often too weak to solve this hogging problem, so another approach is used. - -The `runnablePartThreadLimit` parameter specifies how many part threads are runnable at the same time. -The other part threads will temporarily block and therefore will not consume any CPU power. -*This parameter basically specifies how many CPU cores are donated to Timefold Solver.* -All part threads share the CPU cores in a round-robin manner -to consume (more or less) the same number of CPU cycles: - -image::enterprise-edition/partitionedSearchThreading.png[align="center"] - -The following `runnablePartThreadLimit` options are supported: - -* `UNLIMITED`: Allow Timefold Solver to occupy all CPU cores, do not avoid hogging. -Useful if a no hogging CPU policy is configured on the OS level. -* `AUTO` (default): Let Timefold Solver decide how many CPU cores to occupy. This formula is based on experience. -It does not hog all CPU cores on a multi-core machine. -* Static number: The number of CPU cores to consume. For example: -+ -[source,xml,options="nowrap"] ----- -2 ----- - -[WARNING] -==== -If the `runnablePartThreadLimit` is equal to or higher than the number of available processors, -the host is likely to hang or freeze, -unless there is an OS specific policy in place to avoid Timefold Solver from hogging all the CPU processors. -==== - - -[#customThreadFactory] -==== Custom thread factory (WildFly, GAE, ...) - -The `threadFactoryClass` allows to plug in a custom `ThreadFactory` for environments -where arbitrary thread creation should be avoided, -such as most application servers (including WildFly) or Google App Engine. - -Configure the `ThreadFactory` on the solver to create the <> -and the <> with it: - -[source,xml,options="nowrap"] ----- - - ...MyAppServerThreadFactory - ... - ----- - - -[#automaticNodeSharing] -=== Automatic node sharing - -NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. -It is not available in the open source version. - -When a `ConstraintProvider` does an operation for multiple constraints (such as finding all shifts corresponding to an employee), that work can be shared. -This can significantly improve move evaluation speed if the repeated operation is computationally expensive: - -image::enterprise-edition/nodeSharingValueProposition.png[align="center"] - -==== Configuration - -[tabs] -==== -XML:: - -* Add `true` in your `solverConfig.xml`: -+ -[source,xml,options="nowrap"] ----- - - - org.acme.MyConstraintProvider - true - - ----- - -Spring Boot:: - -Set the property `timefold.solver.constraint-stream-automatic-node-sharing` to `true` in `application.properties`: -+ -[source,properties,options="nowrap"] ----- -timefold.solver.constraint-stream-automatic-node-sharing=true ----- - -Quarkus:: - -Set the property `quarkus.timefold.solver.constraint-stream-automatic-node-sharing` to `true` in `application.properties`: -+ -[source,properties,options="nowrap"] ----- -quarkus.timefold.solver.constraint-stream-automatic-node-sharing=true ----- -==== - -[IMPORTANT] -==== -To use automatic node sharing outside Quarkus, your `ConstraintProvider` class must oblige by several restrictions so a valid subclass can be generated: - -- The `ConstraintProvider` class cannot be final. -- The `ConstraintProvider` class cannot have any final methods. -- The `ConstraintProvider` class cannot access any protected classes, methods or fields. - -Debugging breakpoints put inside your constraints will not be respected, because the `ConstraintProvider` class will be transformed when this feature is enabled. -==== - -[#nodeSharing] -==== What is node sharing? - -When using xref:constraints-and-score/score-calculation.adoc#constraintStreams[constraint streams], each xref:constraints-and-score/score-calculation.adoc#constraintStreamsBuildingBlocks[building block] forms a node in the score calculation network. -When two building blocks are functionally equivalent, they can share the same node in the network. -Sharing nodes allows the operation to be performed only once instead of multiple times, improving the performance of the solver. -To be functionally equivalent, the following must be true: - -* The building blocks must represent the same operation. - -* The building blocks must have functionally equivalent parent building blocks. - -* The building blocks must have functionally equivalent inputs. - -For example, the building blocks below are functionally equivalent: - -[source,java,options="nowrap"] ----- -Predicate predicate = shift -> shift.getEmployee().getName().equals("Ann"); - -var a = factory.forEach(Shift.class) - .filter(predicate); - -var b = factory.forEach(Shift.class) - .filter(predicate); ----- - -Whereas these building blocks are not functionally equivalent: - -[source,java,options="nowrap"] ----- -Predicate predicate1 = shift -> shift.getEmployee().getName().equals("Ann"); -Predicate predicate2 = shift -> shift.getEmployee().getName().equals("Bob"); - -// Different parents -var a = factory.forEach(Shift.class) - .filter(predicate2); - -var b = factory.forEach(Shift.class) - .filter(predicate1) - .filter(predicate2); - -// Different operations -var a = factory.forEach(Shift.class) - .ifExists(Employee.class); - -var b = factory.forEach(Shift.class) - .ifNotExists(Employee.class); - -// Different inputs -var a = factory.forEach(Shift.class) - .filter(predicate1); - -var b = factory.forEach(Shift.class) - .filter(predicate2); ----- - -Counterintuitively, the building blocks produced by these (seemly) identical methods are not necessarily functionally equivalent: - -[source,java,options="nowrap"] ----- -UniConstraintStream a(ConstraintFactory constraintFactory) { - return factory.forEach(Shift.class) - .filter(shift -> shift.getEmployee().getName().equals("Ann")); -} - -UniConstraintStream b(ConstraintFactory constraintFactory) { - return factory.forEach(Shift.class) - .filter(shift -> shift.getEmployee().getName().equals("Ann")); -} ----- - -The Java Virtual Machine is free to (and often does) create different instances of functionally equivalent lambdas. -This severely limits the effectiveness of node sharing, since the only way to know two lambdas are equal is to compare their references. - -When automatic node sharing is used, the `ConstraintProvider` class is transformed so all lambdas are accessed via a static final field. -Consider the following input class: - -[source,java,options="nowrap"] ----- -public class MyConstraintProvider implements ConstraintProvider { - - public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { - return new Constraint[] { - a(constraintFactory), - b(constraintFactory) - }; - } - - Constraint a(ConstraintFactory constraintFactory) { - return factory.forEach(Shift.class) - .filter(shift -> shift.getEmployee().getName().equals("Ann")) - .penalize(SimpleScore.ONE) - .asConstraint("a"); - } - - Constraint b(ConstraintFactory constraintFactory) { - return factory.forEach(Shift.class) - .filter(shift -> shift.getEmployee().getName().equals("Ann")) - .penalize(SimpleScore.ONE) - .asConstraint("b"); - } -} ----- - -When automatic node sharing is enabled, the class will be transformed to look like this: - -[source,java,options="nowrap"] ----- -public class MyConstraintProvider implements ConstraintProvider { - private static final Predicate $predicate1 = shift -> shift.getEmployee().getName().equals("Ann"); - - public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { - return new Constraint[] { - a(constraintFactory), - b(constraintFactory) - }; - } - - Constraint a(ConstraintFactory constraintFactory) { - return factory.forEach(Shift.class) - .filter($predicate1) - .penalize(SimpleScore.ONE) - .asConstraint("a"); - } - - Constraint b(ConstraintFactory constraintFactory) { - return factory.forEach(Shift.class) - .filter($predicate1) - .penalize(SimpleScore.ONE) - .asConstraint("b"); - } -} ----- - -This transformation means that debugging breakpoints placed inside the original `ConstraintProvider` will not be honored in the transformed `ConstraintProvider`. - -From the above, you can see how this feature allows building blocks to share functionally equivalent parents, without needing the `ConstraintProvider` to be written in an awkward way. - -[#constraintProfiling] -=== Constraint Profiling - -NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. -It is not available in the open source version. - -Profiling allows you to identify which constraints are taking the majority of time spent in score calculation and thus are worth optimizing. -Unfortunately, traditional profiling tools only report the internal classes used to calculate constraints instead of the constraints themselves. xref:constraints-and-score/score-calculation.adoc#constraintStreams[Constraint streams] have builtin support for profiling constraints directly. - -==== Configuration - -[tabs] -==== -XML:: - -* Add `true` in your `solverConfig.xml`: -+ -[source,xml,options="nowrap"] ----- - - ...ConstraintProvider - true - ----- - -Spring Boot:: - -Set the property `timefold.solver.constraint-stream-profiling-enabled` to `true` in `application.properties`: -+ -[source,properties,options="nowrap"] ----- -timefold.solver.constraint-stream-profiling-enabled=true ----- - -Quarkus:: - -Set the property `quarkus.timefold.solver.constraint-stream-profiling-enabled` to `true` in `application.properties`: -+ -[source,properties,options="nowrap"] ----- -quarkus.timefold.solver.constraint-stream-profiling-enabled=true ----- -==== - -Constraint profiling will log its results to the `ai.timefold.solver.enterprise.core.api.ConstraintProfiler` class at the end of solving. -For the report to be generated, xref:using-timefold-solver/running-the-solver.adoc#logging[the particular logging levels must be configured and enabled]. - -IMPORTANT: Constraint profiling is only supported for xref:constraints-and-score/score-calculation.adoc#constraintStreams[constraint stream score calculation] -when <> is disabled. - -==== Understanding the report - -When constraint stream profiling is enabled, a breakdown of time spent in each constraint is logged at the end of solving. - -[source,text] ----- -INFO: # Constraint Profiling Summary -| | Time Spent | Of Total | -| :------------------------------------------------- | ---------: | -------: | -| 2: Teacher time efficiency | 6.19 s | 23.15 % | -| 6: Room conflict | 4.17 s | 15.61 % | -| 4: Student group conflict | 3.76 s | 14.07 % | -| 5: Teacher conflict | 3.41 s | 12.76 % | -| 1: Student group subject variety | 3.25 s | 12.14 % | -| 3: Teacher room stability | 3.25 s | 12.14 % | -| Work shared by 1, 2, 3, 4, 5, 6 | 2.71 s | 10.12 % | - -DEBUG: ## Constraint Profiling Node Breakdown -| 2: Teacher time efficiency | Time Spent | Of Total | -| :------------------------------------------------- | ---------: | -------: | -| JOIN Node 26 | 1.56 s | 25.20 % | -| JOIN Left Input 6 | 1.28 s | 20.69 % | -| JOIN Right Input 7 | 1.09 s | 17.55 % | -| FILTER 5 | 1.83 s | 29.49 % | -| SCORING 4 | 0.44 s | 7.06 % | - - - JOIN Node 26 defined at location org.acme.schooltimetabling.solver.TimetableConstraintProvider#teacherTimeEfficiency:77 - - JOIN Left Input 6 defined at location org.acme.schooltimetabling.solver.TimetableConstraintProvider#teacherTimeEfficiency:77 - - JOIN Right Input 7 defined at location org.acme.schooltimetabling.solver.TimetableConstraintProvider#teacherTimeEfficiency:77 - - FILTER 5 defined at location org.acme.schooltimetabling.solver.TimetableConstraintProvider#teacherTimeEfficiency:79 - - SCORING 4 defined at location org.acme.schooltimetabling.solver.TimetableConstraintProvider#teacherTimeEfficiency:84 - -... - -TRACE: ## Constraint Profiling Lifecycle Breakdown -| 2: Teacher time efficiency | INSERT | UPDATE | RETRACT | -| :--------------------------------------- | -------: | -------: | --------: | -| JOIN | 13.12 % | 75.64 % | 11.23 % | -| FILTER | 23.69 % | 50.45 % | 25.86 % | -| SCORING | 13.10 % | 16.12 % | 70.78 % | - -... ----- - -NOTE: The data is printed as a https://www.markdownguide.org/extended-syntax/#tables[Markdown table]. -Any Markdown-enabled text editor will show a well-formatted table. - -Add different logging levels, different information is reported: - -INFO:: How much time spent in score calculation is spent by each constraint (total and relative percentage) in descending order. -+ -[source,text] ----- -| 2: Teacher time efficiency | 6.19 s | 23.15 % | ----- -+ -xref:commercial-editions/commercial-editions.adoc#nodeSharing[When node sharing] is used, the time spent will be reported with "Work shared by", with the numbers identifying constraints that shared the work. -In the example below, work was shared by constraints 1 to 6: -+ -[source,text] ----- -| Work shared by 1, 2, 3, 4, 5, 6 | 2.71 s | 10.12 % | ----- - -`DEBUG`:: A breakdown for each constraint of time spent (total and relative percentage) by each constraint stream building block (e.g., join, filter). -+ -[source,text] ----- -| 2: Teacher time efficiency | Time Spent | Of Total | -| :------------------------------------------------- | ---------: | -------: | -| JOIN Node 26 | 1.56 s | 25.20 % | -| JOIN Left Input 6 | 1.28 s | 20.69 % | -| JOIN Right Input 7 | 1.09 s | 17.55 % | -| FILTER 5 | 1.83 s | 29.49 % | -| SCORING 4 | 0.44 s | 7.06 % | ----- -+ -To make it easier to map the building blocks to your `ConstraintProvider` code, -the location(s) where each building block was defined is also reported: -+ -[source,text] ----- - - JOIN Node 26 defined at location org.acme.schooltimetabling.solver.TimetableConstraintProvider#teacherTimeEfficiency:77 ----- - -`TRACE`:: A breakdown for each constraint of time spent by each building block, -shown as percentage of time spent in each of the lifecycle phases. -+ -[source,text] ----- -| 2: Teacher time efficiency | INSERT | UPDATE | RETRACT | -| :--------------------------------------- | -------: | -------: | --------: | -| JOIN | 13.12 % | 75.64 % | 11.23 % | -| FILTER | 23.69 % | 50.45 % | 25.86 % | -| SCORING | 13.10 % | 16.12 % | 70.78 % | ----- -+ -NOTE: This is only relevant to people intimately familiar with the underlying implementation of Constraint Streams -and can be safely ignored by everyday users. - -==== Interpreting the results - -Constraint profiling measures wall clock time, not CPU time, -and as such includes any garbage collection pauses that may occur during score calculation. -These numbers should be used as a guide to identify which constraints are the most expensive, -not as an exact measurement. - -Due to the nature of the JVM, the results may vary between runs. -The solver needs to run for a sufficient amount of time -to allow the JVM to warm up and stabilize. -We recommend running the solver for minutes rather than seconds, -but more than that is unlikely to bring any additional benefit. -For increased confidence in the results, -you can run the solver multiple times in different JVM processes -and average the results. - - -[#throttlingBestSolutionEvents] -=== Throttling best solution events in `SolverManager` - -NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. -It is not available in the open source version. - -This feature helps you avoid overloading your system with best solution events, -especially in the early phase of the solving process when the solver is typically improving the solution very rapidly. - -To enable event throttling, use `ThrottlingBestSolutionEventConsumer` when starting a new `SolverJob` using `SolverManager`: - -[source,java,options="nowrap"] ----- -... -import ai.timefold.solver.enterprise.core.api.ThrottlingBestSolutionEventConsumer; -import java.time.Duration; -... - -public class TimetableService { - - private SolverManager solverManager; - - public String solve(Timetable problem) { - var bestSolutionEventConsumer = ThrottlingBestSolutionEventConsumer.of( - event -> { - // Your custom event handling code goes here. - }, - Duration.ofSeconds(1)); // Throttle to 1 event per second. - - String jobId = ...; - solverManager.solveBuilder() - .withProblemId(jobId) - .withProblem(problem) - .withBestSolutionEventConsumer(bestSolutionEventConsumer) - .run(); // Start the solver job and listen to best solutions, with throttling. - return jobId; - } - -} ----- - -This will ensure that your system will never receive more than one best solution event per second. -Some other important points to note: - -- If multiple events arrive during the pre-defined 1-second interval, only the last event will be delivered. -- When the `SolverJob` terminates, the last event received will be delivered regardless of the throttle, -unless it was already delivered before. -- If your consumer throws an exception, we will still count the event as delivered. -- If the system is too occupied to start and execute new threads, -event delivery will be delayed until a thread can be started. - -[NOTE] -==== -If you are using the `ThrottlingBestSolutionEventConsumer` for intermediate best solutions -together with a final best solution consumer, -both these consumers will receive the final best solution. -==== - -[#multistageMoves] -=== Multistage Moves - -NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. -It is not available in the open source version. - -Multistage moves are moves composed of one or more stages, where each stage selects a single `Move` to execute. - -Each stage has access to either a `BasicVariableMoveEvaluator` or a `ListVariableMoveEvaluator`, which allows the stage to evaluate moves without executing them. - -Stages are selected from either a `BasicVariableStageProvider` or a `ListVariableStageProvider`, which is initialized from the working solution at phase start. - -Multistage moves are configured from either a xref:optimization-algorithms/move-selector-reference.adoc#multistageMoveSelector[MultistageMoveSelectorConfig] -or a xref:optimization-algorithms/move-selector-reference.adoc#listMultistageMoveSelector[ListMultistageMoveSelectorConfig]. - -Multistage moves are useful for creating specialized ruin-and-recreate moves where the valid values that won't violate hard constraints can be determined in advance. +|=== \ No newline at end of file diff --git a/docs/src/modules/ROOT/pages/commercial-editions/multistage-moves.adoc b/docs/src/modules/ROOT/pages/commercial-editions/multistage-moves.adoc new file mode 100644 index 00000000000..e58925c9fe6 --- /dev/null +++ b/docs/src/modules/ROOT/pages/commercial-editions/multistage-moves.adoc @@ -0,0 +1,15 @@ +[#multistageMoves] += Multistage Moves + +include::.only-enterprise.adoc[] + +Multistage moves are moves composed of one or more stages, where each stage selects a single `Move` to execute. + +Each stage has access to either a `BasicVariableMoveEvaluator` or a `ListVariableMoveEvaluator`, which allows the stage to evaluate moves without executing them. + +Stages are selected from either a `BasicVariableStageProvider` or a `ListVariableStageProvider`, which is initialized from the working solution at phase start. + +Multistage moves are configured from either a xref:optimization-algorithms/move-selector-reference.adoc#multistageMoveSelector[MultistageMoveSelectorConfig] +or a xref:optimization-algorithms/move-selector-reference.adoc#listMultistageMoveSelector[ListMultistageMoveSelectorConfig]. + +Multistage moves are useful for creating specialized ruin-and-recreate moves where the valid values that won't violate hard constraints can be determined in advance. diff --git a/docs/src/modules/ROOT/pages/commercial-editions/performance-improvements.adoc b/docs/src/modules/ROOT/pages/commercial-editions/performance-improvements.adoc new file mode 100644 index 00000000000..36ce893b103 --- /dev/null +++ b/docs/src/modules/ROOT/pages/commercial-editions/performance-improvements.adoc @@ -0,0 +1,93 @@ += Performance Improvements + +Timefold Solver Enterprise brings many performance improvements. These are shortly described here. + +[#fasterCustomShadowVariables] +== Faster shadow variables + +include::.only-enterprise.adoc[] + +Updates to shadow variables happen incrementally with Timefold Solver Enterprise. +For models making intensive use of xref:using-timefold-solver/modeling-planning-problems.adoc#customShadowVariable[shadow variables], this should be visible as a serious performance improvement out-of-the-box. + +This is enabled by default and doesn't require any special considerations. + +[#automaticNodeSharing] +== Node sharing + +include::.only-enterprise.adoc[] + +When a `ConstraintProvider` does an operation for multiple constraints (such as finding all shifts corresponding to an employee), that work can be shared. +This can significantly improve move evaluation speed if the repeated operation is computationally expensive: + +image::enterprise-edition/nodeSharingValueProposition.png[align="center"] + +[IMPORTANT] +==== +Debugging breakpoints put inside your constraints will not be respected, because the `ConstraintProvider` class will be transformed when this feature is enabled. +==== + +[IMPORTANT] +==== +To use automatic node sharing outside Quarkus, your `ConstraintProvider` class must oblige by several restrictions so a valid subclass can be generated: + +- The `ConstraintProvider` class cannot be final. +- The `ConstraintProvider` class cannot have any final methods. +- The `ConstraintProvider` class cannot access any protected classes, methods or fields. +==== + +=== Configuration + +Node sharing is automatically enabled when running Timefold Enterprise Edition. +You can disable the feature by changing the following settings. + +[tabs] +==== +XML:: + +* Add `false` in your `solverConfig.xml`: ++ +[source,xml,options="nowrap"] +---- + + + org.acme.MyConstraintProvider + false + + +---- + +Spring Boot:: + +Set the property `timefold.solver.constraint-stream-automatic-node-sharing` to `false` in `application.properties`: ++ +[source,properties,options="nowrap"] +---- +timefold.solver.constraint-stream-automatic-node-sharing=false +---- + +Quarkus:: + +Set the property `quarkus.timefold.solver.constraint-stream-automatic-node-sharing` to `false` in `application.properties`: ++ +[source,properties,options="nowrap"] +---- +quarkus.timefold.solver.constraint-stream-automatic-node-sharing=false +---- +==== + +[#nodeSharing] +=== What is node sharing? + +When using xref:constraints-and-score/score-calculation.adoc#constraintStreams[constraint streams], each xref:constraints-and-score/score-calculation.adoc#constraintStreamsBuildingBlocks[building block] forms a node in the score calculation network. +When two building blocks are functionally equivalent, they can share the same node in the network. +Sharing nodes allows the operation to be performed only once instead of multiple times, improving the performance of the solver. +To be functionally equivalent, the following must be true: + +* The building blocks must represent the same operation. + +* The building blocks must have functionally equivalent parent building blocks. + +* The building blocks must have functionally equivalent inputs. + +When all of the above is the case, Timefold Solver can rewrite the `ConstraintProvider` code at runtime to benefit from the performance improvements node sharing brings. 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 fea198a4db5..a73051c31b4 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc @@ -307,7 +307,7 @@ The code on the hot path of your application needs to be as fast as possible. [#enableAutomaticNodeSharing] == Enable automatic node sharing -If you are using the xref:commercial-editions/commercial-editions.adoc[Enterprise Edition], you should xref:commercial-editions/commercial-editions.adoc#automaticNodeSharing[enable automatic node sharing] as it can significantly speed up score calculation. +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 @@ -315,16 +315,174 @@ Whatever you do, benchmark on a large and diverse set of inputs. JVM performance may differ by as much as 20% between runs. To decide whether your changes helped or made things worse, make sure to always average the move evaluation speed from multiple runs on the same machine with the same solver configuration. We provide a xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] to help you with that. +[#constraintProfiling] == Constraint Profiling -NOTE: This feature is exclusive to Timefold Solver Enterprise Edition. -It is not available in the open source version. +include::../commercial-editions/.only-enterprise.adoc[] -xref:constraints-and-score/score-calculation.adoc#constraintStreams[Constraint streams] have builtin support for xref:commercial-editions/commercial-editions.adoc#constraintProfiling[profiling constraints]. -Profiling constraints allows you to identify which constraints are taking the majority of the time spent in score calculation and thus are worth optimizing. +Profiling allows you to identify which constraints are taking the majority of time spent in score calculation and thus are worth optimizing. +Unfortunately, traditional profiling tools only report the internal classes used to calculate constraints instead of the constraints themselves. xref:./score-calculation.adoc#constraintStreams[Constraint streams] have builtin support for profiling constraints directly. + +=== Configuration + +[tabs] +==== +XML:: + +* Add `true` in your `solverConfig.xml`: ++ +[source,xml,options="nowrap"] +---- + + ...ConstraintProvider + true + +---- + +Spring Boot:: + +Set the property `timefold.solver.constraint-stream-profiling-enabled` to `true` in `application.properties`: ++ +[source,properties,options="nowrap"] +---- +timefold.solver.constraint-stream-profiling-enabled=true +---- + +Quarkus:: + +Set the property `quarkus.timefold.solver.constraint-stream-profiling-enabled` to `true` in `application.properties`: ++ +[source,properties,options="nowrap"] +---- +quarkus.timefold.solver.constraint-stream-profiling-enabled=true +---- +==== + +Constraint profiling will log its results to the `ai.timefold.solver.enterprise.core.api.ConstraintProfiler` class at the end of solving. +For the report to be generated, xref:using-timefold-solver/running-the-solver.adoc#logging[the particular logging levels must be configured and enabled]. + +IMPORTANT: Constraint profiling is only supported for xref:constraints-and-score/score-calculation.adoc#constraintStreams[constraint stream score calculation] +when <> is disabled. + +=== Understanding the report + +When constraint stream profiling is enabled, a breakdown of time spent in each constraint is logged at the end of solving. + +[source,text] +---- +INFO: # Constraint Profiling Summary +| | Time Spent | Of Total | +| :------------------------------------------------- | ---------: | -------: | +| 2: Teacher time efficiency | 6.19 s | 23.15 % | +| 6: Room conflict | 4.17 s | 15.61 % | +| 4: Student group conflict | 3.76 s | 14.07 % | +| 5: Teacher conflict | 3.41 s | 12.76 % | +| 1: Student group subject variety | 3.25 s | 12.14 % | +| 3: Teacher room stability | 3.25 s | 12.14 % | +| Work shared by 1, 2, 3, 4, 5, 6 | 2.71 s | 10.12 % | + +DEBUG: ## Constraint Profiling Node Breakdown +| 2: Teacher time efficiency | Time Spent | Of Total | +| :------------------------------------------------- | ---------: | -------: | +| JOIN Node 26 | 1.56 s | 25.20 % | +| JOIN Left Input 6 | 1.28 s | 20.69 % | +| JOIN Right Input 7 | 1.09 s | 17.55 % | +| FILTER 5 | 1.83 s | 29.49 % | +| SCORING 4 | 0.44 s | 7.06 % | + + - JOIN Node 26 defined at location org.acme.schooltimetabling.solver.TimetableConstraintProvider#teacherTimeEfficiency:77 + - JOIN Left Input 6 defined at location org.acme.schooltimetabling.solver.TimetableConstraintProvider#teacherTimeEfficiency:77 + - JOIN Right Input 7 defined at location org.acme.schooltimetabling.solver.TimetableConstraintProvider#teacherTimeEfficiency:77 + - FILTER 5 defined at location org.acme.schooltimetabling.solver.TimetableConstraintProvider#teacherTimeEfficiency:79 + - SCORING 4 defined at location org.acme.schooltimetabling.solver.TimetableConstraintProvider#teacherTimeEfficiency:84 + +... + +TRACE: ## Constraint Profiling Lifecycle Breakdown +| 2: Teacher time efficiency | INSERT | UPDATE | RETRACT | +| :--------------------------------------- | -------: | -------: | --------: | +| JOIN | 13.12 % | 75.64 % | 11.23 % | +| FILTER | 23.69 % | 50.45 % | 25.86 % | +| SCORING | 13.10 % | 16.12 % | 70.78 % | + +... +---- + +NOTE: The data is printed as a https://www.markdownguide.org/extended-syntax/#tables[Markdown table]. +Any Markdown-enabled text editor will show a well-formatted table. + +Add different logging levels, different information is reported: + +INFO:: How much time spent in score calculation is spent by each constraint (total and relative percentage) in descending order. ++ +[source,text] +---- +| 2: Teacher time efficiency | 6.19 s | 23.15 % | +---- ++ +xref:commercial-editions/commercial-editions.adoc#nodeSharing[When node sharing] is used, the time spent will be reported with "Work shared by", with the numbers identifying constraints that shared the work. +In the example below, work was shared by constraints 1 to 6: ++ +[source,text] +---- +| Work shared by 1, 2, 3, 4, 5, 6 | 2.71 s | 10.12 % | +---- + +`DEBUG`:: A breakdown for each constraint of time spent (total and relative percentage) by each constraint stream building block (e.g., join, filter). ++ +[source,text] +---- +| 2: Teacher time efficiency | Time Spent | Of Total | +| :------------------------------------------------- | ---------: | -------: | +| JOIN Node 26 | 1.56 s | 25.20 % | +| JOIN Left Input 6 | 1.28 s | 20.69 % | +| JOIN Right Input 7 | 1.09 s | 17.55 % | +| FILTER 5 | 1.83 s | 29.49 % | +| SCORING 4 | 0.44 s | 7.06 % | +---- ++ +To make it easier to map the building blocks to your `ConstraintProvider` code, +the location(s) where each building block was defined is also reported: ++ +[source,text] +---- + - JOIN Node 26 defined at location org.acme.schooltimetabling.solver.TimetableConstraintProvider#teacherTimeEfficiency:77 +---- + +`TRACE`:: A breakdown for each constraint of time spent by each building block, +shown as percentage of time spent in each of the lifecycle phases. ++ +[source,text] +---- +| 2: Teacher time efficiency | INSERT | UPDATE | RETRACT | +| :--------------------------------------- | -------: | -------: | --------: | +| JOIN | 13.12 % | 75.64 % | 11.23 % | +| FILTER | 23.69 % | 50.45 % | 25.86 % | +| SCORING | 13.10 % | 16.12 % | 70.78 % | +---- ++ +NOTE: This is only relevant to people intimately familiar with the underlying implementation of Constraint Streams +and can be safely ignored by everyday users. + +=== Interpreting the results + +Constraint profiling measures wall clock time, not CPU time, +and as such includes any garbage collection pauses that may occur during score calculation. +These numbers should be used as a guide to identify which constraints are the most expensive, +not as an exact measurement. + +Due to the nature of the JVM, the results may vary between runs. +The solver needs to run for a sufficient amount of time +to allow the JVM to warm up and stabilize. +We recommend running the solver for minutes rather than seconds, +but more than that is unlikely to bring any additional benefit. +For increased confidence in the results, +you can run the solver multiple times in different JVM processes +and average the results. NOTE: Traditional profiling tools such as Java Mission Control will report the internal classes used to calculate constraints instead of the constraints themselves. + [#fullAssert] == Validate the implementation using FULL_ASSERT When you are done optimizing your score calculation, make sure to validate it using xref:using-timefold-solver/running-the-solver.adoc#environmentModeFullAssert[`FULL_ASSERT`] mode. Make sure not to run with this mode in production. diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc index 752b1737310..244ae3c5b4a 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc @@ -1,5 +1,5 @@ [#understandingTheScore] -= Understanding the score += Understanding the score (Score Analysis) :doctype: book :sectnums: :icons: font @@ -8,8 +8,7 @@ The score in its pure form is just a number, and does not help us understand the It doesn't say which constraints are broken, or what caused them to break. To understand the score, it needs to be broken down. -NOTE: Explainability is exclusive to Timefold Solver Enterprise Edition. -It is not available in the open source version. +include::../commercial-editions/.only-plus-and-enterprise.adoc[] The easiest way to break down the score during development is to print the score summary: diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc index 35f4a5994ac..c9c54a9463a 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc @@ -802,8 +802,7 @@ Mimic selection is useful to create <>, we can respond to a continuous stream of external changes. However, it is often necessary to respond to adhoc changes too, for example when a call center operator needs to arrange an appointment with a customer. diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc index 0ea1ff8a5bf..1f32de3d792 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc @@ -82,11 +82,11 @@ which is often the case in xref:responding-to-change/responding-to-change.adoc[r There are several ways of running the solver in parallel: -* *xref:commercial-editions/commercial-editions.adoc#multithreadedIncrementalSolving[Multi-threaded incremental solving]*: +* *xref:#multithreadedIncrementalSolving[Multi-threaded incremental solving]*: Solve 1 dataset with multiple threads without sacrificing xref:constraints-and-score/performance.adoc#incrementalScoreCalculationPerformance[incremental score calculation]. This is exclusively available in the xref:commercial-editions/commercial-editions.adoc[Enterprise Edition] of Timefold Solver. -* *xref:commercial-editions/commercial-editions.adoc#partitionedSearch[Partitioned search]*: +* *xref:#partitionedSearch[Partitioned search]*: Split 1 dataset in multiple parts and solve them independently. This is exclusively available in the xref:commercial-editions/commercial-editions.adoc[Enterprise Edition] of Timefold Solver. * *Multi bet solving*: solve 1 dataset with multiple, isolated solvers and take the best result. @@ -97,6 +97,359 @@ This is exclusively available in the xref:commercial-editions/commercial-edition image::using-timefold-solver/running-the-solver/multiThreadingStrategies.png[align="center"] +[#multithreadedIncrementalSolving] +==== Multi-threaded incremental solving + +include::../commercial-editions/.only-enterprise.adoc[] + +With this feature, the solver can run significantly faster, +getting you the right solution earlier. +It has been designed to speed up the solver in cases where move evaluation is the bottleneck. +This typically happens when the constraints are computationally expensive, +or when the dataset is large. + +- The sweet spot for this feature is when the move evaluation speed is up to 10 thousand per second. +In this case, we have observed the algorithm to scale linearly with the number of move threads. +Every additional move thread will bring a speedup, +albeit with diminishing returns. +- For move evaluation speeds on the order of 100 thousand per second, +the algorithm no longer scales linearly, +but using 4 to 8 move threads may still be beneficial. +- For even higher move evaluation speeds, +the feature does not bring any benefit. +At these speeds, move evaluation is no longer the bottleneck. +If the solver continues to underperform, +perhaps you're suffering from xref:constraints-and-score/performance.adoc#scoreTrap[score traps] +or you may benefit from xref:optimization-algorithms/overview.adoc#customMoves[custom moves] +to help the solver escape local optima. + +[NOTE] +==== +These guidelines are strongly dependent on move selector configuration, +size of the dataset and performance of individual constraints. +We recommend you benchmark your use case +to determine the optimal number of move threads for your problem. +==== + +===== Enabling multi-threaded incremental solving + +Enable multi-threaded incremental solving +by xref:using-timefold-solver/modeling-planning-problems.adoc#planningId[adding a `@PlanningId` annotation] +on every planning entity class and planning value class. +Then configure a `moveThreadCount`: + +[tabs] +==== +Quarkus:: ++ +-- +Add the following to your `application.properties`: + +[source,properties] +---- +quarkus.timefold.solver.move-thread-count=AUTO +---- +-- +Spring:: ++ +-- +Add the following to your `application.properties`: + +[source,properties] +---- +timefold.solver.move-thread-count=AUTO +---- +-- +Java:: ++ +-- +Use the `SolverConfig` class: + +[source,java,options="nowrap"] +---- +SolverConfig solverConfig = new SolverConfig() + ... + .withMoveThreadCount("AUTO"); +---- +-- +XML:: ++ +-- +Add the following to your `solverConfig.xml`: +[source,xml,options="nowrap"] +---- + + + ... + AUTO + ... + + +---- +-- +==== + +Setting `moveThreadCount` to `AUTO` allows Timefold Solver to decide how many move threads to run in parallel. +This formula is based on experience and does not hog all CPU cores on a multi-core machine. + +A `moveThreadCount` of `4` xref:integration/integration.adoc#sizingHardwareAndSoftware[saturates almost 5 CPU cores]. +the 4 move threads fill up 4 CPU cores completely +and the solver thread uses most of another CPU core. + +The following ``moveThreadCount``s are supported: + +* `NONE` (default): Don't run any move threads. Use the single threaded code. +* ``AUTO``: Let Timefold Solver decide how many move threads to run in parallel. +On machines or containers with little or no CPUs, this falls back to the single threaded code. +* Static number: The number of move threads to run in parallel. + +It is counter-effective to set a `moveThreadCount` +that is higher than the number of available CPU cores, +as that will slow down the move evaluation speed. + +[IMPORTANT] +==== +In cloud environments where resource use is billed by the hour, +consider the trade-off between cost of the extra CPU cores needed and the time saved. +Compute nodes with higher CPU core counts are typically more expensive to run +and therefore you may end up paying more for the same result, +even though the actual compute time needed will be less. +==== + +[NOTE] +==== +Multi-threaded solving is _still reproducible_, as long as the resolved `moveThreadCount` is stable. +A run of the same solver configuration on 2 machines with a different number of CPUs, +is still reproducible, unless the `moveThreadCount` is set to `AUTO` or a function of `availableProcessorCount`. +==== + +===== Advanced configuration + +There are additional parameters you can supply to your `solverConfig.xml`: + +[source,xml,options="nowrap"] +---- + + 4 + ...MyAppServerThreadFactory + ... + +---- + +To run in an environment that doesn't like arbitrary thread creation, +use `threadFactoryClass` to plug in a <>. + + +[#partitionedSearch] +==== Partitioned search + +include::../commercial-editions/.only-enterprise.adoc[] + +[#partitionedSearchAlgorithm] +===== Algorithm description + +It is often more efficient to partition large data sets (usually above 5000 planning entities) +into smaller pieces and solve them separately. +Partition Search is <>, +so it provides a performance boost on multi-core machines due to higher CPU utilization. +Additionally, even when only using one CPU, it finds an initial solution faster, +because the search space sum of a partitioned Construction Heuristic is far less than its non-partitioned variant. + +However, **partitioning does lead to suboptimal results**, even if the pieces are solved optimally, as shown below: + +image::enterprise-edition/mapReduceIsTerribleForTsp.png[align="center"] + +It effectively trades a short term gain in solution quality for long term loss. +One way to compensate for this loss, +is to run a non-partitioned Local Search after the Partitioned Search phase. + +[NOTE] +==== +Not all use cases can be partitioned. +Partitioning only works for use cases where the planning entities and value ranges can be split into n partitions, +without any of the constraints crossing boundaries between partitions. +==== + +[#partitionedSearchConfiguration] +===== Configuration + +Simplest configuration: + +[source,xml,options="nowrap"] +---- + + ...MyPartitioner + +---- + +Also xref:using-timefold-solver/modeling-planning-problems.adoc#planningId[add a `@PlanningId` annotation] +on every planning entity class and planning value class. +There are several ways to <>. + +Advanced configuration: + +[source,xml,options="nowrap"] +---- + + ... + ...MyPartitioner + 4 + + ... + ... + +---- + +The `runnablePartThreadLimit` allows limiting CPU usage to avoid hanging your machine, see below. + +To run in an environment that doesn't like arbitrary thread creation, +plug in a <>. + +[IMPORTANT] +==== +A xref:using-timefold-solver/running-the-solver.adoc#logging[logging level] of `debug` or `trace` causes congestion in multi-threaded Partitioned Search +and slows down the xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed]. +==== + +Just like a `` element, +the `` element can contain one or more xref:optimization-algorithms/overview.adoc#solverPhase[phases]. +Each of those phases will be run on each partition. + +A common configuration is to first run a Partitioned Search phase +(which includes a Construction Heuristic and a Local Search) +followed by a non-partitioned Local Search phase: + +[source,xml,options="nowrap"] +---- + + ...MyPartitioner + + + + + + + + + +---- + + +[#partitioningASolution] +===== Partitioning a solution + + +[#customSolutionPartitioner] +====== Custom `SolutionPartitioner` + +To use a custom `SolutionPartitioner`, configure one on the Partitioned Search phase: + +[source,xml,options="nowrap"] +---- + + ...MyPartitioner + +---- + +Implement the `SolutionPartitioner` interface: + +[source,java,options="nowrap"] +---- +public interface SolutionPartitioner { + + List splitWorkingSolution(Solution_ workingSolution, Integer runnablePartThreadLimit); + +} +---- + +The `size()` of the returned `List` is the `partCount` (the number of partitions). +This can be decided dynamically, for example, based on the size of the non-partitioned solution. +The `partCount` is unrelated to the `runnablePartThreadLimit`. + +To configure values of a `SolutionPartitioner` dynamically in the solver configuration +(so the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] can tweak those parameters), +add the `solutionPartitionerCustomProperties` element and use xref:using-timefold-solver/configuration.adoc#customPropertiesConfiguration[custom properties]: + +[source,xml,options="nowrap"] +---- + + ...MyPartitioner + + + + + +---- + +[#runnablePartThreadLimit] +===== Runnable part thread limit + +When running a multi-threaded solver, such as Partitioned Search, CPU power can quickly become a scarce resource, +which can cause other processes or threads to hang or freeze. +However, Timefold Solver has a system to prevent CPU starving of +other processes (such as an SSH connection in production or your IDE in development) +or other threads (such as the servlet threads that handle REST requests). + +As explained in xref:integration/integration.adoc#sizingHardwareAndSoftware[sizing hardware and software], +each solver (including each child solver) does no IO during `solve()` and therefore saturates one CPU core completely. +In Partitioned Search, every partition always has its own thread, called a part thread. +It is impossible for two partitions to share a thread, +because of xref:optimization-algorithms/overview.adoc#asynchronousTermination[asynchronous termination]: +the second thread would never run. +Every part thread will try to consume one CPU core entirely, so if there are more partitions than CPU cores, +this will probably hang the system. +`Thread.setPriority()` is often too weak to solve this hogging problem, so another approach is used. + +The `runnablePartThreadLimit` parameter specifies how many part threads are runnable at the same time. +The other part threads will temporarily block and therefore will not consume any CPU power. +*This parameter basically specifies how many CPU cores are donated to Timefold Solver.* +All part threads share the CPU cores in a round-robin manner +to consume (more or less) the same number of CPU cycles: + +image::enterprise-edition/partitionedSearchThreading.png[align="center"] + +The following `runnablePartThreadLimit` options are supported: + +* `UNLIMITED`: Allow Timefold Solver to occupy all CPU cores, do not avoid hogging. +Useful if a no hogging CPU policy is configured on the OS level. +* `AUTO` (default): Let Timefold Solver decide how many CPU cores to occupy. This formula is based on experience. +It does not hog all CPU cores on a multi-core machine. +* Static number: The number of CPU cores to consume. For example: ++ +[source,xml,options="nowrap"] +---- +2 +---- + +[WARNING] +==== +If the `runnablePartThreadLimit` is equal to or higher than the number of available processors, +the host is likely to hang or freeze, +unless there is an OS specific policy in place to avoid Timefold Solver from hogging all the CPU processors. +==== + + +[#customThreadFactory] +==== Custom thread factory (WildFly, GAE, ...) + +The `threadFactoryClass` allows to plug in a custom `ThreadFactory` for environments +where arbitrary thread creation should be avoided, +such as most application servers (including WildFly) or Google App Engine. + +Configure the `ThreadFactory` on the solver to create the <> +and the <> with it: + +[source,xml,options="nowrap"] +---- + + ...MyAppServerThreadFactory + ... + +---- + [#environmentMode] == Environment mode: are there bugs in my code? @@ -748,6 +1101,63 @@ Do not modify the solutions returned by the events in `withFirstInitializedSolut These instances are still utilized during the solving process, and any modifications may lead to solver corruption. ==== +[#throttlingBestSolutionEvents] +==== Throttling best solution events in `SolverManager` + +include::../commercial-editions/.only-enterprise.adoc[] + +This feature helps you avoid overloading your system with best solution events, +especially in the early phase of the solving process when the solver is typically improving the solution very rapidly. + +To enable event throttling, use `ThrottlingBestSolutionEventConsumer` when starting a new `SolverJob` using `SolverManager`: + +[source,java,options="nowrap"] +---- +... +import ai.timefold.solver.enterprise.core.api.ThrottlingBestSolutionEventConsumer; +import java.time.Duration; +... + +public class TimetableService { + + private SolverManager solverManager; + + public String solve(Timetable problem) { + var bestSolutionEventConsumer = ThrottlingBestSolutionEventConsumer.of( + event -> { + // Your custom event handling code goes here. + }, + Duration.ofSeconds(1)); // Throttle to 1 event per second. + + String jobId = ...; + solverManager.solveBuilder() + .withProblemId(jobId) + .withProblem(problem) + .withBestSolutionEventConsumer(bestSolutionEventConsumer) + .run(); // Start the solver job and listen to best solutions, with throttling. + return jobId; + } + +} +---- + +This will ensure that your system will never receive more than one best solution event per second. +Some other important points to note: + +- If multiple events arrive during the pre-defined 1-second interval, only the last event will be delivered. +- When the `SolverJob` terminates, the last event received will be delivered regardless of the throttle, +unless it was already delivered before. +- If your consumer throws an exception, we will still count the event as delivered. +- If the system is too occupied to start and execute new threads, +event delivery will be delayed until a thread can be started. + +[NOTE] +==== +If you are using the `ThrottlingBestSolutionEventConsumer` for intermediate best solutions +together with a final best solution consumer, +both these consumers will receive the final best solution. +==== + To handle errors that may arise during the solving process, set up the handling logic by defining `withExceptionHandler(...)`.