From 5e7eed9638c3b87ded7dc56973dcc41fdc536c02 Mon Sep 17 00:00:00 2001 From: Scott Reimers Date: Thu, 16 Apr 2026 17:19:05 -0400 Subject: [PATCH] Fast startup: defer bootstrap to background, lazy feed load - Node::open_with_bind no longer runs bootstrap (anchor connect, NAT probe, referrals). New run_bootstrap() method called from background task after UI is live. - Background tasks (pull cycle, diff cycle, etc.) start after bootstrap completes, not during block_on. - Feed no longer pre-loaded during welcome screen readiness check. Ready button enables immediately after get_node_info succeeds. - Feed loads on tab switch, not during startup. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/core/src/node.rs | 79 ++++++++++++++++++++++--------------- crates/tauri-app/src/lib.rs | 48 +++++++++++++--------- frontend/app.js | 5 +-- 3 files changed, 79 insertions(+), 53 deletions(-) diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index 9ae8cfa..30d8a5b 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -29,7 +29,7 @@ pub struct Node { pub node_id: NodeId, pub blob_store: Arc, secret_seed: [u8; 32], - bootstrap_anchors: Vec<(NodeId, iroh::EndpointAddr)>, + bootstrap_anchors: tokio::sync::Mutex>, #[allow(dead_code)] profile: DeviceProfile, pub activity_log: Arc>, @@ -113,6 +113,48 @@ impl Node { s.add_follow(&node_id)?; } + // Build the node (fast path — no network I/O beyond endpoint creation) + let activity_log_ref = Arc::clone(&activity_log); + let last_rebalance_ms = Arc::new(AtomicU64::new(0)); + let last_anchor_register_ms = Arc::new(AtomicU64::new(0)); + + let role = network.device_role(); + let (replication_budget, delivery_budget) = (role.replication_limit(), role.delivery_limit()); + let replication_budget_remaining = Arc::new(AtomicU64::new(replication_budget)); + let delivery_budget_remaining = Arc::new(AtomicU64::new(delivery_budget)); + let budget_last_reset_ms = Arc::new(AtomicU64::new( + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default().as_millis() as u64 + )); + blob_store.set_delivery_budget(delivery_budget); + + let mut node = Self { + data_dir: data_dir.clone(), + storage: Arc::clone(&storage), + network: Arc::clone(&network), + node_id, + blob_store, + secret_seed, + bootstrap_anchors: tokio::sync::Mutex::new(Vec::new()), + profile, + activity_log: activity_log_ref, + last_rebalance_ms, + last_anchor_register_ms, + replication_budget_remaining, + delivery_budget_remaining, + budget_last_reset_ms, + }; + + Ok(node) + } + + /// Bootstrap: connect to anchors, pull initial data, NAT probe, referrals. + /// Can be called during open_with_bind (blocking startup) or deferred to background. + pub async fn run_bootstrap(&self, data_dir: &Path) -> anyhow::Result<()> { + let storage = &self.storage; + let network = &self.network; + let node_id = self.node_id; + // Bootstrap: if peers table is empty, try bootstrap.json then default anchor { let s = storage.get().await; @@ -426,35 +468,10 @@ impl Node { } } - // Initialize CDN replication budgets based on device role - let role = network.device_role(); - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as u64; - let replication_budget_remaining = Arc::new(AtomicU64::new(role.replication_limit())); - let delivery_budget_remaining = Arc::new(AtomicU64::new(role.delivery_limit())); - let budget_last_reset_ms = Arc::new(AtomicU64::new(now)); + // Store bootstrap anchors on the node + *self.bootstrap_anchors.lock().await = bootstrap_anchors; - // Set delivery budget on blob store (shared with ConnectionManager) - blob_store.set_delivery_budget(role.delivery_limit()); - - Ok(Self { - data_dir, - storage, - network, - node_id, - blob_store, - secret_seed, - bootstrap_anchors, - profile, - activity_log, - last_rebalance_ms, - last_anchor_register_ms, - replication_budget_remaining, - delivery_budget_remaining, - budget_last_reset_ms, - }) + Ok(()) } /// Get recent activity events (for diagnostics UI). @@ -2487,8 +2504,8 @@ impl Node { storage.list_peer_records() } - pub fn list_bootstrap_anchors(&self) -> &[(NodeId, iroh::EndpointAddr)] { - &self.bootstrap_anchors + pub async fn list_bootstrap_anchors(&self) -> Vec<(NodeId, iroh::EndpointAddr)> { + self.bootstrap_anchors.lock().await.clone() } /// Get connection info for display: (node_id, slot_kind, connected_at) diff --git a/crates/tauri-app/src/lib.rs b/crates/tauri-app/src/lib.rs index 3e830e1..67a6671 100644 --- a/crates/tauri-app/src/lib.rs +++ b/crates/tauri-app/src/lib.rs @@ -2464,33 +2464,43 @@ pub fn run() { Arc::clone(mgr.active_node().expect("just created")) }; - // Start background networking - n.start_accept_loop(); - n.start_pull_cycle(300); - n.start_diff_cycle(120); - n.start_rebalance_cycle(600); - n.start_growth_loop(); - n.start_recovery_loop(); - n.start_social_checkin_cycle(3600); - n.start_anchor_register_cycle(600); - n.start_upnp_renewal_cycle(); - n.start_upnp_tcp_renewal_cycle(); - n.start_http_server(); - n.start_bootstrap_connectivity_check(); - n.start_replication_cycle(600); + Ok::<_, anyhow::Error>((n, mgr)) + })?; + + // Start bootstrap + background tasks AFTER setup completes (non-blocking) + let boot_node = Arc::clone(&node); + let boot_data_dir = data_dir.clone(); + tauri::async_runtime::spawn(async move { + // Bootstrap: connect to anchors, NAT probe, referrals (slow — runs in background) + if let Err(e) = boot_node.run_bootstrap(&boot_data_dir).await { + tracing::warn!(error = %e, "Background bootstrap failed"); + } + + // Start all background networking tasks + boot_node.start_accept_loop(); + boot_node.start_pull_cycle(300); + boot_node.start_diff_cycle(120); + boot_node.start_rebalance_cycle(600); + boot_node.start_growth_loop(); + boot_node.start_recovery_loop(); + boot_node.start_social_checkin_cycle(3600); + boot_node.start_anchor_register_cycle(600); + boot_node.start_upnp_renewal_cycle(); + boot_node.start_upnp_tcp_renewal_cycle(); + boot_node.start_http_server(); + boot_node.start_bootstrap_connectivity_check(); + boot_node.start_replication_cycle(600); let cache_max_bytes: u64 = { - let storage = n.storage.get().await; + let storage = boot_node.storage.get().await; storage.get_setting("cache_size_bytes") .ok() .flatten() .and_then(|s| s.parse().ok()) .unwrap_or(1_073_741_824u64) }; - Node::start_eviction_cycle(Arc::clone(&n), 300, cache_max_bytes); - - Ok::<_, anyhow::Error>((n, mgr)) - })?; + Node::start_eviction_cycle(Arc::clone(&boot_node), 300, cache_max_bytes); + }); // Manage both the swappable Node and the IdentityManager let app_node: AppNode = Arc::new(tokio::sync::RwLock::new(node)); diff --git a/frontend/app.js b/frontend/app.js index af165b6..3e59f03 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -3702,10 +3702,9 @@ async function init() { setupOverlay.classList.remove('hidden'); setupName.focus(); } - // Pre-load feed + messages from local DB (instant — no network needed) - await loadFeed(true).catch(() => {}); + // Pre-load messages (lightweight) — feed loads when user switches to it loadMessages(true).catch(() => {}); - // Mark ready button as clickable + // Mark ready button as clickable immediately — feed loads on tab switch if (readyBar) readyBar.style.width = '100%'; if (readyBtn) { readyBtn.disabled = false;