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}