Files
HomeAgent/internal/sdk/memory.go
JianFeeeee 3e201dd4af perf(webui): pulse 端点不再全量扫描图库;修星图每帧全量开销
用户反馈「非常卡」。实测定位到两处(都在轮询/渲染的每帧路径上):

## 1. 后端:/memory/graph/pulse 名为轻量、实为全量

该端点自称轻量(注释写着「只回窗口内变动的实体,与完整图谱差两个数量级」),
但实现是**先调 GraphData() 取全量快照 → json.Marshal 约 3.3MB →
反序列化 → 才按时间过滤**。生产实测与全量端点**同价**:

    /memory/graph          135-147ms
    /memory/graph/pulse    144-157ms   ← 每 10s 被轮询一次

即每 10 秒白付一次全量序列化。生产库 3190 块 / 2692 边。

改法:新增 `GraphDB.BlocksChangedSince(since, limit)` —— 库侧 SQL 过滤
(`WHERE created_at >= ? OR updated_at >= ?` + LIMIT),响应体只含窗口内
的块。limit 上限保护:窗口内可能有很多块(批量迁移),而星图只需知道
「有新东西」。

接口走内置 `sdk.MemoryAPI`(非 pubsdk),与既有 GraphData 同理,
**不影响外部插件**。

## 2. 前端:星图渲染循环每帧全量遍历 + 永不停止

三处,都是我上一批星图改动引入的:

- **RAF 循环从不停**:切到聊天/终端页签、甚至页面不可见时,仍以 60fps
  跑着每帧 3190 节点的遍历。改用显式开关 + `starmapWake()` 重新点火
  (判据问「星图容器是否可见」,不用 document.hidden —— 星图可能在
  未激活的页签里,那时页面可见但星图看不到)。
- **starmapTickMorph 每帧扫全部节点**:3190 × 60fps = 每秒 19 万次
  userData 读取与分支,而绝大多数帧里「正在形变」的节点是 0 个。
  改用 `starmapMorphSet` 活跃集合,空集时 O(1) 返回。
- **标签层每帧重建 plan + measureText**:加 100ms 节流(文字层 10fps
  肉眼无差别,3D 图形仍 60fps)与 measureText 缓存(同步文字布局很贵,
  同文字同字体宽度不变)+ 缓存上限保护。

## 验证

- `blocks_changed_test.go`(新增):窗口语义、空窗口恒 0(否则星图会每次
  轮询重放生长动画)、limit 真的封顶
- 既有 pulse 判据全过(`OnlyRecent` / `SinceWidens` / `NoVector` /
  `IgnoresFrozenLegacyNodes`)—— 改写没破坏语义。
  测试替身 `fakeGraphMemory` 同步实现新方法,且**忠实反映过滤语义**
  (偷懒返回全部会把「窗口过滤」判据架空)
- `go test ./... -count=1` → 48 包 ok / 0 FAIL
2026-10-08 12:35:28 +08:00

64 lines
2.4 KiB
Go
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.

package sdk
import (
"time"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
)
// MemoryBlockView 是块的最小只读视图(供星图轮询端点)。
//
// ★ 不直接用 memory.MemoryBlock:那是内核内部类型,而本包是**插件边界**。
// SDK 接口只暴露端点真正要用的字段(原来是先 GraphData 全量再转成
// graphBlockView 做同样的事,只是绕了一圈)。
type MemoryBlockView struct {
ID string `json:"id"`
Modality string `json:"modality"`
Text string `json:"text,omitempty"`
PayloadDigest string `json:"payload_digest,omitempty"`
MIME string `json:"mime,omitempty"`
Source string `json:"source,omitempty"`
Tool string `json:"tool,omitempty"`
Scene string `json:"scene,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// MemoryAPI 是内置插件使用的全量图记忆接口。
type MemoryAPI interface {
pubsdk.MemoryAPI
// GraphData 返回整个知识图谱的完整快照。
GraphData() (map[string]interface{}, error)
// BlocksChangedSince 返回自 since 之后创建或更新过的块(供星图轻量轮询)。
//
// ★ 为何单开一个方法而不复用 GraphData:后者是全量快照(生产实测
// 3190 块 + 2692 边,JSON 约 3.3MB、135ms),而轮询端点只想知「有新东西」。
// 原先 pulse 端点是先取全量再过滤 —— 实测与全量端点同价(145ms),
// 「轻量端点」名不副实。库侧 SQL 过滤后响应体差两个数量级。
//
// 属内置接口(非 pubsdk),与 GraphData 同理,不影响外部插件。
BlocksChangedSince(since time.Time, limit int) ([]MemoryBlockView, error)
}
type Entity = pubsdk.Entity
type Relation = pubsdk.Relation
type Triple = pubsdk.Triple
// TextMemoryAPI 是内置插件使用的全量文本记忆接口。
type TextMemoryAPI interface {
pubsdk.TextMemoryAPI
// RecentEvents 返回最近 n 条记忆事件。
RecentEvents(n int) ([]TextEvent, error)
// Stats 返回文本记忆的运行统计。
Stats() map[string]interface{}
}
type TextEvent = pubsdk.TextEvent
// MediaAttachment 是记忆附件(媒体)在插件边界上的表示。
// 与公共 SDK 同一类型,内置插件与外部插件用同一套字段。
type MediaAttachment = pubsdk.MediaAttachment
type DocMemoryAPI = pubsdk.DocMemoryAPI
type Doc = pubsdk.Doc