diff --git a/studio/backend/main.py b/studio/backend/main.py
index 731625ca74..0a5b775775 100644
--- a/studio/backend/main.py
+++ b/studio/backend/main.py
@@ -441,9 +441,30 @@ def _start_llama_cpp_probes_if_enabled(app: FastAPI) -> None:
).start()
+def _warm_rag_embedder() -> None:
+ """Warm RAG embeddings without blocking backend readiness."""
+ try:
+ from storage import rag_db
+
+ if not rag_db.RAG_AVAILABLE:
+ return
+ from core.rag import embeddings
+
+ embeddings.warm()
+ except Exception:
+ pass
+
+
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Startup: detect hardware, seed default admin if needed. Shutdown: clean up compiled cache."""
+
+ import time as _time
+
+ _lifespan_started = _time.perf_counter()
+ import structlog as _structlog
+
+ _lifespan_log = _structlog.get_logger(__name__)
clear_unsloth_compiled_cache()
# Remove stale .venv_overlay from old versions; switching now uses .venv_t5/.
@@ -454,6 +475,11 @@ async def lifespan(app: FastAPI):
# Detect hardware first — sets the DEVICE global used everywhere.
detect_hardware()
+ _lifespan_log.info(
+ "lifespan hardware detection completed in %.1fms",
+ (_time.perf_counter() - _lifespan_started) * 1000,
+ )
+
# Apple Silicon with MLX missing => Train/Export are greyed out (chat-only).
# Reinstall mlx by name on a background thread (off the critical path) and
# re-detect, so a reinstall/update that dropped mlx self-heals. No-op
@@ -465,7 +491,13 @@ async def lifespan(app: FastAPI):
import structlog as _structlog
_structlog.get_logger(__name__).debug("mlx autorepair skipped: %s", _mlx_exc)
- # Reap download workers orphaned by a previous crash before new downloads start.
+ # Reap workers/runs orphaned by a previous crash before new work starts.
+ try:
+ from storage.studio_db import cleanup_orphaned_runs
+ cleanup_orphaned_runs()
+ except Exception as exc:
+ _lifespan_log.warning("cleanup_orphaned_runs failed at startup: %s", exc)
+
reap_hub_orphan_workers()
# llama.cpp probes: capability (MTP support) + freshness (release age).
@@ -479,45 +511,23 @@ async def lifespan(app: FastAPI):
app.state.llama_cpp_freshness = None
_start_llama_cpp_probes_if_enabled(app)
- from storage.studio_db import cleanup_orphaned_runs
-
- try:
- cleanup_orphaned_runs()
- except Exception as exc:
- import structlog
- structlog.get_logger(__name__).warning("cleanup_orphaned_runs failed at startup: %s", exc)
-
- # Same for RAG: fail ingestion jobs stranded mid-ingest by a crash.
try:
from storage.rag_db import reconcile_orphaned_ingestion_jobs
reconcile_orphaned_ingestion_jobs()
except Exception as exc:
- import structlog
- structlog.get_logger(__name__).warning(
- "reconcile_orphaned_ingestion_jobs failed at startup: %s", exc
- )
+ _lifespan_log.warning("reconcile_orphaned_ingestion_jobs failed at startup: %s", exc)
_start_helper_precache_if_enabled()
+ threading.Thread(target = _warm_rag_embedder, daemon = True, name = "rag-embedder-warm").start()
- # Warm the RAG embedder so the first upload skips the cold load. Non-fatal.
- def _warm_rag_embedder():
- try:
- from storage import rag_db
-
- if not rag_db.RAG_AVAILABLE:
- return
- from core.rag import embeddings
-
- embeddings.warm()
- except Exception:
- pass
-
- threading.Thread(target = _warm_rag_embedder, daemon = True).start()
-
- # Initialize RSA key pair for API key encryption (external providers)
+ # Initialize RSA key pair for API key encryption (external providers).
from core.inference.key_exchange import init_key_pair
init_key_pair()
+ _lifespan_log.info(
+ "lifespan pre-auth setup completed in %.1fms",
+ (_time.perf_counter() - _lifespan_started) * 1000,
+ )
if storage.ensure_default_admin():
bootstrap_pw = storage.get_bootstrap_password()
@@ -532,6 +542,11 @@ async def lifespan(app: FastAPI):
print("=" * 60 + "\n")
else:
app.state.bootstrap_password = storage.get_bootstrap_password()
+
+ _lifespan_log.info(
+ "lifespan startup completed in %.1fms",
+ (_time.perf_counter() - _lifespan_started) * 1000,
+ )
yield
from core.inference.llama_http import aclose as _close_llama_http
@@ -919,6 +934,21 @@ install_api_error_handlers(app)
# ============ Health and System Endpoints ============
+@app.get("/api/liveness")
+async def liveness_check():
+ """Cheap process liveness for desktop port validation."""
+ return {
+ "status": "alive",
+ "service": "Unsloth UI Backend",
+ "desktop_protocol_version": 1,
+ "desktop_manageability_version": 1,
+ "supports_desktop_auth": True,
+ "supports_desktop_backend_ownership": True,
+ "studio_root_id": _studio_root_id(),
+ **({"desktop_owner": owner} if (owner := _desktop_owner()) else {}),
+ }
+
+
@app.get("/api/health")
async def health_check(request: Request):
"""Liveness plus launcher capability bits; host fingerprint gated on a bearer.
diff --git a/studio/backend/run.py b/studio/backend/run.py
index d4cbc26b41..6f36f0928a 100644
--- a/studio/backend/run.py
+++ b/studio/backend/run.py
@@ -933,6 +933,9 @@ def run_server(
"""
global _server, _server_thread, _shutdown_event
+ boot_started = time.perf_counter()
+ logger.info("run_server startup begin api_only=%s host=%s port=%s", api_only, host, port)
+
# Reap every child if the parent dies abnormally (terminal close, Task
# Manager kill, SIGKILL); must run before any child can spawn.
from utils.process_lifetime import initialize_parent_lifetime
@@ -984,7 +987,14 @@ def run_server(
from threading import Thread, Event
import uvicorn
+ import_started = time.perf_counter()
+
from main import app, setup_frontend, _IS_COLAB
+
+ logger.info(
+ "Imported FastAPI app in %.1fms",
+ (time.perf_counter() - import_started) * 1000,
+ )
from utils.paths import ensure_studio_directories
# Allow local stdio MCP servers on a loopback bind (the user's own machine),
@@ -997,6 +1007,11 @@ def run_server(
# Create all standard directories on startup.
ensure_studio_directories()
+ logger.info(
+ "Ensured Studio directories in %.1fms",
+ (time.perf_counter() - boot_started) * 1000,
+ )
+
# Auto-find a free port if the requested one is in use.
if not _is_port_free(host, port):
original_port = port
@@ -1060,6 +1075,11 @@ def run_server(
display_host = _resolve_external_ip() if host == "0.0.0.0" else host
_install_uvicorn_startup_log_rewrite(host, display_host)
+ logger.info(
+ "run_server pre-uvicorn setup completed in %.1fms",
+ (time.perf_counter() - boot_started) * 1000,
+ )
+
ready_event = Event()
startup_failed = Event()
startup_errors = []
@@ -1068,6 +1088,10 @@ def run_server(
async def startup(self, *args, **kwargs):
await super().startup(*args, **kwargs)
if getattr(self, "started", False) and not self.should_exit:
+ logger.info(
+ "Uvicorn startup hook completed in %.1fms",
+ (time.perf_counter() - boot_started) * 1000,
+ )
ready_event.set()
# server_header=False suppresses uvicorn's "Server: uvicorn"; SecurityHeadersMiddleware sets its own.
@@ -1150,6 +1174,11 @@ def run_server(
_shutdown_event.set()
raise
+ logger.info(
+ "run_server uvicorn ready after %.1fms",
+ (time.perf_counter() - boot_started) * 1000,
+ )
+
_write_pid_file()
import atexit
diff --git a/studio/frontend/src/app/provider.tsx b/studio/frontend/src/app/provider.tsx
index 914abbbf1d..176665769d 100644
--- a/studio/frontend/src/app/provider.tsx
+++ b/studio/frontend/src/app/provider.tsx
@@ -354,12 +354,11 @@ function TauriWrapper({ children }: { children: ReactNode }) {
);
}
- const showApp = status === "running" && desktopAuthReady;
+ const showApp = status === "running";
+ const desktopBooting = status === "running" && !desktopAuthReady;
+ const showInteractiveApp = showApp && desktopAuthReady;
const startupStatus = status === "running" ? "starting" : status;
- const startupProgressDetail =
- status === "running" && !desktopAuthReady
- ? "Signing in to desktop session..."
- : progressDetail;
+ const startupProgressDetail = progressDetail;
const usesCustomTitlebar = shouldUseCustomWindowTitlebar();
const usesNativeMacTitlebar = shouldUseNativeMacWindowTitlebar();
const hidesTitlebarSidebar = HIDDEN_TITLEBAR_SIDEBAR_ROUTES.has(pathname);
@@ -369,12 +368,23 @@ function TauriWrapper({ children }: { children: ReactNode }) {
-
+ {showInteractiveApp ? : null}
-
- {children}
+ {showInteractiveApp ? : null}
+ {showInteractiveApp ? children : null}
+ {desktopBooting ? (
+
+
+
Preparing Studio
+
The local backend is ready. Signing in to your desktop session before loading chats.
+
+
+ Signing in to desktop session...
+
+
+ ) : null}
>
) : (
number | null,
shouldContinue: () => boolean,
): Promise {
@@ -91,15 +90,7 @@ async function waitForManagedServerReady(
continue;
}
- const healthy = await invoke("check_health", { port });
- if (!shouldContinue()) {
- return { status: "aborted" };
- }
- if (healthy && getPort() === port) {
- return { status: "ready", port };
- }
-
- await wait(MANAGED_STARTUP_POLL_MS);
+ return { status: "ready", port };
}
}
@@ -280,10 +271,9 @@ export function useTauriBackend() {
// backend/run.py keeps the 8888-8908 fallback via server-port/TAURI_PORT.
await invoke("start_managed_server", { port: 8888 });
- // Wait for the owned backend's server-port event. Don't attach to an
- // external backend if the managed start doesn't report a port.
- const startupResult = await waitForManagedServerReady(
- invoke,
+ // Rust emits server-port only after validating the desktop-owned process.
+ // Treat that as the UI handoff point instead of doing a second health poll.
+ const startupResult = await waitForManagedServerPort(
() => portRef.current,
() => startingRef.current,
);
diff --git a/studio/src-tauri/src/commands.rs b/studio/src-tauri/src/commands.rs
index 7c1ce611b8..6bc2116786 100644
--- a/studio/src-tauri/src/commands.rs
+++ b/studio/src-tauri/src/commands.rs
@@ -65,10 +65,18 @@ pub async fn desktop_preflight(
shutdown: tauri::State<'_, ShutdownFlag>,
diagnostics: tauri::State<'_, DiagnosticsState>,
) -> Result {
+ let started = Instant::now();
let (result, adopted_watchdog_generation) =
crate::preflight::desktop_preflight_result_with_state(state.inner()).await?;
diagnostics::record_preflight(&diagnostics, &result);
+ info!(
+ "desktop_preflight completed disposition={:?} port={:?} in {}ms",
+ result.disposition,
+ result.port,
+ started.elapsed().as_millis()
+ );
+
if let Some((generation, newly_adopted)) = adopted_watchdog_generation {
if newly_adopted {
if let Some(port) = result.port {
@@ -205,9 +213,17 @@ pub async fn start_managed_server(
port: u16,
) -> Result<(), String> {
info!("start_managed_server command called with port {}", port);
+
+ let started = Instant::now();
let diagnostics_state = diagnostics.inner().clone();
let generation = process::start_backend(&app, &state, port, &shutdown, &diagnostics_state)?;
+ info!(
+ "start_managed_server spawned generation={} in {}ms",
+ generation,
+ started.elapsed().as_millis()
+ );
+
let watchdog_state = state.inner().clone();
let watchdog_shutdown = shutdown.inner().clone();
let watchdog_app = app.clone();
diff --git a/studio/src-tauri/src/desktop_backend_owner.rs b/studio/src-tauri/src/desktop_backend_owner.rs
index ed4b5f7fd9..c7d0a7b309 100644
--- a/studio/src-tauri/src/desktop_backend_owner.rs
+++ b/studio/src-tauri/src/desktop_backend_owner.rs
@@ -93,7 +93,7 @@ enum PreviousAppPidStatus {
Uncertain,
}
-#[derive(Debug, Deserialize)]
+#[derive(Clone, Debug, Deserialize)]
struct HealthDesktopOwner {
kind: Option,
token_sha256: Option,
@@ -101,15 +101,7 @@ struct HealthDesktopOwner {
#[derive(Debug, Deserialize)]
struct HealthResponse {
- status: Option,
- service: Option,
version: Option,
- desktop_protocol_version: Option,
- desktop_manageability_version: Option,
- supports_desktop_auth: Option,
- supports_desktop_backend_ownership: Option,
- studio_root_id: Option,
- desktop_owner: Option,
}
#[derive(Debug)]
@@ -123,6 +115,18 @@ struct DesktopLoginPayload<'a> {
secret: &'a str,
}
+#[derive(Clone, Debug, Deserialize)]
+pub(crate) struct DesktopLiveness {
+ status: Option,
+ service: Option,
+ desktop_protocol_version: Option,
+ desktop_manageability_version: Option,
+ supports_desktop_auth: Option,
+ supports_desktop_backend_ownership: Option,
+ studio_root_id: Option,
+ desktop_owner: Option,
+}
+
#[derive(Deserialize)]
struct TokenResponse {
access_token: String,
@@ -290,10 +294,10 @@ impl BackendOwnerState {
}
pub(crate) fn verifies_exact_port_blocking(&self, port: u16) -> bool {
- match fetch_health_blocking(port) {
- Ok(Some(health)) => {
- health_verifies_metadata(&health, &self.metadata)
- && lifecycle_control_block_reason(&health).is_none()
+ match fetch_liveness_blocking(port) {
+ Ok(Some(liveness)) => {
+ liveness_verifies_metadata(&liveness, &self.metadata)
+ && lifecycle_control_block_reason(&liveness).is_none()
}
_ => false,
}
@@ -498,66 +502,110 @@ pub(crate) fn test_owner_state(root_id: &str, token: &str, port: u16) -> Backend
}
}
-fn health_verifies_metadata(health: &HealthResponse, metadata: &DesktopBackendMetadata) -> bool {
- let healthy = health.status.as_deref() == Some("healthy")
- && health.service.as_deref() == Some("Unsloth UI Backend");
- let Some(owner) = health.desktop_owner.as_ref() else {
+fn liveness_verifies_metadata(
+ liveness: &DesktopLiveness,
+ metadata: &DesktopBackendMetadata,
+) -> bool {
+ let alive = matches!(liveness.status.as_deref(), Some("alive") | Some("healthy"))
+ && liveness.service.as_deref() == Some("Unsloth UI Backend");
+ let Some(owner) = liveness.desktop_owner.as_ref() else {
return false;
};
- healthy
+ alive
&& owner_matches_metadata(
metadata,
- health.studio_root_id.as_deref(),
+ liveness.studio_root_id.as_deref(),
owner.kind.as_deref(),
owner.token_sha256.as_deref(),
)
}
-fn lifecycle_control_block_reason(health: &HealthResponse) -> Option {
- if health.desktop_protocol_version != Some(crate::preflight::DESKTOP_PROTOCOL_VERSION) {
+fn lifecycle_control_block_reason(liveness: &DesktopLiveness) -> Option {
+ if liveness.desktop_protocol_version != Some(crate::preflight::DESKTOP_PROTOCOL_VERSION) {
return Some("desktop_protocol_incompatible".to_string());
}
- if health.supports_desktop_auth != Some(true) {
+ if liveness.supports_desktop_auth != Some(true) {
return Some("desktop_auth_unsupported".to_string());
}
- if health.desktop_manageability_version.unwrap_or(0)
+ if liveness.desktop_manageability_version.unwrap_or(0)
< crate::preflight::DESKTOP_MANAGEABILITY_VERSION
{
return Some("desktop_manageability_unsupported".to_string());
}
- if health.supports_desktop_backend_ownership != Some(true) {
+ if liveness.supports_desktop_backend_ownership != Some(true) {
return Some("desktop_backend_ownership_unsupported".to_string());
}
None
}
-fn ready_for_use_status(health: &HealthResponse) -> OwnedBackendReadiness {
- match crate::preflight::backend_version_stale_reason(health.version.as_deref()) {
+fn ready_for_use_status(health: Option<&HealthResponse>) -> OwnedBackendReadiness {
+ let version = health
+ .and_then(|h| h.version.as_deref())
+ .filter(|v| !v.is_empty());
+ match crate::preflight::backend_version_stale_reason(version) {
Some(reason) => OwnedBackendReadiness::Stale { reason },
None => OwnedBackendReadiness::Ready,
}
}
-async fn fetch_health(port: u16) -> Result