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:
parent
19a95b7c45
commit
5e7eed9638
3 changed files with 79 additions and 53 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue