mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 00:47:00 +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`].
|
/// The daemon's reply to a [`Request`].
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub enum Response {
|
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 {
|
Status {
|
||||||
username: String,
|
username: String,
|
||||||
mountpoint: String,
|
mountpoint: String,
|
||||||
pinned: usize,
|
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.
|
/// A human-readable success message.
|
||||||
Ok { message: String },
|
Ok { message: String },
|
||||||
|
|||||||
@@ -1571,11 +1571,17 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let response = match serde_json::from_str::<CtlRequest>(line.trim()) {
|
let response = match serde_json::from_str::<CtlRequest>(line.trim()) {
|
||||||
Ok(CtlRequest::Status) => CtlResponse::Status {
|
Ok(CtlRequest::Status) => {
|
||||||
username: username.to_string(),
|
let pins = core.cache.list_pins();
|
||||||
mountpoint: mountpoint.display().to_string(),
|
CtlResponse::Status {
|
||||||
pinned: core.cache.list_pins().len(),
|
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(CtlRequest::Pin { path }) => match rel_to_mount(mountpoint, &path) {
|
||||||
Ok(rel) => match core.pin(&rel) {
|
Ok(rel) => match core.pin(&rel) {
|
||||||
Ok(name) => CtlResponse::Ok {
|
Ok(name) => CtlResponse::Ok {
|
||||||
|
|||||||
+137
-75
@@ -8,13 +8,14 @@
|
|||||||
//! * a **login form** (email / password / optional 2FA) driving
|
//! * a **login form** (email / password / optional 2FA) driving
|
||||||
//! [`pdfs_core::auth::login`]; logging in enables the systemd mount service;
|
//! [`pdfs_core::auth::login`]; logging in enables the systemd mount service;
|
||||||
//! * a **read-only mount status** line (the mount is managed automatically);
|
//! * 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.
|
//! * the **pin list** with per-file unpin.
|
||||||
//!
|
//!
|
||||||
//! State the GUI only displays (cache size, pins, login identity) is read
|
//! Mount status, cache usage and the pin list all ride along on one
|
||||||
//! straight off disk via `pdfs-core`, so the window is useful even with no mount
|
//! [`Request::Status`] round-trip to the daemon (which owns the cache), fetched on
|
||||||
//! daemon running; live mount status comes from the daemon over the control
|
//! a worker thread so the periodic refresh never blocks the GTK main loop. Login
|
||||||
//! socket the CLI and tray already speak.
|
//! 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::cell::{Cell, RefCell};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
@@ -29,8 +30,7 @@ use gtk4::glib;
|
|||||||
use gtk4::glib::BoxedAnyObject;
|
use gtk4::glib::BoxedAnyObject;
|
||||||
|
|
||||||
use pdfs_core::auth;
|
use pdfs_core::auth;
|
||||||
use pdfs_core::cache::ContentCache;
|
use pdfs_core::config::AppDirs;
|
||||||
use pdfs_core::config::{AppDirs, DEFAULT_CACHE_BUDGET_BYTES};
|
|
||||||
use pdfs_core::control::{DirEntry, PhotoItem, Request, Response, send};
|
use pdfs_core::control::{DirEntry, PhotoItem, Request, Response, send};
|
||||||
use pdfs_core::service;
|
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`]).
|
/// is still coming up (see [`load_browser`] / [`load_gallery`]).
|
||||||
const CONNECT_RETRY_INTERVAL: Duration = Duration::from_secs(2);
|
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
|
/// 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
|
/// resolved paths they act on. Wrapped in an [`Rc`] so handlers and the timeout
|
||||||
/// closure share one instance.
|
/// closure share one instance.
|
||||||
@@ -60,6 +68,13 @@ struct Ui {
|
|||||||
/// Keys (relative path / photo uid) of open requests currently in flight, so
|
/// 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.
|
/// a double-click on the same entry is a no-op instead of a second download.
|
||||||
opening: RefCell<HashSet<String>>,
|
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.
|
// Login page.
|
||||||
email: adw::EntryRow,
|
email: adw::EntryRow,
|
||||||
password: adw::PasswordEntryRow,
|
password: adw::PasswordEntryRow,
|
||||||
@@ -74,9 +89,12 @@ struct Ui {
|
|||||||
cache_bar: gtk4::ProgressBar,
|
cache_bar: gtk4::ProgressBar,
|
||||||
cache_label: gtk4::Label,
|
cache_label: gtk4::Label,
|
||||||
pins_group: adw::PreferencesGroup,
|
pins_group: adw::PreferencesGroup,
|
||||||
/// Rows currently shown under [`Self::pins_group`], so a refresh can clear
|
/// Rows currently shown under [`Self::pins_group`], retained so a refresh can
|
||||||
/// the previous batch before re-adding.
|
/// diff against them and only rebuild when the pin set actually changes.
|
||||||
pin_rows: RefCell<Vec<adw::ActionRow>>,
|
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
|
/// Whether the last refresh saw a live mount daemon. Gates the unpin
|
||||||
/// buttons, which need the daemon to evict + re-hydrate.
|
/// buttons, which need the daemon to evict + re-hydrate.
|
||||||
mounted: RefCell<bool>,
|
mounted: RefCell<bool>,
|
||||||
@@ -213,6 +231,8 @@ fn build_window(app: &adw::Application) {
|
|||||||
spinner: spinner.clone(),
|
spinner: spinner.clone(),
|
||||||
busy: Cell::new(0),
|
busy: Cell::new(0),
|
||||||
opening: RefCell::new(HashSet::new()),
|
opening: RefCell::new(HashSet::new()),
|
||||||
|
session: RefCell::new(auth::load().ok()),
|
||||||
|
status_inflight: Cell::new(false),
|
||||||
email: login_widgets.0,
|
email: login_widgets.0,
|
||||||
password: login_widgets.1,
|
password: login_widgets.1,
|
||||||
totp: login_widgets.2,
|
totp: login_widgets.2,
|
||||||
@@ -224,6 +244,7 @@ fn build_window(app: &adw::Application) {
|
|||||||
cache_label: main_widgets.3,
|
cache_label: main_widgets.3,
|
||||||
pins_group: main_widgets.4,
|
pins_group: main_widgets.4,
|
||||||
pin_rows: RefCell::new(Vec::new()),
|
pin_rows: RefCell::new(Vec::new()),
|
||||||
|
pins_state: RefCell::new(None),
|
||||||
mounted: RefCell::new(false),
|
mounted: RefCell::new(false),
|
||||||
switcher: switcher.clone(),
|
switcher: switcher.clone(),
|
||||||
browser_model: browser_widgets.0,
|
browser_model: browser_widgets.0,
|
||||||
@@ -485,6 +506,8 @@ fn wire_login(ui: &Rc<Ui>) {
|
|||||||
ui.login_status.set_text("");
|
ui.login_status.set_text("");
|
||||||
ui.password.set_text("");
|
ui.password.set_text("");
|
||||||
ui.totp.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.
|
// Enable+start the mount service now that we have a session.
|
||||||
service::enable_start();
|
service::enable_start();
|
||||||
refresh(&ui);
|
refresh(&ui);
|
||||||
@@ -543,6 +566,7 @@ fn wire_logout(ui: &Rc<Ui>, button: >k4::Button) {
|
|||||||
if let Err(e) = auth::logout() {
|
if let Err(e) = auth::logout() {
|
||||||
tracing::error!("logout failed: {e}");
|
tracing::error!("logout failed: {e}");
|
||||||
}
|
}
|
||||||
|
*ui.session.borrow_mut() = None;
|
||||||
refresh(&ui);
|
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 from the cached login identity, then kick an async mount-
|
||||||
/// repaint the window. Cheap, synchronous local reads — safe on the main loop.
|
/// 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>) {
|
fn refresh(ui: &Rc<Ui>) {
|
||||||
// Login identity decides which page is shown.
|
// Login identity decides which page is shown. Read the cached session — set
|
||||||
let session = auth::load().ok();
|
// at startup and on login/logout — never the keyring.
|
||||||
match &session {
|
{
|
||||||
Some(s) => {
|
let session = ui.session.borrow();
|
||||||
// Only pull the user onto "main" when they're sitting on the login
|
match session.as_ref() {
|
||||||
// page; otherwise leave whichever signed-in page they navigated to.
|
Some(s) => {
|
||||||
if ui.stack.visible_child_name().as_deref() == Some("login") {
|
// Only pull the user onto "main" when they're sitting on the login
|
||||||
ui.stack.set_visible_child_name("main");
|
// 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".
|
refresh_status(ui);
|
||||||
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}")),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the rows under the pins group with one [`adw::ActionRow`] per pin,
|
/// Fetch mount status + cache stats from the daemon on a worker thread and repaint
|
||||||
/// each with an unpin button enabled only while a mount daemon is running.
|
/// the mount line, cache bar and pin list on the reply. The daemon owns the cache
|
||||||
fn repaint_pins(ui: &Rc<Ui>, pins: &[pdfs_core::cache::Pin]) {
|
/// stats now (`used`/`budget`/`pins` ride along on [`Response::Status`]), so the
|
||||||
for row in ui.pin_rows.borrow_mut().drain(..) {
|
/// GUI never opens the on-disk cache itself. Skipped while a fetch is in flight so
|
||||||
ui.pins_group.remove(&row);
|
/// 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() {
|
if pins.is_empty() {
|
||||||
let row = adw::ActionRow::builder()
|
let row = adw::ActionRow::builder()
|
||||||
.title("No pinned files")
|
.title("No pinned files")
|
||||||
.subtitle("Right-click a file in the mount to keep it offline.")
|
.subtitle("Right-click a file in the mount to keep it offline.")
|
||||||
.build();
|
.build();
|
||||||
ui.pins_group.add(&row);
|
ui.pins_group.add(&row);
|
||||||
ui.pin_rows.borrow_mut().push(row);
|
ui.pin_rows.borrow_mut().push(PinRow { row, unpin: None });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mounted = *ui.mounted.borrow();
|
|
||||||
for pin in pins {
|
for pin in pins {
|
||||||
let name = Path::new(&pin.path)
|
let name = Path::new(&pin.path)
|
||||||
.file_name()
|
.file_name()
|
||||||
@@ -677,7 +736,10 @@ fn repaint_pins(ui: &Rc<Ui>, pins: &[pdfs_core::cache::Pin]) {
|
|||||||
row.add_suffix(&unpin);
|
row.add_suffix(&unpin);
|
||||||
|
|
||||||
ui.pins_group.add(&row);
|
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