Skip to main content

gwr_engine/
executor.rs

1// Copyright (c) 2023 Graphcore Ltd. All rights reserved.
2
3use std::cell::{Cell, RefCell};
4use std::future::Future;
5use std::pin::Pin;
6use std::rc::Rc;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::mpsc::{self, RecvTimeoutError};
10use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
11use std::time::Duration;
12use std::{mem, thread};
13
14use gwr_track::entity::Entity;
15use rand::SeedableRng;
16use rand::rngs::StdRng;
17use rand::seq::SliceRandom;
18
19use crate::time::clock::Clock;
20use crate::time::simtime::SimTime;
21use crate::types::SimResult;
22
23fn no_op(_: *const ()) {}
24
25unsafe fn drop_task(data: *const ()) {
26    unsafe {
27        drop(Rc::from_raw(data as *const Task));
28    }
29}
30
31static VTABLE: RawWakerVTable = RawWakerVTable::new(clone_raw_waker, wake_task, no_op, drop_task);
32
33fn task_raw_waker(task: Rc<Task>) -> RawWaker {
34    let ptr = Rc::into_raw(task) as *const ();
35    RawWaker::new(ptr, &VTABLE)
36}
37
38fn waker_for_task(task: Rc<Task>) -> Waker {
39    unsafe { Waker::from_raw(task_raw_waker(task)) }
40}
41
42unsafe fn clone_raw_waker(data: *const ()) -> RawWaker {
43    unsafe {
44        // Tasks are always wrapped in a reference counter to allow them to be
45        // shared read-only. The input `data` pointer is borrowed — we must not
46        // decrement its refcount, so we mem::forget the reconstructed Rc rather
47        // than letting it drop.
48        let rc_task = Rc::from_raw(data as *const Task);
49        let clone = rc_task.clone();
50        mem::forget(rc_task);
51        let ptr = Rc::into_raw(clone) as *const ();
52        RawWaker::new(ptr, &VTABLE)
53    }
54}
55
56unsafe fn wake_task(data: *const ()) {
57    unsafe {
58        // Tasks are always wrapped in a reference counter to allow them to be
59        // shared read-only.
60        let rc_task = Rc::from_raw(data as *const Task);
61        let cloned = rc_task.clone();
62        rc_task.executor_state.new_tasks.borrow_mut().push(cloned);
63    }
64}
65
66struct Task {
67    future: RefCell<Option<Pin<Box<dyn Future<Output = SimResult>>>>>,
68    executor_state: Rc<ExecutorState>,
69}
70
71impl Task {
72    pub fn new(
73        future: impl Future<Output = SimResult> + 'static,
74        executor_state: Rc<ExecutorState>,
75    ) -> Task {
76        Task {
77            future: RefCell::new(Some(Box::pin(future))),
78            executor_state,
79        }
80    }
81
82    fn poll(&self, context: &mut Context) -> Option<Poll<SimResult>> {
83        let mut future_slot = self.future.borrow_mut();
84        let future = future_slot.as_mut()?;
85
86        let poll_result = future.as_mut().poll(context);
87        if poll_result.is_ready() {
88            future_slot.take();
89        }
90
91        Some(poll_result)
92    }
93}
94
95/// Cumulative future activity from the most recent executor run.
96#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
97pub struct ExecutorStats {
98    /// Number of calls made to an async future's `poll` method.
99    pub futures_polled: usize,
100    /// Number of async futures that returned `Poll::Ready`.
101    pub futures_completed: usize,
102}
103
104/// Executor activity reported at a configured wall-clock interval.
105#[derive(Clone, Copy, Debug, Eq, PartialEq)]
106pub struct ExecutorSnapshot {
107    /// Cumulative future activity so far in this run.
108    pub stats: ExecutorStats,
109    /// Number of tasks currently active in the executor.
110    pub active_task_count: usize,
111}
112
113struct ExecutorState {
114    task_queue: RefCell<Vec<Rc<Task>>>,
115    new_tasks: RefCell<Vec<Rc<Task>>>,
116    time: RefCell<SimTime>,
117    randomize_task_order: Cell<bool>,
118    task_order_rng: RefCell<StdRng>,
119    stats: Cell<ExecutorStats>,
120}
121
122impl ExecutorState {
123    pub fn new(top: &Rc<Entity>) -> Self {
124        Self {
125            task_queue: RefCell::new(Vec::new()),
126            new_tasks: RefCell::new(Vec::new()),
127            time: RefCell::new(SimTime::new(top)),
128            randomize_task_order: Cell::new(false),
129            task_order_rng: RefCell::new(StdRng::seed_from_u64(rand::random())),
130            stats: Cell::new(ExecutorStats::default()),
131        }
132    }
133}
134
135/// Single-threaded executor
136///
137/// This is a thin-wrapper (using [`Rc`]) around the real executor, so that this
138/// struct can be cloned and passed around.
139///
140/// See the [module documentation] for more details.
141///
142/// [module documentation]: index.html
143#[derive(Clone)]
144pub struct Executor {
145    state: Rc<ExecutorState>,
146}
147
148impl Executor {
149    /// Run the executor without observing activity.
150    pub(crate) fn run(&self) -> SimResult {
151        self.run_internal(None, |_| Ok(()))
152    }
153
154    /// Run the executor while reporting activity after each wall-clock
155    /// interval has passed. Observations occur at completed step boundaries.
156    pub(crate) fn run_with_observer(
157        &self,
158        minimum_interval: Duration,
159        observer: impl FnMut(ExecutorSnapshot) -> SimResult,
160    ) -> SimResult {
161        if minimum_interval.is_zero() {
162            return Err(crate::types::SimError(
163                "executor observer interval must be positive".to_string(),
164            ));
165        }
166
167        let refresh_due = Arc::new(AtomicBool::new(false));
168        let timer_refresh_due = Arc::clone(&refresh_due);
169        let (stop_timer, timer_stop) = mpsc::channel::<()>();
170        let timer = thread::spawn(move || {
171            // Use the timeout mechanism on the channel to give us time
172            // intervals. Will break out when the main thread
173            // disconnects the channel.
174            while let Err(RecvTimeoutError::Timeout) = timer_stop.recv_timeout(minimum_interval) {
175                timer_refresh_due.store(true, Ordering::Relaxed);
176            }
177        });
178
179        let result = self.run_internal(Some(&refresh_due), observer);
180        drop(stop_timer);
181        timer
182            .join()
183            .expect("executor observer timer thread panicked");
184        result
185    }
186
187    fn run_internal(
188        &self,
189        refresh_due: Option<&AtomicBool>,
190        mut observer: impl FnMut(ExecutorSnapshot) -> SimResult,
191    ) -> SimResult {
192        let mut stats = ExecutorStats::default();
193        let result = (|| {
194            loop {
195                self.step(&mut stats)?;
196
197                if refresh_due.is_some_and(|due| {
198                    due.load(Ordering::Relaxed) && due.swap(false, Ordering::Relaxed)
199                }) {
200                    observer(ExecutorSnapshot {
201                        stats,
202                        active_task_count: self.state.new_tasks.borrow().len(),
203                    })?;
204                }
205
206                if self.state.new_tasks.borrow().is_empty() {
207                    if self.state.time.borrow().can_exit() {
208                        break;
209                    }
210
211                    if let Some(wakers) = self.state.time.borrow_mut().advance_time() {
212                        // No events left, advance time
213                        for task_waker in wakers.into_iter() {
214                            task_waker.waker.wake();
215                        }
216                    } else {
217                        break;
218                    }
219                }
220            }
221            Ok(())
222        })();
223        self.state.stats.set(stats);
224        result
225    }
226
227    fn step(&self, stats: &mut ExecutorStats) -> SimResult {
228        // Append new tasks created since the last step into the task queue
229        let mut task_queue = self.state.task_queue.borrow_mut();
230        task_queue.append(&mut self.state.new_tasks.borrow_mut());
231        if self.state.randomize_task_order.get() {
232            task_queue.shuffle(&mut *self.state.task_order_rng.borrow_mut());
233        }
234
235        // Loop over all tasks, polling them. If a task is not ready, add it to
236        // the pending tasks.
237        for task in task_queue.drain(..) {
238            // Dummy waker and context (not used as we poll all tasks)
239            let waker = waker_for_task(task.clone());
240            let mut context = Context::from_waker(&waker);
241
242            if let Some(poll_result) = task.poll(&mut context) {
243                stats.futures_polled += 1;
244                match poll_result {
245                    Poll::Ready(Err(e)) => {
246                        stats.futures_completed += 1;
247                        return Err(e);
248                    }
249                    Poll::Ready(Ok(())) => {
250                        stats.futures_completed += 1;
251                    }
252                    Poll::Pending => {}
253                }
254            }
255        }
256        Ok(())
257    }
258
259    /// Return cumulative future activity from the most recent executor run.
260    #[must_use]
261    pub fn stats(&self) -> ExecutorStats {
262        self.state.stats.get()
263    }
264
265    #[must_use]
266    pub fn get_clock(&self, freq_mhz: f64) -> Clock {
267        self.state.time.borrow_mut().get_clock(freq_mhz)
268    }
269
270    #[must_use]
271    pub fn time_now_ns(&self) -> f64 {
272        self.state.time.borrow().time_now_ns()
273    }
274
275    pub fn set_randomize_task_order(&self, randomize: bool) {
276        self.state.randomize_task_order.set(randomize);
277    }
278
279    pub fn set_task_order_seed(&self, seed: u64) {
280        *self.state.task_order_rng.borrow_mut() = StdRng::seed_from_u64(seed);
281    }
282}
283
284/// `Spawner` spawns new futures into the executor.
285#[derive(Clone)]
286pub struct Spawner {
287    state: Rc<ExecutorState>,
288}
289
290impl Spawner {
291    pub fn spawn(&self, future: impl Future<Output = SimResult> + 'static) {
292        self.state
293            .new_tasks
294            .borrow_mut()
295            .push(Rc::new(Task::new(future, self.state.clone())));
296    }
297}
298
299#[must_use]
300pub fn new_executor_and_spawner(top: &Rc<Entity>) -> (Executor, Spawner) {
301    let state = Rc::new(ExecutorState::new(top));
302    (
303        Executor {
304            state: state.clone(),
305        },
306        Spawner { state },
307    )
308}
309
310#[cfg(test)]
311mod tests {
312    use std::future::Future;
313    use std::pin::Pin;
314    use std::rc::Rc;
315    use std::sync::atomic::{AtomicBool, Ordering};
316    use std::task::{Context, Poll};
317    use std::time::Duration;
318
319    use futures::task::noop_waker;
320    use gwr_track::entity::toplevel;
321    use gwr_track::tracker::dev_null_tracker;
322
323    use super::*;
324    use crate::time::clock::TaskWaker;
325
326    struct PendingOnce {
327        was_polled: bool,
328    }
329
330    impl Future for PendingOnce {
331        type Output = SimResult;
332
333        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
334            if self.was_polled {
335                Poll::Ready(Ok(()))
336            } else {
337                self.was_polled = true;
338                #[expect(clippy::waker_clone_wake)]
339                cx.waker().clone().wake();
340                Poll::Pending
341            }
342        }
343    }
344
345    #[test]
346    fn run_exits_when_time_cannot_advance() {
347        let tracker = dev_null_tracker();
348        let top = toplevel(&tracker, "top");
349        let (executor, _spawner) = new_executor_and_spawner(&top);
350        let clock = executor.get_clock(1000.0);
351
352        clock
353            .shared_state
354            .waiting
355            .borrow_mut()
356            .push(vec![TaskWaker {
357                id: 0,
358                waker: noop_waker(),
359                can_exit: false,
360            }]);
361
362        executor.run().unwrap();
363    }
364
365    #[test]
366    fn executor_observer_reports_once_when_refresh_is_due() {
367        let tracker = dev_null_tracker();
368        let top = toplevel(&tracker, "top");
369        let (executor, spawner) = new_executor_and_spawner(&top);
370
371        for _ in 0..5 {
372            spawner.spawn(async { Ok(()) });
373        }
374
375        let mut snapshots = Vec::new();
376        let refresh_due = AtomicBool::new(true);
377        executor
378            .run_internal(Some(&refresh_due), |snapshot| {
379                snapshots.push(snapshot);
380                Ok(())
381            })
382            .unwrap();
383
384        assert!(!refresh_due.load(Ordering::Relaxed));
385
386        assert_eq!(
387            snapshots,
388            vec![ExecutorSnapshot {
389                stats: ExecutorStats {
390                    futures_polled: 5,
391                    futures_completed: 5,
392                },
393                active_task_count: 0,
394            }]
395        );
396        assert_eq!(
397            executor.stats(),
398            ExecutorStats {
399                futures_polled: 5,
400                futures_completed: 5,
401            }
402        );
403    }
404
405    #[test]
406    fn executor_observer_skips_steps_without_a_refresh_signal() {
407        let tracker = dev_null_tracker();
408        let top = toplevel(&tracker, "top");
409        let (executor, spawner) = new_executor_and_spawner(&top);
410        spawner.spawn(async { Ok(()) });
411
412        let refresh_due = AtomicBool::new(false);
413        let mut snapshots = Vec::new();
414        executor
415            .run_internal(Some(&refresh_due), |snapshot| {
416                snapshots.push(snapshot);
417                Ok(())
418            })
419            .unwrap();
420
421        assert!(snapshots.is_empty());
422    }
423
424    #[test]
425    fn executor_stats_count_pending_future_polls_separately_from_completion() {
426        let tracker = dev_null_tracker();
427        let top = toplevel(&tracker, "top");
428        let (executor, spawner) = new_executor_and_spawner(&top);
429        spawner.spawn(PendingOnce { was_polled: false });
430
431        executor.run().unwrap();
432
433        assert_eq!(
434            executor.stats(),
435            ExecutorStats {
436                futures_polled: 2,
437                futures_completed: 1,
438            }
439        );
440    }
441
442    #[test]
443    fn executor_stats_include_futures_that_complete_with_an_error() {
444        let tracker = dev_null_tracker();
445        let top = toplevel(&tracker, "top");
446        let (executor, spawner) = new_executor_and_spawner(&top);
447        spawner.spawn(async { Err(crate::types::SimError("failed".to_string())) });
448
449        let result = executor.run();
450
451        assert!(result.is_err());
452        assert_eq!(
453            executor.stats(),
454            ExecutorStats {
455                futures_polled: 1,
456                futures_completed: 1,
457            }
458        );
459    }
460
461    #[test]
462    fn executor_observer_is_not_called_from_a_step_that_returns_a_task_error() {
463        let tracker = dev_null_tracker();
464        let top = toplevel(&tracker, "top");
465        let (executor, spawner) = new_executor_and_spawner(&top);
466        spawner.spawn(async { Err(crate::types::SimError("task failed".to_string())) });
467
468        let mut snapshots = Vec::new();
469        let refresh_due = AtomicBool::new(true);
470        let error = executor
471            .run_internal(Some(&refresh_due), |snapshot| {
472                snapshots.push(snapshot);
473                Err(crate::types::SimError("observer failed".to_string()))
474            })
475            .unwrap_err();
476
477        assert_eq!(error.to_string(), "task failed");
478        assert!(snapshots.is_empty());
479        assert_eq!(
480            executor.stats(),
481            ExecutorStats {
482                futures_polled: 1,
483                futures_completed: 1,
484            }
485        );
486    }
487
488    #[test]
489    fn observer_error_occurs_after_the_entire_step_batch() {
490        let tracker = dev_null_tracker();
491        let top = toplevel(&tracker, "top");
492        let (executor, spawner) = new_executor_and_spawner(&top);
493        let completions = Rc::new(Cell::new(0));
494        for _ in 0..3 {
495            let completions = completions.clone();
496            spawner.spawn(async move {
497                completions.set(completions.get() + 1);
498                Ok(())
499            });
500        }
501
502        let refresh_due = AtomicBool::new(true);
503        let first_error = executor
504            .run_internal(Some(&refresh_due), |_| {
505                Err(crate::types::SimError("observer failed".to_string()))
506            })
507            .unwrap_err();
508        assert_eq!(first_error.to_string(), "observer failed");
509        assert_eq!(completions.get(), 3);
510        assert_eq!(
511            executor.stats(),
512            ExecutorStats {
513                futures_polled: 3,
514                futures_completed: 3,
515            }
516        );
517    }
518
519    #[test]
520    fn executor_observer_rejects_a_zero_wall_clock_interval() {
521        let tracker = dev_null_tracker();
522        let top = toplevel(&tracker, "top");
523        let (executor, _spawner) = new_executor_and_spawner(&top);
524        let error = executor
525            .run_with_observer(Duration::ZERO, |_| Ok(()))
526            .unwrap_err();
527
528        assert_eq!(
529            error.to_string(),
530            "executor observer interval must be positive"
531        );
532    }
533}