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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/solverforge-solver/WIREFRAME.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ modules are private, so an item has exactly one path:

### Hidden modules

- `__bench` (`#[doc(hidden)]`): handles over the compiled runtime's own cursors that `benches/selector_cursor_gate.rs` drives. No compatibility promise covers them. `CompiledRuntimeRunner<S, V, DM, IDM>::compile(&SolverConfig, SearchContext)` runs the compilation, preparation, and lowering a configured solve runs (the crate-private `compile_runtime_runner` that `try_run_solver_with_config_and_search` also calls) and implements `Phase` by delegating to the compiled runtime phase runner. `into_local_search_neighborhood()` moves out the first phase's lowered acceptor/forager neighborhood as `CompiledNeighborhood`, whose `open_cursor(director, MoveStreamContext)` returns `CompiledNeighborhoodCursor`: the composition cursor a local-search step pulls from, with `next_candidate(&mut director)` lending the solve's provider resources per pull, `visit_candidate(id, CandidateVisitor)`, and `release_candidate(id)`. `into_scalar_construction_placer()` moves out the first phase's descriptor-placement scalar construction as `CompiledScalarPlacer`, whose `open_cursor` yields `CompiledScalarPlacementCursor` and `CompiledScalarPlacement` (`next_candidate`, `visit_candidate`, `candidate_assignment`, `release_candidate`) over the placer the construction phase drives. `CandidateVisitor<S>` inspects one live candidate through the public `Move` contract without naming the runtime's private move carriers.
- `__bench` (`#[doc(hidden)]`): handles over the compiled runtime's own cursors that `benches/selector_cursor_gate.rs` drives. No compatibility promise covers them. `CompiledRuntimeRunner<S, V, DM, IDM>::compile(&SolverConfig, SearchContext)` runs the compilation, preparation, and lowering a configured solve runs (the crate-private `compile_runtime_runner` that `try_run_solver_with_config_and_search` also calls) and implements `Phase` by delegating to the compiled runtime phase runner. `into_local_search_neighborhood()` moves out the first phase's lowered acceptor/forager neighborhood as `CompiledNeighborhood`, whose `open_cursor(director, MoveStreamContext)` returns `CompiledNeighborhoodCursor`: the composition cursor a local-search step pulls from, with `next_candidate(&mut director)` lending the solve's provider resources per pull, `visit_candidate(id, CandidateVisitor)`, and `release_candidate(id)`. `into_scalar_construction_placer()` moves out the first phase's descriptor-placement scalar construction as `CompiledScalarPlacer`, whose `open_cursor` yields `CompiledScalarPlacementCursor` (`next_placement`, and `recycle`, which hands a finished placement back so the next one reuses its candidate store and value buffer, as the construction phase does after each step) and `CompiledScalarPlacement` (`next_candidate`, `visit_candidate`, `candidate_assignment`, `release_candidate`) over the placer the construction phase drives. `CandidateVisitor<S>` inspects one live candidate through the public `Move` contract without naming the runtime's private move carriers.
- `__private` (`#[doc(hidden)]`): the host-provider contracts that `solverforge-bridge` re-exports for host-language bindings: `RuntimeCandidateMetric`, `RuntimeCandidateMetricBinding`, `RuntimeCandidateMetricRegistry`, `RuntimeProviderRegistry`, `ProviderResolutionError`, `RawProviderCandidate`, `RawProviderEdit`, `RuntimeConflictRepairProviderBinding`, `RuntimeHostCompoundProvider`, `RuntimeHostProviderErrorBoundary`, `RuntimeProviderLimits`, `RuntimeScalarGroupProviderBinding`, `NoDynamicExtensions`.

Types that appear only inside public signatures, such as the concrete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ fn construction_full() {
mix(&mut order_hash, value);
assert!(placement.release_candidate(candidate_id));
}
placements.recycle(placement);
}
emit("construction_full", 8_192, || {
let mut count = 0;
Expand All @@ -57,6 +58,7 @@ fn construction_full() {
assert!(placement.release_candidate(candidate_id));
count += 1;
}
placements.recycle(placement);
}
}
(count, order_hash, identity)
Expand Down
6 changes: 6 additions & 0 deletions crates/solverforge-solver/src/bench/construction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ where
.next_placement(score_director, |_| false, || false)
.map(|placement| CompiledScalarPlacement { placement })
}

/// Hands a finished placement back, as a construction phase does after
/// each step, so the next placement reuses its candidate buffers.
pub fn recycle(&mut self, placement: CompiledScalarPlacement<S>) {
self.cursor.recycle(placement.placement);
}
}

impl<S: PlanningSolution> Debug for CompiledScalarPlacementCursor<'_, S> {
Expand Down
90 changes: 65 additions & 25 deletions crates/solverforge-solver/src/builder/context/scalar/variable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,39 +214,73 @@ impl<S> ScalarVariableSlot<S> {
entity_index: usize,
value_candidate_limit: Option<usize>,
) -> Vec<usize> {
if let Some(provider) = self.candidate_values {
let values = provider(solution, entity_index, self.variable_index);
return match value_candidate_limit {
Some(limit) => values.iter().copied().take(limit).collect(),
None => values.to_vec(),
};
let mut values = Vec::new();
self.extend_candidate_values_for_entity(
solution,
entity_index,
value_candidate_limit,
&mut values,
);
values
}

/// Appends [`Self::candidate_values_for_entity`] to `values` in one bulk
/// copy, so a caller refilling its own buffer allocates at most once.
pub(crate) fn extend_candidate_values_for_entity(
&self,
solution: &S,
entity_index: usize,
value_candidate_limit: Option<usize>,
values: &mut Vec<usize>,
) {
match self.candidate_values_of(solution, entity_index, value_candidate_limit) {
CandidateValues::Slice(slice) => values.extend_from_slice(slice),
CandidateValues::Range(range) => values.extend(range),
}
}

/// Visits [`Self::candidate_values_for_entity`] in order without
/// collecting them.
pub(crate) fn visit_candidate_values_for_entity(
&self,
solution: &S,
entity_index: usize,
value_candidate_limit: Option<usize>,
visit: &mut dyn FnMut(usize),
) {
match self.candidate_values_of(solution, entity_index, value_candidate_limit) {
CandidateValues::Slice(slice) => slice.iter().for_each(|&value| visit(value)),
CandidateValues::Range(range) => range.for_each(visit),
}
}

fn candidate_values_of<'a>(
&self,
solution: &'a S,
entity_index: usize,
value_candidate_limit: Option<usize>,
) -> CandidateValues<'a> {
let limit = value_candidate_limit.unwrap_or(usize::MAX);
let truncate = |values: &'a [usize]| &values[..limit.min(values.len())];
if let Some(provider) = self.candidate_values {
return CandidateValues::Slice(truncate(provider(
solution,
entity_index,
self.variable_index,
)));
}
match self.value_source {
ValueSource::Empty => Vec::new(),
ValueSource::Empty => CandidateValues::Range(0..0),
ValueSource::CountableRange { from, to } => {
let end = value_candidate_limit
.map(|limit| from.saturating_add(limit).min(to))
.unwrap_or(to);
(from..end).collect()
CandidateValues::Range(from..from.saturating_add(limit).min(to))
}
ValueSource::SolutionCount {
count_fn,
provider_index,
} => {
let count = count_fn(solution, provider_index);
let end = value_candidate_limit
.map(|limit| limit.min(count))
.unwrap_or(count);
(0..end).collect()
}
ValueSource::EntitySlice { values_for_entity } => {
let values = values_for_entity(solution, entity_index, self.variable_index);
match value_candidate_limit {
Some(limit) => values.iter().copied().take(limit).collect(),
None => values.to_vec(),
}
}
} => CandidateValues::Range(0..limit.min(count_fn(solution, provider_index))),
ValueSource::EntitySlice { values_for_entity } => CandidateValues::Slice(truncate(
values_for_entity(solution, entity_index, self.variable_index),
)),
}
}

Expand Down Expand Up @@ -401,3 +435,9 @@ impl<S> fmt::Debug for ScalarVariableSlot<S> {
.finish()
}
}

/// The candidate values of one entity, already cut to the candidate limit.
enum CandidateValues<'a> {
Slice(&'a [usize]),
Range(std::ops::Range<usize>),
}
26 changes: 23 additions & 3 deletions crates/solverforge-solver/src/builder/context/scalar_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,28 @@ impl<S> RuntimeScalarSlot<S> {
}
}

/// Appends the candidates [`Self::visit_candidate_values`] visits to
/// `values` in one bulk copy, so a caller refilling its own buffer
/// allocates at most once.
pub(crate) fn extend_candidate_values(
&self,
solution: &S,
entity_index: usize,
limit: Option<usize>,
values: &mut Vec<usize>,
) {
match self {
Self::Static(slot) => {
slot.extend_candidate_values_for_entity(solution, entity_index, limit, values);
}
Self::Dynamic(slot) => {
let candidates = slot.candidate_values(solution, entity_index);
let limit = limit.unwrap_or(usize::MAX).min(candidates.len());
values.extend_from_slice(&candidates[..limit]);
}
}
}

/// Visits ordinary candidates in their declared source order. `limit`
/// limits source consumption, never a post-hoc result set, so lazy dynamic
/// bindings and static value ranges share one ordering contract.
Expand All @@ -242,9 +264,7 @@ impl<S> RuntimeScalarSlot<S> {
let limit = limit.unwrap_or(usize::MAX);
match self {
Self::Static(slot) => {
for value in slot.candidate_values_for_entity(solution, entity_index, Some(limit)) {
visit(value);
}
slot.visit_candidate_values_for_entity(solution, entity_index, Some(limit), visit);
}
Self::Dynamic(slot) => {
for &value in slot
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ where

/// Drops every stored candidate while retaining both backing
/// capacities, so a reused store does not re-grow its vectors.
#[cfg(test)]
pub(crate) fn reset(&mut self) {
self.positions.clear();
self.live.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ where
);

step_scope.complete();
placement_cursor.recycle(placement);
}

let previous_best_score = phase_scope.solver_scope().best_score().copied();
Expand Down
12 changes: 12 additions & 0 deletions crates/solverforge-solver/src/phase/construction/placer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ where
&mut self.candidates
}

/// Ends the placement, handing back its candidate cursor.
pub(crate) fn into_candidates(self) -> C {
self.candidates
}

pub(crate) fn take_move(&mut self, candidate_id: CandidateId) -> M {
self.candidates.take_candidate(candidate_id)
}
Expand Down Expand Up @@ -234,6 +239,13 @@ where
D: Director<S>,
IsCompleted: FnMut(&Placement<S, M, Self::CandidateCursor>) -> bool,
ShouldStop: FnMut() -> bool;

/// Takes back a placement the phase has finished with, so a cursor that
/// reuses its candidate buffers across placements can reclaim them. A
/// cursor that keeps no buffers drops it.
fn recycle(&mut self, placement: Placement<S, M, Self::CandidateCursor>) {
drop(placement);
}
}

pub(crate) trait EntityPlacer<S, M>: Send + Debug
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ fn candidate_values<S>(
limit: Option<usize>,
) -> Vec<usize> {
let mut values = Vec::new();
slot.visit_candidate_values(solution, entity, limit, &mut |value| values.push(value));
slot.extend_candidate_values(solution, entity, limit, &mut values);
values
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,33 +208,24 @@ where
indices
}

/// Refills `values` with the entity's candidate values in placement
/// order. Ascending-key order sorts stably, so equal keys keep the order
/// the slot offered them in.
fn ordered_values(
&self,
slot: &RuntimeScalarSlot<S>,
solution: &S,
entity_index: usize,
) -> Vec<usize> {
let mut values = Vec::new();
slot.visit_candidate_values(
solution,
entity_index,
self.value_candidate_limit,
&mut |value| values.push(value),
);
values: &mut Vec<usize>,
) {
values.clear();
slot.extend_candidate_values(solution, entity_index, self.value_candidate_limit, values);
if self.value_order == ValueOrder::AscendingKey {
let mut indexed = values.into_iter().enumerate().collect::<Vec<_>>();
indexed.sort_by(|(left_order, left_value), (right_order, right_value)| {
let left_key = slot
.construction_value_order_key(solution, entity_index, *left_value)
.expect("validated runtime scalar queue construction must provide a value order key");
let right_key = slot
.construction_value_order_key(solution, entity_index, *right_value)
.expect("validated runtime scalar queue construction must provide a value order key");
left_key.cmp(&right_key).then(left_order.cmp(right_order))
values.sort_by_cached_key(|value| {
slot.construction_value_order_key(solution, entity_index, *value)
.expect("validated runtime scalar queue construction must provide a value order key")
});
return indexed.into_iter().map(|(_, value)| value).collect();
}
values
}
}

Expand All @@ -243,7 +234,8 @@ where
S: PlanningSolution,
{
store: CandidateStore<S, RuntimeScalarConstructionMove<S>>,
values: std::vec::IntoIter<usize>,
values: Vec<usize>,
next_value: usize,
slot: RuntimeScalarSlot<S>,
entity_index: usize,
}
Expand All @@ -253,7 +245,8 @@ where
S: PlanningSolution,
{
fn next_candidate(&mut self, _solution: &S) -> Option<CandidateId> {
let value = self.values.next()?;
let value = *self.values.get(self.next_value)?;
self.next_value += 1;
Some(self.store.push(RuntimeScalarConstructionMove::new(
self.slot.clone(),
self.entity_index,
Expand Down Expand Up @@ -285,6 +278,10 @@ where
next_slot_index: usize,
active_slot_index: Option<usize>,
entity_indices: std::vec::IntoIter<usize>,
/// The candidate store and value buffer of the last recycled placement,
/// reused by the next one so placements do not reallocate them.
spare_store: CandidateStore<S, RuntimeScalarConstructionMove<S>>,
spare_values: Vec<usize>,
}

impl<S> EntityPlacer<S, RuntimeScalarConstructionMove<S>> for RuntimeScalarConstructionPlacer<S>
Expand All @@ -303,6 +300,8 @@ where
next_slot_index: 0,
active_slot_index: None,
entity_indices: Vec::new().into_iter(),
spare_store: CandidateStore::new(),
spare_values: Vec::new(),
}
}
}
Expand Down Expand Up @@ -351,17 +350,21 @@ where
if frozen.slot.current_value(solution, entity_index).is_some() {
continue;
}
let values = self
.placer
.ordered_values(&frozen.slot, solution, entity_index);
if values.is_empty() {
self.placer.ordered_values(
&frozen.slot,
solution,
entity_index,
&mut self.spare_values,
);
if self.spare_values.is_empty() {
continue;
}
let placement = Placement::new(
EntityReference::new(frozen.slot.descriptor_index(), entity_index),
RuntimeScalarCandidateCursor {
store: CandidateStore::with_capacity(values.len()),
values: values.into_iter(),
store: std::mem::take(&mut self.spare_store),
values: std::mem::take(&mut self.spare_values),
next_value: 0,
slot: frozen.slot.clone(),
entity_index,
},
Expand All @@ -374,7 +377,20 @@ where
if !is_completed(&placement) {
return Some(placement);
}
self.recycle(placement);
}
None
}

fn recycle(
&mut self,
placement: Placement<S, RuntimeScalarConstructionMove<S>, Self::CandidateCursor>,
) {
let RuntimeScalarCandidateCursor {
mut store, values, ..
} = placement.into_candidates();
store.reset();
self.spare_store = store;
self.spare_values = values;
}
}
Loading
Loading