Skip to main content

ixa/
plan.rs

1//! A priority queue that stores arbitrary data sorted by time and priority
2//!
3//! Defines a [`Queue`]`<T, P>` that is intended to store a queue of items of type
4//! `T` - sorted by `f64` time and definable priority `P` - called 'plans'.
5//! This queue has methods for adding plans, cancelling plans, and retrieving
6//! the earliest plan in the queue. Adding a plan is *O*(log(*n*)) while
7//! cancellation and retrieval are *O*(1).
8//!
9//! This queue is used by [`Context`](crate::Context) to store future events where some callback
10//! closure `FnOnce(&mut Context)` will be executed at a given point in time.
11
12use std::cmp::Ordering;
13use std::collections::BinaryHeap;
14
15use crate::{trace, HashMap, HashMapExt};
16
17/// A priority queue that stores arbitrary data sorted by time
18///
19/// Items of type `T` are stored in order by `f64` time and called [`Plan`]`<T>`.
20/// Plans can have priorities given by some specified orderable type `P`.
21/// When plans are created they are sequentially assigned a [`PlanId`] that is a
22/// wrapped `u64`. If two plans are scheduled for the same time then the plan
23/// with the lowest priority is placed earlier. If two plans have the same time
24/// and priority then the plan that is scheduled first (i.e., that has the
25/// lowest id) is placed earlier.
26///
27/// The time, plan id, and priority are stored in a binary heap of [`PlanSchedule`]`<P>`
28/// objects. The data payload of the event is stored in a hash map by plan id.
29/// Plan cancellation occurs by removing the corresponding entry from the data
30/// hash map.
31pub struct Queue<T, P: Eq + PartialEq + Ord> {
32    queue: BinaryHeap<PlanSchedule<P>>,
33    data_map: HashMap<u64, T>,
34    /// The number of plans that have been added; equivalently, the next plan ID that
35    /// will be issued.
36    plan_counter: u64,
37    /// Tracks the high water mark of plans in flight (scheduled but not yet executed).
38    /// This is the max of `self.queue.len()`, not of `self.data_map.len()`.
39    #[cfg(feature = "profiling")]
40    pub(crate) max_plans_in_flight: u64,
41    #[cfg(feature = "profiling")]
42    pub(crate) max_memory_in_use: u64,
43}
44
45impl<T, P: Eq + PartialEq + Ord> Queue<T, P> {
46    /// Create a new empty `Queue<T>`
47    #[must_use]
48    pub fn new() -> Queue<T, P> {
49        Queue {
50            queue: BinaryHeap::new(),
51            data_map: HashMap::new(),
52            plan_counter: 0,
53            #[cfg(feature = "profiling")]
54            max_plans_in_flight: 0,
55            #[cfg(feature = "profiling")]
56            max_memory_in_use: 0,
57        }
58    }
59
60    /// Add a plan to the queue at the specified time
61    ///
62    /// Returns a [`PlanId`] for the newly-added plan that can be used to cancel it
63    /// if needed.
64    pub fn add_plan(&mut self, time: f64, data: T, priority: P) -> PlanId {
65        trace!("adding plan at {time}");
66        // Add plan to queue, store data, and increment counter
67        let plan_id = self.plan_counter;
68        self.queue.push(PlanSchedule {
69            plan_id,
70            time,
71            priority,
72        });
73        self.data_map.insert(plan_id, data);
74        self.plan_counter += 1;
75        #[cfg(feature = "profiling")]
76        {
77            self.max_plans_in_flight = self.max_plans_in_flight.max(self.queue.len() as u64);
78            self.max_memory_in_use = self
79                .max_memory_in_use
80                .max(self.estimated_memory_in_use() as u64);
81        }
82
83        PlanId(plan_id)
84    }
85
86    /// Cancel a plan that has been added to the queue
87    pub fn cancel_plan(&mut self, plan_id: &PlanId) -> Option<T> {
88        trace!("cancel plan {plan_id:?}");
89        // Delete the plan from the map, but leave in the queue
90        // It will be skipped when the plan is popped from the queue
91        self.data_map.remove(&plan_id.0)
92    }
93
94    #[must_use]
95    pub fn is_empty(&self) -> bool {
96        self.queue.is_empty()
97    }
98
99    #[must_use]
100    pub fn next_time(&self) -> Option<f64> {
101        self.queue.peek().map(|e| e.time)
102    }
103
104    #[allow(dead_code)]
105    pub(crate) fn clear(&mut self) {
106        self.data_map.clear();
107        self.queue.clear();
108        self.plan_counter = 0;
109    }
110
111    #[must_use]
112    #[allow(dead_code)]
113    pub(crate) fn peek(&self) -> Option<(&PlanSchedule<P>, &T)> {
114        // Iterate over queue until we find a plan with data or queue is empty
115        for entry in &self.queue {
116            // Skip plans that have been cancelled and thus have no data
117            if let Some(data) = self.data_map.get(&entry.plan_id) {
118                return Some((entry, data));
119            }
120        }
121        None
122    }
123
124    /// Retrieve the earliest plan in the queue
125    ///
126    /// Returns the next plan if it exists or else `None` if the queue is empty
127    pub fn get_next_plan(&mut self) -> Option<Plan<T>> {
128        trace!("getting next plan");
129        loop {
130            // Pop from queue until we find a plan with data or queue is empty
131            match self.queue.pop() {
132                Some(entry) => {
133                    // Skip plans that have been cancelled and thus have no data
134                    if let Some(data) = self.data_map.remove(&entry.plan_id) {
135                        return Some(Plan {
136                            time: entry.time,
137                            data,
138                        });
139                    }
140                }
141                None => {
142                    return None;
143                }
144            }
145        }
146    }
147
148    /// Returns a list of length `at_most`, or unbounded if `at_most=0`, of active scheduled
149    /// [`PlanSchedule`]s ordered as they are in the queue itself.
150    #[must_use]
151    pub fn list_schedules(&self, at_most: usize) -> Vec<&PlanSchedule<P>> {
152        let mut items = vec![];
153
154        // Iterate over queue until we find a plan with data or queue is empty
155        for entry in &self.queue {
156            // Skip plans that have been cancelled and thus have no data
157            if self.data_map.contains_key(&entry.plan_id) {
158                items.push(entry);
159                if items.len() == at_most {
160                    break;
161                }
162            }
163        }
164        items
165    }
166
167    #[doc(hidden)]
168    pub(crate) fn remaining_plan_count(&self) -> usize {
169        self.queue.len()
170    }
171
172    #[cfg(feature = "profiling")]
173    fn estimated_memory_in_use(&self) -> usize {
174        let queue_bytes = self.queue.capacity() * size_of::<PlanSchedule<P>>();
175
176        let map_entry_bytes = self.data_map.capacity() * size_of::<(u64, T)>();
177
178        queue_bytes + map_entry_bytes
179    }
180}
181
182impl<T, P: Eq + PartialEq + Ord> Default for Queue<T, P> {
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188/// A time, id, and priority object used to order plans in the [`Queue`]`<T>`
189///
190/// [`PlanSchedule`] objects are sorted in increasing order of time, priority and then
191/// plan id
192#[derive(PartialEq, Debug)]
193pub struct PlanSchedule<P: Eq + PartialEq + Ord> {
194    pub plan_id: u64,
195    pub time: f64,
196    pub priority: P,
197}
198
199impl<P: Eq + PartialEq + Ord + Clone> Clone for PlanSchedule<P> {
200    fn clone(&self) -> Self {
201        PlanSchedule {
202            priority: self.priority.clone(),
203            ..*self
204        }
205    }
206}
207
208impl<P: Eq + PartialEq + Ord + Copy + Clone> Copy for PlanSchedule<P> {}
209
210impl<P: Eq + PartialEq + Ord> Eq for PlanSchedule<P> {}
211
212impl<P: Eq + PartialEq + Ord> PartialOrd for PlanSchedule<P> {
213    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
214        Some(self.cmp(other))
215    }
216}
217
218/// Entry objects are ordered in increasing order by time, priority, and then
219/// plan id
220impl<P: Eq + PartialEq + Ord> Ord for PlanSchedule<P> {
221    fn cmp(&self, other: &Self) -> Ordering {
222        let time_ordering = self.time.partial_cmp(&other.time).unwrap().reverse();
223        match time_ordering {
224            // Break time ties in order of priority and then plan id
225            Ordering::Equal => {
226                let priority_ordering = self
227                    .priority
228                    .partial_cmp(&other.priority)
229                    .unwrap()
230                    .reverse();
231                match priority_ordering {
232                    Ordering::Equal => self.plan_id.cmp(&other.plan_id).reverse(),
233                    _ => priority_ordering,
234                }
235            }
236            _ => time_ordering,
237        }
238    }
239}
240
241/// A unique identifier for a plan added to a [`Queue`]`<T>`
242#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
243pub struct PlanId(pub(crate) u64);
244
245/// A plan that holds data of type `T` intended to be used at the specified time
246pub struct Plan<T> {
247    pub time: f64,
248    pub data: T,
249}
250
251#[cfg(test)]
252mod tests {
253    use super::Queue;
254
255    #[test]
256    fn empty_queue() {
257        let mut plan_queue = Queue::<(), ()>::new();
258        assert!(plan_queue.get_next_plan().is_none());
259    }
260
261    #[test]
262    fn add_plans() {
263        let mut plan_queue = Queue::new();
264        plan_queue.add_plan(1.0, 1, ());
265        plan_queue.add_plan(3.0, 3, ());
266        plan_queue.add_plan(2.0, 2, ());
267        assert!(!plan_queue.is_empty());
268
269        let next_plan = plan_queue.get_next_plan().unwrap();
270        assert_eq!(next_plan.time, 1.0);
271        assert_eq!(next_plan.data, 1);
272
273        assert!(!plan_queue.is_empty());
274        let next_plan = plan_queue.get_next_plan().unwrap();
275        assert_eq!(next_plan.time, 2.0);
276        assert_eq!(next_plan.data, 2);
277
278        assert!(!plan_queue.is_empty());
279        let next_plan = plan_queue.get_next_plan().unwrap();
280        assert_eq!(next_plan.time, 3.0);
281        assert_eq!(next_plan.data, 3);
282
283        assert!(plan_queue.is_empty());
284        assert!(plan_queue.get_next_plan().is_none());
285    }
286
287    #[test]
288    fn add_plans_at_same_time_with_same_priority() {
289        let mut plan_queue = Queue::new();
290        plan_queue.add_plan(1.0, 1, ());
291        plan_queue.add_plan(1.0, 2, ());
292        assert!(!plan_queue.is_empty());
293
294        let next_plan = plan_queue.get_next_plan().unwrap();
295        assert_eq!(next_plan.time, 1.0);
296        assert_eq!(next_plan.data, 1);
297
298        assert!(!plan_queue.is_empty());
299        let next_plan = plan_queue.get_next_plan().unwrap();
300        assert_eq!(next_plan.time, 1.0);
301        assert_eq!(next_plan.data, 2);
302
303        assert!(plan_queue.is_empty());
304        assert!(plan_queue.get_next_plan().is_none());
305    }
306
307    #[test]
308    fn add_plans_at_same_time_with_different_priority() {
309        let mut plan_queue = Queue::new();
310        plan_queue.add_plan(1.0, 1, 1);
311        plan_queue.add_plan(1.0, 2, 0);
312
313        assert!(!plan_queue.is_empty());
314        let next_plan = plan_queue.get_next_plan().unwrap();
315        assert_eq!(next_plan.time, 1.0);
316        assert_eq!(next_plan.data, 2);
317
318        let next_plan = plan_queue.get_next_plan().unwrap();
319        assert_eq!(next_plan.time, 1.0);
320        assert_eq!(next_plan.data, 1);
321
322        assert!(plan_queue.is_empty());
323        assert!(plan_queue.get_next_plan().is_none());
324    }
325
326    #[test]
327    fn add_and_cancel_plans() {
328        let mut plan_queue = Queue::new();
329        plan_queue.add_plan(1.0, 1, ());
330        let plan_to_cancel = plan_queue.add_plan(2.0, 2, ());
331        plan_queue.add_plan(3.0, 3, ());
332        plan_queue.cancel_plan(&plan_to_cancel);
333        assert!(!plan_queue.is_empty());
334
335        let next_plan = plan_queue.get_next_plan().unwrap();
336        assert_eq!(next_plan.time, 1.0);
337        assert_eq!(next_plan.data, 1);
338
339        assert!(!plan_queue.is_empty());
340        let next_plan = plan_queue.get_next_plan().unwrap();
341        assert_eq!(next_plan.time, 3.0);
342        assert_eq!(next_plan.data, 3);
343
344        assert!(plan_queue.is_empty());
345        assert!(plan_queue.get_next_plan().is_none());
346    }
347
348    #[test]
349    fn add_and_get_plans() {
350        let mut plan_queue = Queue::new();
351        plan_queue.add_plan(1.0, 1, ());
352        plan_queue.add_plan(2.0, 2, ());
353        assert!(!plan_queue.is_empty());
354
355        let next_plan = plan_queue.get_next_plan().unwrap();
356        assert_eq!(next_plan.time, 1.0);
357        assert_eq!(next_plan.data, 1);
358
359        plan_queue.add_plan(1.5, 3, ());
360
361        assert!(!plan_queue.is_empty());
362        let next_plan = plan_queue.get_next_plan().unwrap();
363        assert_eq!(next_plan.time, 1.5);
364        assert_eq!(next_plan.data, 3);
365
366        assert!(!plan_queue.is_empty());
367        let next_plan = plan_queue.get_next_plan().unwrap();
368        assert_eq!(next_plan.time, 2.0);
369        assert_eq!(next_plan.data, 2);
370
371        assert!(plan_queue.is_empty());
372        assert!(plan_queue.get_next_plan().is_none());
373    }
374
375    #[test]
376    fn cancel_invalid_plan() {
377        let mut plan_queue = Queue::new();
378        let plan_to_cancel = plan_queue.add_plan(1.0, (), ());
379        // is_empty just checks for a plan existing, not whether it is valid/has data
380        assert!(!plan_queue.is_empty());
381        plan_queue.get_next_plan();
382        assert!(plan_queue.is_empty());
383        let result = plan_queue.cancel_plan(&plan_to_cancel);
384        assert!(result.is_none());
385    }
386}