From 9f09b31973f35242283ec18ab81348bf3608fc9f Mon Sep 17 00:00:00 2001 From: Vittorio Distefano Date: Sun, 8 Nov 2026 21:34:05 +0100 Subject: [PATCH] fix(solver): keep a runner's declaration only for partitioned search A compiled runner kept the declaration its graph was compiled from, inline, for the whole solve, although only a partitioned_search phase compiles partition runners from it. Every solve held a second copy of the search context, and every runner grew by the declaration's size. The runner now keeps the declaration, boxed, only when the configuration has a partitioned phase. Against v0.24.0, construction first fit goes from +3.9% to +0.8% instructions and from +27.8% to +4.0% max RSS. Closes #220 --- .../src/runtime/compiler/executor/runner.rs | 25 ++++++++++++++++--- .../src/runtime/compiler/extension_tests.rs | 22 ++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) 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()); +}