update image cache
This commit is contained in:
@@ -1,186 +1,136 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::mem::take;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
App, AppContext, Asset, AssetLogger, Entity, Global, ImageAssetLoader, ImageCache,
|
||||
ImageCacheError, ImageCacheItem, ImageSource, RenderImage, Resource, Window, hash,
|
||||
App, AppContext, Asset, AssetLogger, ElementId, Entity, ImageAssetLoader, ImageCache,
|
||||
ImageCacheItem, ImageCacheProvider, ImageSource, Resource, hash,
|
||||
};
|
||||
|
||||
/// Upper bound on the number of images the shared cache retains. Loading a
|
||||
/// new image evicts the least recently used entry once this is reached.
|
||||
const MAX_IMAGES: usize = 128;
|
||||
/// Default number of images each view's cache retains. Loading a new image
|
||||
/// evicts the least recently used entry once this is reached.
|
||||
pub const MAX_IMAGES: usize = 128;
|
||||
|
||||
/// Global handle to the shared image cache, installed by [`init`].
|
||||
struct SharedImageCache(Entity<LruImageCache>);
|
||||
|
||||
impl Global for SharedImageCache {}
|
||||
|
||||
/// Create the shared image cache and install it as a global. Call once at
|
||||
/// startup (see `desktop/src/main.rs`), before any window opens.
|
||||
pub fn init(cx: &mut App) -> Entity<LruImageCache> {
|
||||
let cache = LruImageCache::new(MAX_IMAGES, cx);
|
||||
cx.set_global(SharedImageCache(cache.clone()));
|
||||
cache
|
||||
pub fn image_cache(id: impl Into<ElementId>, max_items: usize) -> AppImageCacheProvider {
|
||||
AppImageCacheProvider {
|
||||
id: id.into(),
|
||||
max_items,
|
||||
}
|
||||
}
|
||||
|
||||
/// The shared image cache. Panics if [`init`] hasn't been called.
|
||||
pub fn global(cx: &App) -> Entity<LruImageCache> {
|
||||
cx.global::<SharedImageCache>().0.clone()
|
||||
}
|
||||
|
||||
/// Drop every cached image, freeing the decoded image data, the GPU textures
|
||||
/// and the raw fetched bytes. Images currently on screen are re-fetched on
|
||||
/// the next frame.
|
||||
pub fn clear(cx: &mut App, window: &mut Window) {
|
||||
let cache = global(cx);
|
||||
cache.update(cx, |cache, cx| cache.clear(window, cx));
|
||||
}
|
||||
|
||||
/// Clear the shared cache when `view` is released — its last strong
|
||||
/// reference is gone, e.g. the user closed the panel it was rendered in.
|
||||
pub fn clear_on_release<T: 'static>(view: &Entity<T>, window: &Window, cx: &mut App) {
|
||||
let cache = global(cx).downgrade();
|
||||
cx.observe_release_in(view, window, move |_view, window, cx| {
|
||||
if let Some(cache) = cache.upgrade() {
|
||||
cache.update(cx, |cache, cx| cache.clear(window, cx));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// A bounded LRU image cache.
|
||||
///
|
||||
/// Entries keep the [`Resource`] they were loaded from so that eviction,
|
||||
/// clearing and release can also remove the asset from the asset system,
|
||||
/// freeing the raw fetched bytes that would otherwise stay in memory.
|
||||
pub struct LruImageCache {
|
||||
pub struct AppImageCacheProvider {
|
||||
id: ElementId,
|
||||
max_items: usize,
|
||||
/// Most recently used hashes first.
|
||||
usage: VecDeque<u64>,
|
||||
}
|
||||
|
||||
impl ImageCacheProvider for AppImageCacheProvider {
|
||||
fn provide(&mut self, window: &mut gpui::Window, cx: &mut App) -> gpui::AnyImageCache {
|
||||
window
|
||||
.with_global_id(self.id.clone(), |id, window| {
|
||||
window.with_element_state(id, |cache, _| {
|
||||
let cache = cache.unwrap_or_else(|| AppImageCache::new(self.max_items, cx));
|
||||
(cache.clone(), cache)
|
||||
})
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppImageCache {
|
||||
max_items: usize,
|
||||
usage_list: VecDeque<u64>,
|
||||
cache: HashMap<u64, (ImageCacheItem, Resource)>,
|
||||
}
|
||||
|
||||
impl LruImageCache {
|
||||
/// Create a cache that holds at most `max_items` images. Cached images
|
||||
/// are dropped from every window when the cache is released.
|
||||
impl AppImageCache {
|
||||
pub fn new(max_items: usize, cx: &mut App) -> Entity<Self> {
|
||||
let max_items = max_items.max(1);
|
||||
cx.new(|cx| {
|
||||
log::info!("Creating AppImageCacheProvider");
|
||||
cx.on_release(|this: &mut Self, cx| {
|
||||
for (_, entry) in take(&mut this.cache) {
|
||||
unload(entry, None, cx);
|
||||
for (ix, (mut image, resource)) in take(&mut this.cache) {
|
||||
if let Some(Ok(image)) = image.get() {
|
||||
log::info!("Dropping image {ix}");
|
||||
cx.drop_image(image, None);
|
||||
}
|
||||
ImageSource::Resource(resource).remove_asset(cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
Self {
|
||||
|
||||
AppImageCache {
|
||||
max_items,
|
||||
usage: VecDeque::with_capacity(max_items),
|
||||
usage_list: VecDeque::with_capacity(max_items),
|
||||
cache: HashMap::with_capacity(max_items),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Drop every cached image and remove every cached asset.
|
||||
pub fn clear(&mut self, window: &mut Window, cx: &mut App) {
|
||||
self.usage.clear();
|
||||
for (_, entry) in take(&mut self.cache) {
|
||||
unload(entry, Some(window), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop a cache entry's decoded data and remove its resource from the asset
|
||||
/// system, so both the decoded image and the raw fetched bytes are freed.
|
||||
///
|
||||
/// The atlas texture is freed **on the next frame, before it paints**, never
|
||||
/// in the middle of one: the release that empties the cache can run at the
|
||||
/// end of a frame's draw — after the scene was built, before it is presented
|
||||
/// — and eviction runs while a frame is painting (`load` is called from
|
||||
/// paint). The next frame also has to repaint every view instead of replaying
|
||||
/// their recorded paint commands: `cached()` views reuse recorded commands
|
||||
/// across frames, and those commands reference the atlas tiles being freed,
|
||||
/// so a replay would hand the renderer a scene full of freed texture ids.
|
||||
/// `window.refresh()` disables that reuse for exactly one frame.
|
||||
///
|
||||
/// `window` restricts the atlas removal to the current window; `None` removes
|
||||
/// it from all windows (the cache entity is being released at shutdown, when
|
||||
/// no scene is in flight).
|
||||
fn unload(
|
||||
(mut item, resource): (ImageCacheItem, Resource),
|
||||
window: Option<&mut Window>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
if let Some(Ok(image)) = item.get() {
|
||||
match window {
|
||||
Some(window) => {
|
||||
window.on_next_frame(move |window, cx| {
|
||||
window.refresh();
|
||||
cx.drop_image(image, Some(window));
|
||||
});
|
||||
}
|
||||
None => cx.drop_image(image, None),
|
||||
}
|
||||
}
|
||||
ImageSource::Resource(resource).remove_asset(cx);
|
||||
}
|
||||
|
||||
impl ImageCache for LruImageCache {
|
||||
impl ImageCache for AppImageCache {
|
||||
fn load(
|
||||
&mut self,
|
||||
resource: &Resource,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
|
||||
debug_assert_eq!(self.usage.len(), self.cache.len());
|
||||
debug_assert!(self.cache.len() <= self.max_items);
|
||||
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::App,
|
||||
) -> Option<Result<std::sync::Arc<gpui::RenderImage>, gpui::ImageCacheError>> {
|
||||
let hash = hash(resource);
|
||||
|
||||
if let Some((item, _)) = self.cache.get_mut(&hash) {
|
||||
let current_ix = self
|
||||
.usage
|
||||
if let Some(item) = self.cache.get_mut(&hash) {
|
||||
let current_idx = self
|
||||
.usage_list
|
||||
.iter()
|
||||
.position(|used| *used == hash)
|
||||
.expect("cache and usage list must stay in sync");
|
||||
self.usage.remove(current_ix);
|
||||
self.usage.push_front(hash);
|
||||
return item.get();
|
||||
.position(|item| *item == hash)
|
||||
.expect("cache has an item usage_list doesn't");
|
||||
|
||||
self.usage_list.remove(current_idx);
|
||||
self.usage_list.push_front(hash);
|
||||
|
||||
return item.0.get();
|
||||
}
|
||||
|
||||
let fut = AssetLogger::<ImageAssetLoader>::load(resource.clone(), cx);
|
||||
let task = cx.background_executor().spawn(fut).shared();
|
||||
let load_future = AssetLogger::<ImageAssetLoader>::load(resource.clone(), cx);
|
||||
let task = cx.background_executor().spawn(load_future).shared();
|
||||
|
||||
if self.usage.len() >= self.max_items {
|
||||
let oldest = self
|
||||
.usage
|
||||
.pop_back()
|
||||
.expect("usage list and cache must stay in sync");
|
||||
let entry = self
|
||||
if self.usage_list.len() >= self.max_items {
|
||||
log::info!("Image cache is full, evicting oldest item");
|
||||
|
||||
if let Some(oldest) = self.usage_list.pop_back() {
|
||||
let mut image = self
|
||||
.cache
|
||||
.remove(&oldest)
|
||||
.expect("usage list and cache must stay in sync");
|
||||
unload(entry, Some(window), cx);
|
||||
.expect("usage_list has an item cache doesn't");
|
||||
|
||||
if let Some(Ok(image)) = image.0.get() {
|
||||
log::info!("requesting image to be dropped");
|
||||
cx.drop_image(image, Some(window));
|
||||
}
|
||||
|
||||
ImageSource::Resource(image.1).remove_asset(cx);
|
||||
}
|
||||
}
|
||||
|
||||
self.cache.insert(
|
||||
hash,
|
||||
(ImageCacheItem::Loading(task.clone()), resource.clone()),
|
||||
(
|
||||
gpui::ImageCacheItem::Loading(task.clone()),
|
||||
resource.clone(),
|
||||
),
|
||||
);
|
||||
self.usage.push_front(hash);
|
||||
self.usage_list.push_front(hash);
|
||||
|
||||
let entity = window.current_view();
|
||||
|
||||
window
|
||||
.spawn(cx, {
|
||||
async move |cx| {
|
||||
if let Err(error) = task.await {
|
||||
log::error!("failed to load image into cache: {:?}", error);
|
||||
.spawn(cx, async move |cx| {
|
||||
let result = task.await;
|
||||
|
||||
if let Err(err) = result {
|
||||
log::error!("error loading image into cache: {:?}", err);
|
||||
}
|
||||
|
||||
cx.on_next_frame(move |_, cx| {
|
||||
cx.notify(entity);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ use signed_state::{ProfileStore, RepoStore};
|
||||
use utils::relative_time;
|
||||
|
||||
use super::helpers::{placeholder, status_badge};
|
||||
use crate::image_cache::{MAX_IMAGES, image_cache};
|
||||
|
||||
/// Detail panel of a single issue.
|
||||
pub struct IssueDetailView {
|
||||
@@ -35,9 +36,6 @@ impl IssueDetailView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
// Issue author avatars stay in the shared cache until the panel closes.
|
||||
crate::image_cache::clear_on_release(&cx.entity(), window, cx);
|
||||
|
||||
let comment_input =
|
||||
cx.new(|cx| TextareaState::new(window, cx).placeholder("Leave a comment…"));
|
||||
|
||||
@@ -299,6 +297,7 @@ impl Render for IssueDetailView {
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.image_cache(image_cache("issue-detail", MAX_IMAGES))
|
||||
.id("issue-detail")
|
||||
.size_full()
|
||||
.child(
|
||||
|
||||
@@ -28,6 +28,7 @@ use utils::relative_time;
|
||||
|
||||
use super::helpers::{placeholder, status_badge};
|
||||
use super::issue_detail::IssueDetailView;
|
||||
use crate::image_cache::{MAX_IMAGES, image_cache};
|
||||
|
||||
/// Height of one issue row in the virtual list: 8px vertical padding
|
||||
/// (`py_2`) on top and bottom, a 32px title line (`h_8`) and a 24px meta
|
||||
@@ -88,13 +89,9 @@ impl IssuesView {
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
store: Entity<RepoStore>,
|
||||
repo_name: SharedString,
|
||||
window: &mut Window,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
// Issue author avatars stay in the shared cache until the panel
|
||||
// closes; free them then.
|
||||
crate::image_cache::clear_on_release(&cx.entity(), window, cx);
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
dock_area,
|
||||
@@ -468,6 +465,7 @@ impl Render for IssuesView {
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.image_cache(image_cache("issues", MAX_IMAGES))
|
||||
.child(self.render_header(cx))
|
||||
.child(
|
||||
v_flex()
|
||||
|
||||
@@ -27,6 +27,8 @@ use signed_core::Announcement;
|
||||
use signed_git::{CommitList, FileCommit};
|
||||
use signed_state::{GitStore, ProfileStore, RepoStore};
|
||||
|
||||
use crate::image_cache::{MAX_IMAGES, image_cache};
|
||||
|
||||
mod browser;
|
||||
mod commits;
|
||||
mod diff;
|
||||
@@ -145,10 +147,6 @@ impl RepoDetailView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
// Owner/maintainer avatars shown by this view stay in the shared
|
||||
// cache until the panel closes; free them then.
|
||||
crate::image_cache::clear_on_release(&cx.entity(), window, cx);
|
||||
|
||||
// The announcement we opened from already carries the repository's
|
||||
// NIP-34 `relays` tag, so the store can connect to those relays
|
||||
// immediately instead of waiting for the bootstrap fetch.
|
||||
@@ -1239,6 +1237,7 @@ impl Render for RepoDetailView {
|
||||
.unwrap_or_else(|| "Overview".into());
|
||||
|
||||
v_flex()
|
||||
.image_cache(image_cache("repo", MAX_IMAGES))
|
||||
.id("repo")
|
||||
.size_full()
|
||||
.child(self.render_header(cx))
|
||||
|
||||
@@ -33,6 +33,7 @@ use super::helpers::{
|
||||
DIFF_ROW_HEIGHT, DiffRow, build_tree_items, diff_rows, find_item, placeholder, render_diff_row,
|
||||
status_badge, tree_items, tree_row,
|
||||
};
|
||||
use crate::image_cache::{MAX_IMAGES, image_cache};
|
||||
|
||||
/// Width of the changed-files column.
|
||||
const TREE_WIDTH: f32 = 260.;
|
||||
@@ -91,10 +92,6 @@ impl PullRequestDetailView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
// PR author avatars stay in the shared cache until the panel
|
||||
// closes; free them then.
|
||||
crate::image_cache::clear_on_release(&cx.entity(), window, cx);
|
||||
|
||||
let tree_state = cx.new(|cx| TreeState::new(cx));
|
||||
let comment_input =
|
||||
cx.new(|cx| TextareaState::new(window, cx).placeholder("Leave a comment…"));
|
||||
@@ -1160,6 +1157,7 @@ impl Focusable for PullRequestDetailView {
|
||||
impl Render for PullRequestDetailView {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.image_cache(image_cache("pull-request-detail", MAX_IMAGES))
|
||||
.id("pull-request-detail")
|
||||
.size_full()
|
||||
.min_h_0()
|
||||
|
||||
@@ -24,6 +24,7 @@ use utils::relative_time;
|
||||
|
||||
use super::helpers::{placeholder, status_badge};
|
||||
use super::pull_request_detail::PullRequestDetailView;
|
||||
use crate::image_cache::{MAX_IMAGES, image_cache};
|
||||
|
||||
/// Height of one pull request row in the virtual list: same layout as an
|
||||
/// issue row (8px vertical padding (`py_2`) on top and bottom, a 32px title
|
||||
@@ -91,13 +92,9 @@ impl PullRequestsView {
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
store: Entity<RepoStore>,
|
||||
repo_name: SharedString,
|
||||
window: &mut Window,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
// PR author avatars stay in the shared cache until the panel
|
||||
// closes; free them then.
|
||||
crate::image_cache::clear_on_release(&cx.entity(), window, cx);
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
dock_area,
|
||||
@@ -569,6 +566,7 @@ impl Render for PullRequestsView {
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.image_cache(image_cache("pull-requests", MAX_IMAGES))
|
||||
.child(self.render_header(cx))
|
||||
.child(
|
||||
v_flex()
|
||||
|
||||
@@ -16,6 +16,7 @@ use signed_state::{ProfileStore, RepoListStore, Timestamp};
|
||||
use utils::relative_time;
|
||||
|
||||
use super::RepoDetailView;
|
||||
use crate::image_cache::{MAX_IMAGES, image_cache};
|
||||
|
||||
const CARD_HEIGHT: f32 = 160.;
|
||||
|
||||
@@ -201,6 +202,7 @@ impl Render for RepoListView {
|
||||
|
||||
v_flex()
|
||||
.relative()
|
||||
.image_cache(image_cache("repos", MAX_IMAGES))
|
||||
.size_full()
|
||||
.child(
|
||||
h_flex()
|
||||
|
||||
@@ -15,6 +15,7 @@ use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, h_flex, v_
|
||||
use signed_state::{Backend, BackendEvent, Profile, ProfileStore};
|
||||
|
||||
use super::RepoListView;
|
||||
use crate::image_cache::{MAX_IMAGES, image_cache};
|
||||
|
||||
mod import_dialog;
|
||||
mod onboarding_dialog;
|
||||
@@ -209,6 +210,7 @@ impl Render for SidebarPanel {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.image_cache(image_cache("sidebar", MAX_IMAGES))
|
||||
.bg(cx.theme().sidebar)
|
||||
.text_color(cx.theme().sidebar_foreground)
|
||||
.child(
|
||||
|
||||
@@ -4,6 +4,7 @@ use gpui::{Context, Entity, Render, Subscription, Window, div, px};
|
||||
use gpui_component::{Root, StyledExt, Theme};
|
||||
use signed_state::{Backend, BackendEvent};
|
||||
|
||||
use crate::image_cache::{MAX_IMAGES, image_cache};
|
||||
use crate::views::SidebarPanel;
|
||||
use crate::views::sidebar::passphrase_dialog;
|
||||
|
||||
@@ -111,7 +112,7 @@ impl Render for Workspace {
|
||||
let notification_layer = Root::render_notification_layer(window, cx);
|
||||
|
||||
div()
|
||||
.image_cache(crate::image_cache::global(cx))
|
||||
.image_cache(image_cache("workspace", MAX_IMAGES))
|
||||
.id("workspace")
|
||||
.v_flex()
|
||||
.size_full()
|
||||
|
||||
@@ -51,10 +51,6 @@ fn main() {
|
||||
// Sync the theme with the system appearance
|
||||
Theme::sync_system_appearance(None, cx);
|
||||
|
||||
// Install the shared image cache so avatars can be freed when
|
||||
// their views close, instead of staying in memory forever.
|
||||
workspace::image_cache::init(cx);
|
||||
|
||||
// Initialize backend and stores (connects relays, restores session)
|
||||
std::fs::create_dir_all(paths::nostr_dir()).ok();
|
||||
signed_state::init(paths::nostr_dir(), cx);
|
||||
|
||||
Reference in New Issue
Block a user