Skip to main content

perspective_viewer/utils/
debounce.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use std::cell::Cell;
14use std::future::Future;
15use std::rc::Rc;
16
17use async_lock::Mutex;
18use perspective_js::utils::ApiResult;
19
20use super::pubsub::PubSub;
21
22/// Proof that the bearer is executing inside a [`DebounceMutex`]-locked task.
23///
24/// Every plugin-dispatching function (`draw_view`, `activate_plugin`, the
25/// restyle/resize/export wrappers) and the snapshot pipeline
26/// (`bind_snapshot`) require `&RenderGuard`, so an unlocked plugin call or
27/// out-of-pipeline render is a missing witness — a compile error, not a
28/// review rule. Constructed ONLY by [`DebounceMutex::lock_with`] /
29/// [`DebounceMutex::debounce_with`], after the lock is acquired; it is not
30/// `Clone`, so it cannot be stashed for use outside the task that received
31/// it.
32pub struct RenderGuard {
33    _private: (),
34}
35
36#[derive(Default)]
37struct DebounceMutexData {
38    id: Cell<u64>,
39    held: Cell<bool>,
40    mutex: Mutex<u64>,
41    parked: Cell<bool>,
42    settled_id: Cell<u64>,
43    on_settle: PubSub<()>,
44}
45
46/// Clears the `held` flag on drop, so cancellation of a locked task (its
47/// future dropped mid-await) can't leave [`DebounceMutex::is_held`] stuck
48/// `true`.
49struct HeldFlag<'a>(&'a Cell<bool>);
50
51impl<'a> HeldFlag<'a> {
52    fn set(cell: &'a Cell<bool>) -> Self {
53        cell.set(true);
54        Self(cell)
55    }
56}
57
58impl Drop for HeldFlag<'_> {
59    fn drop(&mut self) {
60        self.0.set(false);
61    }
62}
63
64/// RAII for the parked debounce runner: clears `parked` and broadcasts the
65/// runner's settle on drop. Running through `Drop` (not a happy-path call)
66/// makes the release unconditional — a runner cancelled mid-park or
67/// mid-run still frees the parked slot and releases its coalesced waiters,
68/// so they can never be stranded awaiting a settle that no task will emit.
69struct SettleGuard<'a> {
70    data: &'a DebounceMutexData,
71    next: u64,
72}
73
74impl<'a> SettleGuard<'a> {
75    fn park(data: &'a DebounceMutexData, next: u64) -> Self {
76        data.parked.set(true);
77        Self { data, next }
78    }
79}
80
81impl Drop for SettleGuard<'_> {
82    fn drop(&mut self) {
83        self.data.parked.set(false);
84        if self.data.settled_id.get() < self.next {
85            self.data.settled_id.set(self.next);
86        }
87
88        self.data.on_settle.emit(());
89    }
90}
91
92/// An async `Mutex` type specialized for Perspective's rendering, which
93/// debounces calls in addition to providing exclusivity. Calling
94/// [`Self::debounce`] resolves only after at least one complete evaluation
95/// of a call that began no earlier than this one — either by running the
96/// caller's own future, or by coalescing onto the single parked trailing
97/// runner. At most TWO debounce evaluations are ever outstanding (one
98/// running, one parked); every additional concurrent caller resolves
99/// without queueing on the lock.
100#[derive(Clone, Default)]
101pub struct DebounceMutex(Rc<DebounceMutexData>);
102
103impl DebounceMutex {
104    /// `true` while a locked task is executing. Used by lock-acquiring
105    /// public API methods to emit a debug-build warning when they are about
106    /// to queue behind an in-flight run — legitimate for app callers, a
107    /// guaranteed deadlock when reached synchronously from a plugin's render
108    /// (the render-callable contract on `js::plugin` forbids it).
109    pub fn is_held(&self) -> bool {
110        self.0.held.get()
111    }
112
113    /// Lock like a normal `Mutex`.
114    pub async fn lock<T>(&self, f: impl Future<Output = T>) -> T {
115        self.lock_with(|_| f).await
116    }
117
118    /// Lock, passing a [`RenderGuard`] witness into the task builder. The
119    /// task future is CONSTRUCTED after the lock is acquired — a guard can
120    /// never exist outside a locked section.
121    pub async fn lock_with<T, F, Fut>(&self, f: F) -> T
122    where
123        F: FnOnce(RenderGuard) -> Fut,
124        Fut: Future<Output = T>,
125    {
126        let mut last = self.0.mutex.lock().await;
127        let next = self.0.id.get();
128        let held = HeldFlag::set(&self.0.held);
129        let result = f(RenderGuard { _private: () }).await;
130        drop(held);
131        *last = next;
132        result
133    }
134
135    /// Lock and also debounce `f`, which should be cancellable.
136    pub async fn debounce(&self, f: impl Future<Output = ApiResult<()>>) -> ApiResult<()> {
137        self.debounce_with(|_| f).await
138    }
139
140    /// [`Self::debounce`] with a [`RenderGuard`] witness (see
141    pub async fn debounce_with<T, F, Fut>(&self, f: F) -> ApiResult<T>
142    where
143        T: Default,
144        F: FnOnce(RenderGuard) -> Fut,
145        Fut: Future<Output = ApiResult<T>>,
146    {
147        let next = self.0.id.get() + 1;
148        if self.0.parked.get() {
149            self.await_settled(next).await;
150            return Ok(T::default());
151        }
152
153        let settle = SettleGuard::park(&self.0, next);
154        let mut last = self.0.mutex.lock().await;
155        self.0.parked.set(false);
156        let result = if *last < next {
157            let next = self.0.id.get() + 1;
158            self.0.id.set(next);
159            let held = HeldFlag::set(&self.0.held);
160            let result = f(RenderGuard { _private: () }).await;
161            drop(held);
162            if result.is_ok() {
163                *last = next;
164            }
165
166            result
167        } else {
168            Ok(T::default())
169        };
170
171        drop(last);
172        drop(settle);
173        result
174    }
175
176    async fn await_settled(&self, next: u64) {
177        while self.0.settled_id.get() < next {
178            if self.0.on_settle.read_next().await.is_err() {
179                break;
180            }
181        }
182    }
183}