跳转至

渲染器智能体状态性能

状态

该设计已用于渲染器的高频智能体状态路径。它在保持现有事件语义的同时,限制了一次 IPC 突发所执行的同步存储扇出量。

它将分片落地。本文档和 bench:idle-cpu 测试框架先落地,以便存储更改可以针对他人测量的基线进行审查。在存储切片落地之前,下文描述的 setAgentStatuses / transactAgentStatuses 操作和智能体状态写入工作负载尚未在 main 上;测试框架仅测量规模、监听器统计和原始发布扇出。

背景

Orca 可以在一个虚拟化列表行内显示大型展开的工作树谱系。虚拟化根行并不会虚拟化其后代,因此,一个 100 工作树谱系可以一次挂载 100 个 WorktreeCard 实例。

智能体状态 IPC 事件具有突发性。渲染器已经将实时事件分组到 33 毫秒窗口,但原始 flush 使用单独的 Zustand 写入应用每个排队事件。Zustand 会为每次发布同步访问每个监听器。因此,产生的工作同时随状态事件数量和已挂载订阅数量增长:

burst work ~= status events x store listeners x selector work

生产环境跟踪捕获到渲染器反复通过 Set.forEach 进入 flushLiveAgentStatusBurst -> applyAgentStatus -> setAgentStatus -> setState。 确定性 100 工作树测试夹具复现了结构性倍增;参见下文“main 上的基线”,其中包含当前测得的监听器数量和发布成本。

当所有已配置的远程主机被移除后,生产应用随后大幅恢复。移除主机可以停止中继/重连流量、移除已挂载的远程工作树,或两者同时发生,具体取决于主机类型和移除选项。该观察结果将远程存在识别为生产触发因素,但本身无法区分流量规模与已挂载监听器扇出。

只读重连审计排除了完整 PTY 重放导致系统性重复状态发射的可能:重放字节会绕过 OSC 状态解析。重连仍会为每个已附加的远程窗格触发完整终端缓冲区重绘,这是渲染器工作的另一个来源,仍属于后续调查事项。

目标

  • 在密集的智能体状态流量期间,保持大型展开谱系保持响应。
  • 保留每个有序状态转换,包括同一突发内针对单个窗格的重复更新。
  • 对于延迟的实时突发,仅发布一次智能体状态。
  • 保留选择器身份和子渲染隔离。
  • 在不依赖用户生产数据的情况下,使回归可复现。

非目标

  • 更改客户端/服务器状态负载或远程协议。
  • 按窗格对状态事件去重。
  • 更改智能体新鲜度、保留、历史、标题、完成或提供商会话行为。
  • 更改远程重连、PTY 重放或终端重绘行为。
  • 重新设计谱系展示或自动折叠工作树。

设计

限制已挂载订阅的扇出

侧边栏组件使用浅相等选择内聚状态包,而不是为每个字段注册一个监听器。派生数组和映射保留其现有的浅身份行为,因此无关的存储写入不会重新渲染卡片。完整智能体列表模式保留其子级订阅边界;紧凑模式传递已选择的行,以避免两次选择相同输入。

确定性 100 工作树测试夹具固定了由此产生的监听器预算:

界面 监听器预算
工作树卡片状态和缓存 2
智能体行输入 1
工作树活动状态 1
已关闭的上下文菜单 1

卸载测试要求监听器数量返回到先前基线。在捆绑的原型中,未预置智能体的测试夹具从 8,518 个监听器降至 1,218 个;当有 100 个可见智能体行时,候选挂载了 1,618 个。请与“main 上的基线”中的统计进行比较,该值由测试框架直接报告。

共享工作旋转器相位,避免同步挂载查询

工作行保留现有的合成器驱动 CSS 动画和共享视觉相位。animationstart 将每个动画锚定到文档时间零点。将动画查询延迟到该事件,可避免每次挂载时的同步样式刷新,并在 display:none 或运动偏好更改后恢复共享相位。负的挂载时延迟无法在动画重启后保留该相位。

限制旋转器动画开销

工作环保留合成器驱动 CSS 动画,但动画每天重复一次,而不是每秒一次。相同的每秒 12 步现在避免了反复的 React 动画迭代分发。现有的静止包装器和环渲染保持不变。离屏包含经过评估后被拒绝,因为在 1x 显示器低缩放下出现像素回归。

历史记录、隔离测量、完整应用工作区/智能体/子智能体基准测试以及局限性记录在 旋转器渲染性能 中。

按事件顺序折叠突发

存储暴露了单更新操作和两种批量形式:

  • setAgentStatus(paneKey, payload, ...) 为即时实时路径保留位置式单更新 API。
  • setAgentStatuses(updates) 应用预构建的有序列表,而 transactAgentStatuses(operation) 允许 IPC 在单次提交前针对精确的暂存状态推导每个更新。

两个入口点复用相同的单更新状态转换。批量 reducer 将每个结果状态传入下一个更新,因此诸如 working -> waiting -> done 这样的序列会保留与三次顺序调用相同的历史和时间戳。更新在折叠前永远不会按键或去重。

实时 IPC 队列在处理前会被 splice。这保留了现有的重入保证:同步订阅者可以入队另一个事件,而不会导致当前队列被递归清空。活动突发之外的第一个事件仍保持立即执行;在 33 ms 窗口内累积的事件作为一个有序事务应用。启动快照和有界的待水合重试使用相同的事务路径,而不是为每个恢复的面板发布一次。

每个事务只构建一次面板路由所有权,并使用与独立解析器相同的首次匹配语义。分栏布局的叶子成员资格按每个布局根索引一次,因此大型快照执行线性的标签页和叶子工作,而不是为每个面板重新扫描每个已挂载的工作树。

在事务后运行副作用

需要已提交状态的生成标题工作被延迟到事务之后。已接受的更新也会请求新鲜度调度;外层批处理会合并这些请求,并在其单次提交后调度一次共享的新鲜度计时器。生成标题请求按事件顺序折叠并一起发布,包括首次写入和强制替换语义。已解析的标签页标题在事务折叠期间被投影,然后最终标题变更一起发布。完成触发的审查刷新仍保持为延迟的微任务。

批量标题应用保留事件顺序和重复标签页行为,同时只索引一次所有者、克隆每个变更的所有者数组一次,并替换每个顶层映射一次。这使得提交后的标题阶段相对于已挂载标签页加上变更标题保持线性。

这种分离很重要:从 Zustand 更新器内部调用 store 操作会重新进入 store,而在提交前运行副作用会使其观察到过期状态。

语义不变量

顺序应用和批量应用必须在以下方面保持一致:

  • 实时和保留的 agent 映射;
  • 状态历史、updatedAt 和 stateStartedAt;
  • agent 身份、模型、prompt、工具、助手消息和子 agent;
  • 编排和 provider-session 连续性;
  • sleeping-session 和 launch-config 恢复记录;
  • 已退役/已关闭面板的拒绝和继承状态抑制;
  • 保留清理和实时映射驱逐;
  • agentStatusEpoch 和 sortEpoch;
  • 跨中间转换的自动化完成观察;
  • 生成标题输入、新鲜度调度和完成刷新。

等价测试使用固定时间戳,并包含重复的同一面板转换。发布计数测试订阅真实 store,并要求非空批处理产生一次通知,空批处理不产生通知。

基准测试契约

基准测试启动 E2E 模式的 Electron 构建,store 仅暴露用于插桩。它在一个展开的 lineage 中创建 100 个工作树,验证 100 个已挂载卡片,捕获 store 监听器普查,然后通过真实 store 操作应用种子化的有序 agent-status 流量。

基准测试测量同步 store 操作,而不是实时 IPC 前沿或提交后通知路径。真实 store 快照测试覆盖启用自动生成标题时 100 个面板的端到端预算:一次状态、一次批量生成标题和一次批量已解析标题发布。禁用生成标题会移除中间那次发布,与面板数量无关。

产物仅记录比较所需的固定诊断字段:

  • 请求和完成的批处理及更新;
  • store 操作调用和观察到的发布;
  • 经过时间、吞吐量和调度漂移;
  • 最终状态验证;
  • 渲染器平均、p95 和最大 CPU;
  • 渲染器计时器漂移和长任务;
  • 已挂载卡片和监听器数量。

原始进程清单、临时路径、面板标识符和 DOM 文本仅用于诊断,不得嵌入可共享报告中。

在相同机器和操作系统上,以相同 Electron 构建模式运行基线和候选。在该约束下,macOS 和 Linux 的 CPU 样本可比较;Windows 进程 CPU 采集目前无法支持此比较。

测试框架

pnpm run bench:idle-cpu 驱动 config/scripts/run-idle-cpu-benchmark.mjs,它组合了四个模块:

Module Responsibility
idle-cpu-renderer-scale-fixture.mjs 种子化 lineage、agent 行和侧边栏视图状态;采集已挂载卡片和监听器普查
idle-cpu-renderer-timing-probe.mjs 页面内计时器漂移和长任务探针;运行无操作发布工作负载
idle-cpu-process-sampling.mjs 分类 Electron 进程树并按角色采样 CPU/RSS
idle-cpu-synthetic-spinners.mjs 仅用于测量的可见 spinner

采样窗口会超过 --sample-ms 直到工作负载稳定;如果工作负载超出保护阈值,则使运行失败,而不是报告被截断的窗口。这就是为什么 2,000 次发布的运行会报告比请求窗口更长的测量窗口。

--zustand-publications 通过真实 store 发布一个空 partial,因此每次发布的成本恰好是一次完整的订阅者访问,没有其他成本。它将突发成本模型中 listeners x selector work 的一半与 agent-status 负载工作隔离开来,并且不依赖 store API——它在批处理切片前后测量相同的内容。

agent-status 写入工作负载(--agent-status-batches、--agent-status-write-mode)尚未包含在此测试框架中。它依赖 setAgentStatuses,因此会随 store 切片一起落地。

main 上的基线

在 main 的 077f5a11cd4 上测量(macOS、arm64、16 个 CPU),Electron 使用 electron-vite --mode e2e 构建,无头模式,lineage 深度 99 的 100 个工作树,100 个种子化 agent 行,10 秒预热和 30 秒采样窗口。

夹具规模由普查确认,而非假设:100 个 store 工作树、100 个已挂载卡片、100 个已挂载 agent 行,以及 9,279 个 store 监听器。该监听器数量正是设计所针对的乘数。

以 1 ms 频率执行 2,000 次无操作 store 发布,重复三次。每次运行的监听器普查均为 9,279。

指标 中位数 运行
完成墙钟时间 12,325.7 ms 13,148.6 / 12,325.7 / 11,870.5
p50 调度漂移 5,150.3 ms 5,432.3 / 5,150.3 / 4,945.2
p95 调度漂移 9,802.9 ms 10,570.4 / 9,802.9 / 9,390.0
渲染器平均 CPU 18.25% 20.25 / 18.25 / 15.88
渲染器 p95 CPU 32.59% 40.07 / 32.59 / 31.28
渲染器定时器漂移 p95 7.0 ms 7.3 / 5.3 / 7.0

在 2 秒内请求的 2,000 次发布大约需要 12 秒完成,因此在该规模下渲染器大约能维持每秒 160 次发布。每次发布本身都很短——长任务观察器在三次运行中均记录为零条——因此成本表现为调度漂移和持续 CPU,而不是离散的长任务。此处应比较漂移和 CPU,而不是长任务数量。

在同一规模下,使用 --zustand-publications 0 的空闲对照组报告渲染器平均 CPU 为 6.63%,p95 为 17.11%,p95 定时器漂移为 1.6 ms。因此,平均渲染器 CPU 中约 11.6 个百分点可归因于发布扇出,而非已挂载夹具本身。两种情况下都运行 200 个 spinner 动画,因此对照组也将动画成本排除在比较之外。

复现方式:

pnpm run bench:idle-cpu -- --worktrees 100 --lineage-depth 99 \
  --agents-per-worktree 1 --warmup-ms 10000 --sample-ms 30000 \
  --zustand-publications 2000 --zustand-publication-interval-ms 1 \
  --output /tmp/idle-cpu-baseline.json

结果

三次重复使用 100 个已挂载工作树、谱系深度 99、100 个预置 agent 行,并验证了最终状态。重新生成的证据集中的中位数为:

单次 2,000 更新突发 顺序 批量
状态状态发布 2,000 1
store 操作耗时 3,692.0 ms 188.7 ms
更新吞吐量 541.7/s 10,598.8/s
渲染器平均 CPU 36.2% 2.9%
渲染器 p95 CPU 107.3% 8.2%
p95 长任务 4,653 ms 216 ms

直接 store 事务执行的状态状态发布减少 99.95%,在 store 操作上花费的时间减少 94.9%,处理更新的速度快 19.6 倍。渲染器平均 CPU 下降 92.0%,渲染器 p95 CPU 下降 92.4%,p95 长任务下降 95.4%。

33 ms 下的 60 突发 × 32 更新用例是一种持续饱和压力,而非实时生产 SLO。发布次数从 1,920 降至 60,store 操作中位耗时从 2,791.9 ms 降至 323.3 ms。完成中位时间从 5,298.2 ms 降至 2,710.6 ms,p95 调度漂移从 3,073.0 ms 降至 664.9 ms,长任务数量从 57 降至 1。在该频率下,渲染器 p95 CPU 仍处于饱和且噪声较大,因此不作为区分性指标。

20 窗格的人工 OpenCode 回归测试通过,其中按键回显中位数为 12.4 ms,最差按键回显为 25.2 ms,最大定时器漂移为 19.4 ms,渲染器积压丢弃为零。

这些数字来自捆绑的原型,并在此作为目标重新陈述。当 store 切片落地后,将使用测试框架重新测量。

验收标准

  • 100 工作树夹具保持在固定监听器预算之内或以下。
  • 延迟事务执行一次状态状态发布,同时保持有序最终状态,包括在 500 行上限处进行 live-map 淘汰。
  • 100 窗格的启动快照执行一次状态发布和一次批量已解析标题发布,且禁用生成标题;启用生成标题最多增加一次有序批量发布,同时保持最终状态和标题。
  • 顺序与批量等价性测试在相同窗格转换和带有副作用的更新上通过。
  • 在重复候选运行中,渲染器 CPU 尾部和调度漂移得到改善。
  • 20 窗格的人工终端测试报告无输出积压丢弃,也无实质性打字延迟回归。
  • Web 类型检查、聚焦单元测试、lint、max-lines ratchet 和 E2E 构建通过。

兼容性

这是渲染器本地的。它不添加任何 RPC 字段、流操作码、持久化数据、Git 命令或特定于提供者的契约。Native、WSL、SSH、relay、folder workspace 和 git-worktree 状态事件进入同一个渲染器操作。因此,混合客户端/服务器版本无需能力协商。

故障遏制

第一个实时事件仍然立即处理。启动重放和有界待处理重试同步折叠,而不等待 33 ms 实时突发窗口,但会一起发布其已接受的更新。空批次是无操作。如果某个更新已过期或目标为已退役的权威,reducer 仅跳过该更新,并继续按顺序折叠后续事件。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 orca48/orca