mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
performance fixes
This commit is contained in:
1 parent
8e8d42ad3b
commit
b3e8e8ab96
3 files changed
+157
-81
No files matched your search
@@ -85,11 +85,19 @@ pub struct PhotoItem {
|
||||
/// The daemon's reply to a [`Request`].
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum Response {
|
||||
/// Current mount status.
|
||||
/// Current mount status. Carries the cache stats the daemon already holds
|
||||
/// (`used`/`budget` bytes and the pin list) so a front-end never has to open
|
||||
/// the on-disk cache itself on its UI thread.
|
||||
Status {
|
||||
username: String,
|
||||
mountpoint: String,
|
||||
pinned: usize,
|
||||
/// Bytes of cached content blobs (see [`crate::cache::ContentCache::usage`]).
|
||||
used: u64,
|
||||
/// Configured soft byte cap (`0` = unlimited).
|
||||
budget: u64,
|
||||
/// The pin registry.
|
||||
pins: Vec<Pin>,
|
||||
},
|
||||
/// A human-readable success message.
|
||||
Ok { message: String },
|
||||
|
||||
@@ -1571,11 +1571,17 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U
|
||||
return;
|
||||
}
|
||||
let response = match serde_json::from_str::<CtlRequest>(line.trim()) {
|
||||
Ok(CtlRequest::Status) => CtlResponse::Status {
|
||||
username: username.to_string(),
|
||||
mountpoint: mountpoint.display().to_string(),
|
||||
pinned: core.cache.list_pins().len(),
|
||||
},
|
||||
Ok(CtlRequest::Status) => {
|
||||
let pins = core.cache.list_pins();
|
||||
CtlResponse::Status {
|
||||
username: username.to_string(),
|
||||
mountpoint: mountpoint.display().to_string(),
|
||||
pinned: pins.len(),
|
||||
used: core.cache.usage(),
|
||||
budget: core.cache.budget(),
|
||||
pins,
|
||||
}
|
||||
}
|
||||
Ok(CtlRequest::Pin { path }) => match rel_to_mount(mountpoint, &path) {
|
||||
Ok(rel) => match core.pin(&rel) {
|
||||
Ok(name) => CtlResponse::Ok {
|
||||
|
||||
+137
-75
@@ -8,13 +8,14 @@
|
||||
//! * a **login form** (email / password / optional 2FA) driving
|
||||
//! [`pdfs_core::auth::login`]; logging in enables the systemd mount service;
|
||||
//! * a **read-only mount status** line (the mount is managed automatically);
|
||||
//! * a **cache usage** read-out from the on-disk [`ContentCache`]; and
|
||||
//! * a **cache usage** read-out; and
|
||||
//! * the **pin list** with per-file unpin.
|
||||
//!
|
||||
//! State the GUI only displays (cache size, pins, login identity) is read
|
||||
//! straight off disk via `pdfs-core`, so the window is useful even with no mount
|
||||
//! daemon running; live mount status comes from the daemon over the control
|
||||
//! socket the CLI and tray already speak.
|
||||
//! Mount status, cache usage and the pin list all ride along on one
|
||||
//! [`Request::Status`] round-trip to the daemon (which owns the cache), fetched on
|
||||
//! a worker thread so the periodic refresh never blocks the GTK main loop. Login
|
||||
//! identity is cached in [`Ui`] and refreshed only on login/logout, so the 2s tick
|
||||
//! never touches the keyring.
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::HashSet;
|
||||
@@ -29,8 +30,7 @@ use gtk4::glib;
|
||||
use gtk4::glib::BoxedAnyObject;
|
||||
|
||||
use pdfs_core::auth;
|
||||
use pdfs_core::cache::ContentCache;
|
||||
use pdfs_core::config::{AppDirs, DEFAULT_CACHE_BUDGET_BYTES};
|
||||
use pdfs_core::config::AppDirs;
|
||||
use pdfs_core::control::{DirEntry, PhotoItem, Request, Response, send};
|
||||
use pdfs_core::service;
|
||||
|
||||
@@ -47,6 +47,14 @@ const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
|
||||
/// is still coming up (see [`load_browser`] / [`load_gallery`]).
|
||||
const CONNECT_RETRY_INTERVAL: Duration = Duration::from_secs(2);
|
||||
|
||||
/// One rendered pin row, retained so [`repaint_pins`] can flip the unpin button's
|
||||
/// `sensitive` in place (when the pin set is unchanged) instead of rebuilding.
|
||||
struct PinRow {
|
||||
row: adw::ActionRow,
|
||||
/// The unpin button, absent on the placeholder row.
|
||||
unpin: Option<gtk4::Button>,
|
||||
}
|
||||
|
||||
/// All widgets the periodic refresh and the action handlers mutate, plus the
|
||||
/// resolved paths they act on. Wrapped in an [`Rc`] so handlers and the timeout
|
||||
/// closure share one instance.
|
||||
@@ -60,6 +68,13 @@ struct Ui {
|
||||
/// Keys (relative path / photo uid) of open requests currently in flight, so
|
||||
/// a double-click on the same entry is a no-op instead of a second download.
|
||||
opening: RefCell<HashSet<String>>,
|
||||
/// Resolved login identity, cached so the periodic [`refresh`] never hits the
|
||||
/// keyring (a DBus round-trip). Populated at startup and updated only on
|
||||
/// login / logout. `None` = signed out.
|
||||
session: RefCell<Option<auth::StoredSession>>,
|
||||
/// Whether a [`Request::Status`] round-trip is already in flight, so the 2s
|
||||
/// refresh tick doesn't pile worker threads up on a slow/wedged daemon.
|
||||
status_inflight: Cell<bool>,
|
||||
// Login page.
|
||||
email: adw::EntryRow,
|
||||
password: adw::PasswordEntryRow,
|
||||
@@ -74,9 +89,12 @@ struct Ui {
|
||||
cache_bar: gtk4::ProgressBar,
|
||||
cache_label: gtk4::Label,
|
||||
pins_group: adw::PreferencesGroup,
|
||||
/// Rows currently shown under [`Self::pins_group`], so a refresh can clear
|
||||
/// the previous batch before re-adding.
|
||||
pin_rows: RefCell<Vec<adw::ActionRow>>,
|
||||
/// Rows currently shown under [`Self::pins_group`], retained so a refresh can
|
||||
/// diff against them and only rebuild when the pin set actually changes.
|
||||
pin_rows: RefCell<Vec<PinRow>>,
|
||||
/// The pin paths last rendered, the diff baseline for [`repaint_pins`].
|
||||
/// `None` = nothing built yet; `Some(empty)` = the placeholder is shown.
|
||||
pins_state: RefCell<Option<Vec<String>>>,
|
||||
/// Whether the last refresh saw a live mount daemon. Gates the unpin
|
||||
/// buttons, which need the daemon to evict + re-hydrate.
|
||||
mounted: RefCell<bool>,
|
||||
@@ -213,6 +231,8 @@ fn build_window(app: &adw::Application) {
|
||||
spinner: spinner.clone(),
|
||||
busy: Cell::new(0),
|
||||
opening: RefCell::new(HashSet::new()),
|
||||
session: RefCell::new(auth::load().ok()),
|
||||
status_inflight: Cell::new(false),
|
||||
email: login_widgets.0,
|
||||
password: login_widgets.1,
|
||||
totp: login_widgets.2,
|
||||
@@ -224,6 +244,7 @@ fn build_window(app: &adw::Application) {
|
||||
cache_label: main_widgets.3,
|
||||
pins_group: main_widgets.4,
|
||||
pin_rows: RefCell::new(Vec::new()),
|
||||
pins_state: RefCell::new(None),
|
||||
mounted: RefCell::new(false),
|
||||
switcher: switcher.clone(),
|
||||
browser_model: browser_widgets.0,
|
||||
@@ -485,6 +506,8 @@ fn wire_login(ui: &Rc<Ui>) {
|
||||
ui.login_status.set_text("");
|
||||
ui.password.set_text("");
|
||||
ui.totp.set_text("");
|
||||
// Cache the new identity so `refresh` never hits the keyring.
|
||||
*ui.session.borrow_mut() = auth::load().ok();
|
||||
// Enable+start the mount service now that we have a session.
|
||||
service::enable_start();
|
||||
refresh(&ui);
|
||||
@@ -543,6 +566,7 @@ fn wire_logout(ui: &Rc<Ui>, button: >k4::Button) {
|
||||
if let Err(e) = auth::logout() {
|
||||
tracing::error!("logout failed: {e}");
|
||||
}
|
||||
*ui.session.borrow_mut() = None;
|
||||
refresh(&ui);
|
||||
});
|
||||
}
|
||||
@@ -563,87 +587,122 @@ fn wire_retry(ui: &Rc<Ui>) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Re-read mount status, login identity, cache usage and the pin list, and
|
||||
/// repaint the window. Cheap, synchronous local reads — safe on the main loop.
|
||||
/// Repaint the window from the cached login identity, then kick an async mount-
|
||||
/// status fetch. Runs on the 2s tick: the identity check is instant (no keyring),
|
||||
/// and the status round-trip is offloaded to a worker so the main loop never
|
||||
/// blocks on a slow or wedged daemon.
|
||||
fn refresh(ui: &Rc<Ui>) {
|
||||
// Login identity decides which page is shown.
|
||||
let session = auth::load().ok();
|
||||
match &session {
|
||||
Some(s) => {
|
||||
// Only pull the user onto "main" when they're sitting on the login
|
||||
// page; otherwise leave whichever signed-in page they navigated to.
|
||||
if ui.stack.visible_child_name().as_deref() == Some("login") {
|
||||
ui.stack.set_visible_child_name("main");
|
||||
// Login identity decides which page is shown. Read the cached session — set
|
||||
// at startup and on login/logout — never the keyring.
|
||||
{
|
||||
let session = ui.session.borrow();
|
||||
match session.as_ref() {
|
||||
Some(s) => {
|
||||
// Only pull the user onto "main" when they're sitting on the login
|
||||
// page; otherwise leave whichever signed-in page they navigated to.
|
||||
if ui.stack.visible_child_name().as_deref() == Some("login") {
|
||||
ui.stack.set_visible_child_name("main");
|
||||
}
|
||||
ui.switcher.set_reveal(true);
|
||||
ui.account_row.set_title(&s.username);
|
||||
ui.account_row.set_subtitle("Proton account");
|
||||
}
|
||||
None => {
|
||||
ui.stack.set_visible_child_name("login");
|
||||
ui.switcher.set_reveal(false);
|
||||
return;
|
||||
}
|
||||
ui.switcher.set_reveal(true);
|
||||
ui.account_row.set_title(&s.username);
|
||||
ui.account_row.set_subtitle("Proton account");
|
||||
}
|
||||
None => {
|
||||
ui.stack.set_visible_child_name("login");
|
||||
ui.switcher.set_reveal(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Live mount status from the daemon, falling back to "not mounted".
|
||||
let socket = ui.dirs.control_socket();
|
||||
let mount = match send(&socket, &Request::Status) {
|
||||
Ok(Response::Status { mountpoint, .. }) => Some(mountpoint),
|
||||
_ => None,
|
||||
};
|
||||
*ui.mounted.borrow_mut() = mount.is_some();
|
||||
|
||||
// Read-only status: the service mounts automatically, we only report.
|
||||
ui.mount_row.set_subtitle(&match &mount {
|
||||
Some(mp) => format!("Mounted at {mp}"),
|
||||
None => "Not mounted".into(),
|
||||
});
|
||||
|
||||
// Cache usage + pins, read straight off disk so they work offline too.
|
||||
let cache = ContentCache::open(
|
||||
ui.dirs.content_cache_dir(),
|
||||
ui.dirs.pins_path(),
|
||||
DEFAULT_CACHE_BUDGET_BYTES,
|
||||
);
|
||||
match cache {
|
||||
Ok(cache) => {
|
||||
let used = cache.usage();
|
||||
let budget = cache.budget();
|
||||
let fraction = if budget == 0 {
|
||||
0.0
|
||||
} else {
|
||||
(used as f64 / budget as f64).min(1.0)
|
||||
};
|
||||
ui.cache_bar.set_fraction(fraction);
|
||||
ui.cache_label.set_text(&format!(
|
||||
"{} of {} used",
|
||||
human_bytes(used),
|
||||
human_bytes(budget)
|
||||
));
|
||||
repaint_pins(ui, &cache.list_pins());
|
||||
}
|
||||
Err(e) => ui.cache_label.set_text(&format!("Cache unavailable: {e}")),
|
||||
}
|
||||
refresh_status(ui);
|
||||
}
|
||||
|
||||
/// Replace the rows under the pins group with one [`adw::ActionRow`] per pin,
|
||||
/// each with an unpin button enabled only while a mount daemon is running.
|
||||
fn repaint_pins(ui: &Rc<Ui>, pins: &[pdfs_core::cache::Pin]) {
|
||||
for row in ui.pin_rows.borrow_mut().drain(..) {
|
||||
ui.pins_group.remove(&row);
|
||||
/// Fetch mount status + cache stats from the daemon on a worker thread and repaint
|
||||
/// the mount line, cache bar and pin list on the reply. The daemon owns the cache
|
||||
/// stats now (`used`/`budget`/`pins` ride along on [`Response::Status`]), so the
|
||||
/// GUI never opens the on-disk cache itself. Skipped while a fetch is in flight so
|
||||
/// the tick can't stack threads on a stalled daemon.
|
||||
fn refresh_status(ui: &Rc<Ui>) {
|
||||
if ui.status_inflight.get() {
|
||||
return;
|
||||
}
|
||||
ui.status_inflight.set(true);
|
||||
let rx = spawn_request(ui.dirs.control_socket(), Request::Status);
|
||||
let ui = ui.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
let result = rx.recv().await;
|
||||
ui.status_inflight.set(false);
|
||||
match result {
|
||||
Ok(Ok(Response::Status {
|
||||
mountpoint,
|
||||
used,
|
||||
budget,
|
||||
pins,
|
||||
..
|
||||
})) => {
|
||||
*ui.mounted.borrow_mut() = true;
|
||||
ui.mount_row.set_subtitle(&format!("Mounted at {mountpoint}"));
|
||||
let fraction = if budget == 0 {
|
||||
0.0
|
||||
} else {
|
||||
(used as f64 / budget as f64).min(1.0)
|
||||
};
|
||||
ui.cache_bar.set_fraction(fraction);
|
||||
ui.cache_label.set_text(&format!(
|
||||
"{} of {} used",
|
||||
human_bytes(used),
|
||||
human_bytes(budget)
|
||||
));
|
||||
repaint_pins(&ui, &pins, true);
|
||||
}
|
||||
// Daemon unreachable (still starting, or down): report not-mounted and
|
||||
// grey out the unpin buttons in place, but leave the last-known pin
|
||||
// rows and cache read-out so the page doesn't flicker on a blip.
|
||||
_ => {
|
||||
*ui.mounted.borrow_mut() = false;
|
||||
ui.mount_row.set_subtitle("Not mounted");
|
||||
for r in ui.pin_rows.borrow().iter() {
|
||||
if let Some(b) = &r.unpin {
|
||||
b.set_sensitive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Render the pins group from `pins`, with the unpin buttons enabled only while a
|
||||
/// mount daemon is running (`mounted`). Diffs against the last batch by path: when
|
||||
/// the set is unchanged (the common case on the 2s tick) it only flips the unpin
|
||||
/// buttons' `sensitive` flag, avoiding the rebuild that used to flicker the list
|
||||
/// and drop scroll/selection every tick.
|
||||
fn repaint_pins(ui: &Rc<Ui>, pins: &[pdfs_core::cache::Pin], mounted: bool) {
|
||||
let desired: Vec<String> = pins.iter().map(|p| p.path.clone()).collect();
|
||||
if ui.pins_state.borrow().as_ref() == Some(&desired) {
|
||||
for r in ui.pin_rows.borrow().iter() {
|
||||
if let Some(b) = &r.unpin {
|
||||
b.set_sensitive(mounted);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for pr in ui.pin_rows.borrow_mut().drain(..) {
|
||||
ui.pins_group.remove(&pr.row);
|
||||
}
|
||||
*ui.pins_state.borrow_mut() = Some(desired);
|
||||
|
||||
if pins.is_empty() {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title("No pinned files")
|
||||
.subtitle("Right-click a file in the mount to keep it offline.")
|
||||
.build();
|
||||
ui.pins_group.add(&row);
|
||||
ui.pin_rows.borrow_mut().push(row);
|
||||
ui.pin_rows.borrow_mut().push(PinRow { row, unpin: None });
|
||||
return;
|
||||
}
|
||||
|
||||
let mounted = *ui.mounted.borrow();
|
||||
for pin in pins {
|
||||
let name = Path::new(&pin.path)
|
||||
.file_name()
|
||||
@@ -677,7 +736,10 @@ fn repaint_pins(ui: &Rc<Ui>, pins: &[pdfs_core::cache::Pin]) {
|
||||
row.add_suffix(&unpin);
|
||||
|
||||
ui.pins_group.add(&row);
|
||||
ui.pin_rows.borrow_mut().push(row);
|
||||
ui.pin_rows.borrow_mut().push(PinRow {
|
||||
row,
|
||||
unpin: Some(unpin),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user