scheduler: read placed demands through the shared index
The confirmation that follows the sweep called the public feasibility test, which scans the task list once to collect the boundaries and again, per boundary, to sum the demand of every covering placement. That is quadratic per task and dominated the search: removing the confirmation alone cut the crowded case by eight, which located the cost. The index built for the sweep is now passed into that test, so both the boundary collection and the demand sum read a task by its identifier instead of searching for it. The public entry point keeps its signature and builds the index for a caller that has none. Planning three thousand two hundred mutually contending tasks falls from 6.7 seconds to 0.48, and the plans are identical: the check compares the start time and end of every placement, before and after, across two resources with disjoint availability windows.
This commit is contained in:
parent
5da069e966
commit
472a052cfb
|
|
@ -94,6 +94,24 @@ pub fn feasible_at(
|
||||||
schedule: &Schedule,
|
schedule: &Schedule,
|
||||||
resources: &ResourceSet,
|
resources: &ResourceSet,
|
||||||
start: f64,
|
start: f64,
|
||||||
|
) -> bool {
|
||||||
|
let index = crate::sweep::demand_index(tasks);
|
||||||
|
feasible_at_indexed(task, schedule, resources, start, &index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when a task placed at a time is feasible, using a demand
|
||||||
|
/// index the caller built once.
|
||||||
|
///
|
||||||
|
/// The index replaces two linear scans of the task list: the one that finds a
|
||||||
|
/// placed task while collecting the boundaries, and the one inside that sums
|
||||||
|
/// the demand over every placement covering a boundary. The boundary
|
||||||
|
/// arithmetic is unchanged, so the answer is the same as the scans give.
|
||||||
|
pub(crate) fn feasible_at_indexed(
|
||||||
|
task: &Task,
|
||||||
|
schedule: &Schedule,
|
||||||
|
resources: &ResourceSet,
|
||||||
|
start: f64,
|
||||||
|
index: &crate::sweep::DemandIndex,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if !start.is_finite() {
|
if !start.is_finite() {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -112,10 +130,7 @@ pub fn feasible_at(
|
||||||
// overlapping placements, where the total is greatest.
|
// overlapping placements, where the total is greatest.
|
||||||
let mut boundaries: Vec<f64> = vec![start];
|
let mut boundaries: Vec<f64> = vec![start];
|
||||||
for placement in schedule.placements() {
|
for placement in schedule.placements() {
|
||||||
let Some(other) = tasks.iter().find(|other| other.id == placement.task) else {
|
if crate::sweep::demand_on(index, placement.task, resource.id).is_none() {
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if other.demand_for(resource.id).is_none() {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if placement.start < end && start < placement.end() {
|
if placement.start < end && start < placement.end() {
|
||||||
|
|
@ -127,12 +142,7 @@ pub fn feasible_at(
|
||||||
.placements()
|
.placements()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|placement| placement.covers(boundary))
|
.filter(|placement| placement.covers(boundary))
|
||||||
.filter_map(|placement| {
|
.filter_map(|placement| crate::sweep::demand_on(index, placement.task, resource.id))
|
||||||
tasks
|
|
||||||
.iter()
|
|
||||||
.find(|other| other.id == placement.task)
|
|
||||||
.and_then(|other| other.demand_for(resource.id))
|
|
||||||
})
|
|
||||||
.sum();
|
.sum();
|
||||||
if placed + requirement.demand > resource.capacity() + 1e-9 {
|
if placed + requirement.demand > resource.capacity() + 1e-9 {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
//! capacity boundary. The fast path is therefore an accelerator, never a
|
//! capacity boundary. The fast path is therefore an accelerator, never a
|
||||||
//! second definition of what is feasible.
|
//! second definition of what is feasible.
|
||||||
|
|
||||||
use crate::greedy::{feasible_at, resources_have_capacity};
|
use crate::greedy::{feasible_at_indexed, resources_have_capacity};
|
||||||
use apsis_resource::ResourceId;
|
use apsis_resource::ResourceId;
|
||||||
use apsis_resource::{Resource, ResourceSet};
|
use apsis_resource::{Resource, ResourceSet};
|
||||||
use apsis_schedule::Schedule;
|
use apsis_schedule::Schedule;
|
||||||
|
|
@ -68,7 +68,7 @@ pub fn earliest_feasible_start(
|
||||||
// The sweep, confirmed by the direct test.
|
// The sweep, confirmed by the direct test.
|
||||||
if let Some(candidate) = swept_earliest_start(task, schedule, resources, latest_start, &demands)
|
if let Some(candidate) = swept_earliest_start(task, schedule, resources, latest_start, &demands)
|
||||||
{
|
{
|
||||||
if feasible_at(task, tasks, schedule, resources, candidate) {
|
if feasible_at_indexed(task, schedule, resources, candidate, &demands) {
|
||||||
return Some(candidate);
|
return Some(candidate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +76,7 @@ pub fn earliest_feasible_start(
|
||||||
// The enumeration, which is exact. It runs when the sweep could not
|
// The enumeration, which is exact. It runs when the sweep could not
|
||||||
// produce a confirmed answer, which is when the task does not fit at all
|
// produce a confirmed answer, which is when the task does not fit at all
|
||||||
// or when rounding made the two disagree.
|
// or when rounding made the two disagree.
|
||||||
enumerate_earliest_start(task, tasks, schedule, resources, latest_start)
|
enumerate_earliest_start(task, schedule, resources, latest_start, &demands)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the earliest start by testing every candidate in turn.
|
/// Returns the earliest start by testing every candidate in turn.
|
||||||
|
|
@ -84,10 +84,10 @@ pub fn earliest_feasible_start(
|
||||||
/// This is the original formulation, kept as the fallback for the sweep.
|
/// This is the original formulation, kept as the fallback for the sweep.
|
||||||
fn enumerate_earliest_start(
|
fn enumerate_earliest_start(
|
||||||
task: &Task,
|
task: &Task,
|
||||||
tasks: &[Task],
|
|
||||||
schedule: &Schedule,
|
schedule: &Schedule,
|
||||||
resources: &ResourceSet,
|
resources: &ResourceSet,
|
||||||
latest_start: f64,
|
latest_start: f64,
|
||||||
|
index: &DemandIndex,
|
||||||
) -> Option<f64> {
|
) -> Option<f64> {
|
||||||
let mut candidates: Vec<f64> = vec![task.earliest_start];
|
let mut candidates: Vec<f64> = vec![task.earliest_start];
|
||||||
for requirement in &task.requirements {
|
for requirement in &task.requirements {
|
||||||
|
|
@ -96,10 +96,7 @@ fn enumerate_earliest_start(
|
||||||
candidates.push(window.start());
|
candidates.push(window.start());
|
||||||
}
|
}
|
||||||
for placement in schedule.placements() {
|
for placement in schedule.placements() {
|
||||||
let overlaps = tasks
|
let overlaps = demand_on(index, placement.task, resource.id).is_some();
|
||||||
.iter()
|
|
||||||
.find(|other| other.id == placement.task)
|
|
||||||
.is_some_and(|other| other.demand_for(resource.id).is_some());
|
|
||||||
if overlaps {
|
if overlaps {
|
||||||
candidates.push(placement.end());
|
candidates.push(placement.end());
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +114,7 @@ fn enumerate_earliest_start(
|
||||||
|
|
||||||
candidates
|
candidates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|start| feasible_at(task, tasks, schedule, resources, *start))
|
.find(|start| feasible_at_indexed(task, schedule, resources, *start, index))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the earliest start derived from the swept profile.
|
/// Returns the earliest start derived from the swept profile.
|
||||||
|
|
@ -264,10 +261,10 @@ fn blocked_intervals(
|
||||||
/// vector indexed by it rather than in a map. Reading one is then an array
|
/// vector indexed by it rather than in a map. Reading one is then an array
|
||||||
/// index rather than a hash, and a hash of a small integer costs more than
|
/// index rather than a hash, and a hash of a small integer costs more than
|
||||||
/// the scan it replaces.
|
/// the scan it replaces.
|
||||||
type DemandIndex = Vec<Option<std::collections::HashMap<ResourceId, f64>>>;
|
pub(crate) type DemandIndex = Vec<Option<std::collections::HashMap<ResourceId, f64>>>;
|
||||||
|
|
||||||
/// Builds a [`DemandIndex`] in one pass over the task list.
|
/// Builds a [`DemandIndex`] in one pass over the task list.
|
||||||
fn demand_index(tasks: &[Task]) -> DemandIndex {
|
pub(crate) fn demand_index(tasks: &[Task]) -> DemandIndex {
|
||||||
let highest = tasks.iter().map(|task| task.id.index()).max().unwrap_or(0);
|
let highest = tasks.iter().map(|task| task.id.index()).max().unwrap_or(0);
|
||||||
let mut index: DemandIndex = (0..=highest).map(|_| None).collect();
|
let mut index: DemandIndex = (0..=highest).map(|_| None).collect();
|
||||||
for task in tasks {
|
for task in tasks {
|
||||||
|
|
@ -285,7 +282,7 @@ fn demand_index(tasks: &[Task]) -> DemandIndex {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the demand a placed task puts on a resource, if any.
|
/// Returns the demand a placed task puts on a resource, if any.
|
||||||
fn demand_on(index: &DemandIndex, task: TaskId, resource: ResourceId) -> Option<f64> {
|
pub(crate) fn demand_on(index: &DemandIndex, task: TaskId, resource: ResourceId) -> Option<f64> {
|
||||||
index
|
index
|
||||||
.get(task.index())
|
.get(task.index())
|
||||||
.and_then(Option::as_ref)
|
.and_then(Option::as_ref)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue