mirror of
https://github.com/hoornet/vega.git
synced 2026-05-14 22:08:36 -07:00
Fix feed OOM: cap follow feed at 30, WebKit software rendering, dedup notification fetches
- followNotes capped at 30 (was 80) — following feed was rendering 2.7x more notes than global, causing 4GB+ spike on media-heavy follow content - fetchFollowFeed limit 80→30 to match - WEBKIT_FORCE_SOFTWARE_RENDERING=1 replaces WEBKIT_DISABLE_COMPOSITING_MODE=1 (compositing mode killed Wayland path → blank window on Hyprland) - HardwareAccelerationPolicy::Never → OnDemand (Never also caused blank screen) - set_enable_page_cache(false) — SPA never navigates, bfcache is pure waste - Removed duplicate fetchNotifications calls on login (was firing 3x in 8s) - First notification poll delayed 8s→90s to avoid competing with feed load - Result: login 3600MB→453MB, following feed crash→737MB, plateau at ~950MB
This commit is contained in:
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -5320,7 +5320,7 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vega"
|
name = "vega"
|
||||||
version = "0.12.6"
|
version = "0.12.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
|
|||||||
@@ -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")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
let main_window = app.get_webview_window("main").unwrap();
|
let main_window = app.get_webview_window("main").unwrap();
|
||||||
@@ -444,15 +444,20 @@ pub fn run() {
|
|||||||
use webkit2gtk::{CacheModel, SettingsExt, WebContextExt, WebViewExt};
|
use webkit2gtk::{CacheModel, SettingsExt, WebContextExt, WebViewExt};
|
||||||
let wv = webview.inner();
|
let wv = webview.inner();
|
||||||
if let Some(settings) = wv.settings() {
|
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(
|
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() {
|
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);
|
ctx.set_cache_model(CacheModel::DocumentViewer);
|
||||||
}
|
}
|
||||||
}).ok();
|
}).ok();
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ fn main() {
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||||
// Required on Linux with large RAM/swap: WebKitGTK compositor pre-allocates
|
// Force CPU-only rasterization while keeping the Wayland compositor path intact.
|
||||||
// ~25% of total virtual memory (RAM+swap) for its tile cache. On a 14GB RAM +
|
// WEBKIT_DISABLE_COMPOSITING_MODE=1 kills the GPU process but also breaks Wayland
|
||||||
// 19GB swap system this is ~4 GB, filling all RAM and freezing the machine.
|
// rendering (blank window on Hyprland). WEBKIT_FORCE_SOFTWARE_RENDERING=1 cuts GPU
|
||||||
// Software rendering is slower but memory-safe. Fix: reduce swap or implement
|
// RAM without disrupting the Wayland surface — the right tradeoff on this machine.
|
||||||
// virtual scrolling (fewer compositor layers).
|
std::env::set_var("WEBKIT_FORCE_SOFTWARE_RENDERING", "1");
|
||||||
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vega_lib::run()
|
vega_lib::run()
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function Feed() {
|
|||||||
try {
|
try {
|
||||||
await ensureConnected();
|
await ensureConnected();
|
||||||
const events = await diagWrapFetch("follow_fetch", () => fetchFollowFeed(follows));
|
const events = await diagWrapFetch("follow_fetch", () => fetchFollowFeed(follows));
|
||||||
setFollowNotes(events);
|
setFollowNotes(events.slice(0, 30));
|
||||||
const prev = useFeedStore.getState().lastUpdated;
|
const prev = useFeedStore.getState().lastUpdated;
|
||||||
useFeedStore.setState({ lastUpdated: { ...prev, following: Date.now() } });
|
useFeedStore.setState({ lastUpdated: { ...prev, following: Date.now() } });
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const NoteCard = memo(function NoteCard({ event, focused, onReplyInThread
|
|||||||
<article
|
<article
|
||||||
ref={cardRef}
|
ref={cardRef}
|
||||||
data-note-id={event.id}
|
data-note-id={event.id}
|
||||||
className={`border-b border-border px-4 py-3 hover:bg-bg-hover transition-colors cursor-pointer group/card [content-visibility:auto] [contain-intrinsic-size:auto_120px]${focused ? " bg-accent/10 border-l-2 border-l-accent" : ""}`}
|
className={`border-b border-border px-4 py-3 hover:bg-bg-hover transition-colors cursor-pointer group/card${focused ? " bg-accent/10 border-l-2 border-l-accent" : ""}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
// Don't navigate if clicking on interactive elements
|
// Don't navigate if clicking on interactive elements
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export async function fetchMediaFeed(limit: number = 500): Promise<NDKEvent[]> {
|
|||||||
return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
|
return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchFollowFeed(pubkeys: string[], limit = 80): Promise<NDKEvent[]> {
|
export async function fetchFollowFeed(pubkeys: string[], limit = 30): Promise<NDKEvent[]> {
|
||||||
if (pubkeys.length === 0) return [];
|
if (pubkeys.length === 0) return [];
|
||||||
const instance = getNDK();
|
const instance = getNDK();
|
||||||
const since = Math.floor(Date.now() / 1000) - 24 * 3600; // last 24h for follows
|
const since = Math.floor(Date.now() / 1000) - 24 * 3600; // last 24h for follows
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ async function pollOnce(pubkey: string) {
|
|||||||
const name = await getProfileName(e.pubkey);
|
const name = await getProfileName(e.pubkey);
|
||||||
notifyMention(name, e.content?.slice(0, 120) || "mentioned you").catch(() => {});
|
notifyMention(name, e.content?.slice(0, 120) || "mentioned you").catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
// Only refresh the full store when there's actually something new to show
|
||||||
// Also update the notifications store
|
|
||||||
useNotificationsStore.getState().fetchNotifications(pubkey).catch(() => {});
|
useNotificationsStore.getState().fetchNotifications(pubkey).catch(() => {});
|
||||||
|
}
|
||||||
} catch { /* non-critical */ }
|
} catch { /* non-critical */ }
|
||||||
|
|
||||||
// Zaps
|
// Zaps
|
||||||
@@ -107,18 +107,15 @@ export function startNotificationPoller(pubkey: string) {
|
|||||||
// Instant: load cached notifications from DB (no flicker)
|
// Instant: load cached notifications from DB (no flicker)
|
||||||
useNotificationsStore.getState().loadFromDb(pubkey);
|
useNotificationsStore.getState().loadFromDb(pubkey);
|
||||||
|
|
||||||
// Then connect to relays and fetch new data in background
|
// The full fetchNotifications() sweep is already called by the login flow
|
||||||
(async () => {
|
// (loginWithNsec / loginWithPubkey / restoreSession) before this function runs.
|
||||||
try {
|
// Starting it again here would fire two concurrent 7-day sweeps on every login.
|
||||||
const connected = await ensureConnected();
|
// We only need the incremental pollOnce() loop from here on.
|
||||||
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(() => {});
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Run first full poll after a longer delay (give relays more time)
|
// Delay the first poll to give the app and relays time to fully initialize.
|
||||||
setTimeout(() => pollOnce(pubkey).catch(() => {}), 8000);
|
// 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);
|
intervalId = setInterval(() => pollOnce(pubkey).catch(() => {}), POLL_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user