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 node_id: NodeId,
|
||||||
pub blob_store: Arc<BlobStore>,
|
pub blob_store: Arc<BlobStore>,
|
||||||
secret_seed: [u8; 32],
|
secret_seed: [u8; 32],
|
||||||
bootstrap_anchors: Vec<(NodeId, iroh::EndpointAddr)>,
|
bootstrap_anchors: tokio::sync::Mutex<Vec<(NodeId, iroh::EndpointAddr)>>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
profile: DeviceProfile,
|
profile: DeviceProfile,
|
||||||
pub activity_log: Arc<std::sync::Mutex<ActivityLog>>,
|
pub activity_log: Arc<std::sync::Mutex<ActivityLog>>,
|
||||||
|
|
@ -113,6 +113,48 @@ impl Node {
|
||||||
s.add_follow(&node_id)?;
|
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
|
// Bootstrap: if peers table is empty, try bootstrap.json then default anchor
|
||||||
{
|
{
|
||||||
let s = storage.get().await;
|
let s = storage.get().await;
|
||||||
|
|
@ -426,35 +468,10 @@ impl Node {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize CDN replication budgets based on device role
|
// Store bootstrap anchors on the node
|
||||||
let role = network.device_role();
|
*self.bootstrap_anchors.lock().await = bootstrap_anchors;
|
||||||
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));
|
|
||||||
|
|
||||||
// Set delivery budget on blob store (shared with ConnectionManager)
|
Ok(())
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get recent activity events (for diagnostics UI).
|
/// Get recent activity events (for diagnostics UI).
|
||||||
|
|
@ -2487,8 +2504,8 @@ impl Node {
|
||||||
storage.list_peer_records()
|
storage.list_peer_records()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_bootstrap_anchors(&self) -> &[(NodeId, iroh::EndpointAddr)] {
|
pub async fn list_bootstrap_anchors(&self) -> Vec<(NodeId, iroh::EndpointAddr)> {
|
||||||
&self.bootstrap_anchors
|
self.bootstrap_anchors.lock().await.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get connection info for display: (node_id, slot_kind, connected_at)
|
/// 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"))
|
Arc::clone(mgr.active_node().expect("just created"))
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start background networking
|
Ok::<_, anyhow::Error>((n, mgr))
|
||||||
n.start_accept_loop();
|
})?;
|
||||||
n.start_pull_cycle(300);
|
|
||||||
n.start_diff_cycle(120);
|
// Start bootstrap + background tasks AFTER setup completes (non-blocking)
|
||||||
n.start_rebalance_cycle(600);
|
let boot_node = Arc::clone(&node);
|
||||||
n.start_growth_loop();
|
let boot_data_dir = data_dir.clone();
|
||||||
n.start_recovery_loop();
|
tauri::async_runtime::spawn(async move {
|
||||||
n.start_social_checkin_cycle(3600);
|
// Bootstrap: connect to anchors, NAT probe, referrals (slow — runs in background)
|
||||||
n.start_anchor_register_cycle(600);
|
if let Err(e) = boot_node.run_bootstrap(&boot_data_dir).await {
|
||||||
n.start_upnp_renewal_cycle();
|
tracing::warn!(error = %e, "Background bootstrap failed");
|
||||||
n.start_upnp_tcp_renewal_cycle();
|
}
|
||||||
n.start_http_server();
|
|
||||||
n.start_bootstrap_connectivity_check();
|
// Start all background networking tasks
|
||||||
n.start_replication_cycle(600);
|
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 cache_max_bytes: u64 = {
|
||||||
let storage = n.storage.get().await;
|
let storage = boot_node.storage.get().await;
|
||||||
storage.get_setting("cache_size_bytes")
|
storage.get_setting("cache_size_bytes")
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.and_then(|s| s.parse().ok())
|
.and_then(|s| s.parse().ok())
|
||||||
.unwrap_or(1_073_741_824u64)
|
.unwrap_or(1_073_741_824u64)
|
||||||
};
|
};
|
||||||
Node::start_eviction_cycle(Arc::clone(&n), 300, cache_max_bytes);
|
Node::start_eviction_cycle(Arc::clone(&boot_node), 300, cache_max_bytes);
|
||||||
|
});
|
||||||
Ok::<_, anyhow::Error>((n, mgr))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Manage both the swappable Node and the IdentityManager
|
// Manage both the swappable Node and the IdentityManager
|
||||||
let app_node: AppNode = Arc::new(tokio::sync::RwLock::new(node));
|
let app_node: AppNode = Arc::new(tokio::sync::RwLock::new(node));
|
||||||
|
|
|
||||||
|
|
@ -3702,10 +3702,9 @@ async function init() {
|
||||||
setupOverlay.classList.remove('hidden');
|
setupOverlay.classList.remove('hidden');
|
||||||
setupName.focus();
|
setupName.focus();
|
||||||
}
|
}
|
||||||
// Pre-load feed + messages from local DB (instant — no network needed)
|
// Pre-load messages (lightweight) — feed loads when user switches to it
|
||||||
await loadFeed(true).catch(() => {});
|
|
||||||
loadMessages(true).catch(() => {});
|
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 (readyBar) readyBar.style.width = '100%';
|
||||||
if (readyBtn) {
|
if (readyBtn) {
|
||||||
readyBtn.disabled = false;
|
readyBtn.disabled = false;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue