487 lines
18 KiB
Rust
487 lines
18 KiB
Rust
//! The schedule itself.
|
|
|
|
use crate::error::{ScheduleError, ScheduleErrorKind};
|
|
use crate::placement::{Evaluation, Placement, ResourceUsage};
|
|
use apsis_resource::ResourceId;
|
|
use apsis_task::TaskId;
|
|
use std::collections::BTreeMap;
|
|
|
|
/// A set of task placements.
|
|
///
|
|
/// The schedule stores placements in the order they were made and keeps an
|
|
/// index from task to placement, so that a lookup by task is direct. A task
|
|
/// may be placed at most once: placing it again is an error rather than a
|
|
/// silent overwrite, because a schedule that contains two placements for one
|
|
/// task is not a schedule.
|
|
///
|
|
/// 一组任务放置。
|
|
///
|
|
/// 该计划按放置发生的顺序保存各项,并维护从任务到放置的索引,故按任务查找
|
|
/// 是直接的。一个任务至多放置一次:重复放置是错误而非静默覆盖,因为含有
|
|
/// 同一任务两处放置的集合已不是一份计划。
|
|
#[derive(Debug, Clone, PartialEq, Default)]
|
|
pub struct Schedule {
|
|
placements: Vec<Placement>,
|
|
index: BTreeMap<usize, usize>,
|
|
}
|
|
|
|
impl Schedule {
|
|
/// Creates an empty schedule.
|
|
///
|
|
/// 创建一份空计划。
|
|
pub fn new() -> Self {
|
|
Self {
|
|
placements: Vec::new(),
|
|
index: BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Returns the placements in the order they were made.
|
|
///
|
|
/// 返回按放置顺序排列的各项。
|
|
pub fn placements(&self) -> &[Placement] {
|
|
&self.placements
|
|
}
|
|
|
|
/// Returns the number of placements.
|
|
///
|
|
/// 返回放置的数量。
|
|
pub fn len(&self) -> usize {
|
|
self.placements.len()
|
|
}
|
|
|
|
/// Returns `true` when there are no placements.
|
|
///
|
|
/// 当没有放置时返回 `true`。
|
|
pub fn is_empty(&self) -> bool {
|
|
self.placements.is_empty()
|
|
}
|
|
|
|
/// Returns `true` when a task has been placed.
|
|
///
|
|
/// 当某任务已被放置时返回 `true`。
|
|
pub fn is_placed(&self, task: TaskId) -> bool {
|
|
self.index.contains_key(&task.index())
|
|
}
|
|
|
|
/// Returns the placement of a task.
|
|
///
|
|
/// 返回某任务的放置。
|
|
pub fn placement_of(&self, task: TaskId) -> Option<&Placement> {
|
|
self.index
|
|
.get(&task.index())
|
|
.map(|position| &self.placements[*position])
|
|
}
|
|
|
|
/// Places a task at a time.
|
|
///
|
|
/// The placement is checked against the task's window. The resources of
|
|
/// the task are recorded with the placement through the task itself, so
|
|
/// this method does not need them; the capacity check is performed by
|
|
/// [`Schedule::validate_capacity`], which needs the resources.
|
|
///
|
|
/// Returns an error when the task is already placed, when the start time
|
|
/// is not finite, or when the placement falls outside the task's window.
|
|
///
|
|
/// 把某任务放置在某时刻。
|
|
///
|
|
/// 放置会对照该任务的窗口校验。任务的资源通过任务本身随放置一同记录,
|
|
/// 故本方法无需它们;容量检查由需要资源的
|
|
/// [`Schedule::validate_capacity`] 完成。
|
|
///
|
|
/// 该任务已被放置、起始时刻非有限,或放置落在任务窗口之外时返回错误。
|
|
pub fn place(
|
|
&mut self,
|
|
task: &apsis_task::Task,
|
|
start: f64,
|
|
) -> Result<&Placement, ScheduleError> {
|
|
if !start.is_finite() {
|
|
return Err(ScheduleError::new(ScheduleErrorKind::InvalidStart)
|
|
.with_detail(format!("start {start}")));
|
|
}
|
|
if self.is_placed(task.id) {
|
|
return Err(ScheduleError::new(ScheduleErrorKind::AlreadyPlaced)
|
|
.with_detail(format!("{}", task.id)));
|
|
}
|
|
if start < task.earliest_start {
|
|
return Err(
|
|
ScheduleError::new(ScheduleErrorKind::BeforeWindow).with_detail(format!(
|
|
"task {} starts at {start}, before its window opens at {}",
|
|
task.id, task.earliest_start
|
|
)),
|
|
);
|
|
}
|
|
if start + task.duration > task.latest_end {
|
|
return Err(
|
|
ScheduleError::new(ScheduleErrorKind::AfterWindow).with_detail(format!(
|
|
"task {} ends at {}, after its window closes at {}",
|
|
task.id,
|
|
start + task.duration,
|
|
task.latest_end
|
|
)),
|
|
);
|
|
}
|
|
let placement = Placement::new(task.id, start, task.duration);
|
|
self.index.insert(task.id.index(), self.placements.len());
|
|
self.placements.push(placement);
|
|
Ok(self
|
|
.placements
|
|
.last()
|
|
.expect("the placement was just pushed"))
|
|
}
|
|
|
|
/// Removes the placement of a task.
|
|
///
|
|
/// Returns an error when the task is not placed.
|
|
///
|
|
/// 移除某任务的放置。
|
|
///
|
|
/// 该任务未被放置时返回错误。
|
|
pub fn unplace(&mut self, task: TaskId) -> Result<Placement, ScheduleError> {
|
|
let position = self.index.remove(&task.index()).ok_or_else(|| {
|
|
ScheduleError::new(ScheduleErrorKind::NotPlaced).with_detail(format!("{task}"))
|
|
})?;
|
|
let removed = self.placements.remove(position);
|
|
// Every placement after the removed one shifts down by one, so the
|
|
// index must be rebuilt. The schedule is small enough that this is
|
|
// cheaper than maintaining a linked structure.
|
|
// 被移除项之后的每项都前移一位,故索引须重建。计划规模足够小,
|
|
// 这比维护链式结构更划算。
|
|
self.rebuild_index();
|
|
Ok(removed)
|
|
}
|
|
|
|
/// Rebuilds the task to position index.
|
|
///
|
|
/// 重建从任务到位置的索引。
|
|
fn rebuild_index(&mut self) {
|
|
self.index.clear();
|
|
for (position, placement) in self.placements.iter().enumerate() {
|
|
self.index.insert(placement.task.index(), position);
|
|
}
|
|
}
|
|
|
|
/// Returns the placements that use a resource, in time order.
|
|
///
|
|
/// 返回使用某资源的放置,按时间排序。
|
|
pub fn placements_on(
|
|
&self,
|
|
resource: ResourceId,
|
|
tasks: &[apsis_task::Task],
|
|
) -> Vec<Placement> {
|
|
let mut selected: Vec<Placement> = self
|
|
.placements
|
|
.iter()
|
|
.filter(|placement| {
|
|
tasks
|
|
.iter()
|
|
.find(|task| task.id == placement.task)
|
|
.is_some_and(|task| task.requires(resource))
|
|
})
|
|
.copied()
|
|
.collect();
|
|
selected.sort_by(|first, second| {
|
|
first
|
|
.start
|
|
.partial_cmp(&second.start)
|
|
.unwrap_or(core::cmp::Ordering::Equal)
|
|
});
|
|
selected
|
|
}
|
|
|
|
/// Returns `true` when two placements of different tasks overlap.
|
|
///
|
|
/// This checks the schedule against itself, ignoring the resources. Two
|
|
/// tasks on disjoint resources may legitimately overlap, so a caller that
|
|
/// wants to know whether a resource is overbooked should use
|
|
/// [`Schedule::validate_capacity`] instead.
|
|
///
|
|
/// 当两个不同任务的放置重叠时返回 `true`。
|
|
///
|
|
/// 这是计划与自身的比较,忽略资源。位于不相交资源上的两个任务本就可
|
|
/// 以合法重叠,故欲知某资源是否超订的调用方应改用
|
|
/// [`Schedule::validate_capacity`]。
|
|
pub fn has_overlapping_placements(&self) -> bool {
|
|
for (index, placement) in self.placements.iter().enumerate() {
|
|
if self.placements[..index]
|
|
.iter()
|
|
.any(|earlier| earlier.overlaps(placement))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Returns the total placed duration, in seconds.
|
|
///
|
|
/// 返回已放置的总时长,单位为秒。
|
|
pub fn scheduled_seconds(&self) -> f64 {
|
|
self.placements
|
|
.iter()
|
|
.map(|placement| placement.duration)
|
|
.sum()
|
|
}
|
|
|
|
/// Returns the earliest start of any placement.
|
|
///
|
|
/// Returns `None` when the schedule is empty.
|
|
///
|
|
/// 返回所有放置中最早的开始时刻。
|
|
///
|
|
/// 计划为空时返回 `None`。
|
|
pub fn earliest_start(&self) -> Option<f64> {
|
|
self.placements
|
|
.iter()
|
|
.map(|placement| placement.start)
|
|
.min_by(|first, second| {
|
|
first
|
|
.partial_cmp(second)
|
|
.unwrap_or(core::cmp::Ordering::Equal)
|
|
})
|
|
}
|
|
|
|
/// Returns the latest end of any placement.
|
|
///
|
|
/// Returns `None` when the schedule is empty.
|
|
///
|
|
/// 返回所有放置中最晚的结束时刻。
|
|
///
|
|
/// 计划为空时返回 `None`。
|
|
pub fn latest_end(&self) -> Option<f64> {
|
|
self.placements
|
|
.iter()
|
|
.map(Placement::end)
|
|
.max_by(|first, second| {
|
|
first
|
|
.partial_cmp(second)
|
|
.unwrap_or(core::cmp::Ordering::Equal)
|
|
})
|
|
}
|
|
|
|
/// Validates a schedule against a set of resources.
|
|
///
|
|
/// The check has two parts. First every placement must agree with the
|
|
/// requirements of its task, which catches a schedule built with a
|
|
/// different task list. Then, for each resource, the total demand placed
|
|
/// at any instant must not exceed the capacity.
|
|
///
|
|
/// The second check walks the placement boundaries of each resource in
|
|
/// time order, which is exact because the total demand is constant
|
|
/// between boundaries.
|
|
///
|
|
/// Returns an error describing the first violation.
|
|
///
|
|
/// 对照一组资源校验该计划。
|
|
///
|
|
/// 检查分两部分。其一,每处放置须与其任务的需求一致,这可捕获用另
|
|
/// 一份任务列表建成的计划。其二,对每个资源,任意瞬时的总需求不得超
|
|
/// 过容量。
|
|
///
|
|
/// 第二部分按时间序遍历各资源的放置边界,由于相邻边界之间总需求恒定,
|
|
/// 该遍历是精确的。
|
|
///
|
|
/// 返回描述首个违规的错误。
|
|
pub fn validate_capacity(
|
|
&self,
|
|
tasks: &[apsis_task::Task],
|
|
resources: &apsis_resource::ResourceSet,
|
|
) -> Result<(), ScheduleError> {
|
|
for placement in &self.placements {
|
|
let task = tasks
|
|
.iter()
|
|
.find(|task| task.id == placement.task)
|
|
.ok_or_else(|| {
|
|
ScheduleError::new(ScheduleErrorKind::RequirementMismatch)
|
|
.with_detail(format!("{} is not in the task list", placement.task))
|
|
})?;
|
|
if (placement.duration - task.duration).abs() > 1e-12 {
|
|
return Err(ScheduleError::new(ScheduleErrorKind::RequirementMismatch)
|
|
.with_detail(format!(
|
|
"{} is placed for {} s but takes {} s",
|
|
task.id, placement.duration, task.duration
|
|
)));
|
|
}
|
|
}
|
|
|
|
for resource in resources.resources() {
|
|
// The boundaries at which the total demand can change.
|
|
// 总需求可能发生变化的各边界。
|
|
let mut events: Vec<(f64, f64)> = Vec::new();
|
|
for placement in &self.placements {
|
|
let Some(task) = tasks.iter().find(|task| task.id == placement.task) else {
|
|
continue;
|
|
};
|
|
if let Some(demand) = task.demand_for(resource.id) {
|
|
events.push((placement.start, demand));
|
|
events.push((placement.end(), -demand));
|
|
}
|
|
}
|
|
if events.is_empty() {
|
|
continue;
|
|
}
|
|
// Sort by time, with a release before an acquisition at the same
|
|
// instant so that two placements that touch are not counted as
|
|
// overlapping.
|
|
// 按时间排序;同一瞬先释放后获取,使两个相接的放置不被计为
|
|
// 重叠。
|
|
events.sort_by(|first, second| {
|
|
first
|
|
.0
|
|
.partial_cmp(&second.0)
|
|
.unwrap_or(core::cmp::Ordering::Equal)
|
|
.then_with(|| {
|
|
first
|
|
.1
|
|
.partial_cmp(&second.1)
|
|
.unwrap_or(core::cmp::Ordering::Equal)
|
|
})
|
|
});
|
|
|
|
let mut current = 0.0;
|
|
for (time, change) in events {
|
|
current += change;
|
|
if current > resource.capacity() + 1e-9 {
|
|
return Err(ScheduleError::new(ScheduleErrorKind::CapacityExceeded)
|
|
.with_detail(format!(
|
|
"{} carries demand {} against capacity {} at {time}",
|
|
resource.id,
|
|
current,
|
|
resource.capacity()
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Evaluates the schedule against a task list and a resource set.
|
|
///
|
|
/// The evaluation reports the counts, the makespan, and the usage of each
|
|
/// resource. The peak demand of a resource is found by the same event
|
|
/// sweep that the capacity check uses, so the two agree.
|
|
///
|
|
/// 对照任务列表与资源集合评估该计划。
|
|
///
|
|
/// 评估给出各项计数、makespan 以及每个资源的使用情况。资源的峰值
|
|
/// 需求由与容量检查相同的事件扫掠求得,故二者一致。
|
|
pub fn evaluate(
|
|
&self,
|
|
tasks: &[apsis_task::Task],
|
|
resources: &apsis_resource::ResourceSet,
|
|
) -> Evaluation {
|
|
let mut resource_usage = Vec::with_capacity(resources.len());
|
|
for resource in resources.resources() {
|
|
let mut events: Vec<(f64, f64)> = Vec::new();
|
|
let mut placement_count = 0;
|
|
for placement in &self.placements {
|
|
let Some(task) = tasks.iter().find(|task| task.id == placement.task) else {
|
|
continue;
|
|
};
|
|
if let Some(demand) = task.demand_for(resource.id) {
|
|
events.push((placement.start, demand));
|
|
events.push((placement.end(), -demand));
|
|
placement_count += 1;
|
|
}
|
|
}
|
|
events.sort_by(|first, second| {
|
|
first
|
|
.0
|
|
.partial_cmp(&second.0)
|
|
.unwrap_or(core::cmp::Ordering::Equal)
|
|
.then_with(|| {
|
|
first
|
|
.1
|
|
.partial_cmp(&second.1)
|
|
.unwrap_or(core::cmp::Ordering::Equal)
|
|
})
|
|
});
|
|
|
|
let mut current = 0.0;
|
|
let mut peak = 0.0_f64;
|
|
let mut busy = 0.0_f64;
|
|
let mut previous: Option<f64> = None;
|
|
for (time, change) in events {
|
|
if let Some(previous) = previous {
|
|
if current > 0.0 {
|
|
busy += time - previous;
|
|
}
|
|
}
|
|
current += change;
|
|
peak = peak.max(current);
|
|
previous = Some(time);
|
|
}
|
|
|
|
resource_usage.push(ResourceUsage {
|
|
resource: resource.id,
|
|
peak_demand: peak,
|
|
busy_seconds: busy,
|
|
placement_count,
|
|
});
|
|
}
|
|
|
|
Evaluation {
|
|
placed_count: self.placements.len(),
|
|
offered_count: tasks.len(),
|
|
scheduled_seconds: self.scheduled_seconds(),
|
|
makespan_start: self.earliest_start().unwrap_or(0.0),
|
|
makespan_end: self.latest_end().unwrap_or(0.0),
|
|
resource_usage,
|
|
}
|
|
}
|
|
|
|
/// Returns the schedule sorted by start time, leaving the original alone.
|
|
///
|
|
/// A schedule is stored in the order it was built, which is the order a
|
|
/// scheduler considered the tasks. A caller that wants to read the plan
|
|
/// in time order uses this.
|
|
///
|
|
/// 返回按开始时刻排序的计划副本,原计划不变。
|
|
///
|
|
/// 计划按其构建顺序存储,即调度器考虑任务的顺序。欲按时间阅读该计
|
|
/// 划的调用方使用本方法。
|
|
pub fn in_time_order(&self) -> Vec<Placement> {
|
|
let mut ordered = self.placements.clone();
|
|
ordered.sort_by(|first, second| {
|
|
first
|
|
.start
|
|
.partial_cmp(&second.start)
|
|
.unwrap_or(core::cmp::Ordering::Equal)
|
|
.then_with(|| first.task.cmp(&second.task))
|
|
});
|
|
ordered
|
|
}
|
|
|
|
/// Returns the time at which a resource becomes free if it is busy at a
|
|
/// given instant.
|
|
///
|
|
/// Returns `None` when the resource is idle there. This is what a
|
|
/// sequential scheduler asks when it looks for the next gap.
|
|
///
|
|
/// 若某资源在给定瞬时忙碌,返回其变为空闲的时刻。
|
|
///
|
|
/// 该资源在彼处空闲时返回 `None`。顺序调度器寻找下一个空隙时正是
|
|
/// 询问这一点。
|
|
pub fn resource_busy_until(
|
|
&self,
|
|
resource: ResourceId,
|
|
tasks: &[apsis_task::Task],
|
|
time: f64,
|
|
) -> Option<f64> {
|
|
self.placements
|
|
.iter()
|
|
.filter(|placement| {
|
|
tasks
|
|
.iter()
|
|
.find(|task| task.id == placement.task)
|
|
.is_some_and(|task| task.requires(resource) && placement.covers(time))
|
|
})
|
|
.map(Placement::end)
|
|
.max_by(|first, second| {
|
|
first
|
|
.partial_cmp(second)
|
|
.unwrap_or(core::cmp::Ordering::Equal)
|
|
})
|
|
}
|
|
}
|