mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
perf: reduce graph panel refresh churn during streaming
This commit is contained in:
186
tests/panel-graph-refresh.mjs
Normal file
186
tests/panel-graph-refresh.mjs
Normal file
@@ -0,0 +1,186 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
buildVisibleGraphRefreshToken,
|
||||
resolveVisibleGraphWorkspaceMode,
|
||||
} from "../ui/panel-graph-refresh-utils.js";
|
||||
|
||||
assert.equal(
|
||||
resolveVisibleGraphWorkspaceMode({
|
||||
overlayActive: false,
|
||||
isMobile: false,
|
||||
currentTabId: "dashboard",
|
||||
currentGraphView: "graph",
|
||||
}),
|
||||
"hidden",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
resolveVisibleGraphWorkspaceMode({
|
||||
overlayActive: true,
|
||||
isMobile: false,
|
||||
currentTabId: "config",
|
||||
currentGraphView: "graph",
|
||||
}),
|
||||
"hidden",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
resolveVisibleGraphWorkspaceMode({
|
||||
overlayActive: true,
|
||||
isMobile: false,
|
||||
currentTabId: "dashboard",
|
||||
currentGraphView: "graph",
|
||||
}),
|
||||
"desktop:graph",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
resolveVisibleGraphWorkspaceMode({
|
||||
overlayActive: true,
|
||||
isMobile: false,
|
||||
currentTabId: "memory",
|
||||
currentGraphView: "cognition",
|
||||
}),
|
||||
"desktop:cognition",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
resolveVisibleGraphWorkspaceMode({
|
||||
overlayActive: true,
|
||||
isMobile: false,
|
||||
currentTabId: "actions",
|
||||
currentGraphView: "summary",
|
||||
}),
|
||||
"desktop:summary",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
resolveVisibleGraphWorkspaceMode({
|
||||
overlayActive: true,
|
||||
isMobile: true,
|
||||
currentTabId: "dashboard",
|
||||
currentMobileGraphView: "graph",
|
||||
}),
|
||||
"hidden",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
resolveVisibleGraphWorkspaceMode({
|
||||
overlayActive: true,
|
||||
isMobile: true,
|
||||
currentTabId: "graph",
|
||||
currentMobileGraphView: "graph",
|
||||
}),
|
||||
"mobile:graph",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
resolveVisibleGraphWorkspaceMode({
|
||||
overlayActive: true,
|
||||
isMobile: true,
|
||||
currentTabId: "graph",
|
||||
currentMobileGraphView: "cognition",
|
||||
}),
|
||||
"mobile:cognition",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
resolveVisibleGraphWorkspaceMode({
|
||||
overlayActive: true,
|
||||
isMobile: true,
|
||||
currentTabId: "graph",
|
||||
currentMobileGraphView: "summary",
|
||||
}),
|
||||
"mobile:summary",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
buildVisibleGraphRefreshToken({
|
||||
visibleMode: "hidden",
|
||||
chatId: "chat-main",
|
||||
loadState: "loaded",
|
||||
revision: 12,
|
||||
nodeCount: 40,
|
||||
edgeCount: 55,
|
||||
lastProcessedSeq: 9,
|
||||
}),
|
||||
"hidden",
|
||||
);
|
||||
|
||||
const baseToken = buildVisibleGraphRefreshToken({
|
||||
visibleMode: "desktop:graph",
|
||||
chatId: "chat-main",
|
||||
loadState: "loaded",
|
||||
revision: 12,
|
||||
nodeCount: 40,
|
||||
edgeCount: 55,
|
||||
lastProcessedSeq: 9,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
baseToken,
|
||||
buildVisibleGraphRefreshToken({
|
||||
visibleMode: "desktop:graph",
|
||||
chatId: "chat-main",
|
||||
loadState: "loaded",
|
||||
revision: 12,
|
||||
nodeCount: 40,
|
||||
edgeCount: 55,
|
||||
lastProcessedSeq: 9,
|
||||
}),
|
||||
);
|
||||
|
||||
assert.notEqual(
|
||||
baseToken,
|
||||
buildVisibleGraphRefreshToken({
|
||||
visibleMode: "desktop:graph",
|
||||
chatId: "chat-main",
|
||||
loadState: "loaded",
|
||||
revision: 13,
|
||||
nodeCount: 40,
|
||||
edgeCount: 55,
|
||||
lastProcessedSeq: 9,
|
||||
}),
|
||||
);
|
||||
|
||||
assert.notEqual(
|
||||
baseToken,
|
||||
buildVisibleGraphRefreshToken({
|
||||
visibleMode: "desktop:cognition",
|
||||
chatId: "chat-main",
|
||||
loadState: "loaded",
|
||||
revision: 12,
|
||||
nodeCount: 40,
|
||||
edgeCount: 55,
|
||||
lastProcessedSeq: 9,
|
||||
}),
|
||||
);
|
||||
|
||||
assert.notEqual(
|
||||
baseToken,
|
||||
buildVisibleGraphRefreshToken({
|
||||
visibleMode: "desktop:graph",
|
||||
chatId: "chat-side",
|
||||
loadState: "loaded",
|
||||
revision: 12,
|
||||
nodeCount: 40,
|
||||
edgeCount: 55,
|
||||
lastProcessedSeq: 9,
|
||||
}),
|
||||
);
|
||||
|
||||
assert.notEqual(
|
||||
baseToken,
|
||||
buildVisibleGraphRefreshToken({
|
||||
visibleMode: "desktop:graph",
|
||||
chatId: "chat-main",
|
||||
loadState: "loaded",
|
||||
revision: 12,
|
||||
nodeCount: 41,
|
||||
edgeCount: 55,
|
||||
lastProcessedSeq: 9,
|
||||
}),
|
||||
);
|
||||
|
||||
console.log("panel-graph-refresh tests passed");
|
||||
59
ui/panel-graph-refresh-utils.js
Normal file
59
ui/panel-graph-refresh-utils.js
Normal file
@@ -0,0 +1,59 @@
|
||||
export function resolveVisibleGraphWorkspaceMode({
|
||||
overlayActive = false,
|
||||
isMobile = false,
|
||||
currentTabId = "dashboard",
|
||||
currentGraphView = "graph",
|
||||
currentMobileGraphView = "graph",
|
||||
} = {}) {
|
||||
if (!overlayActive) return "hidden";
|
||||
if (isMobile) {
|
||||
if (currentTabId !== "graph") return "hidden";
|
||||
const mobileView = String(currentMobileGraphView || "graph").trim() || "graph";
|
||||
return mobileView === "cognition"
|
||||
? "mobile:cognition"
|
||||
: mobileView === "summary"
|
||||
? "mobile:summary"
|
||||
: "mobile:graph";
|
||||
}
|
||||
if (currentTabId === "config") return "hidden";
|
||||
const desktopView = String(currentGraphView || "graph").trim() || "graph";
|
||||
return desktopView === "cognition"
|
||||
? "desktop:cognition"
|
||||
: desktopView === "summary"
|
||||
? "desktop:summary"
|
||||
: "desktop:graph";
|
||||
}
|
||||
|
||||
export function buildVisibleGraphRefreshToken({
|
||||
visibleMode = "hidden",
|
||||
chatId = "",
|
||||
loadState = "",
|
||||
revision = 0,
|
||||
nodeCount = -1,
|
||||
edgeCount = -1,
|
||||
lastProcessedSeq = -1,
|
||||
} = {}) {
|
||||
const normalizedMode = String(visibleMode || "hidden").trim() || "hidden";
|
||||
if (normalizedMode === "hidden") return "hidden";
|
||||
const normalizedRevision = Number.isFinite(Number(revision))
|
||||
? Math.trunc(Number(revision))
|
||||
: 0;
|
||||
const normalizedNodeCount = Number.isFinite(Number(nodeCount))
|
||||
? Math.trunc(Number(nodeCount))
|
||||
: -1;
|
||||
const normalizedEdgeCount = Number.isFinite(Number(edgeCount))
|
||||
? Math.trunc(Number(edgeCount))
|
||||
: -1;
|
||||
const normalizedLastProcessedSeq = Number.isFinite(Number(lastProcessedSeq))
|
||||
? Math.trunc(Number(lastProcessedSeq))
|
||||
: -1;
|
||||
return [
|
||||
normalizedMode,
|
||||
String(chatId || "").trim(),
|
||||
String(loadState || "").trim() || "unknown",
|
||||
normalizedRevision,
|
||||
normalizedNodeCount,
|
||||
normalizedEdgeCount,
|
||||
normalizedLastProcessedSeq,
|
||||
].join("|");
|
||||
}
|
||||
190
ui/panel.js
190
ui/panel.js
@@ -1,6 +1,10 @@
|
||||
// ST-BME: 操控面板交互逻辑
|
||||
|
||||
import { GraphRenderer } from "./graph-renderer.js";
|
||||
import {
|
||||
buildVisibleGraphRefreshToken,
|
||||
resolveVisibleGraphWorkspaceMode,
|
||||
} from "./panel-graph-refresh-utils.js";
|
||||
import { getNodeDisplayName } from "../graph/node-labels.js";
|
||||
import {
|
||||
buildRegionLine,
|
||||
@@ -335,6 +339,12 @@ let fetchedBackendEmbeddingModels = [];
|
||||
let fetchedDirectEmbeddingModels = [];
|
||||
let viewportSyncBound = false;
|
||||
let popupRuntimePromise = null;
|
||||
const GRAPH_LIVE_REFRESH_THROTTLE_MS = 240;
|
||||
let pendingVisibleGraphRefreshTimer = null;
|
||||
let pendingVisibleGraphRefreshToken = "";
|
||||
let pendingVisibleGraphRefreshForce = false;
|
||||
let lastVisibleGraphRefreshToken = "";
|
||||
let lastVisibleGraphRefreshAt = 0;
|
||||
|
||||
// 由 index.js 注入的引用
|
||||
let _getGraph = null;
|
||||
@@ -684,6 +694,143 @@ function bindViewportSync() {
|
||||
window.visualViewport?.addEventListener("scroll", update);
|
||||
}
|
||||
|
||||
function _getVisibleGraphWorkspaceMode() {
|
||||
return resolveVisibleGraphWorkspaceMode({
|
||||
overlayActive: overlayEl?.classList.contains("active") === true,
|
||||
isMobile: _isMobile(),
|
||||
currentTabId,
|
||||
currentGraphView,
|
||||
currentMobileGraphView,
|
||||
});
|
||||
}
|
||||
|
||||
function _getCurrentGraphRefreshToken() {
|
||||
const graph = _getGraph?.();
|
||||
const persistence = _getGraphPersistenceSnapshot();
|
||||
return buildVisibleGraphRefreshToken({
|
||||
visibleMode: _getVisibleGraphWorkspaceMode(),
|
||||
chatId: persistence?.chatId,
|
||||
loadState: persistence?.loadState,
|
||||
revision:
|
||||
persistence?.revision ??
|
||||
persistence?.lastAcceptedRevision ??
|
||||
persistence?.lastSyncedRevision ??
|
||||
0,
|
||||
nodeCount: Array.isArray(graph?.nodes) ? graph.nodes.length : -1,
|
||||
edgeCount: Array.isArray(graph?.edges) ? graph.edges.length : -1,
|
||||
lastProcessedSeq: graph?.historyState?.lastProcessedAssistantFloor ?? -1,
|
||||
});
|
||||
}
|
||||
|
||||
function _clearScheduledVisibleGraphRefresh() {
|
||||
if (pendingVisibleGraphRefreshTimer) {
|
||||
clearTimeout(pendingVisibleGraphRefreshTimer);
|
||||
pendingVisibleGraphRefreshTimer = null;
|
||||
}
|
||||
pendingVisibleGraphRefreshToken = "";
|
||||
pendingVisibleGraphRefreshForce = false;
|
||||
}
|
||||
|
||||
function _refreshVisibleGraphWorkspace({ force = false } = {}) {
|
||||
const visibleMode = _getVisibleGraphWorkspaceMode();
|
||||
if (visibleMode === "hidden") {
|
||||
return { refreshed: false, reason: "hidden" };
|
||||
}
|
||||
|
||||
const graph = _getGraph?.();
|
||||
const nextToken = _getCurrentGraphRefreshToken();
|
||||
if (!force && nextToken === lastVisibleGraphRefreshToken) {
|
||||
return { refreshed: false, reason: "unchanged", token: nextToken };
|
||||
}
|
||||
|
||||
const hints = { userPovAliases: _hostUserPovAliasHintsForGraph() };
|
||||
if (visibleMode === "desktop:graph") {
|
||||
if (graph && graphRenderer) {
|
||||
graphRenderer.loadGraph(graph, hints);
|
||||
}
|
||||
} else if (visibleMode === "desktop:cognition") {
|
||||
_refreshCognitionWorkspace();
|
||||
} else if (visibleMode === "desktop:summary") {
|
||||
_refreshSummaryWorkspace();
|
||||
} else if (visibleMode === "mobile:graph") {
|
||||
if (graph && mobileGraphRenderer) {
|
||||
mobileGraphRenderer.loadGraph(graph, hints);
|
||||
}
|
||||
_buildMobileLegend();
|
||||
} else if (visibleMode === "mobile:cognition") {
|
||||
_refreshMobileCognitionFull();
|
||||
} else if (visibleMode === "mobile:summary") {
|
||||
_refreshMobileSummaryFull();
|
||||
}
|
||||
|
||||
lastVisibleGraphRefreshToken = nextToken;
|
||||
lastVisibleGraphRefreshAt = Date.now();
|
||||
return {
|
||||
refreshed: true,
|
||||
reason: force ? "forced" : "changed",
|
||||
token: nextToken,
|
||||
visibleMode,
|
||||
};
|
||||
}
|
||||
|
||||
function _flushScheduledVisibleGraphRefresh() {
|
||||
const shouldForce = pendingVisibleGraphRefreshForce === true;
|
||||
_clearScheduledVisibleGraphRefresh();
|
||||
return _refreshVisibleGraphWorkspace({ force: shouldForce });
|
||||
}
|
||||
|
||||
function _scheduleVisibleGraphWorkspaceRefresh({ force = false } = {}) {
|
||||
const nextToken = _getCurrentGraphRefreshToken();
|
||||
if (nextToken === "hidden") {
|
||||
_clearScheduledVisibleGraphRefresh();
|
||||
return { scheduled: false, reason: "hidden" };
|
||||
}
|
||||
|
||||
if (force) {
|
||||
_clearScheduledVisibleGraphRefresh();
|
||||
return _refreshVisibleGraphWorkspace({ force: true });
|
||||
}
|
||||
|
||||
if (nextToken === lastVisibleGraphRefreshToken) {
|
||||
return { scheduled: false, reason: "unchanged", token: nextToken };
|
||||
}
|
||||
|
||||
if (
|
||||
pendingVisibleGraphRefreshTimer &&
|
||||
pendingVisibleGraphRefreshToken === nextToken &&
|
||||
pendingVisibleGraphRefreshForce !== true
|
||||
) {
|
||||
return { scheduled: true, reason: "pending", token: nextToken };
|
||||
}
|
||||
|
||||
const delay = Math.max(
|
||||
0,
|
||||
GRAPH_LIVE_REFRESH_THROTTLE_MS - (Date.now() - lastVisibleGraphRefreshAt),
|
||||
);
|
||||
pendingVisibleGraphRefreshToken = nextToken;
|
||||
pendingVisibleGraphRefreshForce = false;
|
||||
|
||||
if (pendingVisibleGraphRefreshTimer) {
|
||||
clearTimeout(pendingVisibleGraphRefreshTimer);
|
||||
pendingVisibleGraphRefreshTimer = null;
|
||||
}
|
||||
|
||||
if (delay <= 0) {
|
||||
return _flushScheduledVisibleGraphRefresh();
|
||||
}
|
||||
|
||||
pendingVisibleGraphRefreshTimer = setTimeout(() => {
|
||||
_flushScheduledVisibleGraphRefresh();
|
||||
}, delay);
|
||||
|
||||
return {
|
||||
scheduled: true,
|
||||
reason: "throttled",
|
||||
token: nextToken,
|
||||
delay,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化面板(由 index.js 调用一次)
|
||||
*/
|
||||
@@ -981,7 +1128,6 @@ export function openPanel() {
|
||||
panelEl?.querySelector(".bme-tab-btn.active")?.dataset.tab || currentTabId;
|
||||
_switchTab(activeTabId);
|
||||
_refreshRuntimeStatus();
|
||||
_refreshGraph();
|
||||
_buildLegend();
|
||||
}
|
||||
|
||||
@@ -991,6 +1137,8 @@ export function openPanel() {
|
||||
export function closePanel() {
|
||||
if (!overlayEl) return;
|
||||
overlayEl.classList.remove("active");
|
||||
_clearScheduledVisibleGraphRefresh();
|
||||
lastVisibleGraphRefreshToken = "";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1032,7 +1180,7 @@ export function refreshLiveState() {
|
||||
_refreshMessageTraceWorkspace();
|
||||
}
|
||||
|
||||
_refreshGraph();
|
||||
_scheduleVisibleGraphWorkspaceRefresh();
|
||||
}
|
||||
|
||||
// ==================== Tab 切换 ====================
|
||||
@@ -1047,6 +1195,7 @@ function _bindTabs() {
|
||||
}
|
||||
|
||||
function _switchTab(tabId) {
|
||||
const previousVisibleGraphMode = _getVisibleGraphWorkspaceMode();
|
||||
let next = tabId || "dashboard";
|
||||
// 「图谱」仅移动端底部 Tab 可用;桌面端图谱在右侧主工作区,侧栏不设该 Tab
|
||||
if (!_isMobile() && next === "graph") {
|
||||
@@ -1078,11 +1227,17 @@ function _switchTab(tabId) {
|
||||
_refreshConfigTab();
|
||||
break;
|
||||
case "graph":
|
||||
_refreshMobileGraphTab();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const nextVisibleGraphMode = _getVisibleGraphWorkspaceMode();
|
||||
if (nextVisibleGraphMode !== previousVisibleGraphMode) {
|
||||
_scheduleVisibleGraphWorkspaceRefresh({ force: true });
|
||||
} else {
|
||||
_scheduleVisibleGraphWorkspaceRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
function _getPlannerApi() {
|
||||
@@ -1161,8 +1316,7 @@ function _switchGraphView(view) {
|
||||
if (cogWorkspace) cogWorkspace.style.display = isCognition ? "" : "none";
|
||||
if (summaryWorkspace) summaryWorkspace.style.display = isSummary ? "" : "none";
|
||||
|
||||
if (isCognition) _refreshCognitionWorkspace();
|
||||
if (isSummary) _refreshSummaryWorkspace();
|
||||
_refreshGraph({ force: true });
|
||||
}
|
||||
|
||||
// ==================== 移动端图谱 Tab ====================
|
||||
@@ -1187,14 +1341,7 @@ function _switchMobileGraphSubView(view) {
|
||||
}
|
||||
|
||||
function _refreshMobileGraphTab() {
|
||||
if (currentMobileGraphView === "graph") {
|
||||
_refreshGraph();
|
||||
_buildMobileLegend();
|
||||
} else if (currentMobileGraphView === "cognition") {
|
||||
_refreshMobileCognitionFull();
|
||||
} else if (currentMobileGraphView === "summary") {
|
||||
_refreshMobileSummaryFull();
|
||||
}
|
||||
_refreshGraph({ force: true });
|
||||
}
|
||||
|
||||
function _buildMobileLegend() {
|
||||
@@ -2990,20 +3137,8 @@ function _hostUserPovAliasHintsForGraph() {
|
||||
return getHostUserAliasHints();
|
||||
}
|
||||
|
||||
function _refreshGraph() {
|
||||
const graph = _getGraph?.();
|
||||
if (!graph) return;
|
||||
const hints = { userPovAliases: _hostUserPovAliasHintsForGraph() };
|
||||
graphRenderer?.loadGraph(graph, hints);
|
||||
mobileGraphRenderer?.loadGraph(graph, hints);
|
||||
if (currentGraphView === "cognition") {
|
||||
_refreshCognitionWorkspace();
|
||||
} else if (currentGraphView === "summary") {
|
||||
_refreshSummaryWorkspace();
|
||||
}
|
||||
if (currentTabId === "graph") {
|
||||
_refreshMobileGraphTab();
|
||||
}
|
||||
function _refreshGraph(options = {}) {
|
||||
return _refreshVisibleGraphWorkspace({ force: options.force !== false });
|
||||
}
|
||||
|
||||
function _buildLegend() {
|
||||
@@ -9591,6 +9726,7 @@ function _setText(id, text) {
|
||||
|
||||
function _getGraphPersistenceSnapshot() {
|
||||
return _getGraphPersistenceState?.() || {
|
||||
revision: 0,
|
||||
loadState: "no-chat",
|
||||
reason: "",
|
||||
writesBlocked: true,
|
||||
|
||||
Reference in New Issue
Block a user