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) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-04-16 17:19:05 -04:00
parent 19a95b7c45
commit 5e7eed9638
3 changed files with 79 additions and 53 deletions

View file

@ -29,7 +29,7 @@ pub struct Node {
pub node_id: NodeId,
pub blob_store: Arc<BlobStore>,
secret_seed: [u8; 32],
bootstrap_anchors: Vec<(NodeId, iroh::EndpointAddr)>,
bootstrap_anchors: tokio::sync::Mutex<Vec<(NodeId, iroh::EndpointAddr)>>,
#[allow(dead_code)]
profile: DeviceProfile,
pub activity_log: Arc<std::sync::Mutex<ActivityLog>>,
@ -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)

View file

@ -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));

View file

@ -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;