Files
HomeAgent/shared/starmap/README.md
JianFeeeee cf3d3769b1 perf(starmap): WebUI/GUI 星图卡顿修复 + 抽出共享「布局数学」单一源
## 症状与实测

WebUI 星图建图阻塞主线程 9339ms、draw calls 7514;GUI 建图 2466ms、
draw calls 3477。两者在空闲时仍在空转渲染。

## WebUI(internal/plugins/webui/dashboard.js)

- 力导向布局 O(n²) 两两互斥改**网格近邻**,并移入 **Web Worker**
  ⇒ 建图阻塞 **9339ms → 57ms**
- 每节点光晕 3753 个 draw call → 1 个 InstancedMesh
- 边 966 → 4 个 LineSegments
- 空闲帧跳过 + 星空时间驱动
- 结果:draw calls **7514 → 3760**,空闲阻塞 **99% → 3%**

## GUI(cmd/gui/renderer/app.js)

- draw calls **3477 → 1809**,建图 **2466ms → 943ms**
- 首屏可见性守卫(GUI 默认 chat 视图,星图不该在后台初始化)
- 空闲 12fps → 0.2fps

修 GUI 两个自引 bug:
  1. 抄 WebUI 的 `starmapMorphSet`(GUI 实际用 `state.starmapGrown`)⇒
     每帧 ReferenceError、星图 0 帧;
  2. `camKey` 读 `starmapCam.position` 无守卫(`switchChatPanel` 同步调
     `starmapAnimate` 时 cam 为 nil)。**同一缺陷 WebUI 也存在,两处均已补守卫。**

## 共享源(方案 A:只共享「布局数学」这一层)

新增 `shared/starmap/layout.js`(3 个纯函数,零 DOM/全局引用)+
生成器 `tools/starmap-sync.mjs` + 门禁 `make check-starmap-shared`(已进
make test)。三处(共享源 / dashboard.js / app.js)的标记块必须逐字节相同。

**为何只共享这一层**(实测依据):web 星图 54 个函数、gui 24 个,两者同名
函数逐字节相同数为 **0**。GUI 有设备桥/computer use/托盘/自有面板,WebUI 有
侧栏页签体系 —— 共享受众完全不同的 UI 代码是错的,能安全共享的只有布局数学。

**为何用生成器而非运行时加载**:GUI 用 file:// (main.js 的 loadFile),可能
离线;让其拉服务端 dashboard.js 会把「星图能否初始化」绑到网络与登录态上。

**为何 GUI 不用 Web Worker**:file:// 下外链与 Blob Worker 实测都能跑,但不把
「星图能否初始化」押在打包环境的 Worker 行为上。

## 判据

- `cmd/gui/starmap-perf.test.mjs`(21 条,接入 npm test)
- 扩充 `internal/plugins/webui/starmap_perf_test.go`(同源判据 +
  checkStarmapSharedBlock)
- 变异 M1/M2/M3 全部报红

## 附带

cmd/gui/renderer/app.js 同时含上一提交的 GUI `system` 事件消费(协议修复),
因同文件交织未再拆分,见提交 fix(clients)。
2026-10-13 16:21:04 +08:00

70 lines
3.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 星图布局共享源(WebUI / GUI 共用)
## 一句话
这里只共享**布局数学**——3 个不碰 DOM、不碰全局状态的纯函数。
**不是**在合并 WebUI 与 GUI 的星图实现,两者本就不该合并。
## 共享范围(就这么大)
| 文件 | 内容 |
|---|---|
| `layout.js` | `starmapLayoutRelax`(力导向松弛)、`starmapLayoutIterationsFor`(迭代数自适应)、`starmapLayoutMaxMoveThreshold`(收敛早停阈值) |
判据实测:**153 行代码、3 个函数、零 `document`/`window`/`THREE`/`state.*` 引用**。
入参只有 `flat`(扁平坐标数组)、`nodes`、`edges`、`maxR`,出参是同一份坐标。
没有 UI、没有状态、没有渲染——纯数学。
## ★ 明确不共享的部分(别去"补完"它)
WebUI 与 GUI 是**两种不同的客户端**,不是同一份前端的两个皮肤:
| | WebUI | GUI |
|---|---|---|
| 形态 | 浏览器页面,Go `//go:embed` 进二进制 | Electron 桌面应用,`file://` 加载 |
| 视图体系 | 侧栏页签(概览/对话/星图/插件/设置/适配器/内核) | 自有面板 + 托盘 + 设备桥 |
| 星图承载 | 概览页门面 + 聊天面板 + 独立页签(三容器搬运) | 聊天内的星图子面板 |
| 节点标签 | `starmapDrawLabels` 2D 覆盖层 | 每节点 canvas sprite |
| 活跃集概念 | `starmapMorphSet`(mesh 集合) | `state.starmapGrown`(nodeId→时间戳) |
| 独有能力 | SSE `Last-Event-ID` 重放、`smAdaptGraph` 归一化、星图页签 | computer use、`disposeStarmapObject` 显式 GPU 释放、`starmapPanelActive` 面板判据 |
实测:WebUI 星图侧有 **54 个函数**,GUI 只有 **24 个**;两份文件 69 个同名函数里
**逐字节相同的为 0**(缩进风格都不同:GUI 2 空格 / WebUI tab)。
强行统一这些是**跨客户端 UI 重构**,风险远大于收益。
⇒ 只有「布局数学」这一层,两边的**语义本就该完全相同**、且**分叉的代价已被证明高昂**,
才值得抽取。其余部分请各自演化。
## 为什么必须共享这一层
分叉的代价不是理论上的,已经兑现过两次:
1. WebUI 把 O(n²) 全对全布局换成网格近邻(生产库 3754 块:3.52 亿次配对 → n·k 次),
**GUI 那份没跟着改** ⇒ 换客户端卡顿原样存在(实测单次建图阻塞 1.8–9 秒)。
2. 把空闲帧跳过从 WebUI 抄进 GUI 时抄了 `starmapMorphSet`(**WebUI 独有**),
GUI 实际用 `state.starmapGrown` ⇒ 渲染循环**每帧 ReferenceError、星图 0 帧**。
JS 不校验未声明标识符,语法检查看不出来。
## 机制
```
shared/starmap/layout.js ← 唯一事实源(改这里)
│
├─ tools/starmap-sync.mjs ─→ internal/plugins/webui/dashboard.js [BEGIN/END 标记块]
└─────────────────────────→ cmd/gui/renderer/app.js [BEGIN/END 标记块]
```
为什么用「编辑期生成」而不是「运行时加载」:GUI 用 `file://` 且可能离线启动,
让它去服务端拉 `dashboard.js` 会把「星图能否初始化」绑到网络与登录态上;
反过来让 WebUI 内联 GUI 的代码要动 `go:embed` 与启动顺序。
生成器两边风险都避开:**运行时零依赖,两侧各自仍是自包含文件**。
## 用法
node tools/starmap-sync.mjs # 同步
node tools/starmap-sync.mjs --check # 只校验,漂移则退出 1
make check-starmap-shared # 同上(已接入 make test)
★ 改布局算法**只改 `layout.js`**,然后跑 sync。
手改任一副本会被三处门禁同时拦下(生成器 `--check` + WebUI 的 Go 判据 + GUI 的 `.mjs` 判据)。