diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ca383c8..64abe15 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -5320,7 +5320,7 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vega" -version = "0.12.6" +version = "0.12.7" dependencies = [ "futures-util", "hex", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 47507aa..0eb0add 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -436,7 +436,7 @@ pub fn run() { } } - // ── WebKit GPU workaround for Linux (webkit2gtk 2.50+ black screen) ── + // ── WebKit memory tuning for Linux (webkit2gtk) ────────────────── #[cfg(target_os = "linux")] { let main_window = app.get_webview_window("main").unwrap(); @@ -444,15 +444,20 @@ pub fn run() { use webkit2gtk::{CacheModel, SettingsExt, WebContextExt, WebViewExt}; let wv = webview.inner(); if let Some(settings) = wv.settings() { + // OnDemand: use GPU if available, CPU fallback otherwise. + // HardwareAccelerationPolicy::Never + WEBKIT_DISABLE_COMPOSITING_MODE=1 + // both kill the Wayland compositor path → blank window on Hyprland. + // WEBKIT_FORCE_SOFTWARE_RENDERING=1 (set in main.rs) forces CPU + // rasterization without disrupting the Wayland surface. settings.set_hardware_acceleration_policy( - webkit2gtk::HardwareAccelerationPolicy::Never, + webkit2gtk::HardwareAccelerationPolicy::OnDemand, ); + // Vega is a SPA — no back/forward navigation, page cache is pure waste. + settings.set_enable_page_cache(false); } - // Minimize WebKit's in-memory content cache (decoded images, scripts, etc.) - // Default is WebBrowser which caches aggressively. DocumentViewer is the - // minimum: no back/forward page cache, smallest memory footprint. - // This is safe for Vega — it's a single-page app, never navigates between pages. if let Some(ctx) = wv.context() { + // DocumentViewer: smallest in-memory content cache footprint. + // No back/forward page cache, only the active document cached. ctx.set_cache_model(CacheModel::DocumentViewer); } }).ok(); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 54fd2af..0f4a7cf 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -7,12 +7,11 @@ fn main() { #[cfg(target_os = "linux")] { std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); - // Required on Linux with large RAM/swap: WebKitGTK compositor pre-allocates - // ~25% of total virtual memory (RAM+swap) for its tile cache. On a 14GB RAM + - // 19GB swap system this is ~4 GB, filling all RAM and freezing the machine. - // Software rendering is slower but memory-safe. Fix: reduce swap or implement - // virtual scrolling (fewer compositor layers). - std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); + // Force CPU-only rasterization while keeping the Wayland compositor path intact. + // WEBKIT_DISABLE_COMPOSITING_MODE=1 kills the GPU process but also breaks Wayland + // rendering (blank window on Hyprland). WEBKIT_FORCE_SOFTWARE_RENDERING=1 cuts GPU + // RAM without disrupting the Wayland surface — the right tradeoff on this machine. + std::env::set_var("WEBKIT_FORCE_SOFTWARE_RENDERING", "1"); } vega_lib::run() diff --git a/src/components/feed/Feed.tsx b/src/components/feed/Feed.tsx index b32f971..2d7ea91 100644 --- a/src/components/feed/Feed.tsx +++ b/src/components/feed/Feed.tsx @@ -89,7 +89,7 @@ export function Feed() { try { await ensureConnected(); const events = await diagWrapFetch("follow_fetch", () => fetchFollowFeed(follows)); - setFollowNotes(events); + setFollowNotes(events.slice(0, 30)); const prev = useFeedStore.getState().lastUpdated; useFeedStore.setState({ lastUpdated: { ...prev, following: Date.now() } }); } finally { diff --git a/src/components/feed/NoteCard.tsx b/src/components/feed/NoteCard.tsx index 2253f39..09fc301 100644 --- a/src/components/feed/NoteCard.tsx +++ b/src/components/feed/NoteCard.tsx @@ -71,7 +71,7 @@ export const NoteCard = memo(function NoteCard({ event, focused, onReplyInThread
{ // Don't navigate if clicking on interactive elements const target = e.target as HTMLElement; diff --git a/src/lib/nostr/notes.ts b/src/lib/nostr/notes.ts index 09ccde5..a09a839 100644 --- a/src/lib/nostr/notes.ts +++ b/src/lib/nostr/notes.ts @@ -20,7 +20,7 @@ export async function fetchMediaFeed(limit: number = 500): Promise { return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)); } -export async function fetchFollowFeed(pubkeys: string[], limit = 80): Promise { +export async function fetchFollowFeed(pubkeys: string[], limit = 30): Promise { if (pubkeys.length === 0) return []; const instance = getNDK(); const since = Math.floor(Date.now() / 1000) - 24 * 3600; // last 24h for follows diff --git a/src/lib/notificationPoller.ts b/src/lib/notificationPoller.ts index 45e570b..c256983 100644 --- a/src/lib/notificationPoller.ts +++ b/src/lib/notificationPoller.ts @@ -44,9 +44,9 @@ async function pollOnce(pubkey: string) { const name = await getProfileName(e.pubkey); notifyMention(name, e.content?.slice(0, 120) || "mentioned you").catch(() => {}); } + // Only refresh the full store when there's actually something new to show + useNotificationsStore.getState().fetchNotifications(pubkey).catch(() => {}); } - // Also update the notifications store - useNotificationsStore.getState().fetchNotifications(pubkey).catch(() => {}); } catch { /* non-critical */ } // Zaps @@ -107,18 +107,15 @@ export function startNotificationPoller(pubkey: string) { // Instant: load cached notifications from DB (no flicker) useNotificationsStore.getState().loadFromDb(pubkey); - // Then connect to relays and fetch new data in background - (async () => { - try { - const connected = await ensureConnected(); - debug.log("notif:poller ensureConnected →", connected); - } catch { /* continue anyway */ } - debug.log("notif:poller initial fetch for", pubkey.slice(0, 8)); - useNotificationsStore.getState().fetchNotifications(pubkey).catch(() => {}); - })(); + // The full fetchNotifications() sweep is already called by the login flow + // (loginWithNsec / loginWithPubkey / restoreSession) before this function runs. + // Starting it again here would fire two concurrent 7-day sweeps on every login. + // We only need the incremental pollOnce() loop from here on. - // Run first full poll after a longer delay (give relays more time) - setTimeout(() => pollOnce(pubkey).catch(() => {}), 8000); + // Delay the first poll to give the app and relays time to fully initialize. + // 8s was too aggressive — it would fire a heavy fetchNotifications during the + // initial feed load, contributing to the login memory spike. + setTimeout(() => pollOnce(pubkey).catch(() => {}), 90_000); intervalId = setInterval(() => pollOnce(pubkey).catch(() => {}), POLL_INTERVAL); }