From c1caa79eb4d5662e21cad5c895d518a8fed5588d Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Wed, 22 Apr 2026 21:24:22 +0800 Subject: [PATCH] Integrate native rollout UI and tune hydrate gating --- runtime/settings-defaults.js | 24 +- sync/bme-db.js | 2 +- tests/default-settings.mjs | 25 +- tests/native-hydrate-hook.mjs | 6 +- tests/native-rollout-matrix.mjs | 153 +++++++++++++ ui/panel.html | 193 ++++++++++++++++ ui/panel.js | 391 +++++++++++++++++++++++++++++++- 7 files changed, 781 insertions(+), 13 deletions(-) create mode 100644 tests/native-rollout-matrix.mjs diff --git a/runtime/settings-defaults.js b/runtime/settings-defaults.js index 409d8b4..69708b7 100644 --- a/runtime/settings-defaults.js +++ b/runtime/settings-defaults.js @@ -9,7 +9,9 @@ function clampIntValue(value, fallback = 0, min = 0, max = 9999) { return Math.min(max, Math.max(min, Math.trunc(numeric))); } -const NATIVE_ROLLOUT_VERSION = 1; +const NATIVE_ROLLOUT_VERSION = 2; +const LEGACY_NATIVE_HYDRATE_THRESHOLD_RECORDS = 12000; +const DEFAULT_NATIVE_HYDRATE_THRESHOLD_RECORDS = 30000; export const defaultSettings = { enabled: true, @@ -125,7 +127,7 @@ export const defaultSettings = { persistNativeDeltaThresholdSerializedChars: 4000000, persistNativeDeltaBridgeMode: "json", loadUseNativeHydrate: true, - loadNativeHydrateThresholdRecords: 12000, + loadNativeHydrateThresholdRecords: DEFAULT_NATIVE_HYDRATE_THRESHOLD_RECORDS, nativeRolloutVersion: NATIVE_ROLLOUT_VERSION, nativeEngineFailOpen: true, graphNativeForceDisable: false, @@ -252,11 +254,27 @@ export function migrateNativeRolloutSettings(loaded = {}) { 0, NATIVE_ROLLOUT_VERSION, ); - if (rolloutVersion < NATIVE_ROLLOUT_VERSION) { + if (rolloutVersion < 1) { migrated.graphUseNativeLayout = defaultSettings.graphUseNativeLayout; migrated.persistUseNativeDelta = defaultSettings.persistUseNativeDelta; migrated.loadUseNativeHydrate = defaultSettings.loadUseNativeHydrate; } + if ( + rolloutVersion < 2 && + (!Object.prototype.hasOwnProperty.call( + migrated, + "loadNativeHydrateThresholdRecords", + ) || + clampIntValue( + migrated.loadNativeHydrateThresholdRecords, + LEGACY_NATIVE_HYDRATE_THRESHOLD_RECORDS, + 0, + 1000000, + ) === LEGACY_NATIVE_HYDRATE_THRESHOLD_RECORDS) + ) { + migrated.loadNativeHydrateThresholdRecords = + defaultSettings.loadNativeHydrateThresholdRecords; + } migrated.nativeRolloutVersion = NATIVE_ROLLOUT_VERSION; return migrated; } diff --git a/sync/bme-db.js b/sync/bme-db.js index 0d51e4e..02ea663 100644 --- a/sync/bme-db.js +++ b/sync/bme-db.js @@ -19,7 +19,7 @@ const DEFAULT_PERSIST_NATIVE_DELTA_THRESHOLD_RECORDS = 20000; const DEFAULT_PERSIST_NATIVE_DELTA_THRESHOLD_STRUCTURAL_DELTA = 600; const DEFAULT_PERSIST_NATIVE_DELTA_THRESHOLD_SERIALIZED_CHARS = 4000000; const DEFAULT_PERSIST_NATIVE_DELTA_BRIDGE_MODE = "json"; -const DEFAULT_NATIVE_HYDRATE_THRESHOLD_RECORDS = 12000; +const DEFAULT_NATIVE_HYDRATE_THRESHOLD_RECORDS = 30000; const SUPPORTED_PERSIST_NATIVE_DELTA_BRIDGE_MODES = new Set(["json", "hash"]); const PERSIST_RECORD_SERIALIZATION_CACHE_LIMIT = 50000; diff --git a/tests/default-settings.mjs b/tests/default-settings.mjs index 3c763d2..474b2a6 100644 --- a/tests/default-settings.mjs +++ b/tests/default-settings.mjs @@ -77,8 +77,8 @@ assert.equal(defaultSettings.persistNativeDeltaThresholdStructuralDelta, 600); assert.equal(defaultSettings.persistNativeDeltaThresholdSerializedChars, 4000000); assert.equal(defaultSettings.persistNativeDeltaBridgeMode, "json"); assert.equal(defaultSettings.loadUseNativeHydrate, true); -assert.equal(defaultSettings.loadNativeHydrateThresholdRecords, 12000); -assert.equal(defaultSettings.nativeRolloutVersion, 1); +assert.equal(defaultSettings.loadNativeHydrateThresholdRecords, 30000); +assert.equal(defaultSettings.nativeRolloutVersion, 2); assert.equal(defaultSettings.nativeEngineFailOpen, true); assert.equal(defaultSettings.graphNativeForceDisable, false); assert.equal(defaultSettings.taskProfilesVersion, 3); @@ -126,11 +126,12 @@ const migratedLegacyNativeDisabled = mergePersistedSettings({ assert.equal(migratedLegacyNativeDisabled.graphUseNativeLayout, true); assert.equal(migratedLegacyNativeDisabled.persistUseNativeDelta, true); assert.equal(migratedLegacyNativeDisabled.loadUseNativeHydrate, true); +assert.equal(migratedLegacyNativeDisabled.loadNativeHydrateThresholdRecords, 30000); assert.equal(migratedLegacyNativeDisabled.graphNativeForceDisable, true); -assert.equal(migratedLegacyNativeDisabled.nativeRolloutVersion, 1); +assert.equal(migratedLegacyNativeDisabled.nativeRolloutVersion, 2); const migratedVersionedManualNativeDisabled = mergePersistedSettings({ - nativeRolloutVersion: 1, + nativeRolloutVersion: 2, graphUseNativeLayout: false, persistUseNativeDelta: false, loadUseNativeHydrate: false, @@ -140,6 +141,20 @@ assert.equal(migratedVersionedManualNativeDisabled.graphUseNativeLayout, false); assert.equal(migratedVersionedManualNativeDisabled.persistUseNativeDelta, false); assert.equal(migratedVersionedManualNativeDisabled.loadUseNativeHydrate, false); assert.equal(migratedVersionedManualNativeDisabled.graphNativeForceDisable, true); -assert.equal(migratedVersionedManualNativeDisabled.nativeRolloutVersion, 1); +assert.equal(migratedVersionedManualNativeDisabled.nativeRolloutVersion, 2); + +const migratedLegacyHydrateThresholdDefault = mergePersistedSettings({ + nativeRolloutVersion: 1, + loadNativeHydrateThresholdRecords: 12000, +}); +assert.equal(migratedLegacyHydrateThresholdDefault.loadNativeHydrateThresholdRecords, 30000); +assert.equal(migratedLegacyHydrateThresholdDefault.nativeRolloutVersion, 2); + +const preservedCustomHydrateThreshold = mergePersistedSettings({ + nativeRolloutVersion: 1, + loadNativeHydrateThresholdRecords: 45000, +}); +assert.equal(preservedCustomHydrateThreshold.loadNativeHydrateThresholdRecords, 45000); +assert.equal(preservedCustomHydrateThreshold.nativeRolloutVersion, 2); console.log("default-settings tests passed"); diff --git a/tests/native-hydrate-hook.mjs b/tests/native-hydrate-hook.mjs index e3d57b6..421c43c 100644 --- a/tests/native-hydrate-hook.mjs +++ b/tests/native-hydrate-hook.mjs @@ -110,14 +110,14 @@ const snapshot = { }; const defaultGate = resolveNativeHydrateGateOptions({}); -assert.equal(defaultGate.minSnapshotRecords, 12000); +assert.equal(defaultGate.minSnapshotRecords, 30000); const gatedSmall = evaluateNativeHydrateGate(snapshot, {}); assert.equal(gatedSmall.allowed, false); assert.deepEqual(gatedSmall.reasons, ["below-min-snapshot-records"]); const gatedLarge = evaluateNativeHydrateGate( { - nodes: new Array(6000).fill({ id: "node-x" }), - edges: new Array(6000).fill({ id: "edge-x" }), + nodes: new Array(15000).fill({ id: "node-x" }), + edges: new Array(15000).fill({ id: "edge-x" }), }, {}, ); diff --git a/tests/native-rollout-matrix.mjs b/tests/native-rollout-matrix.mjs new file mode 100644 index 0000000..a139c5d --- /dev/null +++ b/tests/native-rollout-matrix.mjs @@ -0,0 +1,153 @@ +import assert from "node:assert/strict"; + +import { + defaultSettings, + mergePersistedSettings, +} from "../runtime/settings-defaults.js"; +import { + evaluateNativeHydrateGate, + evaluatePersistNativeDeltaGate, + resolveNativeHydrateGateOptions, + resolvePersistNativeDeltaGateOptions, +} from "../sync/bme-db.js"; +import { + GraphNativeLayoutBridge, + normalizeGraphNativeRuntimeOptions, +} from "../ui/graph-native-bridge.js"; + +const migratedLegacy = mergePersistedSettings({ + graphUseNativeLayout: false, + persistUseNativeDelta: false, + loadUseNativeHydrate: false, +}); +assert.equal(migratedLegacy.graphUseNativeLayout, true); +assert.equal(migratedLegacy.persistUseNativeDelta, true); +assert.equal(migratedLegacy.loadUseNativeHydrate, true); +assert.equal(migratedLegacy.loadNativeHydrateThresholdRecords, 30000); +assert.equal(migratedLegacy.nativeRolloutVersion, defaultSettings.nativeRolloutVersion); + +const preservedManualOptOut = mergePersistedSettings({ + nativeRolloutVersion: defaultSettings.nativeRolloutVersion, + graphUseNativeLayout: false, + persistUseNativeDelta: false, + loadUseNativeHydrate: false, + graphNativeForceDisable: true, +}); +assert.equal(preservedManualOptOut.graphUseNativeLayout, false); +assert.equal(preservedManualOptOut.persistUseNativeDelta, false); +assert.equal(preservedManualOptOut.loadUseNativeHydrate, false); +assert.equal(preservedManualOptOut.graphNativeForceDisable, true); + +const migratedLegacyHydrateThreshold = mergePersistedSettings({ + nativeRolloutVersion: 1, + loadNativeHydrateThresholdRecords: 12000, +}); +assert.equal(migratedLegacyHydrateThreshold.loadNativeHydrateThresholdRecords, 30000); + +const preservedCustomHydrateThreshold = mergePersistedSettings({ + nativeRolloutVersion: 1, + loadNativeHydrateThresholdRecords: 42000, +}); +assert.equal(preservedCustomHydrateThreshold.loadNativeHydrateThresholdRecords, 42000); + +const normalizedRuntimeOptions = normalizeGraphNativeRuntimeOptions({ + graphNativeLayoutThresholdNodes: 0, + graphNativeLayoutThresholdEdges: 999999, + graphNativeLayoutWorkerTimeoutMs: 10, + nativeEngineFailOpen: 0, + graphNativeForceDisable: "true", +}); +assert.equal(normalizedRuntimeOptions.graphNativeLayoutThresholdNodes, 1); +assert.equal(normalizedRuntimeOptions.graphNativeLayoutThresholdEdges, 50000); +assert.equal(normalizedRuntimeOptions.graphNativeLayoutWorkerTimeoutMs, 40); +assert.equal(normalizedRuntimeOptions.nativeEngineFailOpen, false); +assert.equal(normalizedRuntimeOptions.graphNativeForceDisable, true); + +const layoutBridge = new GraphNativeLayoutBridge({ + graphUseNativeLayout: true, + graphNativeLayoutThresholdNodes: 280, + graphNativeLayoutThresholdEdges: 1600, +}); +assert.equal(layoutBridge.shouldRunForGraph(279, 1599), false); +assert.equal(layoutBridge.shouldRunForGraph(280, 0), true); +assert.equal(layoutBridge.shouldRunForGraph(0, 1600), true); +layoutBridge.updateRuntimeOptions({ graphNativeForceDisable: true }); +assert.equal(layoutBridge.shouldRunForGraph(500, 5000), false); + +const hydrateGateDefaults = resolveNativeHydrateGateOptions({}); +assert.equal(hydrateGateDefaults.minSnapshotRecords, 30000); + +const hydrateBlocked = evaluateNativeHydrateGate( + { nodes: new Array(29999).fill({}), edges: [] }, + { loadNativeHydrateThresholdRecords: 30000 }, +); +assert.equal(hydrateBlocked.allowed, false); +assert.deepEqual(hydrateBlocked.reasons, ["below-min-snapshot-records"]); +assert.equal(hydrateBlocked.recordCount, 29999); + +const hydrateAllowed = evaluateNativeHydrateGate( + { nodes: new Array(30000).fill({}), edges: [] }, + { loadNativeHydrateThresholdRecords: 30000 }, +); +assert.equal(hydrateAllowed.allowed, true); +assert.deepEqual(hydrateAllowed.reasons, []); +assert.equal(hydrateAllowed.recordCount, 30000); + +const persistGateDefaults = resolvePersistNativeDeltaGateOptions({}); +assert.equal(persistGateDefaults.minSnapshotRecords, 20000); +assert.equal(persistGateDefaults.minStructuralDelta, 600); +assert.equal(persistGateDefaults.minCombinedSerializedChars, 4000000); + +const persistBlocked = evaluatePersistNativeDeltaGate( + { + nodes: new Array(500).fill({}), + edges: new Array(200).fill({}), + tombstones: [], + }, + { + nodes: new Array(520).fill({}), + edges: new Array(210).fill({}), + tombstones: [], + }, + { + persistNativeDeltaThresholdRecords: 20000, + persistNativeDeltaThresholdStructuralDelta: 600, + persistNativeDeltaThresholdSerializedChars: 4000000, + measuredCombinedSerializedChars: 1024, + }, +); +assert.equal(persistBlocked.allowed, false); +assert.deepEqual(persistBlocked.reasons, [ + "below-record-threshold", + "below-structural-delta-threshold", + "below-serialized-chars-threshold", +]); +assert.equal(persistBlocked.maxSnapshotRecords, 730); +assert.equal(persistBlocked.structuralDelta, 30); +assert.equal(persistBlocked.combinedSerializedChars, 1024); + +const persistAllowed = evaluatePersistNativeDeltaGate( + { + nodes: new Array(10000).fill({}), + edges: new Array(10000).fill({}), + tombstones: [], + }, + { + nodes: new Array(10400).fill({}), + edges: new Array(10400).fill({}), + tombstones: new Array(250).fill({}), + }, + { + persistNativeDeltaThresholdRecords: 20000, + persistNativeDeltaThresholdStructuralDelta: 600, + persistNativeDeltaThresholdSerializedChars: 4000000, + measuredCombinedSerializedChars: 5000000, + }, +); +assert.equal(persistAllowed.allowed, true); +assert.deepEqual(persistAllowed.reasons, []); +assert.equal(persistAllowed.maxSnapshotRecords, 21050); +assert.equal(persistAllowed.structuralDelta, 1050); +assert.equal(persistAllowed.combinedSerializedChars, 5000000); + +console.log("native-rollout-matrix tests passed"); diff --git a/ui/panel.html b/ui/panel.html index aa485ac..d99622a 100644 --- a/ui/panel.html +++ b/ui/panel.html @@ -1462,6 +1462,199 @@ +
+
+
+
Native 性能加速
+
+ 控制图布局、图谱增量写回与加载 hydrate 是否尝试使用 Worker / WASM 加速;默认按阈值自动命中。 +
+
+
+ + + + + +
+ 当前会在这里显示 native rollout 总状态与最近一次命中/回退摘要。 +
+
+
+
+
+ +
+
阈值与超时
+
+ 调整 layout / persist / hydrate 什么时候值得尝试 native;通常保持默认即可。 +
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
diff --git a/ui/panel.js b/ui/panel.js index 0bd708b..3e6acbf 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -1250,6 +1250,7 @@ export function refreshLiveState() { if (!overlayEl?.classList.contains("active")) return; _applyGraphRuntimeConfig(_getSettings?.() || {}); _refreshRuntimeStatus(); + _refreshNativeRolloutStatusUi(_getSettings?.() || {}); switch (currentTabId) { case "dashboard": @@ -1468,6 +1469,132 @@ function _readPersistenceDiagnosticObject(snapshot = null) { return snapshot; } +function _formatNativeHydrateGateReasonText(reasons = []) { + const labels = { + "below-min-snapshot-records": "记录数不足", + }; + const normalized = Array.isArray(reasons) + ? reasons.map((item) => String(item || "").trim()).filter(Boolean) + : []; + if (!normalized.length) return "—"; + return normalized.map((item) => labels[item] || item).join(" · "); +} + +function _formatNativeHydrateGateText(diagnostics = null) { + if (!diagnostics || typeof diagnostics !== "object") return "—"; + if (diagnostics.hydrateNativeRequested !== true) return "未请求 native"; + if (diagnostics.hydrateNativeForceDisabled === true) return "已强制关闭"; + if (diagnostics.hydrateNativeGateAllowed === true) return "通过"; + return `已拦截 · ${_formatNativeHydrateGateReasonText(diagnostics.hydrateNativeGateReasons)}`; +} + +function _formatNativeHydrateResultText(diagnostics = null) { + if (!diagnostics || typeof diagnostics !== "object") return "暂无"; + if (diagnostics.hydrateNativeRequested !== true) return "未请求 native"; + if (diagnostics.hydrateNativeForceDisabled === true) return "已强制关闭"; + if (diagnostics.hydrateNativeGateAllowed !== true) return "已拦截"; + if (diagnostics.hydrateNativeUsed === true) { + const status = String(diagnostics.hydrateNativeStatus || "").trim(); + return status ? `已命中 · ${status}` : "已命中"; + } + const fallbackReason = + String(diagnostics.hydrateNativeStatus || "").trim() || + String(diagnostics.hydrateNativePreloadStatus || "").trim() || + "js"; + return `已回退 · ${fallbackReason}`; +} + +function _formatNativeHydrateModuleText(diagnostics = null) { + if (!diagnostics || typeof diagnostics !== "object") return "—"; + const parts = []; + const preload = String(diagnostics.hydrateNativePreloadStatus || "").trim(); + const source = String(diagnostics.hydrateNativeModuleSource || "").trim(); + if (preload) parts.push(`preload ${preload}`); + if (diagnostics.hydrateNativeModuleLoaded === true) parts.push("loaded"); + if (source) parts.push(source); + return parts.join(" · ") || "—"; +} + +function _formatNativeLayoutStatusSummary(layout = null, settings = _getSettings?.() || {}) { + if (settings.graphNativeForceDisable === true) return "已强制关闭"; + if (settings.graphUseNativeLayout !== true) return "已关闭"; + if (!layout || typeof layout !== "object") return "暂无最近布局诊断"; + const parts = [String(layout.mode || layout.solver || "unknown").trim() || "unknown"]; + const totalText = _formatDurationMs(layout.totalMs); + const moduleSource = String(layout.moduleSource || "").trim(); + const reason = String(layout.reason || "").trim(); + if (totalText !== "—") parts.push(totalText); + if (moduleSource) parts.push(moduleSource); + if (reason && reason !== parts[0]) parts.push(reason); + return parts.join(" · "); +} + +function _formatNativePersistStatusSummary(diagnostics = null, settings = _getSettings?.() || {}) { + if (settings.graphNativeForceDisable === true) return "已强制关闭"; + if (settings.persistUseNativeDelta !== true) return "已关闭"; + const snapshot = _readPersistenceDiagnosticObject(diagnostics); + if (!snapshot) return "暂无最近写回诊断"; + const parts = [String(snapshot.path || "pending")]; + const gateText = String(_formatPersistDeltaGateText(snapshot) || "").trim(); + const fallbackReason = String(snapshot.fallbackReason || "").trim(); + if (gateText && gateText !== "—") parts.push(gateText); + if (fallbackReason) parts.push(`回退 ${fallbackReason}`); + return parts.join(" · "); +} + +function _formatNativeHydrateStatusSummary(diagnostics = null, settings = _getSettings?.() || {}) { + if (settings.graphNativeForceDisable === true) return "已强制关闭"; + if (settings.loadUseNativeHydrate !== true) return "已关闭"; + const snapshot = _readPersistenceDiagnosticObject(diagnostics); + if (!snapshot) return "暂无最近加载诊断"; + const parts = [_formatNativeHydrateResultText(snapshot)]; + const gateText = String(_formatNativeHydrateGateText(snapshot) || "").trim(); + const preload = String(snapshot.hydrateNativePreloadStatus || "").trim(); + if (gateText && gateText !== "—" && gateText !== "通过") parts.push(gateText); + if (preload && preload !== "loaded" && preload !== "not-requested") { + parts.push(`preload ${preload}`); + } + return Array.from(new Set(parts.filter(Boolean))).join(" · "); +} + +function _refreshNativeRolloutStatusUi( + settings = _getSettings?.() || {}, + loadInfo = _getGraphPersistenceSnapshot(), +) { + const summaryEl = document.getElementById("bme-native-rollout-status"); + const layoutEl = document.getElementById("bme-native-layout-status"); + const persistEl = document.getElementById("bme-native-persist-status"); + const hydrateEl = document.getElementById("bme-native-hydrate-status"); + if (!summaryEl && !layoutEl && !persistEl && !hydrateEl) return; + + const panelDebug = _getRuntimeDebugSnapshot?.() || {}; + const runtimeDebug = panelDebug.runtimeDebug || {}; + const layout = runtimeDebug?.graphLayout || null; + const persistDelta = _readPersistenceDiagnosticObject( + loadInfo?.persistDelta || runtimeDebug?.graphPersistence?.persistDelta, + ); + const loadDiagnostics = _readPersistenceDiagnosticObject( + loadInfo?.loadDiagnostics || runtimeDebug?.graphPersistence?.loadDiagnostics, + ); + const rolloutVersion = Math.max( + 0, + Math.floor(Number(settings?.nativeRolloutVersion || 0)), + ); + const summaryText = settings.graphNativeForceDisable === true + ? `rollout v${rolloutVersion} · 全局强制关闭 · ${settings.nativeEngineFailOpen !== false ? "fail-open 已启用" : "严格模式"}` + : `rollout v${rolloutVersion} · 按阈值自动尝试 native · ${settings.nativeEngineFailOpen !== false ? "fail-open 已启用" : "严格模式"}`; + if (summaryEl) summaryEl.textContent = summaryText; + if (layoutEl) { + layoutEl.textContent = `Layout:${_formatNativeLayoutStatusSummary(layout, settings)}`; + } + if (persistEl) { + persistEl.textContent = `Persist:${_formatNativePersistStatusSummary(persistDelta, settings)}`; + } + if (hydrateEl) { + hydrateEl.textContent = `Hydrate:${_formatNativeHydrateStatusSummary(loadDiagnostics, settings)}`; + } +} + function _formatLoadDiagnosticsStageLabel(stage = "") { const normalized = String(stage || "").trim(); if (!normalized) return "—"; @@ -1522,6 +1649,9 @@ function _formatPersistenceLoadSummary(loadDiagnostics = null) { const parts = [statusText]; if (stageLabel !== "—") parts.push(stageLabel); if (totalText !== "—") parts.push(`total ${totalText}`); + if (diagnostics.hydrateNativeRequested === true) { + parts.push(`native ${_formatNativeHydrateResultText(diagnostics)}`); + } if (reasonText) parts.push(reasonText); return parts.join(" · "); } @@ -1679,6 +1809,12 @@ function _buildLoadDiagnosticRows(loadDiagnostics = null) { const updatedAtText = diagnostics.updatedAt ? _formatTaskProfileTime(diagnostics.updatedAt) : "—"; + const nativeErrorText = String( + diagnostics.hydrateNativeModuleError || + diagnostics.hydrateNativePreloadError || + diagnostics.hydrateNativeError || + "", + ).trim(); return [ ["Load 阶段", _formatLoadDiagnosticsStageLabel(diagnostics.stage)], @@ -1691,6 +1827,11 @@ function _buildLoadDiagnosticRows(loadDiagnostics = null) { ["前置(除导出)", _formatDurationMs(diagnostics.preApplyOtherMs)], ["Hydrate", _formatDurationMs(diagnostics.hydrateMs)], ["Hydrate 细分", _formatLoadHydrateBreakdownText(diagnostics)], + ["Hydrate Native Gate", _formatNativeHydrateGateText(diagnostics)], + ["Hydrate Native 结果", _formatNativeHydrateResultText(diagnostics)], + ["Hydrate Native Module", _formatNativeHydrateModuleText(diagnostics)], + ["Hydrate Native Records", _formatDurationMs(diagnostics.hydrateNativeRecordsMs)], + ["Hydrate Native 错误", nativeErrorText || "—"], ["Apply 调用", _formatDurationMs(diagnostics.applyInvokeMs)], ["Apply 运行", _formatDurationMs(diagnostics.applyRuntimeMs)], ["Load 未归因", _formatDurationMs(diagnostics.untrackedMs)], @@ -6507,6 +6648,26 @@ function _refreshConfigTab() { "bme-setting-ai-monitor-enabled", settings.enableAiMonitor ?? true, ); + _setCheckboxValue( + "bme-setting-graph-native-force-disable", + settings.graphNativeForceDisable === true, + ); + _setCheckboxValue( + "bme-setting-native-engine-fail-open", + settings.nativeEngineFailOpen !== false, + ); + _setCheckboxValue( + "bme-setting-graph-use-native-layout", + settings.graphUseNativeLayout === true, + ); + _setCheckboxValue( + "bme-setting-persist-use-native-delta", + settings.persistUseNativeDelta === true, + ); + _setCheckboxValue( + "bme-setting-load-use-native-hydrate", + settings.loadUseNativeHydrate === true, + ); _setCheckboxValue( "bme-setting-hide-old-messages-enabled", settings.hideOldMessagesEnabled ?? false, @@ -6841,6 +7002,34 @@ function _refreshConfigTab() { settings.probRecallChance ?? 0.15, ); _setInputValue("bme-setting-reflect-every", settings.reflectEveryN ?? 10); + _setInputValue( + "bme-setting-graph-native-layout-threshold-nodes", + settings.graphNativeLayoutThresholdNodes ?? 280, + ); + _setInputValue( + "bme-setting-graph-native-layout-threshold-edges", + settings.graphNativeLayoutThresholdEdges ?? 1600, + ); + _setInputValue( + "bme-setting-graph-native-layout-worker-timeout-ms", + settings.graphNativeLayoutWorkerTimeoutMs ?? 260, + ); + _setInputValue( + "bme-setting-persist-native-delta-threshold-records", + settings.persistNativeDeltaThresholdRecords ?? 20000, + ); + _setInputValue( + "bme-setting-persist-native-delta-threshold-structural-delta", + settings.persistNativeDeltaThresholdStructuralDelta ?? 600, + ); + _setInputValue( + "bme-setting-persist-native-delta-threshold-serialized-chars", + settings.persistNativeDeltaThresholdSerializedChars ?? 4000000, + ); + _setInputValue( + "bme-setting-load-native-hydrate-threshold-records", + settings.loadNativeHydrateThresholdRecords ?? 12000, + ); _setInputValue("bme-setting-llm-url", settings.llmApiUrl || ""); _setInputValue("bme-setting-llm-key", settings.llmApiKey || ""); @@ -6910,6 +7099,7 @@ function _refreshConfigTab() { _refreshPromptCardStates(settings); _refreshTaskProfileWorkspace(settings); _refreshMessageTraceWorkspace(settings); + _refreshNativeRolloutStatusUi(settings); _highlightThemeChoice(settings.panelTheme || "crimson"); _syncConfigSectionState(); } @@ -6936,6 +7126,21 @@ function _bindConfigControls() { _patchSettings({ enableAiMonitor: checked }); _refreshDashboard(); }); + bindCheckbox("bme-setting-graph-native-force-disable", (checked) => { + _patchSettings({ graphNativeForceDisable: checked }); + }); + bindCheckbox("bme-setting-native-engine-fail-open", (checked) => { + _patchSettings({ nativeEngineFailOpen: checked }); + }); + bindCheckbox("bme-setting-graph-use-native-layout", (checked) => { + _patchSettings({ graphUseNativeLayout: checked }); + }); + bindCheckbox("bme-setting-persist-use-native-delta", (checked) => { + _patchSettings({ persistUseNativeDelta: checked }); + }); + bindCheckbox("bme-setting-load-use-native-hydrate", (checked) => { + _patchSettings({ loadUseNativeHydrate: checked }); + }); bindCheckbox("bme-setting-hide-old-messages-enabled", (checked) => { _patchSettings({ hideOldMessagesEnabled: checked }); }); @@ -7353,6 +7558,55 @@ function _bindConfigControls() { bindNumber("bme-setting-reflect-every", 10, 1, 200, (value) => _patchSettings({ reflectEveryN: value }), ); + bindNumber( + "bme-setting-graph-native-layout-threshold-nodes", + 280, + 1, + 20000, + (value) => _patchSettings({ graphNativeLayoutThresholdNodes: value }), + ); + bindNumber( + "bme-setting-graph-native-layout-threshold-edges", + 1600, + 1, + 50000, + (value) => _patchSettings({ graphNativeLayoutThresholdEdges: value }), + ); + bindNumber( + "bme-setting-graph-native-layout-worker-timeout-ms", + 260, + 40, + 15000, + (value) => _patchSettings({ graphNativeLayoutWorkerTimeoutMs: value }), + ); + bindNumber( + "bme-setting-persist-native-delta-threshold-records", + 20000, + 0, + 200000, + (value) => _patchSettings({ persistNativeDeltaThresholdRecords: value }), + ); + bindNumber( + "bme-setting-persist-native-delta-threshold-structural-delta", + 600, + 0, + 200000, + (value) => _patchSettings({ persistNativeDeltaThresholdStructuralDelta: value }), + ); + bindNumber( + "bme-setting-persist-native-delta-threshold-serialized-chars", + 4000000, + 0, + 50000000, + (value) => _patchSettings({ persistNativeDeltaThresholdSerializedChars: value }), + ); + bindNumber( + "bme-setting-load-native-hydrate-threshold-records", + 12000, + 0, + 200000, + (value) => _patchSettings({ loadNativeHydrateThresholdRecords: value }), + ); const llmPresetSelect = document.getElementById("bme-llm-preset-select"); if (llmPresetSelect && llmPresetSelect.dataset.bmeBound !== "true") { @@ -8282,6 +8536,7 @@ function _getMessageTraceWorkspaceState(settings = _getSettings?.() || {}) { runtimeDebug: null, }; const runtimeDebug = panelDebug.runtimeDebug || {}; + const graphPersistence = _getGraphPersistenceSnapshot(); return { settings, @@ -8289,7 +8544,12 @@ function _getMessageTraceWorkspaceState(settings = _getSettings?.() || {}) { runtimeDebug, recallInjection: runtimeDebug?.injections?.recall || null, graphLayout: runtimeDebug?.graphLayout || null, - persistDelta: runtimeDebug?.graphPersistence?.persistDelta || null, + persistDelta: + graphPersistence?.persistDelta || runtimeDebug?.graphPersistence?.persistDelta || null, + loadDiagnostics: + graphPersistence?.loadDiagnostics || + runtimeDebug?.graphPersistence?.loadDiagnostics || + null, messageTrace: runtimeDebug?.messageTrace || null, recallLlmRequest: runtimeDebug?.taskLlmRequests?.recall || null, recallPromptBuild: runtimeDebug?.taskPromptBuilds?.recall || null, @@ -8315,6 +8575,7 @@ function _renderMessageTraceWorkspace(state) { state.recallInjection?.updatedAt, state.graphLayout?.updatedAt, state.persistDelta?.updatedAt, + state.loadDiagnostics?.updatedAt, state.recallLlmRequest?.updatedAt, state.extractLlmRequest?.updatedAt, state.extractPromptBuild?.updatedAt, @@ -8353,6 +8614,9 @@ function _renderMessageTraceWorkspace(state) {
${_renderPersistDeltaTraceCard(state)}
+
+ ${_renderHydrateNativeTraceCard(state)} +
`; @@ -9060,6 +9324,97 @@ function _renderPersistDeltaTraceCard(state) { `; } +function _renderHydrateNativeTraceCard(state) { + const diagnostics = _readPersistenceDiagnosticObject(state.loadDiagnostics); + if (!diagnostics) { + return ` +
Hydrate / Native 诊断
+
+ 还没有 hydrate 诊断快照。等图谱完成一次真实加载后,这里会显示 load hydrate 是否命中 native、是否被 gate 拦截,以及 preload / module / fallback 状态。 +
+ `; + } + + const errorText = String( + diagnostics.hydrateNativeModuleError || + diagnostics.hydrateNativePreloadError || + diagnostics.hydrateNativeError || + diagnostics.error || + "", + ).trim(); + + return ` +
+
+
Hydrate / Native 诊断
+
+ 记录最近一次图谱加载的 hydrate 是否尝试 native、是否命中、以及 preload / module / fallback 明细。 +
+
+ ${_escHtml(_formatTaskProfileTime(diagnostics.updatedAt))} +
+
+
+ Load 阶段 + ${_escHtml(_formatLoadDiagnosticsStageLabel(diagnostics.stage))} +
+
+ Load 来源 + ${_escHtml(String(diagnostics.source || diagnostics.statusLabel || "—"))} +
+
+ Load 状态 + ${_escHtml( + diagnostics.success === true + ? "成功" + : diagnostics.success === false + ? "失败" + : "未知", + )} +
+
+ Hydrate Native Gate + ${_escHtml(_formatNativeHydrateGateText(diagnostics))} +
+
+ Hydrate Native 结果 + ${_escHtml(_formatNativeHydrateResultText(diagnostics))} +
+
+ Preload + ${_escHtml(String(diagnostics.hydrateNativePreloadStatus || "—"))} +
+
+ Module + ${_escHtml(_formatNativeHydrateModuleText(diagnostics))} +
+
+ Load / Hydrate + ${_escHtml( + `${_formatDurationMs(diagnostics.totalMs)} / ${_formatDurationMs(diagnostics.hydrateMs)}`, + )} +
+
+ Hydrate 细分 + ${_escHtml(_formatLoadHydrateBreakdownText(diagnostics))} +
+
+ Native Records + ${_escHtml(_formatDurationMs(diagnostics.hydrateNativeRecordsMs))} +
+
+ 未归因 + ${_escHtml(_formatDurationMs(diagnostics.untrackedMs))} +
+
+ ${_renderMessageTraceTextBlock( + "Hydrate / native error", + errorText, + "当前没有 hydrate / native error。", + )} + `; +} + function _renderMessageTraceTextBlock(title, text, emptyText = "暂无内容") { const normalized = String(text || "").trim(); return ` @@ -10269,6 +10624,15 @@ function _renderTaskDebugGraphPersistenceCard(graphPersistence) { } const persistDelta = graphPersistence.persistDelta || null; + const loadDiagnostics = _readPersistenceDiagnosticObject( + graphPersistence.loadDiagnostics, + ); + const hydrateNativeError = String( + loadDiagnostics?.hydrateNativeModuleError || + loadDiagnostics?.hydrateNativePreloadError || + loadDiagnostics?.hydrateNativeError || + "", + ).trim(); return `
@@ -10375,6 +10739,30 @@ function _renderTaskDebugGraphPersistenceCard(graphPersistence) { : "—", )}
+
+ Hydrate Native Gate + ${_escHtml( + _formatNativeHydrateGateText(loadDiagnostics), + )} +
+
+ Hydrate Native 结果 + ${_escHtml( + _formatNativeHydrateResultText(loadDiagnostics), + )} +
+
+ Hydrate Native Module + ${_escHtml( + _formatNativeHydrateModuleText(loadDiagnostics), + )} +
+
+ Hydrate Native 错误 + ${_escHtml( + hydrateNativeError || "—", + )} +
Persist Delta 路径 ${_escHtml(String(persistDelta?.path || "—"))} @@ -12723,6 +13111,7 @@ function _patchSettings(patch = {}, options = {}) { if (options.refreshTheme) _highlightThemeChoice(settings.panelTheme || "crimson"); _refreshCloudStorageModeUi(settings); + _refreshNativeRolloutStatusUi(settings); return settings; }