diff --git a/crates/solverforge-solver/src/runtime/compiler/executor/runner.rs b/crates/solverforge-solver/src/runtime/compiler/executor/runner.rs index 582cb5d6..76fae73e 100644 --- a/crates/solverforge-solver/src/runtime/compiler/executor/runner.rs +++ b/crates/solverforge-solver/src/runtime/compiler/executor/runner.rs @@ -54,9 +54,11 @@ where IDM: Clone + Send + Sync + Debug + CrossEntityDistanceMeter + 'static, H: RuntimeSource, { - /// The declaration the graph was compiled from: a partitioned phase - /// compiles each partition's runner from it. - source: H, + /// The declaration the graph was compiled from, kept only when a + /// partitioned phase compiles each partition's runner from it, so a + /// solve without one does not hold a second copy of the search context. + /// Boxed so the runner itself stays the size it is without one. + source: Option>, execution: PreparedRuntimeExecution>, prepared_phases: Vec>>, phases: Vec>, @@ -139,6 +141,12 @@ where H: RuntimeSource, RuntimeGraphInput: Sync, { + /// Whether the runner kept its declaration for a partitioned phase. + #[cfg(test)] + pub(crate) fn keeps_declaration(&self) -> bool { + self.source.is_some() + } + /// Instantiates extensions in frozen configuration order, lowers every /// frozen local-search node once, and creates the one solve-owned provider /// resource set. It does not bind a list source or invoke a provider. @@ -177,8 +185,14 @@ where ) }) .collect::>>()?; + let partitioned = prepared_phases.iter().any(|phase| { + matches!( + phase, + PreparedRuntimePhase::Extension(RuntimeExtensionInstance::Partitioned(_)) + ) + }); Ok(Self { - source: graph.source, + source: partitioned.then(|| Box::new(graph.source)), execution, prepared_phases, phases, @@ -277,6 +291,9 @@ where match extension { RuntimeExtensionInstance::Custom(phase) => phase.solve(solver_scope), RuntimeExtensionInstance::Partitioned(instance) => { + let source = source + .as_ref() + .expect("a runner with a partitioned phase keeps its declaration"); solve_partitioned_instance(instance, source.input(), solver_scope); } } diff --git a/crates/solverforge-solver/src/runtime/compiler/extension_tests.rs b/crates/solverforge-solver/src/runtime/compiler/extension_tests.rs index 1238529c..f2f59a60 100644 --- a/crates/solverforge-solver/src/runtime/compiler/extension_tests.rs +++ b/crates/solverforge-solver/src/runtime/compiler/extension_tests.rs @@ -394,3 +394,25 @@ fn partitioned_termination_rejects_limits_on_the_merged_score() { )); } } + +#[test] +fn a_runner_keeps_its_declaration_only_for_a_partitioned_phase() { + let runner = |phases: Vec| { + let config = SolverConfig { + phases, + ..SolverConfig::default() + }; + let declaration = context(config.resolved_random_seed()) + .defaults() + .phase("repair", |_| MarkerExtension) + .partitioner("by_task", |_| MarkerPartitioner); + let (context, extensions) = declaration.into_runtime_parts(); + let graph = compile_runtime_graph(&config, RuntimeGraphInput::new(context, extensions)) + .expect("registered extensions compile"); + super::executor::CompiledRuntimePhaseRunner::try_new(CompiledRuntimeExecutor::new(graph)) + .expect("the runner prepares") + }; + + assert!(!runner(vec![custom("repair")]).keeps_declaration()); + assert!(runner(vec![custom("repair"), partitioned(Some("by_task"))]).keeps_declaration()); +}