Skip to main content

gpui/
asset_cache.rs

1use crate::{App, AppContext, EntityId, SharedString, SharedUri, Task};
2use collections::FxHashSet;
3use futures::{Future, TryFutureExt};
4
5use std::cell::RefCell;
6use std::fmt::Debug;
7use std::hash::{BuildHasher, Hash};
8use std::marker::PhantomData;
9use std::mem;
10use std::path::{Path, PathBuf};
11use std::rc::Rc;
12use std::sync::Arc;
13
14pub(crate) struct CachedLoad<T> {
15    state: Rc<RefCell<CachedLoadState<T>>>,
16    // Keep the owner outside the state so publishing a result cannot cancel its own task.
17    _task: Task<()>,
18}
19
20enum CachedLoadState<T> {
21    Loading(FxHashSet<EntityId>),
22    Loaded(T),
23}
24
25impl<T: Clone + Send + 'static> CachedLoad<T> {
26    pub(crate) fn new(future: impl Future<Output = T> + Send + 'static, cx: &App) -> Self {
27        let state = Rc::new(RefCell::new(CachedLoadState::Loading(FxHashSet::default())));
28        let task = cx.background_executor().spawn(future);
29        let task = cx.spawn({
30            let state = Rc::downgrade(&state);
31            async move |cx| {
32                let result = task.await;
33                let Some(state) = state.upgrade() else {
34                    return;
35                };
36                let previous =
37                    mem::replace(&mut *state.borrow_mut(), CachedLoadState::Loaded(result));
38                let CachedLoadState::Loading(views) = previous else {
39                    unreachable!("a cached load completes only once");
40                };
41                cx.update(|cx| {
42                    for view in views {
43                        cx.notify(view);
44                    }
45                });
46            }
47        });
48        Self { state, _task: task }
49    }
50
51    pub(crate) fn get(&self) -> Option<T> {
52        match &*self.state.borrow() {
53            CachedLoadState::Loading(_) => None,
54            CachedLoadState::Loaded(result) => Some(result.clone()),
55        }
56    }
57
58    pub(crate) fn use_by(&self, view: EntityId) -> Option<T> {
59        match &mut *self.state.borrow_mut() {
60            CachedLoadState::Loading(views) => {
61                views.insert(view);
62                None
63            }
64            CachedLoadState::Loaded(result) => Some(result.clone()),
65        }
66    }
67}
68
69/// Where an image or other asset is loaded from: a URL, a file path, or a name in the registry.
70///
71/// Equal resources share one cache entry.
72#[derive(Debug, PartialEq, Eq, Hash, Clone)]
73pub enum Resource {
74    /// A URL, fetched with the app's HTTP client.
75    Uri(SharedUri),
76    /// A file on disk, read with [`std::fs::read`]. A relative path is relative to the working
77    /// directory.
78    Path(Arc<Path>),
79    /// A name in the app's [`crate::AssetRegistry`]. Despite the name, this is any registry entry, not
80    /// only one from the `embedded-assets` feature.
81    Embedded(SharedString),
82}
83
84impl From<SharedUri> for Resource {
85    fn from(value: SharedUri) -> Self {
86        Self::Uri(value)
87    }
88}
89
90impl From<PathBuf> for Resource {
91    fn from(value: PathBuf) -> Self {
92        Self::Path(value.into())
93    }
94}
95
96impl From<Arc<Path>> for Resource {
97    fn from(value: Arc<Path>) -> Self {
98        Self::Path(value)
99    }
100}
101
102/// A value that GPUI loads in the background and shares between callers, such as an image.
103///
104/// Implement it on a type that holds no data, such as an empty enum. Then call
105/// [`crate::Window::use_asset`] or [`crate::Window::get_asset`] to load it. The first call for a
106/// source starts the load and returns `None`. Later calls share that load and return its output
107/// once it is ready. [`App::remove_asset`] discards the app-level entry so the next fetch starts a
108/// new load.
109pub trait Asset: 'static {
110    /// The key that identifies the asset. Equal keys share one cache entry.
111    type Source: Clone + Hash + Send;
112
113    /// The result of loading. Callers receive clones of the cached output, so keep it cheap to
114    /// clone.
115    type Output: Clone + Send;
116
117    /// Starts loading `source`, and returns the future that produces the output.
118    ///
119    /// The cache calls this on the foreground thread, where `cx` is available. Read any app state
120    /// needed to construct the request here; the returned future runs on the background executor.
121    fn load(
122        source: Self::Source,
123        cx: &mut App,
124    ) -> impl Future<Output = Self::Output> + Send + 'static;
125}
126
127/// Wraps an [`Asset`] whose output is a [`Result`], and logs each error as it loads.
128///
129/// The wrapped loader's result is returned unchanged. [`img`](crate::img) loads images through
130/// `AssetLogger`, so a failed load appears in the log.
131pub enum AssetLogger<T> {
132    #[doc(hidden)]
133    _Phantom(PhantomData<T>, &'static dyn crate::seal::Sealed),
134}
135
136impl<T, R, E> Asset for AssetLogger<T>
137where
138    T: Asset<Output = Result<R, E>>,
139    R: Clone + Send,
140    E: Clone + Send + Debug,
141{
142    type Source = T::Source;
143
144    type Output = T::Output;
145
146    fn load(
147        source: Self::Source,
148        cx: &mut App,
149    ) -> impl Future<Output = Self::Output> + Send + 'static {
150        let load = T::load(source, cx);
151        load.inspect_err(|e| log::error!("Failed to load asset: {:?}", e))
152    }
153}
154
155/// Hashes `data` with a fast, non-cryptographic hash, for use as a cache key within one process.
156///
157/// The value can change between builds, so don't store it or compare it across runs.
158pub fn hash<T: Hash>(data: &T) -> u64 {
159    collections::FxBuildHasher.hash_one(data)
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::{AppContext, TestAppContext};
166    use futures::channel::oneshot;
167    use std::{
168        cell::Cell,
169        collections::VecDeque,
170        hash::{Hash, Hasher},
171        rc::Rc,
172        sync::{
173            Arc, Mutex,
174            atomic::{AtomicUsize, Ordering},
175        },
176    };
177
178    #[gpui::test]
179    fn cached_load_caches_success_and_error(cx: &mut TestAppContext) {
180        let successful_load =
181            cx.update(|cx| CachedLoad::new(async { Ok::<_, &'static str>(42) }, cx));
182        let failed_load =
183            cx.update(|cx| CachedLoad::new(async { Err::<i32, _>("load failed") }, cx));
184
185        assert_eq!(successful_load.get(), None);
186        assert_eq!(failed_load.get(), None);
187
188        cx.run_until_parked();
189
190        assert_eq!(successful_load.get(), Some(Ok(42)));
191        assert_eq!(successful_load.get(), Some(Ok(42)));
192        assert_eq!(failed_load.get(), Some(Err("load failed")));
193        assert_eq!(failed_load.get(), Some(Err("load failed")));
194    }
195
196    #[gpui::test]
197    fn completed_load_without_subscribers_is_cached_and_deduplicated(cx: &mut TestAppContext) {
198        let (sender, receiver) = oneshot::channel();
199        let source = TestAssetSource::new(1, [receiver]);
200
201        assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
202        assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
203        assert_eq!(source.load_count(), 1);
204
205        assert!(sender.send(Ok(42)).is_ok());
206        cx.run_until_parked();
207
208        assert_eq!(
209            cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)),
210            Some(Ok(42))
211        );
212        assert_eq!(
213            cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)),
214            Some(Ok(42))
215        );
216        assert_eq!(source.load_count(), 1);
217    }
218
219    #[gpui::test]
220    fn load_completion_notifies_each_observing_entity_once(cx: &mut TestAppContext) {
221        let (sender, receiver) = oneshot::channel::<i32>();
222        let load = cx.update(|cx| {
223            CachedLoad::new(
224                async move { receiver.await.expect("test sends a result") },
225                cx,
226            )
227        });
228        let first_notification_count = Rc::new(Cell::new(0));
229        let second_notification_count = Rc::new(Cell::new(0));
230        let (first_entity, second_entity) = cx.update(|cx| {
231            let first_entity = cx.new(|_| ());
232            let second_entity = cx.new(|_| ());
233            cx.observe(&first_entity, {
234                let first_notification_count = first_notification_count.clone();
235                move |_, _| first_notification_count.set(first_notification_count.get() + 1)
236            })
237            .detach();
238            cx.observe(&second_entity, {
239                let second_notification_count = second_notification_count.clone();
240                move |_, _| second_notification_count.set(second_notification_count.get() + 1)
241            })
242            .detach();
243            (first_entity, second_entity)
244        });
245
246        assert_eq!(load.use_by(first_entity.entity_id()), None);
247        assert_eq!(load.use_by(first_entity.entity_id()), None);
248        assert_eq!(load.use_by(second_entity.entity_id()), None);
249
250        assert!(sender.send(42).is_ok());
251        cx.run_until_parked();
252
253        assert_eq!(first_notification_count.get(), 1);
254        assert_eq!(second_notification_count.get(), 1);
255        assert_eq!(load.use_by(first_entity.entity_id()), Some(42));
256        assert_eq!(load.use_by(second_entity.entity_id()), Some(42));
257    }
258
259    #[gpui::test]
260    fn removing_pending_asset_cancels_it_and_replacement_ignores_stale_completion(
261        cx: &mut TestAppContext,
262    ) {
263        let (stale_sender, stale_receiver) = oneshot::channel();
264        let (replacement_sender, replacement_receiver) = oneshot::channel();
265        let source = TestAssetSource::new(2, [stale_receiver, replacement_receiver]);
266
267        assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
268        assert_eq!(source.load_count(), 1);
269        cx.run_until_parked();
270
271        cx.update(|cx| cx.remove_asset::<TestAsset>(&source));
272        assert!(!cx.update(|cx| cx.has_asset::<TestAsset>(&source)));
273        cx.run_until_parked();
274        assert!(stale_sender.send(Ok(1)).is_err());
275
276        assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
277        assert_eq!(source.load_count(), 2);
278
279        assert!(replacement_sender.send(Err("replacement failed")).is_ok());
280        cx.run_until_parked();
281
282        assert_eq!(
283            cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)),
284            Some(Err("replacement failed"))
285        );
286        assert_eq!(source.load_count(), 2);
287    }
288
289    #[gpui::test]
290    fn eviction_after_worker_completion_does_not_publish_into_replacement(cx: &mut TestAppContext) {
291        let (sender, receiver) = oneshot::channel();
292        let (replacement_sender, replacement_receiver) = oneshot::channel();
293        let source = TestAssetSource::new(3, [receiver, replacement_receiver]);
294
295        assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
296        assert!(sender.send(Ok(1)).is_ok());
297        while source.completion_count.load(Ordering::SeqCst) == 0 {
298            assert!(cx.background_executor.tick());
299        }
300        assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
301
302        cx.update(|cx| {
303            cx.remove_asset::<TestAsset>(&source);
304            assert_eq!(cx.fetch_asset::<TestAsset>(&source), None);
305        });
306        cx.run_until_parked();
307        assert_eq!(cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)), None);
308
309        assert!(replacement_sender.send(Ok(2)).is_ok());
310        cx.run_until_parked();
311        assert_eq!(
312            cx.update(|cx| cx.fetch_asset::<TestAsset>(&source)),
313            Some(Ok(2))
314        );
315        assert_eq!(source.load_count(), 2);
316    }
317
318    struct TestAsset;
319
320    #[derive(Clone)]
321    struct TestAssetSource {
322        id: usize,
323        load_count: Arc<AtomicUsize>,
324        completion_count: Arc<AtomicUsize>,
325        receivers: Arc<Mutex<VecDeque<oneshot::Receiver<Result<i32, &'static str>>>>>,
326    }
327
328    impl TestAssetSource {
329        fn new(
330            id: usize,
331            receivers: impl IntoIterator<Item = oneshot::Receiver<Result<i32, &'static str>>>,
332        ) -> Self {
333            Self {
334                id,
335                load_count: Arc::new(AtomicUsize::new(0)),
336                completion_count: Arc::new(AtomicUsize::new(0)),
337                receivers: Arc::new(Mutex::new(receivers.into_iter().collect())),
338            }
339        }
340
341        fn load_count(&self) -> usize {
342            self.load_count.load(Ordering::SeqCst)
343        }
344    }
345
346    impl Hash for TestAssetSource {
347        fn hash<H: Hasher>(&self, state: &mut H) {
348            self.id.hash(state);
349        }
350    }
351
352    impl Asset for TestAsset {
353        type Source = TestAssetSource;
354        type Output = Result<i32, &'static str>;
355
356        fn load(
357            source: Self::Source,
358            _cx: &mut App,
359        ) -> impl Future<Output = Self::Output> + Send + 'static {
360            source.load_count.fetch_add(1, Ordering::SeqCst);
361            let receiver = source
362                .receivers
363                .lock()
364                .expect("test receiver mutex should not be poisoned")
365                .pop_front()
366                .expect("each test load should have a receiver");
367            async move {
368                let result = match receiver.await {
369                    Ok(result) => result,
370                    Err(_) => Err("cancelled"),
371                };
372                source.completion_count.fetch_add(1, Ordering::SeqCst);
373                result
374            }
375        }
376    }
377}