response_time_analysis/ros2/
bw.rs

1use itertools::Itertools;
2
3use crate::fixed_point;
4use crate::supply::SupplyBound;
5use crate::time::{Duration, Offset, Service};
6
7use crate::arrival::ArrivalBound;
8use crate::wcet::JobCostModel;
9
10pub use super::rr::{is_higher_callback_priority_than, CallbackType};
11
12const EPSILON: Duration = Duration::epsilon();
13const EPSILON_SERVICE: Service = Service::in_interval(EPSILON);
14
15/// A shallow wrapper type for callbacks to compute the direct and
16/// self-interference bounds needed for the busy-window- and
17/// round-robin-aware analysis (Theorem 3 of [the paper](https://people.mpi-sws.org/~bbb/papers/pdf/rtss21-ros.pdf)).
18///
19/// Note: this is intentionally not the same type as
20/// [rr::Callback][super::rr::Callback], even though they look very
21/// similar, since different assumptions on the wrapped
22/// `arrival_bound` are made.
23pub struct Callback<'a, 'b, AB: ArrivalBound + ?Sized, CM: JobCostModel + ?Sized> {
24    response_time_bound: Duration,
25    arrival_bound: &'a AB,
26    cost_model: &'b CM,
27    kind: CallbackType,
28}
29
30impl<'a, 'b, AB: ArrivalBound + ?Sized, CM: JobCostModel + ?Sized> Callback<'a, 'b, AB, CM> {
31    /// Create a new wrapper around a given arrival model, cost
32    /// model, and assumed response-time bound.
33    ///
34    /// **NB**: the `arrival_bound` should conform to the
35    /// busy-window-aware propagation rules (Definition 4 in the
36    /// paper).
37    pub fn new(
38        response_time_bound: Duration,
39        arrival_bound: &'a AB,
40        cost_model: &'b CM,
41        kind: CallbackType,
42    ) -> Callback<'a, 'b, AB, CM> {
43        Callback {
44            response_time_bound,
45            arrival_bound,
46            cost_model,
47            kind,
48        }
49    }
50
51    /// Busy-window interference bound (see Def. 5 in the paper).
52    fn busy_window_rbf(
53        &self,
54        interfered_with: &CallbackType,
55        delta: Duration,
56        activation_time: Offset,
57        num_polling_points: usize,
58    ) -> Service {
59        // arrivals in the half-open interval [0, delta)
60        let arrived = self.arrival_bound.number_arrivals(delta);
61        // arrivals in the half-open interval [0, activation_time)
62        let arrived_bw = self
63            .arrival_bound
64            .number_arrivals(activation_time.since_time_zero())
65            + num_polling_points;
66        let n = match self.kind {
67            CallbackType::Timer | CallbackType::EventSource => arrived,
68            CallbackType::PolledUnknownPrio => arrived.min(arrived_bw + 1),
69            CallbackType::Polled(inf_prio) => match *interfered_with {
70                CallbackType::Polled(ref_prio) => arrived.min(
71                    arrived_bw + is_higher_callback_priority_than(inf_prio, ref_prio) as usize,
72                ),
73                _ => arrived.min(arrived_bw + 1),
74            },
75        };
76        self.cost_model.cost_of_jobs(n)
77    }
78
79    /// A bound on the maximum number of self-interfering instances
80    /// in a busy window, where the instance under analysis is
81    /// activated at the given activation offset.
82    fn max_self_interfering_instances(&self, activation: Offset) -> usize {
83        // activations in the closed interval [0, activation]
84        self.arrival_bound
85            .number_arrivals(activation.closed_since_time_zero())
86            .saturating_sub(1)
87    }
88
89    /// A bound on self-interference in a busy window, where the
90    /// instance under analysis is activated at the given activation
91    /// offset.
92    fn self_interference_rbf(&self, activation: Offset) -> Service {
93        self.cost_model
94            .cost_of_jobs(self.max_self_interfering_instances(activation))
95    }
96
97    /// A bound on the total workload of the callback in a busy
98    /// window, where `A_star` denotes the end point of the half-open
99    /// busy-window interval `[0, A_star)`.
100    ///
101    /// See Lemma 18 of [the paper](https://people.mpi-sws.org/~bbb/papers/pdf/rtss21-ros.pdf).
102    #[allow(non_snake_case)]
103    fn own_workload_rbf(&self, A_star: Offset) -> Service {
104        let n_activations = self.arrival_bound.number_arrivals(A_star.since_time_zero());
105        self.cost_model.cost_of_jobs(n_activations)
106    }
107
108    /// A bound on the marginal execution cost, denoted `\Omega` in Theorem 2.
109    fn marginal_execution_cost(&self, activation: Offset) -> Service {
110        let n = self.max_self_interfering_instances(activation);
111        self.cost_model.cost_of_jobs(n + 1) - self.cost_model.cost_of_jobs(n)
112    }
113
114    /// A bound on the maximum number of polling points "incurred"
115    /// (see Def. 3 in the paper).
116    fn polling_point_bound(&self) -> usize {
117        self.arrival_bound.number_arrivals(self.response_time_bound)
118    }
119
120    /// The total polling-point bound of a subchain (i.e., a sequence of
121    /// callbacks). The subchain is expressed as a slice of callback
122    /// references. See Def. 3 in the paper.
123    fn subchain_polling_point_bound(subchain: &[&Callback<AB, CM>]) -> usize {
124        subchain.iter().map(|cb| cb.polling_point_bound()).sum()
125    }
126}
127
128/// Hybrid round-robin- and busy-window-aware chain analysis (Theorem
129/// 3 in [the paper](https://people.mpi-sws.org/~bbb/papers/pdf/rtss21-ros.pdf)).
130///
131/// # Parameters
132///
133/// - `supply`: the supply model of the shared executor
134/// - `workload`: all callbacks served by the shared executor
135/// - `subchain`: the subchain under analysis --- each reference must
136///               be unique and point to one of the callbacks in `workload`
137/// - `limit`: the divergence threshold at which the search for a
138///   fixed point is aborted
139///
140/// If no fixed point is found below the divergence limit given by
141/// `limit`, return a [SearchFailure][fixed_point::SearchFailure]
142/// instead.
143#[allow(non_snake_case)]
144pub fn rta_subchain<SBF, AB, CM>(
145    supply: &SBF,
146    workload: &[Callback<AB, CM>],
147    subchain: &[&Callback<AB, CM>],
148    limit: Duration,
149) -> fixed_point::SearchResult
150where
151    SBF: SupplyBound + ?Sized,
152    AB: ArrivalBound + ?Sized,
153    CM: JobCostModel + ?Sized,
154{
155    // First, compute a bound on the maximum number of polling points
156    // in the analysis window.
157    let max_num_polling_points = Callback::subchain_polling_point_bound(subchain);
158
159    // callback at the end of the chain under analysis
160    let eoc = subchain.last().expect("subchain must not be empty");
161
162    // do we have a proper chain, or just a single callback?
163    let singleton_chain = subchain.len() == 1;
164
165    // check that we are actually given a proper subchain, i.e., all references
166    // in the subchain must point to something in the workload
167    debug_assert!(
168        subchain
169            .iter()
170            .all(|sc_cb| workload.iter().any(|wl_cb| std::ptr::eq(*sc_cb, wl_cb))),
171        "subchain not wholly part of workload"
172    );
173
174    // Busy-window-aware interference (Def. 5)
175    let bw_interference = |delta: Duration, activation: Offset| {
176        workload
177            .iter()
178            .map(|cb| {
179                if std::ptr::eq(*eoc, cb) {
180                    // Don't count the end of the chain, which is
181                    // accounted for as self-interference.
182                    Service::none()
183                } else {
184                    cb.busy_window_rbf(&eoc.kind, delta, activation, max_num_polling_points)
185                }
186            })
187            .sum()
188    };
189
190    // The response-time analysis for a given offset (Theorem 3).
191    let rta = |activation: Offset| -> fixed_point::SearchResult {
192        // Step 1: bound the starting time S*.
193        let si = eoc.self_interference_rbf(activation);
194
195        let rhs_S_star = |S_star: Duration| {
196            let di = bw_interference(S_star, activation);
197            EPSILON_SERVICE + di + si
198        };
199
200        let S_star = fixed_point::search(supply, limit, rhs_S_star)?;
201
202        // Step 2: find the response-time bound R*.
203
204        let supply_star = supply.provided_service(S_star);
205        let omega = eoc.marginal_execution_cost(activation);
206        let rhs_F_star = supply_star.saturating_sub(EPSILON_SERVICE) + omega;
207        let F_star = supply.service_time(rhs_F_star);
208
209        // the response-time bound
210        if singleton_chain {
211            // special case: A = t_a, so we can subtract in Theorem 3
212            Ok(F_star.saturating_sub(activation.since_time_zero()))
213        } else {
214            // we don't know A, so safely approximate it as A=0
215            Ok(F_star)
216        }
217    };
218
219    // Bound the maximum offset (Lemma 18).
220    let rhs_max = |ta_star: Duration| {
221        let si = eoc.own_workload_rbf(Offset::from_time_zero(ta_star));
222        let di = bw_interference(ta_star, Offset::from_time_zero(ta_star));
223        EPSILON_SERVICE + di + si
224    };
225    let max_offset = Offset::from_time_zero(fixed_point::search(supply, limit, rhs_max)?);
226
227    // Define the search space of relevant offsets (Lemma 19).
228    // First, find all relevant steps.
229    let all_steps = workload
230        .iter()
231        // we only care about polled callbacks and the end of the
232        // subchain under analysis
233        .filter(|cb| cb.kind.is_pp() || std::ptr::eq(*eoc, *cb))
234        .map(|cb| {
235            cb.arrival_bound.steps_iter().map(move |delta| {
236                if std::ptr::eq(*eoc, cb) {
237                    // This is the callback under analysis.
238                    // The steps_iter() gives us the values of delta such that
239                    // number_arrivals(delta-1) < number_arrivals_delta().
240                    // However, we are looking for t_a such that
241                    // number_arrivals(t_a) < number_arrivals(t_a + 1).
242                    // Thus, we need to subtract one from delta.
243                    Offset::from_time_zero(delta.saturating_sub(EPSILON))
244                } else {
245                    // This is some interfering polled callback.
246                    // The steps_iter() gives us the values of delta such that
247                    // number_arrivals(delta-1) < number_arrivals_delta().
248                    // That's exactly what we are looking for here, so we
249                    // can just pass it through.
250                    Offset::from_time_zero(delta)
251                }
252            })
253        })
254        .kmerge()
255        .dedup();
256
257    // In a debug build, let's double-check the steps computed above
258    // with a brute-force solution.
259    #[cfg(debug_assertions)]
260    let mut brute_force_steps = (0..)
261        .filter(|t_a| {
262            workload.iter().any(|cb|
263                // Negated conditions of Lemma 19.
264                if std::ptr::eq(*eoc, cb) {
265                    cb.arrival_bound.number_arrivals(Duration::from(*t_a)) !=
266                    cb.arrival_bound.number_arrivals(Duration::from(*t_a + 1))
267                } else {
268                    cb.kind.is_pp() && *t_a > 0 &&
269                    cb.arrival_bound.number_arrivals(Duration::from(*t_a - 1)) !=
270                    cb.arrival_bound.number_arrivals(Duration::from(*t_a))
271                }
272            )
273        })
274        .map(Offset::from)
275        .peekable();
276
277    // In a debug build, shadow all_steps with the checked version.
278    #[cfg(debug_assertions)]
279    let all_steps = {
280        let mut wrapped = all_steps.peekable();
281        // Manually check the first point to make sure we're not calling
282        // zip on an empty iterator.
283        assert_eq!(brute_force_steps.peek(), wrapped.peek());
284        wrapped.zip(brute_force_steps).map(|(a, bf)| {
285            assert_eq!(a, bf);
286            a
287        })
288    };
289
290    // The search space is given by t_a=0 and each relevant step below max_offset.
291    let search_space = all_steps.take_while(|activation| *activation < max_offset);
292
293    // Apply the offset-specific RTA to each offset in the search space and
294    // return the maximum response-time bound.
295    fixed_point::max_response_time(search_space.map(rta))
296}