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}