Fix feed OOM: lazy image loading, inView gating, WebKit memory tuning

- NoteContent: remove ImageGrid from inline mode — images now only load
  when note is inView (via mediaOnly), stopping runaway scrolling leak
- NoteCard: content-visibility:auto to skip layout/paint for off-screen
  cards; inView-gated media, NoteActions, NIP-05 verification
- useInView: new IntersectionObserver hook with 300px rootMargin
- useProfile: MAX_PROFILE_CONCURRENT=8 throttle with fetch queue
- useReplyCount/useZapCount/useReactions: enabled param, throttled queues
- feed.ts: MAX_FEED_SIZE 200→30, live sub disabled (pendingNotes pattern),
  250ms batch debounce on live events
- core.ts: MAX_CONCURRENT_FETCHES=25 global NDK cap, fetchWithTimeout
  uses subscribe+stop instead of fetchEvents (no zombie subscriptions)
- lib.rs: HardwareAccelerationPolicy::Never + CacheModel::DocumentViewer
- main.rs: WEBKIT_DISABLE_COMPOSITING_MODE=1 for Linux
- relay/db.rs: TTL eviction + 5000 event cap
- feedDiagnostics.ts: file-flushing diag log survives crashes
This commit is contained in:
Jure
2026-04-15 20:36:14 +02:00
parent 018ee0e0f3
commit 0894389fe0
20 changed files with 540 additions and 105 deletions
+52
View File
@@ -3,6 +3,14 @@ use crate::relay::filter::Filter;
use rusqlite::{params, Connection};
use std::path::Path;
/// Keep at most this many text notes (kind 1) in the local relay cache.
/// Older notes beyond this limit are evicted on startup to bound memory usage.
const MAX_KIND1_EVENTS: usize = 5_000;
/// Delete text notes (kind 1) older than this many seconds on startup.
/// 7 days — remote relays have anything older.
const KIND1_TTL_SECS: i64 = 7 * 24 * 3600;
pub fn open_relay_db(data_dir: &Path) -> rusqlite::Result<Connection> {
std::fs::create_dir_all(data_dir).ok();
let path = data_dir.join("relay.db");
@@ -34,9 +42,53 @@ pub fn open_relay_db(data_dir: &Path) -> rusqlite::Result<Connection> {
CREATE INDEX IF NOT EXISTS idx_tags_name_value ON event_tags(tag_name, tag_value);
CREATE INDEX IF NOT EXISTS idx_tags_event ON event_tags(event_id);",
)?;
evict_old_events(&conn)?;
Ok(conn)
}
/// Remove stale text notes on startup to keep the relay cache bounded.
///
/// Two passes:
/// 1. Delete all kind-1 events older than KIND1_TTL_SECS (7 days).
/// 2. If more than MAX_KIND1_EVENTS remain, delete the oldest ones beyond that cap.
///
/// Other kinds (profiles, contact lists, etc.) are not evicted — they are
/// replaceable/parameterized-replaceable and stay small by design.
fn evict_old_events(conn: &Connection) -> rusqlite::Result<()> {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
// Pass 1: TTL — delete kind-1 events older than 7 days
let cutoff = now - KIND1_TTL_SECS;
conn.execute(
"DELETE FROM events WHERE kind = 1 AND created_at < ?1",
params![cutoff],
)?;
// Pass 2: count cap — keep only the most recent MAX_KIND1_EVENTS kind-1 events
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM events WHERE kind = 1",
[],
|row| row.get(0),
)?;
if count > MAX_KIND1_EVENTS as i64 {
conn.execute(
"DELETE FROM events WHERE kind = 1 AND id NOT IN (
SELECT id FROM events WHERE kind = 1
ORDER BY created_at DESC LIMIT ?1
)",
params![MAX_KIND1_EVENTS as i64],
)?;
}
Ok(())
}
/// Store an event. Returns true if the event was newly inserted, false if it already existed.
/// Handles replaceable (kind 0/3/10000-19999) and parameterized replaceable (30000-39999) events.
pub fn store_event(conn: &Connection, event: &Event, raw: &str) -> rusqlite::Result<bool> {