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;
}