response_time_analysis/edf/
fully_nonpreemptive.rs

1//! RTA for EDF scheduling with fully non-preemptive jobs (**NP-EDF**)
2
3use itertools::Itertools;
4
5use crate::arrival::ArrivalBound;
6use crate::demand::{self, RequestBound};
7use crate::time::{Duration, Offset, Service};
8use crate::{fixed_point, supply, wcet};
9
10/// The per-task information required to perform the analysis.
11pub struct Task<'a, AB: ArrivalBound + ?Sized> {
12    /// The task's worst-case execution time.
13    pub wcet: wcet::Scalar,
14    /// The task's arrival curve.
15    pub arrivals: &'a AB,
16    /// The task's relative deadline.
17    pub deadline: Duration,
18}
19
20impl<'a, AB: ArrivalBound + ?Sized> Task<'a, AB> {
21    fn rbf(&self) -> impl RequestBound + '_ {
22        demand::RBF::new(self.arrivals, self.wcet)
23    }
24}
25
26/// The information about the task under analysis required to perform
27/// the analysis.
28/// Create one struct of this type to represent the task under analysis.
29pub type TaskUnderAnalysis<'a, T> = Task<'a, T>;
30
31/// The per-task information required to account for interfering tasks.
32/// Create a struct of this type for each interfering task.
33pub type InterferingTask<'a, T> = Task<'a, T>;
34
35/// Bound the maximum response time of a task under *fully nonpreemptive
36/// earliest-deadline first* (**NP-EDF**) scheduling on a dedicated,
37/// ideal uniprocessor.
38///
39/// That is, the analysis assumes that:
40///
41/// 1. The processor is available to the tasks 100% of the time.
42/// 2. Scheduling overheads are negligible (i.e., already integrated
43///    into the task parameters).
44/// 3. Jobs are prioritized in order of increasing absolute deadlines
45///    (with ties broken arbitrarily).
46/// 4. Jobs have run-to-completion semantics: once started, a job will
47///    not relinquish the processor until it is complete.
48///
49/// The analysis further assumes that all tasks are independent and that each
50/// task is characterized by an arbitrary arrival curve and a WCET
51/// bound.
52///
53/// This analysis is an implementation of the corresponding  verified
54/// instantiation of [the abstract RTA of Bozhko and Brandenburg
55/// (ECRTS 2020)](https://drops.dagstuhl.de/opus/volltexte/2020/12385/pdf/LIPIcs-ECRTS-2020-22.pdf).
56/// Refer to the [the Coq-verified instantiation](http://prosa.mpi-sws.org/branches/master/pretty/prosa.results.edf.rta.fully_nonpreemptive.html)
57/// for the latest version.
58///
59/// The task for which a response-time bound is to be found is
60/// represented by `tua`, the set of interfering tasks that share the
61/// same processor is given by `other_tasks`.
62///
63/// If no fixed point is found below the divergence limit given by
64/// `limit`, the function returns a
65/// [SearchFailure][fixed_point::SearchFailure] instead.
66#[allow(non_snake_case)]
67pub fn dedicated_uniproc_rta<AB1, AB2>(
68    tua: &TaskUnderAnalysis<AB1>,
69    other_tasks: &[InterferingTask<AB2>],
70    limit: Duration,
71) -> fixed_point::SearchResult
72where
73    AB1: ArrivalBound + ?Sized,
74    AB2: ArrivalBound + ?Sized,
75{
76    // This analysis is specific to dedicated uniprocessors.
77    let proc = supply::Dedicated::new();
78
79    // For convenience, define the RBFs for the task under analysis...
80    let task_under_analysis = tua.rbf();
81    // ...and for the interfering tasks.
82    let rbfs: Vec<_> = other_tasks.iter().map(Task::rbf).collect();
83
84    // First, bound the maximum possible busy-window length.
85    let L = fixed_point::search(&proc, limit, |L| {
86        let interference_bound: Service = rbfs.iter().map(|rbf| rbf.service_needed(L)).sum();
87        interference_bound + task_under_analysis.service_needed(L)
88    })?;
89
90    // Second, define the RTA for a given offset A. To this end, we
91    // first define some components of the fixed-point equation.
92
93    // The run-to-completion threshold of the task under analysis. In
94    // the fully non-preemptive model, no job can be preempted prior to
95    // its completion. In other words, once a job starts running, it is
96    // guaranteed to finish. Thus, we can set the task-level
97    // run-to-completion threshold to epsilon.
98    // See also: http://prosa.mpi-sws.org/branches/master/pretty/prosa.model.task.preemption.fully_nonpreemptive.html#fully_nonpreemptive
99    let rtct = Service::epsilon();
100
101    // The remaining cost after the run-to-completion threshold has been
102    // reached.
103    let rem_cost = tua.wcet.wcet - rtct;
104
105    // Now define the offset-specific RTA.
106    let rta = |A: Offset| {
107        // Bound on the priority inversion caused by jobs with lower priority.
108        let blocking_bound = other_tasks
109            .iter()
110            .filter(|ot| {
111                ot.deadline > tua.deadline + A.since_time_zero()
112                    && ot.rbf().service_needed(Duration::epsilon()) > Service::none()
113            })
114            .map(|ot| ot.wcet.wcet.saturating_sub(Service::epsilon()))
115            .max()
116            .unwrap_or_else(Service::none);
117
118        // Define the RHS of the equation in theorem 31 of the aRTA paper,
119        // where AF = A + F.
120        let rhs = |AF: Duration| {
121            // demand of the task under analysis
122            let self_interference = task_under_analysis.service_needed(A.closed_since_time_zero());
123            let tua_demand = self_interference - rem_cost;
124
125            // demand of all interfering tasks
126            let bound_on_total_hep_workload: Service = other_tasks
127                .iter()
128                .map(|ot| {
129                    ot.rbf().service_needed(std::cmp::min(
130                        AF,
131                        (A.closed_since_time_zero() + tua.deadline).saturating_sub(ot.deadline),
132                    ))
133                })
134                .sum();
135
136            blocking_bound + tua_demand + bound_on_total_hep_workload
137        };
138
139        // Find the solution A+F that is the least fixed point.
140        let AF = fixed_point::search(&proc, limit, rhs)?;
141        // Extract the corresponding bound.
142        let F = AF.saturating_sub(A.since_time_zero());
143        Ok(F + Duration::from(rem_cost))
144    };
145
146    // Third, define the search space. The search space is given by
147    // A=0 and each step below L of the task under analysis's RBF.
148    // The case of A=0 is not handled explicitly since `steps_iter()`
149    // necessarily yields delta=1, which results in A=0 being
150    // included in the search space.
151    let max_offset = Offset::from_time_zero(L);
152    let search_space_tua =
153        demand::step_offsets(&task_under_analysis).take_while(|A| *A < max_offset);
154    let search_space = other_tasks
155        .iter()
156        .zip(rbfs.iter())
157        .map(|(ot, rbf)| {
158            demand::step_offsets(rbf)
159                .map(move |delta| {
160                    Offset::from_time_zero(
161                        (delta + ot.deadline)
162                            .since_time_zero()
163                            .saturating_sub(tua.deadline),
164                    )
165                })
166                .take_while(|A| *A < max_offset)
167        })
168        .kmerge()
169        .merge(search_space_tua)
170        .dedup();
171
172    // Finally, apply the offset-specific RTA to each offset in the
173    // search space and return the maximum response-time bound.
174    fixed_point::max_response_time(search_space.map(rta))
175}