From a070e04e56b1ea4ab871238c31f3b739eb94e8a0 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Wed, 29 Apr 2026 17:03:41 +0800 Subject: [PATCH] fix(panel): prevent UI freeze when opening panel during extraction Root cause: onStreamProgress callback fires 20-50x/sec during LLM streaming, each calling setLastExtractionStatus with syncRuntime=true (default), which triggers setRuntimeStatus -> refreshPanelLiveState() -> _refreshDashboard(). _refreshDashboard iterates all graph nodes (.filter x2) and performs heavy DOM updates each time. When the panel is open, this blocks the main thread. Fixes: 1. panel.js: Add requestAnimationFrame throttle to refreshLiveState() with minimum 80ms gap between actual DOM refreshes. Rapid calls are collapsed into a single animation frame, preventing main thread saturation. 2. extraction-controller.js: Change onStreamProgress to use syncRuntime=false so streaming updates no longer trigger setRuntimeStatus (which would also update the floating ball on each chunk). 3. recall-controller.js: Same fix for recall onStreamProgress which had the identical syncRuntime=true issue. --- maintenance/extraction-controller.js | 2 +- retrieval/recall-controller.js | 20 ++++++++++---------- ui/panel.js | 27 +++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/maintenance/extraction-controller.js b/maintenance/extraction-controller.js index 618b791..20d3b36 100644 --- a/maintenance/extraction-controller.js +++ b/maintenance/extraction-controller.js @@ -843,7 +843,7 @@ export async function executeExtractionBatchController( "AI 生成中", `${preview} [${receivedChars}字]`, "running", - { noticeMarquee: true }, + { syncRuntime: false, noticeMarquee: true }, ); }, }); diff --git a/retrieval/recall-controller.js b/retrieval/recall-controller.js index d2b915a..8e1f5bd 100644 --- a/retrieval/recall-controller.js +++ b/retrieval/recall-controller.js @@ -688,16 +688,16 @@ export async function runRecallController(runtime, options = {}) { schema: runtime.getSchema(), signal: recallSignal, settings, - onStreamProgress: ({ previewText, receivedChars }) => { - const preview = - previewText?.length > 60 - ? "…" + previewText.slice(-60) - : previewText || ""; - runtime.setLastRecallStatus( - "AI 生成中", - `${preview} [${receivedChars}字]`, - "running", - { syncRuntime: true, noticeMarquee: true }, + onStreamProgress: ({ previewText, receivedChars }) => { + const preview = + previewText?.length > 60 + ? "…" + previewText.slice(-60) + : previewText || ""; + runtime.setLastRecallStatus( + "AI 生成中", + `${preview} [${receivedChars}字]`, + "running", + { syncRuntime: false, noticeMarquee: true }, ); }, options: runtime.buildRecallRetrieveOptions(settings, context), diff --git a/ui/panel.js b/ui/panel.js index 9a66d63..61ede94 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -353,6 +353,9 @@ let fetchedDirectEmbeddingModels = []; let viewportSyncBound = false; let popupRuntimePromise = null; const GRAPH_LIVE_REFRESH_THROTTLE_MS = 240; +let _pendingRafRefreshId = null; +let _lastRafRefreshAt = 0; +const PANEL_LIVE_STATE_REFRESH_MIN_MS = 80; let pendingVisibleGraphRefreshTimer = null; let pendingVisibleGraphRefreshToken = ""; let pendingVisibleGraphRefreshForce = false; @@ -1282,6 +1285,30 @@ export function updatePanelTheme(themeName) { export function refreshLiveState() { if (!overlayEl?.classList.contains("active")) return; + + const now = Date.now(); + const elapsed = now - _lastRafRefreshAt; + + if (elapsed < PANEL_LIVE_STATE_REFRESH_MIN_MS) { + if (!_pendingRafRefreshId) { + _pendingRafRefreshId = requestAnimationFrame(() => { + _pendingRafRefreshId = null; + _lastRafRefreshAt = Date.now(); + _doRefreshLiveState(); + }); + } + return; + } + + if (_pendingRafRefreshId) { + cancelAnimationFrame(_pendingRafRefreshId); + _pendingRafRefreshId = null; + } + _lastRafRefreshAt = now; + _doRefreshLiveState(); +} + +function _doRefreshLiveState() { _applyGraphRuntimeConfig(_getSettings?.() || {}); _refreshRuntimeStatus(); _refreshNativeRolloutStatusUi(_getSettings?.() || {});