ZIm/crates/gpui/examples/image_gallery.rs
Mikayla Maki 4758173c33
Use image cache to stop leaking images (#29452)
This PR fixes several possible memory leaks due to loading images in
markdown files and the image viewer, using the new image cache APIs

TODO: 
- [x] Ensure this didn't break rendering in any of the affected
components.

Release Notes:

- Fixed several image related memory leaks
2025-04-29 19:30:16 +00:00

290 lines
9.6 KiB
Rust

use futures::FutureExt;
use gpui::{
App, AppContext, Application, Asset as _, AssetLogger, Bounds, ClickEvent, Context, ElementId,
Entity, ImageAssetLoader, ImageCache, ImageCacheProvider, KeyBinding, Menu, MenuItem,
RetainAllImageCache, SharedString, TitlebarOptions, Window, WindowBounds, WindowOptions,
actions, div, hash, image_cache, img, prelude::*, px, rgb, size,
};
use reqwest_client::ReqwestClient;
use std::{collections::HashMap, sync::Arc};
const IMAGES_IN_GALLERY: usize = 30;
struct ImageGallery {
image_key: String,
items_count: usize,
total_count: usize,
image_cache: Entity<RetainAllImageCache>,
}
impl ImageGallery {
fn on_next_image(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
self.image_cache
.update(cx, |image_cache, cx| image_cache.clear(window, cx));
let t = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis();
self.image_key = format!("{}", t);
self.total_count += self.items_count;
cx.notify();
}
}
impl Render for ImageGallery {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let image_url: SharedString =
format!("https://picsum.photos/400/200?t={}", self.image_key).into();
div()
.flex()
.flex_col()
.text_color(gpui::white())
.child("Manually managed image cache:")
.child(
div()
.image_cache(self.image_cache.clone())
.id("main")
.font_family(".SystemUIFont")
.text_color(gpui::black())
.bg(rgb(0xE9E9E9))
.overflow_y_scroll()
.p_4()
.size_full()
.flex()
.flex_col()
.items_center()
.gap_2()
.child(
div()
.w_full()
.flex()
.flex_row()
.justify_between()
.child(format!(
"Example to show images and test memory usage (Rendered: {} images).",
self.total_count
))
.child(
div()
.id("btn")
.py_1()
.px_4()
.bg(gpui::black())
.hover(|this| this.opacity(0.8))
.text_color(gpui::white())
.text_center()
.w_40()
.child("Next Photos")
.on_click(cx.listener(Self::on_next_image)),
),
)
.child(
div()
.id("image-gallery")
.flex()
.flex_row()
.flex_wrap()
.gap_x_4()
.gap_y_2()
.justify_around()
.children(
(0..self.items_count)
.map(|ix| img(format!("{}-{}", image_url, ix)).size_20()),
),
),
)
.child(
"Automatically managed image cache:"
)
.child(image_cache(simple_lru_cache("lru-cache", IMAGES_IN_GALLERY)).child(
div()
.id("main")
.font_family(".SystemUIFont")
.bg(rgb(0xE9E9E9))
.text_color(gpui::black())
.overflow_y_scroll()
.p_4()
.size_full()
.flex()
.flex_col()
.items_center()
.gap_2()
.child(
div()
.id("image-gallery")
.flex()
.flex_row()
.flex_wrap()
.gap_x_4()
.gap_y_2()
.justify_around()
.children(
(0..self.items_count)
.map(|ix| img(format!("{}-{}", image_url, ix)).size_20()),
),
)
))
}
}
fn simple_lru_cache(id: impl Into<ElementId>, max_items: usize) -> SimpleLruCacheProvider {
SimpleLruCacheProvider {
id: id.into(),
max_items,
}
}
struct SimpleLruCacheProvider {
id: ElementId,
max_items: usize,
}
impl ImageCacheProvider for SimpleLruCacheProvider {
fn provide(&mut self, window: &mut Window, cx: &mut App) -> gpui::AnyImageCache {
window
.with_global_id(self.id.clone(), |global_id, window| {
window.with_element_state::<Entity<SimpleLruCache>, _>(
global_id,
|lru_cache, _window| {
let mut lru_cache = lru_cache.unwrap_or_else(|| {
cx.new(|cx| SimpleLruCache::new(self.max_items, cx))
});
if lru_cache.read(cx).max_items != self.max_items {
lru_cache = cx.new(|cx| SimpleLruCache::new(self.max_items, cx));
}
(lru_cache.clone(), lru_cache)
},
)
})
.into()
}
}
struct SimpleLruCache {
max_items: usize,
usages: Vec<u64>,
cache: HashMap<u64, gpui::ImageCacheItem>,
}
impl SimpleLruCache {
fn new(max_items: usize, cx: &mut Context<Self>) -> Self {
cx.on_release(|simple_cache, cx| {
for (_, mut item) in std::mem::take(&mut simple_cache.cache) {
if let Some(Ok(image)) = item.get() {
cx.drop_image(image, None);
}
}
})
.detach();
Self {
max_items,
usages: Vec::with_capacity(max_items),
cache: HashMap::with_capacity(max_items),
}
}
}
impl ImageCache for SimpleLruCache {
fn load(
&mut self,
resource: &gpui::Resource,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<gpui::RenderImage>, gpui::ImageCacheError>> {
assert_eq!(self.usages.len(), self.cache.len());
assert!(self.cache.len() <= self.max_items);
let hash = hash(resource);
if let Some(item) = self.cache.get_mut(&hash) {
let current_ix = self
.usages
.iter()
.position(|item| *item == hash)
.expect("cache and usages must stay in sync");
self.usages.remove(current_ix);
self.usages.insert(0, hash);
return item.get();
}
let fut = AssetLogger::<ImageAssetLoader>::load(resource.clone(), cx);
let task = cx.background_executor().spawn(fut).shared();
if self.usages.len() == self.max_items {
let oldest = self.usages.pop().unwrap();
let mut image = self
.cache
.remove(&oldest)
.expect("cache and usages must be in sync");
if let Some(Ok(image)) = image.get() {
cx.drop_image(image, Some(window));
}
}
self.cache
.insert(hash, gpui::ImageCacheItem::Loading(task.clone()));
self.usages.insert(0, hash);
let entity = window.current_view();
window
.spawn(cx, {
async move |cx| {
_ = task.await;
cx.on_next_frame(move |_, cx| {
cx.notify(entity);
});
}
})
.detach();
None
}
}
actions!(image, [Quit]);
fn main() {
env_logger::init();
Application::new().run(move |cx: &mut App| {
let http_client = ReqwestClient::user_agent("gpui example").unwrap();
cx.set_http_client(Arc::new(http_client));
cx.activate(true);
cx.on_action(|_: &Quit, cx| cx.quit());
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
cx.set_menus(vec![Menu {
name: "Image Gallery".into(),
items: vec![MenuItem::action("Quit", Quit)],
}]);
let window_options = WindowOptions {
titlebar: Some(TitlebarOptions {
title: Some(SharedString::from("Image Gallery")),
appears_transparent: false,
..Default::default()
}),
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
None,
size(px(1100.), px(860.)),
cx,
))),
..Default::default()
};
cx.open_window(window_options, |_, cx| {
cx.new(|ctx| ImageGallery {
image_key: "".into(),
items_count: IMAGES_IN_GALLERY,
total_count: 0,
image_cache: RetainAllImageCache::new(ctx),
})
})
.unwrap();
});
}