From 2d389af7279ae0b51c878eac6869899e7ded1bce Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Sun, 3 May 2026 02:57:42 +0200 Subject: [PATCH] Defer Office desktop startup Make the Office canvas mount passive so Xpra starts only when the Desktop surface is opened or an official Office document is created/opened. Track Desktop host visibility to unload hidden frames, stop monitors, dedupe viewport resize work, and set Xpra offscreen mode according to HTTPS support. Add a near-future note for the tunnel memory footprint. Show Office desktop startup progress Display a loading message while the Agent Zero Desktop environment is starting or restarting, so the right-canvas Desktop button gives immediate feedback before Xpra finishes waking up. --- docs/agents/office-tunnel-memory-note.md | 20 +++ helpers/virtual_desktop.py | 2 +- .../startup_migration/_20_office_routes.py | 5 +- .../right-canvas-panels/office-panel.html | 1 + .../_office/helpers/libreoffice_desktop.py | 8 +- plugins/_office/webui/office-store.js | 144 ++++++++++++++---- tests/test_office_canvas_setup.py | 15 +- tests/test_office_document_store.py | 15 +- 8 files changed, 162 insertions(+), 48 deletions(-) create mode 100644 docs/agents/office-tunnel-memory-note.md diff --git a/docs/agents/office-tunnel-memory-note.md b/docs/agents/office-tunnel-memory-note.md new file mode 100644 index 000000000..8aee825e6 --- /dev/null +++ b/docs/agents/office-tunnel-memory-note.md @@ -0,0 +1,20 @@ +# Office Tunnel Memory Footprint Note + +During the ARM Desktop audit on 2026-05-03, `run_tunnel.py` was the clearest near-future memory optimization candidate. The process held roughly 573 MiB PSS while the main UI process held roughly 706 MiB PSS, even though the tunnel should mostly be a lightweight network edge. + +## Observed Shape + +- `run_tunnel.py` imports enough of the framework stack to pull in heavy provider and API dependencies. +- The tunnel stays resident for the life of the container, so every eagerly imported module becomes steady-state memory. +- The Desktop/Xpra service itself was not the largest outlier; the always-on tunnel process was. + +## Future Work + +- Split tunnel startup into a small import surface that only loads routing, auth, and socket plumbing required for health and proxy operation. +- Lazy-import provider/framework modules only when a tunnel request truly needs them. +- Review any `ApiHandler` or helper imports used by the tunnel path and replace broad framework imports with narrower functions. +- Measure with `smem -P run_tunnel.py`, `/proc//smaps_rollup`, and before/after cold-start RSS/PSS on ARM64. + +## Success Signal + +The tunnel process should remain useful as an always-on edge while dropping its idle PSS substantially below the main UI process. A good first target is under 250 MiB PSS on ARM64 without changing tunnel behavior. diff --git a/helpers/virtual_desktop.py b/helpers/virtual_desktop.py index 55d33808f..622588429 100644 --- a/helpers/virtual_desktop.py +++ b/helpers/virtual_desktop.py @@ -131,7 +131,7 @@ def session_url(token: str, *, title: str = "Desktop") -> str: "printing": "true", "file_transfer": "true", "sound": "false", - "offscreen": "false", + "offscreen": "true", "floating_menu": "false", "xpramenu": "false", }, diff --git a/plugins/_office/extensions/python/startup_migration/_20_office_routes.py b/plugins/_office/extensions/python/startup_migration/_20_office_routes.py index 0c6148129..ca8ece500 100644 --- a/plugins/_office/extensions/python/startup_migration/_20_office_routes.py +++ b/plugins/_office/extensions/python/startup_migration/_20_office_routes.py @@ -3,7 +3,7 @@ from __future__ import annotations from helpers.extension import Extension from helpers.print_style import PrintStyle from plugins._office import hooks -from plugins._office.helpers import libreoffice_desktop, libreoffice_desktop_routes +from plugins._office.helpers import libreoffice_desktop_routes class OfficeStartupCleanup(Extension): @@ -14,6 +14,3 @@ class OfficeStartupCleanup(Extension): PrintStyle.warning("Office runtime preparation reported errors:", result["errors"]) elif result.get("installed") or result.get("removed"): PrintStyle.info("Office runtime prepared:", result) - desktop = libreoffice_desktop.get_manager().ensure_system_desktop() - if not desktop.get("available"): - PrintStyle.warning("Office desktop startup was deferred:", desktop.get("error") or desktop) diff --git a/plugins/_office/extensions/webui/right-canvas-panels/office-panel.html b/plugins/_office/extensions/webui/right-canvas-panels/office-panel.html index 7c15b4f2e..694aed418 100644 --- a/plugins/_office/extensions/webui/right-canvas-panels/office-panel.html +++ b/plugins/_office/extensions/webui/right-canvas-panels/office-panel.html @@ -2,6 +2,7 @@ class="right-canvas-surface-panel office-canvas-surface" data-surface-id="office" x-show="$store.rightCanvas && $store.rightCanvas.isOpen && $store.rightCanvas.activeSurfaceId === 'office'" + x-effect="$store.office?.setDesktopHostVisible?.($store.rightCanvas && $store.rightCanvas.isOpen && $store.rightCanvas.activeSurfaceId === 'office')" style="display: none;" > diff --git a/plugins/_office/helpers/libreoffice_desktop.py b/plugins/_office/helpers/libreoffice_desktop.py index 5ca978de1..dde29054c 100644 --- a/plugins/_office/helpers/libreoffice_desktop.py +++ b/plugins/_office/helpers/libreoffice_desktop.py @@ -271,21 +271,23 @@ class LibreOfficeDesktopManager: session = self.get(session_id) if not session: return {"ok": False, "error": "LibreOffice desktop session not found."} + is_system_desktop = session.session_id == SYSTEM_SESSION_ID and session.extension == "desktop" result = virtual_desktop.resize_display( display=session.display, width=width, height=height, max_width=MAX_SCREEN_WIDTH, max_height=MAX_SCREEN_HEIGHT, - window_class="libreoffice", - keys=("Escape",), + window_class="" if is_system_desktop else "libreoffice", + keys=() if is_system_desktop else ("Escape",), xauthority=self._xauthority(session), home=str(session.profile_dir), ) if result.get("ok"): session.width = int(result["width"]) session.height = int(result["height"]) - self._dismiss_blocking_dialogs(session) + if not is_system_desktop: + self._dismiss_blocking_dialogs(session) return result def proxy_for_token(self, token: str) -> tuple[str, int] | None: diff --git a/plugins/_office/webui/office-store.js b/plugins/_office/webui/office-store.js index 459f13599..3dcc9c139 100644 --- a/plugins/_office/webui/office-store.js +++ b/plugins/_office/webui/office-store.js @@ -12,6 +12,7 @@ const SAVE_MESSAGE_MS = 1800; const INPUT_PUSH_DELAY_MS = 650; const DESKTOP_HEARTBEAT_MS = 3500; const DESKTOP_RESIZE_DELAY_MS = 80; +const DESKTOP_START_MESSAGE = "Starting Agent Zero Desktop environment"; const XPRA_DESKTOP_PRIME_INTERVAL_MS = 220; const XPRA_DESKTOP_PRIME_ATTEMPTS = 120; const SYSTEM_DESKTOP_FILE_ID = "system-desktop"; @@ -219,8 +220,11 @@ const model = { _desktopResizeTarget: null, _desktopResizeTimer: null, _desktopResizeKey: "", + _desktopResizePendingKey: "", _desktopResizeSuspended: false, _desktopResizePending: false, + _desktopViewportSyncTimers: [], + _desktopHostVisible: false, _desktopPrimeTimer: null, _desktopPrimeAttempts: 0, _desktopKeyboardActive: false, @@ -237,10 +241,12 @@ const model = { async onMount(element = null, options = {}) { if (element) this._root = element; this._mode = options?.mode === "modal" ? "modal" : "canvas"; - if (this._mode === "modal") this.setupFloatingModal(element); - await this.refresh(); - await this.ensureDesktopSession({ select: !this.session }); - this.ensureActiveTab(); + if (this._mode === "modal") { + this._desktopHostVisible = true; + this.setupFloatingModal(element); + await this.onOpen({ source: "modal" }); + return; + } this.queueRender(); }, @@ -259,7 +265,9 @@ const model = { }, beforeHostHidden(options = {}) { + this._desktopHostVisible = false; this.flushInput(); + this.clearDesktopViewportSyncTimers(); this.unloadDesktopFrames(); }, @@ -267,6 +275,7 @@ const model = { this.flushInput(); this.stopDesktopMonitor(); this.stopDesktopResizeObserver(); + this.clearDesktopViewportSyncTimers(); this.stopXpraDesktopPrime(); this.stopDesktopKeyboardBridge(); this.stopDesktopClipboardBridge(); @@ -292,10 +301,23 @@ const model = { this.updateDesktopMonitor(); return existing; } - if (this._desktopStarting) return await this._desktopStarting; + const showProgress = options.progress !== false; + const progressMessage = String(options.message || DESKTOP_START_MESSAGE); + if (this._desktopStarting) { + if (showProgress) { + this.loading = true; + this.message = progressMessage; + } + return await this._desktopStarting; + } this._desktopStarting = (async () => { try { + if (showProgress) { + this.loading = true; + this.message = progressMessage; + this.error = ""; + } const response = await callOffice("desktop"); if (response?.ok === false) throw new Error(response.error || "Desktop session could not be opened."); const session = normalizeSession(response); @@ -327,6 +349,10 @@ const model = { this.error = error instanceof Error ? error.message : String(error); return null; } finally { + if (showProgress) { + this.loading = false; + if (this.message === progressMessage) this.message = ""; + } this._desktopStarting = null; } })(); @@ -493,7 +519,7 @@ const model = { this.ensureActiveTab(); } this.updateDesktopMonitor(); - await this.ensureDesktopSession({ select: !this.session }); + this.ensureActiveTab(); await this.refresh(); }, @@ -836,7 +862,36 @@ const model = { }, officialOfficeUrl(tab = this.session) { - return tab?.desktop?.url || ""; + const url = tab?.desktop?.url || ""; + if (!url) return ""; + try { + const parsed = new URL(url, window.location.href); + const secureContext = globalThis.isSecureContext === true; + parsed.searchParams.set("offscreen", secureContext ? "true" : "false"); + parsed.searchParams.set("clipboard_poll", secureContext ? "true" : "false"); + if (parsed.origin === window.location.origin) return `${parsed.pathname}${parsed.search}${parsed.hash}`; + return parsed.href; + } catch { + return url; + } + }, + + isDesktopHostVisible() { + if (this._mode === "modal") return true; + const canvas = globalThis.Alpine?.store?.("rightCanvas") || rightCanvasStore; + return Boolean(canvas?.isOpen && canvas.activeSurfaceId === "office"); + }, + + setDesktopHostVisible(visible) { + const next = Boolean(visible); + if (!next && this._mode === "modal") return; + if (this._desktopHostVisible === next) return; + this._desktopHostVisible = next; + if (next) { + this.afterDesktopHostShown({ source: "canvas-visibility" }); + } else { + this.beforeHostHidden({ reason: "hidden" }); + } }, desktopFrames() { @@ -879,6 +934,7 @@ const model = { }, restoreDesktopFrames() { + if (!this.isDesktopHostVisible()) return; const url = this.officialOfficeUrl(); if (!url) return; for (const frame of this.desktopFrames()) { @@ -892,22 +948,21 @@ const model = { afterDesktopHostShown() { if (!this.hasOfficialOffice()) return; + this._desktopHostVisible = true; this._desktopResizeKey = ""; + this._desktopResizePendingKey = ""; this._desktopResizeSuspended = false; this._desktopResizePending = false; this.restoreDesktopFrames(); this.requestDesktopViewportSync({ force: true, frame: this.desktopFrame() }); - for (const delay of [720, 1280]) { - globalThis.setTimeout(() => { - this.requestDesktopViewportSync({ force: true, frame: this.desktopFrame() }); - }, delay); - } }, beforeDesktopHostHandoff() { this.stopDesktopResizeObserver(); + this.clearDesktopViewportSyncTimers(); this.stopXpraDesktopPrime(); this._desktopResizeKey = ""; + this._desktopResizePendingKey = ""; this._desktopResizeSuspended = true; this._desktopResizePending = true; }, @@ -920,6 +975,7 @@ const model = { onDesktopFrameLoaded(event = null) { if (event?.target?.getAttribute?.("src") === "about:blank") return; + if (!this.isDesktopHostVisible()) return; this.error = ""; this.queueDesktopFrameFocus(event?.target || null); this.requestDesktopViewportSync({ force: true, frame: event?.target || null }); @@ -955,7 +1011,7 @@ const model = { }, updateDesktopMonitor() { - if (!this.hasOfficialOffice()) { + if (!this.hasOfficialOffice() || !this.isDesktopHostVisible()) { this.stopDesktopMonitor(); this.stopDesktopResizeObserver(); this._desktopKeyboardActive = false; @@ -975,7 +1031,7 @@ const model = { }, startDesktopResizeObserver() { - if (!this.hasOfficialOffice()) { + if (!this.hasOfficialOffice() || !this.isDesktopHostVisible()) { this.stopDesktopResizeObserver(); return; } @@ -1017,6 +1073,7 @@ const model = { this._desktopResizeCleanup = null; this._desktopResizeTarget = null; this._desktopResizeKey = ""; + this._desktopResizePendingKey = ""; this._desktopResizeSuspended = false; this._desktopResizePending = false; }, @@ -1027,6 +1084,7 @@ const model = { globalThis.clearTimeout(this._desktopResizeTimer); this._desktopResizeTimer = null; } + this._desktopResizePendingKey = ""; }, resumeDesktopResize() { @@ -1046,7 +1104,15 @@ const model = { ); }, + clearDesktopViewportSyncTimers() { + for (const timer of this._desktopViewportSyncTimers.splice(0)) { + globalThis.clearTimeout(timer); + } + }, + requestDesktopViewportSync(options = {}) { + if (!this.hasOfficialOffice() || !this.isDesktopHostVisible()) return; + if (options.force) this.clearDesktopViewportSyncTimers(); const run = (force = false) => { this.syncDesktopViewport({ ...options, force }); }; @@ -1055,13 +1121,16 @@ const model = { } else { globalThis.setTimeout(() => run(Boolean(options.force)), 0); } - for (const delay of [140, 420]) { - globalThis.setTimeout(() => run(false), delay); - } + if (options.followup === false) return; + const timer = globalThis.setTimeout(() => { + this._desktopViewportSyncTimers = this._desktopViewportSyncTimers.filter((item) => item !== timer); + run(false); + }, options.force ? 260 : 180); + this._desktopViewportSyncTimers.push(timer); }, syncDesktopViewport(options = {}) { - if (!this.hasOfficialOffice()) return false; + if (!this.hasOfficialOffice() || !this.isDesktopHostVisible()) return false; const frame = this.desktopFrame(options.frame || null); if (!frame) return false; this.startDesktopResizeObserver(); @@ -1554,7 +1623,7 @@ const model = { }, queueDesktopResize(options = {}) { - if (!this.hasOfficialOffice()) return; + if (!this.hasOfficialOffice() || !this.isDesktopHostVisible()) return; const token = this.session?.desktop?.token || ""; const frame = this.desktopFrame(options.frame || null); const target = frame?.parentElement || frame; @@ -1565,18 +1634,33 @@ const model = { const width = Math.round(rect.width); const height = Math.round(rect.height); if (width < 320 || height < 220) return; - this.applyXpraDesktopFrameMode(frame, { requestServerResize: false, requestRefresh: false }); + const key = `${token}:${width}x${height}`; + const refreshFrameOnly = () => { + this.applyXpraDesktopFrameMode(frame, { requestServerResize: false, requestRefresh: false }); + }; + if (!serverResize) { + refreshFrameOnly(); + return; + } + if (key === this._desktopResizeKey || key === this._desktopResizePendingKey) { + refreshFrameOnly(); + return; + } + refreshFrameOnly(); if (!force && this.shouldDeferDesktopResize()) { this._desktopResizePending = true; return; } - const key = `${token}:${width}x${height}`; - if (!serverResize) return; - if (!force && key === this._desktopResizeKey) return; if (this._desktopResizeTimer) globalThis.clearTimeout(this._desktopResizeTimer); + this._desktopResizePendingKey = key; this._desktopResizeTimer = globalThis.setTimeout(async () => { this._desktopResizeTimer = null; + if (!this.hasOfficialOffice() || !this.isDesktopHostVisible()) { + if (this._desktopResizePendingKey === key) this._desktopResizePendingKey = ""; + return; + } if (!force && this.shouldDeferDesktopResize()) { + if (this._desktopResizePendingKey === key) this._desktopResizePendingKey = ""; this._desktopResizePending = true; return; } @@ -1603,6 +1687,8 @@ const model = { } } catch (error) { console.warn("Desktop resize skipped", error); + } finally { + if (this._desktopResizePendingKey === key) this._desktopResizePendingKey = ""; } }, DESKTOP_RESIZE_DELAY_MS); }, @@ -1711,7 +1797,7 @@ const model = { startDesktopMonitor() { this.stopDesktopMonitor(); - if (!this.hasOfficialOffice()) return; + if (!this.hasOfficialOffice() || !this.isDesktopHostVisible()) return; const tabId = this.session?.tab_id || ""; const sessionId = this.session?.desktop_session_id || this.session?.session_id || ""; if (!tabId || !sessionId) return; @@ -1720,7 +1806,7 @@ const model = { this._desktopHeartbeatMisses = 0; const tick = async () => { - if (!this.session || this.session.tab_id !== tabId || !this.hasOfficialOffice()) return; + if (!this.session || this.session.tab_id !== tabId || !this.hasOfficialOffice() || !this.isDesktopHostVisible()) return; try { const response = await callOffice("desktop_sync", { desktop_session_id: sessionId, @@ -1773,8 +1859,12 @@ const model = { this.stopDesktopMonitor(); this.stopDesktopResizeObserver(); this.stopXpraDesktopPrime(); - this.setMessage("Desktop is restarting"); - await this.ensureDesktopSession({ force: true, select: this.activeTabId === tabId || Boolean(hiddenDesktopDocument) }); + this.message = "Desktop is restarting"; + await this.ensureDesktopSession({ + force: true, + select: this.activeTabId === tabId || Boolean(hiddenDesktopDocument), + message: "Desktop is restarting", + }); target._desktopClosed = false; await this.refresh(); }, diff --git a/tests/test_office_canvas_setup.py b/tests/test_office_canvas_setup.py index f7ed3ca0f..8776d6d45 100644 --- a/tests/test_office_canvas_setup.py +++ b/tests/test_office_canvas_setup.py @@ -13,6 +13,9 @@ def test_document_canvas_uses_markdown_editor_and_official_libreoffice_desktop_f store = (PROJECT_ROOT / "plugins" / "_office" / "webui" / "office-store.js").read_text( encoding="utf-8", ) + canvas_panel = ( + PROJECT_ROOT / "plugins" / "_office" / "extensions" / "webui" / "right-canvas-panels" / "office-panel.html" + ).read_text(encoding="utf-8") assert "office-source-editor" in panel assert "data-office-source" in panel @@ -47,10 +50,18 @@ def test_document_canvas_uses_markdown_editor_and_official_libreoffice_desktop_f assert "zoom: 1" not in store assert 'callOffice("desktop")' in store assert "ensureDesktopSession" in store + assert 'await this.onOpen({ source: "modal" });' in store + assert "setDesktopHostVisible" in store + assert "isDesktopHostVisible" in store + assert "clearDesktopViewportSyncTimers" in store + assert "setDesktopHostVisible" in canvas_panel + assert "Starting Agent Zero Desktop environment" in store assert "handleOfficialOfficeClosed" in store assert "ResizeObserver" in store assert "_desktopResizeSuspended" in store assert "_desktopResizePending" in store + assert "_desktopResizePendingKey" in store + assert "_desktopViewportSyncTimers" in store assert "shouldDeferDesktopResize" in store assert "right-canvas-resize-start" in store assert "right-canvas-resize-end" in store @@ -91,6 +102,8 @@ def test_document_canvas_uses_markdown_editor_and_official_libreoffice_desktop_f assert "_desktopHeartbeatTimer" in store assert "office-modal-focus-button" in store assert "officialOfficeUrl" in store + assert 'parsed.searchParams.set("offscreen", secureContext ? "true" : "false")' in store + assert 'parsed.searchParams.set("clipboard_poll", secureContext ? "true" : "false")' in store assert "hasOfficialOffice" in store assert "isOfficeSocketData" in store assert "office_command" not in store @@ -233,7 +246,7 @@ def test_official_libreoffice_desktop_route_and_packages_are_declared(): assert '"quality": "85"' in primitive assert '"speed": "80"' in primitive assert '"printing": "true"' in primitive - assert "offscreen" in primitive + assert '"offscreen": "true"' in primitive assert "xpra" in desktop assert "xpra-html5" in desktop assert "Xvfb" in desktop diff --git a/tests/test_office_document_store.py b/tests/test_office_document_store.py index 0c3ca81f5..6ef58b194 100644 --- a/tests/test_office_document_store.py +++ b/tests/test_office_document_store.py @@ -684,7 +684,7 @@ def test_cleanup_hook_removes_stale_runtime_state_idempotently(tmp_path, monkeyp assert marker.exists() -def test_office_startup_bootstraps_persistent_desktop_runtime(monkeypatch): +def test_office_startup_defers_persistent_desktop_runtime(monkeypatch): calls = [] routes_module = types.ModuleType("plugins._office.helpers.libreoffice_desktop_routes") routes_module.install_route_hooks = lambda: calls.append("routes") @@ -697,25 +697,16 @@ def test_office_startup_bootstraps_persistent_desktop_runtime(monkeypatch): from plugins._office.extensions.python.startup_migration import _20_office_routes as office_startup - class Manager: - def ensure_system_desktop(self): - calls.append("desktop") - return {"available": True, "session_id": "agent-zero-desktop"} - monkeypatch.setattr( office_startup.hooks, "cleanup_stale_runtime_state", lambda: {"ok": True, "errors": [], "installed": [], "removed": []}, ) - monkeypatch.setattr( - office_startup.libreoffice_desktop, - "get_manager", - lambda: Manager(), - ) office_startup.OfficeStartupCleanup(agent=None).execute() - assert calls == ["routes", "desktop"] + assert calls == ["routes"] + assert not hasattr(office_startup, "libreoffice_desktop") def test_cleanup_hook_reruns_when_stale_packages_exist_after_old_marker(tmp_path, monkeypatch):