Skip to main content

hrx/execution/
completion.rs

1use crate::{Error, Result};
2use std::{
3    future::Future,
4    pin::Pin,
5    sync::{
6        Arc, Condvar, Mutex,
7        atomic::{AtomicBool, AtomicUsize, Ordering},
8    },
9    task::{Context, Poll, Waker},
10    time::{Duration, Instant},
11};
12
13pub(super) struct State {
14    submitted: Option<Instant>,
15    started: Option<Instant>,
16    finished: Option<Instant>,
17    pub done: bool,
18    pub failure: Option<Arc<Error>>,
19    pub cancelled: bool,
20    wakers: [Option<Waker>; 4],
21    overflow_wakers: Vec<Waker>,
22}
23pub(super) struct Signal {
24    pub state: Mutex<State>,
25    changed: Condvar,
26    pub cancel: AtomicBool,
27    // Public observers prevent replay; internal finalizer references do not.
28    pub observers: AtomicUsize,
29}
30impl Signal {
31    pub fn new() -> Arc<Self> {
32        Arc::new(Self {
33            state: Mutex::new(State {
34                submitted: None,
35                started: None,
36                finished: None,
37                done: true,
38                failure: None,
39                cancelled: false,
40                wakers: std::array::from_fn(|_| None),
41                overflow_wakers: Vec::new(),
42            }),
43            changed: Condvar::new(),
44            cancel: AtomicBool::new(false),
45            observers: AtomicUsize::new(0),
46        })
47    }
48    pub fn reset(&self) {
49        let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
50        state.done = false;
51        state.submitted = Some(Instant::now());
52        state.started = None;
53        state.finished = None;
54        state.failure = None;
55        state.cancelled = false;
56        state.wakers.fill(None);
57        state.overflow_wakers.clear();
58        self.cancel.store(false, Ordering::Release);
59    }
60    #[cfg(test)]
61    pub fn finish(&self, failure: Option<Arc<Error>>) {
62        self.publish(failure)();
63    }
64    pub fn publish(&self, failure: Option<Arc<Error>>) -> impl FnOnce() + '_ {
65        let (wakers, overflow) = {
66            let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
67            state.failure = failure;
68            state.cancelled = self.cancel.load(Ordering::Acquire);
69            state.done = true;
70            state.finished = Some(Instant::now());
71            (
72                std::mem::replace(&mut state.wakers, std::array::from_fn(|_| None)),
73                std::mem::take(&mut state.overflow_wakers),
74            )
75        };
76        move || {
77            self.changed.notify_all();
78            for waker in wakers.into_iter().flatten().chain(overflow) {
79                // A task-supplied waker must not take down an execution worker.
80                let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| waker.wake()));
81            }
82        }
83    }
84}
85impl Signal {
86    pub fn started(&self, now: Instant) {
87        self.state
88            .lock()
89            .unwrap_or_else(|e| e.into_inner())
90            .started
91            .get_or_insert(now);
92    }
93}
94
95/// Host-observed submission latency, including dependency and driver overhead.
96/// This is not a measurement of GPU kernel execution time.
97#[derive(Clone, Copy, Debug, serde::Serialize)]
98pub struct CompletionProfile {
99    /// Time from accepted submission to its terminal result, in microseconds.
100    pub elapsed_us: f64,
101    /// Time until its first native region starts, or None if none ran.
102    pub queue_us: Option<f64>,
103}
104
105fn outcome(state: &State) -> Result<()> {
106    if let Some(message) = &state.failure {
107        return Err(Error::Execution {
108            source: message.clone(),
109        });
110    }
111    if state.cancelled {
112        return Err(Error::Cancelled);
113    }
114    Ok(())
115}
116/// An owned completion observer. Dropping it does not cancel submitted work.
117///
118/// Polling never waits for a device. Clones may be waited on by different tasks.
119pub struct Completion {
120    pub(super) signal: Arc<Signal>,
121    pub(super) core: std::sync::Weak<super::scheduler::Core>,
122}
123impl Clone for Completion {
124    fn clone(&self) -> Self {
125        Self::new(self.signal.clone(), self.core.clone())
126    }
127}
128impl Drop for Completion {
129    fn drop(&mut self) {
130        let previous = self.signal.observers.fetch_sub(1, Ordering::AcqRel);
131        debug_assert!(previous > 0);
132    }
133}
134impl Completion {
135    pub(super) fn new(signal: Arc<Signal>, core: std::sync::Weak<super::scheduler::Core>) -> Self {
136        signal.observers.fetch_add(1, Ordering::AcqRel);
137        Self { signal, core }
138    }
139    /// Host monotonic time at which execution reached a terminal state. This
140    /// permits pipeline boundary timing without including a caller's late wait.
141    /// It is not a device timestamp and does not imply successful execution.
142    pub fn finished_at(&self) -> Option<Instant> {
143        self.signal
144            .state
145            .lock()
146            .unwrap_or_else(|e| e.into_inner())
147            .finished
148    }
149    /// Inspect final host-observed timing without waiting for completion.
150    pub fn profile(&self) -> Option<CompletionProfile> {
151        let state = self.signal.state.lock().unwrap_or_else(|e| e.into_inner());
152        let submitted = state.submitted?;
153        Some(CompletionProfile {
154            elapsed_us: state.finished?.duration_since(submitted).as_secs_f64() * 1e6,
155            queue_us: state
156                .started
157                .map(|start| start.duration_since(submitted).as_secs_f64() * 1e6),
158        })
159    }
160    pub(crate) fn validate_runtime(&self, runtime: &super::Runtime) -> Result<()> {
161        if let Some(owner) = self.core.upgrade() {
162            if !Arc::ptr_eq(&runtime.inner.core, &owner) {
163                return Err(Error::Message(
164                    "completion belongs to another runtime".into(),
165                ));
166            }
167        } else if !self.is_complete() {
168            return Err(Error::Message("orphaned incomplete producer".into()));
169        }
170        Ok(())
171    }
172    /// Observe a terminal result without blocking, or `None` while pending.
173    pub fn result(&self) -> Option<Result<()>> {
174        let state = self.signal.state.lock().unwrap_or_else(|e| e.into_inner());
175        state.done.then(|| outcome(&state))
176    }
177
178    pub(crate) fn ready() -> Self {
179        Self::new(Signal::new(), std::sync::Weak::new())
180    }
181
182    /// Whether the operation reached a terminal state, including failure.
183    pub fn is_complete(&self) -> bool {
184        self.signal
185            .state
186            .lock()
187            .unwrap_or_else(|e| e.into_inner())
188            .done
189    }
190    /// Wait for device completion, propagating execution failure.
191    /// The caller may execute ready regions from this submission to avoid a
192    /// worker handoff. Engine limits and all dependency checks still apply.
193    pub fn wait(&self) -> Result<()> {
194        if let Some(core) = self.core.upgrade() {
195            super::scheduler::help(&core, &self.signal);
196        }
197        let mut state = self.signal.state.lock().unwrap_or_else(|e| e.into_inner());
198        while !state.done {
199            state = self
200                .signal
201                .changed
202                .wait(state)
203                .unwrap_or_else(|e| e.into_inner());
204        }
205        outcome(&state)
206    }
207    /// Wait at most `timeout`. `false` leaves execution and ownership intact.
208    pub fn wait_timeout(&self, timeout: Duration) -> Result<bool> {
209        let start = Instant::now();
210        let mut state = self.signal.state.lock().unwrap_or_else(|e| e.into_inner());
211        while !state.done {
212            let Some(left) = timeout.checked_sub(start.elapsed()) else {
213                return Ok(false);
214            };
215            let (next, _) = self
216                .signal
217                .changed
218                .wait_timeout(state, left)
219                .unwrap_or_else(|e| e.into_inner());
220            state = next;
221        }
222        outcome(&state)?;
223        Ok(true)
224    }
225    /// Cancel unscheduled nodes. Running native work is drained before completion.
226    /// This request does not revoke resources or abort an unrelated submission.
227    pub fn cancel(&self) {
228        self.signal.cancel.store(true, Ordering::Release);
229        if let Some(core) = self.core.upgrade() {
230            core.changed.notify_all();
231        }
232    }
233}
234impl Future for Completion {
235    type Output = Result<()>;
236    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
237        let mut state = self.signal.state.lock().unwrap_or_else(|e| e.into_inner());
238        if state.done {
239            return Poll::Ready(outcome(&state));
240        }
241        if !state
242            .wakers
243            .iter()
244            .flatten()
245            .chain(&state.overflow_wakers)
246            .any(|w| w.will_wake(context.waker()))
247        {
248            if let Some(slot) = state.wakers.iter_mut().find(|slot| slot.is_none()) {
249                *slot = Some(context.waker().clone());
250            } else {
251                state.overflow_wakers.push(context.waker().clone());
252            }
253        }
254        Poll::Pending
255    }
256}
257#[cfg(test)]
258mod tests {
259    use super::*;
260    #[test]
261    fn timeout_does_not_cancel_and_detach_retains_state() {
262        let signal = Signal::new();
263        signal.reset();
264        let completion = Completion::new(signal.clone(), std::sync::Weak::new());
265        assert!(!completion.wait_timeout(Duration::ZERO).unwrap());
266        assert!(!signal.cancel.load(Ordering::Acquire));
267        drop(completion);
268        signal.finish(None);
269        Completion::new(signal, std::sync::Weak::new())
270            .wait()
271            .unwrap();
272    }
273    #[test]
274    fn future_wakeup_and_error() {
275        use std::task::Wake;
276        struct Counter(std::sync::atomic::AtomicUsize);
277        impl Wake for Counter {
278            fn wake(self: Arc<Self>) {
279                self.0.fetch_add(1, Ordering::Relaxed);
280            }
281        }
282        let counter = Arc::new(Counter(std::sync::atomic::AtomicUsize::new(0)));
283        let waker = Waker::from(counter.clone());
284        let mut context = Context::from_waker(&waker);
285        let signal = Signal::new();
286        signal.reset();
287        let mut completion = Completion::new(signal.clone(), std::sync::Weak::new());
288        assert!(Pin::new(&mut completion).poll(&mut context).is_pending());
289        signal.finish(Some(Arc::new(Error::DeviceLost(
290            "injected native failure".into(),
291        ))));
292        assert_eq!(counter.0.load(Ordering::Relaxed), 1);
293        assert!(matches!(
294            Pin::new(&mut completion).poll(&mut context),
295            Poll::Ready(Err(Error::Execution { .. }))
296        ));
297    }
298}