mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-04 00:47:00 +01:00
feat(app,stream): storage and hardware-decoding settings
- the streaming cache's budget (1-16 GiB) is chosen in Settings, shows what it holds, and can be emptied; a lower budget evicts at once - hardware decoding can be turned off for a driver that shows green frames, from the next file - both live in desktop.json rather than the playback prefs shared with Android, whose bridge record would reset them on every save. A file that will not parse is never overwritten
This commit is contained in:
1 parent
935a985589
commit
a432a93cf9
9 files changed
+315
-5
No files matched your search
@@ -163,6 +163,8 @@ pub enum Action {
|
|||||||
SetSpeed(f64),
|
SetSpeed(f64),
|
||||||
/// Seek past openings and credits without being asked.
|
/// Seek past openings and credits without being asked.
|
||||||
SetAutoSkip(bool),
|
SetAutoSkip(bool),
|
||||||
|
SetDesktopPrefs(crate::desktop_prefs::DesktopPrefs),
|
||||||
|
ClearStreamCache,
|
||||||
/// Repaint the window in a different palette.
|
/// Repaint the window in a different palette.
|
||||||
SetAppearance(Appearance),
|
SetAppearance(Appearance),
|
||||||
/// In or out of fullscreen, from the player page.
|
/// In or out of fullscreen, from the player page.
|
||||||
@@ -1240,6 +1242,8 @@ impl App {
|
|||||||
self.note(ctx, text, false);
|
self.note(ctx, text, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Action::SetDesktopPrefs(prefs) => self.engine.set_desktop_prefs(prefs),
|
||||||
|
Action::ClearStreamCache => self.engine.clear_stream_cache(),
|
||||||
Action::SetAutoSkip(auto_skip) => {
|
Action::SetAutoSkip(auto_skip) => {
|
||||||
let mut prefs = self.engine.playback_prefs();
|
let mut prefs = self.engine.playback_prefs();
|
||||||
prefs.auto_skip = auto_skip;
|
prefs.auto_skip = auto_skip;
|
||||||
@@ -1509,6 +1513,10 @@ impl eframe::App for App {
|
|||||||
autoplay: playback_prefs.autoplay_next,
|
autoplay: playback_prefs.autoplay_next,
|
||||||
auto_skip: playback_prefs.auto_skip,
|
auto_skip: playback_prefs.auto_skip,
|
||||||
appearance: self.engine.appearance(),
|
appearance: self.engine.appearance(),
|
||||||
|
desktop: self.engine.desktop_prefs(),
|
||||||
|
cache_used: (self.page == Page::Settings)
|
||||||
|
.then(|| self.engine.cache_used())
|
||||||
|
.flatten(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const NAV_MARGIN: egui::Vec2 = egui::vec2(14.0, 10.0);
|
const NAV_MARGIN: egui::Vec2 = egui::vec2(14.0, 10.0);
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
//! Settings only the desktop app has.
|
||||||
|
//!
|
||||||
|
//! Kept out of [`pstr_core::prefs::PlaybackPrefs`] on purpose: that one is
|
||||||
|
//! shared with Android through a bridge record, and a field Android neither
|
||||||
|
//! shows nor keeps would be reset to its default every time Android saved.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// What the settings page offers for the streaming cache, in GiB.
|
||||||
|
pub const CACHE_CHOICES: [u32; 5] = [1, 2, 4, 8, 16];
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
|
// A file from an older version, or edited by hand, missing a field: the rest
|
||||||
|
// still counts, and the missing one takes its default.
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct DesktopPrefs {
|
||||||
|
/// Let mpv decode on the GPU. Off is for a driver that shows green frames
|
||||||
|
/// or nothing; it costs CPU, not correctness.
|
||||||
|
pub hardware_decoding: bool,
|
||||||
|
/// How much decrypted video the disk cache may keep.
|
||||||
|
pub cache_gib: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DesktopPrefs {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
hardware_decoding: true,
|
||||||
|
cache_gib: 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DesktopPrefs {
|
||||||
|
pub fn cache_bytes(&self) -> u64 {
|
||||||
|
u64::from(self.cache_gib.clamp(1, 64)) << 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The prefs and where they live.
|
||||||
|
pub struct DesktopPrefsFile {
|
||||||
|
path: PathBuf,
|
||||||
|
/// False when the file was there and would not parse. Such a file is never
|
||||||
|
/// overwritten — it is far more likely a bug than something to replace —
|
||||||
|
/// so changes then last only for the session.
|
||||||
|
writable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DesktopPrefsFile {
|
||||||
|
pub fn load(path: PathBuf) -> (Self, DesktopPrefs) {
|
||||||
|
match pstr_core::config::read_json::<DesktopPrefs>(&path) {
|
||||||
|
Ok(prefs) => (
|
||||||
|
Self {
|
||||||
|
path,
|
||||||
|
writable: true,
|
||||||
|
},
|
||||||
|
prefs.unwrap_or_default(),
|
||||||
|
),
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!("read the desktop settings: {error}");
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
path,
|
||||||
|
writable: false,
|
||||||
|
},
|
||||||
|
DesktopPrefs::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self, prefs: &DesktopPrefs) -> pstr_core::Result<()> {
|
||||||
|
if !self.writable {
|
||||||
|
tracing::warn!(
|
||||||
|
"not saving the desktop settings over {}, which would not parse",
|
||||||
|
self.path.display()
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
pstr_core::config::write_json(&self.path, prefs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_file_missing_a_field_keeps_the_rest() {
|
||||||
|
let prefs: DesktopPrefs = serde_json::from_str(r#"{ "cache_gib": 8 }"#).unwrap();
|
||||||
|
assert_eq!(prefs.cache_gib, 8);
|
||||||
|
assert!(prefs.hardware_decoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_unparseable_file_is_never_overwritten() {
|
||||||
|
let path = std::env::temp_dir().join(format!("pstr-desktop-{}.json", std::process::id()));
|
||||||
|
std::fs::write(&path, "{ not json").unwrap();
|
||||||
|
let (file, prefs) = DesktopPrefsFile::load(path.clone());
|
||||||
|
assert_eq!(prefs, DesktopPrefs::default());
|
||||||
|
file.save(&DesktopPrefs::default()).unwrap();
|
||||||
|
assert_eq!(std::fs::read_to_string(&path).unwrap(), "{ not json");
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,8 @@ use pstr_core::proton_drive_rs::ThumbnailType;
|
|||||||
use pstr_core::proton_sdk::ids::{LinkId, NodeUid, VolumeId};
|
use pstr_core::proton_sdk::ids::{LinkId, NodeUid, VolumeId};
|
||||||
use pstr_core::{Share, ShareStore, SharedLibrary};
|
use pstr_core::{Share, ShareStore, SharedLibrary};
|
||||||
use pstr_meta::MetadataService;
|
use pstr_meta::MetadataService;
|
||||||
|
|
||||||
|
use crate::desktop_prefs::{DesktopPrefs, DesktopPrefsFile};
|
||||||
use pstr_stream::{
|
use pstr_stream::{
|
||||||
BlockSource, DiskCacheConfig, FileBlocks, LibraryOpener, StreamConfig, StreamSource,
|
BlockSource, DiskCacheConfig, FileBlocks, LibraryOpener, StreamConfig, StreamSource,
|
||||||
VideoStream,
|
VideoStream,
|
||||||
@@ -318,6 +320,9 @@ pub struct Engine {
|
|||||||
download_permits: Arc<tokio::sync::Semaphore>,
|
download_permits: Arc<tokio::sync::Semaphore>,
|
||||||
/// Whether the player page is on screen. See [`Engine::wake`].
|
/// Whether the player page is on screen. See [`Engine::wake`].
|
||||||
picture_shown: Arc<AtomicBool>,
|
picture_shown: Arc<AtomicBool>,
|
||||||
|
/// Settings only the desktop has, and the file they are kept in.
|
||||||
|
desktop: Arc<Mutex<DesktopPrefs>>,
|
||||||
|
desktop_file: Arc<DesktopPrefsFile>,
|
||||||
/// Bumped to stop the crawls in flight; each watches it from its start.
|
/// Bumped to stop the crawls in flight; each watches it from its start.
|
||||||
crawl_cancel: Arc<tokio::sync::watch::Sender<u64>>,
|
crawl_cancel: Arc<tokio::sync::watch::Sender<u64>>,
|
||||||
events: Sender<Event>,
|
events: Sender<Event>,
|
||||||
@@ -371,6 +376,7 @@ impl Engine {
|
|||||||
) -> anyhow::Result<(Self, Receiver<Event>)> {
|
) -> anyhow::Result<(Self, Receiver<Event>)> {
|
||||||
let catalog = Catalog::open(&dirs.catalog_db())?;
|
let catalog = Catalog::open(&dirs.catalog_db())?;
|
||||||
let (events, receiver) = channel();
|
let (events, receiver) = channel();
|
||||||
|
let (desktop_file, desktop) = DesktopPrefsFile::load(dirs.desktop_prefs_file());
|
||||||
|
|
||||||
// An unreadable settings file must not stop the app starting: the
|
// An unreadable settings file must not stop the app starting: the
|
||||||
// library is the point and enrichment is decoration. It is reported and
|
// library is the point and enrichment is decoration. It is reported and
|
||||||
@@ -417,6 +423,8 @@ impl Engine {
|
|||||||
download_permits: Arc::new(tokio::sync::Semaphore::new(DOWNLOAD_CONCURRENCY)),
|
download_permits: Arc::new(tokio::sync::Semaphore::new(DOWNLOAD_CONCURRENCY)),
|
||||||
picture_shown: Arc::new(AtomicBool::new(false)),
|
picture_shown: Arc::new(AtomicBool::new(false)),
|
||||||
crawl_cancel: Arc::new(tokio::sync::watch::channel(0).0),
|
crawl_cancel: Arc::new(tokio::sync::watch::channel(0).0),
|
||||||
|
desktop: Arc::new(Mutex::new(desktop)),
|
||||||
|
desktop_file: Arc::new(desktop_file),
|
||||||
events,
|
events,
|
||||||
ctx,
|
ctx,
|
||||||
};
|
};
|
||||||
@@ -667,7 +675,10 @@ impl Engine {
|
|||||||
// mpv brings its own read-ahead; a second one under it only competes
|
// mpv brings its own read-ahead; a second one under it only competes
|
||||||
// for the bandwidth mpv is blocked on. See `pstr_player::READAHEAD_BLOCKS`.
|
// for the bandwidth mpv is blocked on. See `pstr_player::READAHEAD_BLOCKS`.
|
||||||
.with_readahead(pstr_player::READAHEAD_BLOCKS)
|
.with_readahead(pstr_player::READAHEAD_BLOCKS)
|
||||||
.with_disk_cache(DiskCacheConfig::new(self.dirs.block_cache()));
|
.with_disk_cache(
|
||||||
|
DiskCacheConfig::new(self.dirs.block_cache())
|
||||||
|
.with_budget(self.desktop_prefs().cache_bytes()),
|
||||||
|
);
|
||||||
|
|
||||||
let source = match StreamSource::new(opener, config).await {
|
let source = match StreamSource::new(opener, config).await {
|
||||||
Ok(source) => source,
|
Ok(source) => source,
|
||||||
@@ -1683,6 +1694,54 @@ impl Engine {
|
|||||||
// ---------------------------------------------------------------- enrichment
|
// ---------------------------------------------------------------- enrichment
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
|
pub fn desktop_prefs(&self) -> DesktopPrefs {
|
||||||
|
*self.desktop.lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the desktop-only settings, write them, and apply what can be
|
||||||
|
/// applied now. Hardware decoding takes effect from the next file.
|
||||||
|
pub fn set_desktop_prefs(&self, prefs: DesktopPrefs) {
|
||||||
|
*self.desktop.lock() = prefs;
|
||||||
|
let engine = self.clone();
|
||||||
|
self.runtime.spawn(async move {
|
||||||
|
let file = Arc::clone(&engine.desktop_file);
|
||||||
|
let saved = tokio::task::spawn_blocking(move || file.save(&prefs)).await;
|
||||||
|
if let Ok(Err(error)) = saved {
|
||||||
|
engine.fail("save the desktop settings", error);
|
||||||
|
}
|
||||||
|
if let Some((_, source)) = engine.opened() {
|
||||||
|
source.set_disk_budget(prefs.cache_bytes()).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How much the streaming cache holds, once the shares are open.
|
||||||
|
pub fn cache_used(&self) -> Option<u64> {
|
||||||
|
let (_, source) = self.opened()?;
|
||||||
|
source.disk_stats().map(|stats| stats.stored_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Empty the streaming cache.
|
||||||
|
pub fn clear_stream_cache(&self) {
|
||||||
|
let engine = self.clone();
|
||||||
|
self.runtime.spawn(async move {
|
||||||
|
match engine.opened() {
|
||||||
|
Some((_, source)) => source.clear_disk_cache().await,
|
||||||
|
// Not open, so nothing holds an index of it to fall out of step
|
||||||
|
// with: the directory can simply go.
|
||||||
|
None => {
|
||||||
|
let root = engine.dirs.block_cache();
|
||||||
|
if let Err(error) = tokio::fs::remove_dir_all(&root).await
|
||||||
|
&& error.kind() != std::io::ErrorKind::NotFound
|
||||||
|
{
|
||||||
|
return engine.fail("clear the streaming cache", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
engine.emit(Event::Status("cleared the streaming cache".into()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// The enrichment settings as they stand.
|
/// The enrichment settings as they stand.
|
||||||
pub fn metadata_config(&self) -> MetadataConfig {
|
pub fn metadata_config(&self) -> MetadataConfig {
|
||||||
self.metadata.lock().config.clone()
|
self.metadata.lock().config.clone()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
//! `examples/embedded_video.rs`, which is the smoke test for [`video`].
|
//! `examples/embedded_video.rs`, which is the smoke test for [`video`].
|
||||||
|
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
pub mod desktop_prefs;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod inhibit;
|
pub mod inhibit;
|
||||||
pub mod media;
|
pub mod media;
|
||||||
|
|||||||
@@ -423,6 +423,7 @@ fn build_player(
|
|||||||
// controller would be a second set of controls over the same file.
|
// controller would be a second set of controls over the same file.
|
||||||
on_screen_controller: false,
|
on_screen_controller: false,
|
||||||
default_keybindings: false,
|
default_keybindings: false,
|
||||||
|
hardware_decoding: engine.desktop_prefs().hardware_decoding,
|
||||||
..from_prefs(prefs, target.track_prefs.as_deref())
|
..from_prefs(prefs, target.track_prefs.as_deref())
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -456,6 +457,7 @@ fn build_player(
|
|||||||
|
|
||||||
let config = PlayerConfig {
|
let config = PlayerConfig {
|
||||||
window_title: title,
|
window_title: title,
|
||||||
|
hardware_decoding: engine.desktop_prefs().hardware_decoding,
|
||||||
..from_prefs(prefs, target.track_prefs.as_deref())
|
..from_prefs(prefs, target.track_prefs.as_deref())
|
||||||
};
|
};
|
||||||
let player = Player::new(engine.runtime().clone(), config)
|
let player = Player::new(engine.runtime().clone(), config)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use pstr_core::appearance::{Accent, Appearance, Flavor};
|
|||||||
use pstr_core::metadata::{MetadataConfig, ProviderId};
|
use pstr_core::metadata::{MetadataConfig, ProviderId};
|
||||||
|
|
||||||
use crate::app::Action;
|
use crate::app::Action;
|
||||||
|
use crate::desktop_prefs::{CACHE_CHOICES, DesktopPrefs};
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use crate::ui;
|
use crate::ui;
|
||||||
|
|
||||||
@@ -22,6 +23,9 @@ pub struct Prefs {
|
|||||||
pub autoplay: bool,
|
pub autoplay: bool,
|
||||||
pub auto_skip: bool,
|
pub auto_skip: bool,
|
||||||
pub appearance: Appearance,
|
pub appearance: Appearance,
|
||||||
|
pub desktop: DesktopPrefs,
|
||||||
|
/// What the streaming cache holds, once the shares are open.
|
||||||
|
pub cache_used: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The widest the settings column gets. Rows that run the width of a 2K window
|
/// The widest the settings column gets. Rows that run the width of a 2K window
|
||||||
@@ -49,6 +53,7 @@ pub fn show(
|
|||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
appearance_form(ui, prefs.appearance, actions);
|
appearance_form(ui, prefs.appearance, actions);
|
||||||
playback_form(ui, prefs, actions);
|
playback_form(ui, prefs, actions);
|
||||||
|
storage_form(ui, prefs, actions);
|
||||||
metadata_form(ui, settings, api_key, actions);
|
metadata_form(ui, settings, api_key, actions);
|
||||||
about(ui);
|
about(ui);
|
||||||
});
|
});
|
||||||
@@ -256,6 +261,65 @@ fn playback_form(ui: &mut egui::Ui, prefs: Prefs, actions: &mut Vec<Action>) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
let mut desktop = prefs.desktop;
|
||||||
|
ui::widgets::settings_row(
|
||||||
|
ui,
|
||||||
|
"Hardware decoding",
|
||||||
|
Some(
|
||||||
|
"Decode on the graphics card. Turn off if video shows green frames or nothing at all; it costs CPU, not quality. From the next file.",
|
||||||
|
),
|
||||||
|
|ui| {
|
||||||
|
if ui::widgets::toggle(ui, &mut desktop.hardware_decoding, "").changed() {
|
||||||
|
actions.push(Action::SetDesktopPrefs(desktop));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The streaming cache: how big it may get, and a way to empty it.
|
||||||
|
fn storage_form(ui: &mut egui::Ui, prefs: Prefs, actions: &mut Vec<Action>) {
|
||||||
|
ui::widgets::settings_group(
|
||||||
|
ui,
|
||||||
|
"Storage",
|
||||||
|
Some(
|
||||||
|
"Video already fetched is kept on disk, so watching it again or seeking back costs nothing. Downloads are separate and never count towards this.",
|
||||||
|
),
|
||||||
|
|ui| {
|
||||||
|
let budget = ui::format_size(prefs.desktop.cache_bytes() as i64);
|
||||||
|
let used = match prefs.cache_used {
|
||||||
|
Some(used) => format!("{} of {budget} in use", ui::format_size(used as i64)),
|
||||||
|
None => format!("Up to {budget}"),
|
||||||
|
};
|
||||||
|
ui::widgets::settings_row(ui, "Streaming cache", Some(&used), |ui| {
|
||||||
|
let choices: Vec<(u32, String)> = CACHE_CHOICES
|
||||||
|
.iter()
|
||||||
|
.map(|&gib| (gib, format!("{gib} GiB")))
|
||||||
|
.collect();
|
||||||
|
let labels: Vec<(u32, &str)> = choices
|
||||||
|
.iter()
|
||||||
|
.map(|(gib, label)| (*gib, label.as_str()))
|
||||||
|
.collect();
|
||||||
|
if let Some(cache_gib) =
|
||||||
|
ui::widgets::segmented(ui, prefs.desktop.cache_gib, &labels)
|
||||||
|
{
|
||||||
|
actions.push(Action::SetDesktopPrefs(DesktopPrefs {
|
||||||
|
cache_gib,
|
||||||
|
..prefs.desktop
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui::widgets::settings_row(
|
||||||
|
ui,
|
||||||
|
"Clear the streaming cache",
|
||||||
|
Some("Frees the space now. Whatever plays next is fetched again."),
|
||||||
|
|ui| {
|
||||||
|
if ui.button("Clear").clicked() {
|
||||||
|
actions.push(Action::ClearStreamCache);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ impl AppDirs {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Settings only the desktop app has: hardware decoding, the cache budget.
|
||||||
|
pub fn desktop_prefs_file(&self) -> PathBuf {
|
||||||
|
self.config.join("desktop.json")
|
||||||
|
}
|
||||||
|
|
||||||
/// The desktop window's size, for the next launch to open at.
|
/// The desktop window's size, for the next launch to open at.
|
||||||
pub fn window_file(&self) -> PathBuf {
|
pub fn window_file(&self) -> PathBuf {
|
||||||
self.config.join("window.json")
|
self.config.join("window.json")
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ pub struct DiskStats {
|
|||||||
|
|
||||||
pub(crate) struct DiskCache {
|
pub(crate) struct DiskCache {
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
budget: u64,
|
/// Atomic so it can be changed under a running cache, from settings.
|
||||||
|
budget: std::sync::atomic::AtomicU64,
|
||||||
index: Mutex<Index>,
|
index: Mutex<Index>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +116,7 @@ impl DiskCache {
|
|||||||
|
|
||||||
let cache = Self {
|
let cache = Self {
|
||||||
root: config.root,
|
root: config.root,
|
||||||
budget: config.budget_bytes,
|
budget: std::sync::atomic::AtomicU64::new(config.budget_bytes),
|
||||||
index: Mutex::new(Index {
|
index: Mutex::new(Index {
|
||||||
lru,
|
lru,
|
||||||
bytes,
|
bytes,
|
||||||
@@ -169,7 +170,7 @@ impl DiskCache {
|
|||||||
/// written is a slower app, not a broken one.
|
/// written is a slower app, not a broken one.
|
||||||
pub(crate) async fn put(&self, key: &BlockKey, block: &[u8]) {
|
pub(crate) async fn put(&self, key: &BlockKey, block: &[u8]) {
|
||||||
let len = block.len() as u64;
|
let len = block.len() as u64;
|
||||||
if len > self.budget {
|
if len > self.budget() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +191,30 @@ impl DiskCache {
|
|||||||
self.evict_to_budget().await;
|
self.evict_to_budget().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn budget(&self) -> u64 {
|
||||||
|
self.budget.load(std::sync::atomic::Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the budget, shrinking the cache to it at once if it is now over.
|
||||||
|
pub(crate) async fn set_budget(&self, bytes: u64) {
|
||||||
|
self.budget
|
||||||
|
.store(bytes, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
self.evict_to_budget().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete every entry. The cache carries on, empty, under the same budget.
|
||||||
|
pub(crate) async fn clear(&self) {
|
||||||
|
let doomed: Vec<PathBuf> = {
|
||||||
|
let mut index = self.lock();
|
||||||
|
let doomed = std::iter::from_fn(|| index.lru.pop_lru().map(|(path, _)| path)).collect();
|
||||||
|
index.bytes = 0;
|
||||||
|
doomed
|
||||||
|
};
|
||||||
|
for path in doomed {
|
||||||
|
self.discard(&path).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn stats(&self) -> DiskStats {
|
pub(crate) fn stats(&self) -> DiskStats {
|
||||||
let index = self.lock();
|
let index = self.lock();
|
||||||
DiskStats {
|
DiskStats {
|
||||||
@@ -204,7 +229,7 @@ impl DiskCache {
|
|||||||
loop {
|
loop {
|
||||||
let doomed = {
|
let doomed = {
|
||||||
let mut index = self.lock();
|
let mut index = self.lock();
|
||||||
if index.bytes <= self.budget {
|
if index.bytes <= self.budget() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match index.lru.pop_lru() {
|
match index.lru.pop_lru() {
|
||||||
@@ -511,6 +536,31 @@ mod tests {
|
|||||||
std::fs::remove_dir_all(&root).ok();
|
std::fs::remove_dir_all(&root).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_cleared_cache_is_empty_on_disk_and_in_its_index() {
|
||||||
|
let root = temp_root("clear");
|
||||||
|
let cache = DiskCache::open(DiskCacheConfig::new(&root)).await.unwrap();
|
||||||
|
for index in 0..3 {
|
||||||
|
cache.put(&key("a", "rev1", index), &[7u8; 100]).await;
|
||||||
|
}
|
||||||
|
cache.clear().await;
|
||||||
|
assert_eq!(cache.stats().stored_bytes, 0);
|
||||||
|
assert!(cache.get(&key("a", "rev1", 0)).await.is_none());
|
||||||
|
let reopened = DiskCache::open(DiskCacheConfig::new(&root)).await.unwrap();
|
||||||
|
assert_eq!(reopened.stats().entries, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn lowering_the_budget_shrinks_a_running_cache() {
|
||||||
|
let root = temp_root("lower");
|
||||||
|
let cache = DiskCache::open(DiskCacheConfig::new(&root)).await.unwrap();
|
||||||
|
for index in 0..5 {
|
||||||
|
cache.put(&key("a", "rev1", index), &[7u8; 100]).await;
|
||||||
|
}
|
||||||
|
cache.set_budget(250).await;
|
||||||
|
assert!(cache.stats().stored_bytes <= 250);
|
||||||
|
}
|
||||||
|
|
||||||
/// Reopening with a smaller budget must shrink the cache, not sit over it
|
/// Reopening with a smaller budget must shrink the cache, not sit over it
|
||||||
/// until enough new blocks arrive to trigger eviction.
|
/// until enough new blocks arrive to trigger eviction.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -200,6 +200,21 @@ impl StreamSource {
|
|||||||
pub fn disk_stats(&self) -> Option<crate::disk::DiskStats> {
|
pub fn disk_stats(&self) -> Option<crate::disk::DiskStats> {
|
||||||
self.inner.disk.as_ref().map(|disk| disk.stats())
|
self.inner.disk.as_ref().map(|disk| disk.stats())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Change the disk cache's budget, evicting at once if it is now over.
|
||||||
|
pub async fn set_disk_budget(&self, bytes: u64) {
|
||||||
|
if let Some(disk) = &self.inner.disk {
|
||||||
|
disk.set_budget(bytes).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Empty the disk cache. Streams already open carry on; what they had
|
||||||
|
/// cached is fetched again if it is wanted.
|
||||||
|
pub async fn clear_disk_cache(&self) {
|
||||||
|
if let Some(disk) = &self.inner.disk {
|
||||||
|
disk.clear().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Inner {
|
impl Inner {
|
||||||
|
|||||||
Reference in new issue
Block a user