Files
goodbuddy/docs/architecture/performance-principles.md
T

19 KiB
Raw Blame History

GoodBuddy 性能原则

文档信息

  • 适用范围: Desktop(Electron Main、Preload、Renderer)及其后台进程和线程。
  • 用途: 新功能设计和代码评审的约束。多个 agent 并行修改代码时,以本文为统一标准。
  • 关联文档: docs/roadmap/product-performance-experience-improvement-plan.md(测量记录与实施进度)。
  • 架构与验收: 桌面存储与并发执行定义一次完整交付的职责和范围;桌面并发验收定义多项目、多会话、多子任务负载及证据。本文件是性能规范的唯一数值来源。
  • 参照: VS Code 的进程分工和渲染做法。我们没有在同一台机器上实测过 VS Code,下面的预算按常见的流畅标准设定。

1. 性能预算

“流畅”用下面的数字判断,不靠感觉。“标准机”指记录了硬件环境、不降速运行的参考机;Renderer 降速场景使用 GB_PERF_CPU_THROTTLE=4。该降速仅作用于 Renderer,不等于真实低配整机,也不模拟慢 Main 或磁盘。预算适用于单场景及规定的并发负载。

指标 标准机 低端机
按键到下一帧,p95 ≤ 16 ms ≤ 50 ms
点击到界面响应(切换会话、打开面板),p95 ≤ 100 ms ≤ 300 ms
稳定交互期间(打字、滚动、流式输出)超过 50 ms 的长任务 0 尽量为 0,总时长不超过场景时长的 5%
流式输出、滚动时的帧间隔,p95 ≤ 17 ms ≤ 33 ms
Main 事件循环单次阻塞 ≤ 16 ms —
Main 事件循环延迟,p99 ≤ 30 ms —
DOM 节点数 不随会话数、消息数增长 同左

当前 npm run perf:check 使用历史回归上限,部分数值宽于本表,也未覆盖全部并发入口。检查通过不能宣称本表达标。目标交付须在验收报告中分别给出规范预算和回归上限的结果;低端机降速目前只做记录,真实低配和新增自动检查的完成状态另行记录。

点击反馈、所需内容可用和后台工作完成分别计时。显示加载状态只能证明及时反馈;不能用它替代结果就绪。每个进程分别报告长任务/事件循环和排队耗时,禁止把多个独立探针相加为端到端延迟。

2. 原则

P1 Main 只做调度

Main 负责窗口、生命周期、权限和消息转发。数据库查询、文档解析、哈希、向量计算、批量文件读写,都放到 worker 线程或 utilityProcess 里执行。

  • 新代码不能在 Main 里使用 DatabaseSync 和同步文件 API。lint 已有白名单,名单只能减少。
  • 暂时必须留在 Main 的同步操作,单次耗时要能证明在 16 ms 以内,并在代码里注明理由。
  • 耗时可能超过 16 ms 的操作,必须支持取消和进度上报,不能一直占着 Main。
  • 目标交付后 Main 不再直接或通过 helper 打开桌面业务 SQLite;只读、失败回退、维护及退出均在检查范围内。当前迁移例外不是新增同步工作的许可。存储宿主可继续使用同步 SQLite,事务和计算仍需有界。

P2 界面状态局部订阅

状态按领域放进外部 store,组件通过 selector 只订阅自己用到的那一部分。

  • 不再往 App.tsx 里加 useState、useEffect 或领域逻辑。App.tsx 有行数上限,只能往下调。
  • IPC 订阅放在 store 模块里,组件不直接调用 window.goodbuddy.*.on*。
  • 一个按键、一个流式片段,只能让显示它的那一小块界面重新渲染。
  • 传给 memo 组件的回调和 context 值,引用必须稳定。

模块约定(便于维护,所有拆分都按这个结构做):

层 文件命名 内容 不允许
Store <domain>-store.ts create<Domain>Store() 返回 getState / subscribe / 领域动作;纯 TS,不依赖 React;可单测 访问 DOM、调用 React hook
选择器 / Hook <domain>-selectors.ts 或 use-<domain>.ts 基于 useSyncExternalStore 的 selector,返回值引用稳定 在 selector 里新建对象却不做相等比较
同步层 <domain>-sync.ts 与 Main 的 IPC:首次加载、订阅推送、增量保存队列、失败重试、退出前刷盘 放在组件里;整份数据回写
视图 <Domain>*.tsx memo 组件,只从 selector 和稳定的动作读数据 接收 App 的大对象或内联回调
  • App.tsx 只负责布局外壳、路由和把 store 实例通过 context 提供给子树。新增领域状态一律走上表四层。
  • 每个 store 的单元测试和 store 文件放在一起(<domain>-store.test.ts),覆盖顺序、失败重试和边界情况。
  • 领域数据库访问集中在 repository,目标架构中由存储宿主执行;Main 的 IPC handler 只做校验并等待领域操作。受管只读 Worker 与远端 Agent 按各自明确的连接归属执行,不能按源码目录把它们误判为 Main 访问。
  • 多项目/多会话流式更新只通知相关领域与实体;后台子任务不得让全部项目视图重渲染。隐藏视图保留草稿、选择和必要执行订阅,停止无关绘制与全量重读,切回时按已有摘要及增量恢复。

P3 高频数据走专门通道,按帧合并

流式文本、终端输出、进度、滚动这类高频数据,不进全局状态。

  • 多条更新合并后,每帧最多提交一次;同步到全局状态的频率要更低,目前是 250 ms。
  • 高频路径上不能读取布局(getBoundingClientRect、scrollHeight 等),布局读取要集中到每帧一次。
  • 合并更新不能丢失、重复或打乱顺序。每条数据带序号,并有测试覆盖。
  • 显示合并与持久化分开。可以合并进度或替换过时刷新,不能静默丢弃消息、工具结果或需确认的事件;多生产者缓冲只有在用户设置、外部协议、数据安全或实测故障证据要求时才设置容量,否则将负载交给所属 Host。

P4 数据只有一份来源,只传变化部分

  • 每类数据只有一个权威来源,由数据所在进程负责持久化。界面不把整份数据写回。
  • 跨进程的通知直接带上变化的内容,接收方不再“收到通知后重新查全量”。
  • 写入合并成批次和事务,不按每个事件单独开一个事务。
  • 一个数据库有一个业务写入所有者,完整事务不跨 IPC 拆成单条 SQL。Main 不保存第二份可写业务状态;依赖写入的读取等待对应提交,远程 ACK 等待所需事件与 checkpoint 提交成功。
  • 缓存必须说明归属、失效和释放条件,不按浏览过的项目/会话无限积累。查询返回摘要、分页或所需详情,保持完整信息可访问,不用截断历史满足预算。
  • GoodBuddy 自身的中转数据优先使用有界内存;已有完整记录或文件时复用引用,避免重复落盘。预览、最终回答与工具全文分别核实,不能以“已保存消息”推断全文已保存。需要超出内存保留的内容直接使用所属存储的唯一正文,分页读取同一内容,避免临时全文再复制到正式存储。必要文件及清理范围遵守输出与临时资源设计。

P5 工作量和可见内容成正比

  • 长列表只渲染可见部分,包括消息、会话、笔记、文件树和知识库列表。
  • 解析和渲染的结果要缓存,比如 Markdown 语法树。内容没变,就不重新计算。
  • 增量处理:只解析变化的部分。没有把握时,退回到慢但正确的完整处理,并记录在测试里。

P6 启动只做首屏需要的工作

  • 首屏之前只做显示应用壳和恢复上次界面所需的工作。
  • 其他初始化(Provider、Runtime、知识库、通知渠道等)按需执行,或等首屏显示后再空闲执行。
  • 后台模块在第一次使用时才加载。
  • 冷启动、首次进入功能和暖返回分别验收;预加载只使用有界空闲工作,不能为每个项目预建进程、连接或整份历史缓存。进程 spawn 不代表服务就绪,首次可用反馈与数据实际可用分开记录。

P7 先测量,再修改

  • 先跑测量,拿到数据再改代码;每项改动都要有前后对比,基线用临时 worktree 构建。
  • 每个版本至少跑 3 轮,交替运行;结果差异小于同一版本多次运行之间的波动时,不能说成“有提升”。
  • 测量用隔离的用户目录和本地假模型,不发出外部模型请求,结果里不含用户内容。
  • 必测多项目、多会话、多子任务及导入/检索/回顾发布重叠场景,负载和采样按并发验收。记录 offered load、实际吞吐、排队和排空、资源高水位;不得降低输入速率、串行化夹具或丢事件来取得流畅结果。
  • 基准使用固定版本、数据和环境。性能测量与真实模型功能验收分开;受发布构建限制时遵守发布手册,不以性能原则授权额外生产打包。

P8 正确性优先于速度

  • 事件顺序、持久化、取消、崩溃恢复和显示结果都不能因为性能而改变。
  • 异步改造要先确认“先写入、后读取”的顺序,没有测试覆盖就不合并。
  • 风险高、收益小,或者会改变现有功能的优化先不做,在计划文档里记录理由。
  • 取消请求、执行结算和容量释放不是同一时刻;调用方停止等待不能提前释放仍运行的工作。取消一个会话或子任务不得杀死仍服务其他任务的共享宿主。已提交事务如实报告成功,不伪装成回滚;未确认写入不盲目重放。

P10 体验不倒退

性能优化必须保留数据、结果和用户交互语义,不能用体验换速度:

  • 动画和过渡保持原样。 平滑滚动(如“到底部”按钮)、展开收起、悬停、淡入淡出都不能因为优化而消失。任何瞬间写入 scrollTop/scrollTo 的代码都会打断正在进行的平滑滚动,必须在平滑滚动期间暂停。
  • 只渲染可见部分(P5)时,用户状态不能丢。 行被卸载后,它的局部状态也会消失。以下行必须保持挂载,或者把状态存到行外:有焦点的行、用户展开过 <details> 或输入过内容的行、选中文本的两端、待回答的问题和待审批项、打开菜单或正在重命名的行。
  • 只渲染可见部分时,基本操作要和完整列表一致: 跨行选择和复制、滚动位置不因上方插入或删除而跳动(关闭原生 overflow-anchor 时要自己做锚定)、快速拖动滚动条时没有空白帧、渲染结果缓存让重新挂载的行不出现“加载中”。
  • 合并和批处理不能增加交互延迟。 第一条数据立即处理(leading edge),只合并之后同一帧内的数据。打字回显、终端回显不能多等一帧。
  • 低频同步不能让用户看到旧数据。 复制、导出、保存这类读取全局状态的操作,要先把待同步的数据刷进去。
  • 体验要有测试。 涉及动画、滚动、焦点的改动,需要真实 Electron 测试(逐帧采样)或单元测试,并确认测试在旧代码上会失败。

P9 用工具守住规则

  • lint 规则和性能阈值是合并前的硬性条件,不靠人工记得。
  • 规则只能收紧,白名单和上限只能往下调。
  • 每个 agent 只提交自己改的文件和片段,不把别人未提交的改动带进自己的提交。
  • 检查直接 SQL 导入及已知间接打开函数/构造点,按实际宿主区分测试、存储与远端代码。白名单和上限需要与已提交基线比较,不能只靠注释约定;全局同步回退同样要被发现。
  • 普通 CI 与性能执行环境分别提供功能和预算证据;新增并发与首次进入场景纳入候选必需检查,缺少运行环境或结果时保留验收缺口,不由单测通过推断达标。

P11 并发隔离与有界工作

同一会话的事件顺序和同一数据库的提交顺序必须保持;不同项目、会话、子任务的模型等待、重查询与清理不因此共用一把长时间全局锁。

  • Main 全局控制队列不得等待模型、网络、原生启动或耗时清理。必要登记和状态提交可短暂串行,等待在所属操作范围内完成。
  • 重查询复用有界 Worker;必要写入不排在整个导入解析或模型请求之后。长事务先减少事务内无关计算,再测写入排队,不把阻塞简单搬进唯一存储队列。
  • 复用现有池、批处理和队列,记录在途数、排队数及实际资源使用。只有有明确依据时才设置容量;没有依据时将负载交给所属 Host,不因内部数字拒绝用户工作。有实测饥饿才引入最小调度调整,不预建通用优先级框架。
  • 控制和取消不等待新的执行槽位;如果外部或用户设置要求排队,按该语义执行。禁止静默丢失、无限重启或自动扩增进程;不能把未经证明的队列上限包装成稳定性。
  • 每个异步响应携带已有项目/会话/任务/操作归属;迟到响应不得覆盖新选择或新操作。归属检查先于改变共享请求代次、缓存或队列状态。

P12 架构完整性与 KISS

一次性交付按最终职责边界验收,不以全面 Promise 化或局部热点迁出代替。目标范围内正常、后台、失败、重试、取消和退出路径均使用同一权威实现,清理原直接入口和回退。

保持现有技术栈、数据库文件及业务合同,复用已有资源机制。不得顺带新增分布式事务、持久 RPC receipt、通用恢复框架、每项目独立服务或产品审批模式。同步内存缓存读取可以保留,不把纯函数和每个 getter 远程化。设计新增机制必须指出当前失败、现有机制的不足与对应验证,不以“将来可能并发”代替证据。

3. 评审清单

提交性能相关改动,或者会影响热点路径的功能改动时,逐项确认:

  1. Main 里有没有新增同步数据库调用、同步文件 IO,或者耗时可能超过 16 ms 的计算?
  2. 有没有往 App.tsx 里加状态?一次按键或一个流式片段,会让多大范围的界面重新渲染?
  3. 高频路径上有没有读取布局,有没有每次事件都提交状态?
  4. 有没有整份数据回写,或者“收到通知后重新查全量”?
  5. 列表的 DOM 数量会不会随数据量增长?
  6. 有没有在启动阶段新增同步初始化?
  7. 是否附上了 perf:app、perf:main 或 perf:knowledge 的前后对比?
  8. 顺序、取消、持久化和恢复,有没有测试覆盖?
  9. 动画、平滑滚动、过渡还在吗?有没有新增会打断平滑滚动的瞬间滚动写入?
  10. 只渲染可见部分的列表,滚走再滚回来,展开状态、输入内容、选区、焦点还在吗?
  11. 合并或延迟处理后,第一次响应(打字回显、点击反馈)有没有变慢?复制、导出读到的是不是屏幕上的最新内容?
  12. 多项目/多会话/多子任务同时运行时,取消或清理一个请求是否拖住其他任务?队列和缓存是否持续增长?
  13. 存储提交、远程 ACK、调用方返回和资源释放的顺序是否有真实路径及故障注入验证?
  14. 是否删除目标范围内的旧直接访问和同步回退?新增进程或调度机制是否复用现有设施并有实际必要性?
  15. 请求或输出是否产生非必要临时文件、重复全文?是否核实完整内容与预览的区别,并验证分页、引用释放、必要文件的创建失败清理及升级后的残留回收?

4. 当前差距

截至 2026-10-05,以下状态结合存储升级实施记录和并发验收规范核对。表中“已实现”只表示代码或定向测试已有证据,不表示 PERF-15、PERF-16 或 C01-C07 已完成。

原则 差距
P1 Assistant、Knowledge、文件和 Runtime 生产域已由 DesktopStorageClient 接入 utility process;剩余缺口是全量间接访问扫描、异常/维护/退出路径核验,以及 Main 阻塞和存储排队的并发测量。文档解析、分块和大文件哈希仍在各自 worker。
P2 App.tsx 约 7,900 行。输入区、隐藏路由、任务和结果事件、切换会话已不再整体重渲染 App;当前会话的流式吸收(每 250 ms)、队列和活动记录仍经过 App
P3 流式输出和终端输出已有按帧合并;C01/C02/C06 的长期负载仍需验证是否出现排队增长、长帧或内容丢失。
P4 存储域已统一到各自 owner,输出 backing 和附件引用已接入;部分 *Changed 通知仍会重新读取全量,重复缓存和跨域通知的并发影响待测。
P5 消息、会话、笔记、文件树、知识库文档表格已有窗口化;C07 仍需记录冷/暖内容可用时刻和切换后的状态保留。
P6 空配置和真实数据启动有历史测量;当前 C07 尚未完成监督概览、图谱、历史选择和来源正文的完整冷/暖验收。
P9 已有 lint 和历史回归上限;间接 SQLite 打开、白名单单调收紧、并发指标缺失和 CI/性能环境接入仍待补齐。
P10 已有局部输入、刷新和项目切换修复;需要在 C01/C02/C06 中继续验证动画、焦点、滚动、草稿和取消语义没有因降载或合并而退化。
P11 存储请求已移除产品侧 admission 上限并透传给 storage host;运行时仍有若干并发与消息容量数字,需逐项证明属于用户设置、外部协议、数据安全或实测故障约束,否则应删除。C02、C04、C05、C06 尚未证明健康 peer 隔离、故障恢复和队列排空。
P12 目标边界和 KISS 约束已写入架构文档;S01-S07 已有生产组合,但删除旧直接入口、异常路径和全候选统一验收仍未完成。

2026-10-04 补充:监督回顾的来源初始化和继续前版本校验已移入现有 Worker 入口的独立连接,扫描不持有写锁,清单按 200 条提交,生产失败不回退 Main 同步扫描。最终发布事务仍在 Main;实现与实测边界见监督者进度。

2026-10-05 局部修复记录:监督者 UI 的概览、图谱、故事读取也已迁到现有只读 Worker;概览按需加载、列表窗口化及摘要缓存见KISS 局部修复。这与工具侧 Story Graph 读取是不同入口。Main 最终发布及部分大响应仍有超预算样本,不能把局部完成视为全项达标。

随后用户确定按完整存储与并发架构一次性交付。当前已完成存储 owner、主要生产组合和部分输出迁移,但没有完成全面架构验收。P11/P12 与并发验收矩阵约束当前候选的剩余验证,不能作为已有实现或实测结果;上述第一期历史状态也不替代当前验收。