Skip to content

perf(chat): 长会话渲染进程 CPU 尖峰与隐藏窗口空转 - #800

Merged
coder-hhx merged 1 commit into
mainfrom
fix/perf-frontend-render
Sep 11, 2026
Merged

perf(chat): 长会话渲染进程 CPU 尖峰与隐藏窗口空转#800
coder-hhx merged 1 commit into
mainfrom
fix/perf-frontend-render

Conversation

@coder-hhx

Copy link
Copy Markdown
Collaborator

Linked issue

Closes #797

Summary

现场:3.7 小时会话把 WebKit WebContent 顶到 88~124% CPU、RSS 从 520MB 涨到 1281MB
连带把 WindowServer 拖到 34~70%。sample 的主线程 1119/1119 采样全在 run loop 内,主导路径是
定时器驱动的 JS:timerFired → dispatchEvent → JSEventListener::handleEvent → JSC::constructIntlDateTimeFormat → udat_open → icu::SimpleDateFormat

也就是说,热点不是"某处算了太多",而是渲染/tick 路径里反复新建 Intl formatter
Intl.*Format 的每次构造都会走一次 ICU 初始化,而在每秒重渲染的组件里等于每 tick 一次。

本次改动:

  • lib/shared/intlFormatters.ts(新增):按 ${variant}|${locale} 缓存
    Intl.NumberFormat / DateTimeFormat / RelativeTimeFormat。键刻意不用"序列化 options",
    而是让调用点给一个稳定的 variant 名——否则每次调用都要付一次 JSON.stringify
    等于把省下来的 ICU 开销换成字符串开销(同一份 sample 里 WTF::findCommon 那类热点)。
  • 替换所有 per-call 构造点trajectory/stats.ts(状态栏渲染体逐项调用)、
    trajectory/presentation.ts(含 toLocaleTimeString 内部构造)、ConversationSearchDialog
    SharedHistoryManagerModalmemory/panelModelchat/userMessageContent
    以及 WebUI 的 StatusDashboardPage(此前每行每秒构造一次)。
  • lib/shared/documentVisibility.ts(新增):合并设置页原有的 hidden 判定
    document.hiddenvisibilityState 都算隐藏,WKWebView 后台启动出现过两者不同步)。
    ConversationStatsBar / AssistantWorkTrace / BackgroundTasksPanel 的 1s 心跳在窗口
    不可见时不再重渲染——这些帧用户看不到,代价却是每秒重排 + 重建 formatter。
  • 共享副本补早退agent-ui/lib/chat/uiMessages.tsrebalanceHostedSearchTextBoundaries
    在没有任何 hosted-search 块时返回同一数组(桌面端副本早有这个早退),避免每个文本增量
    都重建整个块数组。
  • 表格 containmentMarkdown.tsx 的表格滚动容器加 contain: layout paint style
    RenderTableSection::paintObject 那类重排/重绘限制在自身子树内。

刻意没做:行级 contain(虚拟列表靠 ResizeObserver 量高,需要真机验证)、
回合内全量重新测量与轨迹账本每秒重建(见 #797 的"未覆盖"一节,风险与前缀收益不成比例)。

Change scope

  • Modules: agent-ui(shared UI)、agent-gui(tests)、agent-gateway/web(WebUI 镜像)
  • Key paths:
    • crates/agent-ui/src/lib/shared/intlFormatters.ts(新增)
    • crates/agent-ui/src/lib/shared/documentVisibility.ts(新增)
    • crates/agent-ui/src/lib/trajectory/{stats,presentation}.ts
    • crates/agent-ui/src/components/chat/{ConversationStatsBar,AssistantWorkTrace,ConversationSearchDialog,SharedHistoryManagerModal}.tsx
    • crates/agent-ui/src/components/{Markdown.tsx,project-tools/BackgroundTasksPanel.tsx}
    • crates/agent-ui/src/{lib/chat/{uiMessages.ts,userMessageContent.tsx},pages/settings/{SettingsShell.tsx,memory/panelModel.ts}}
    • crates/agent-gateway/web/src/pages/StatusDashboardPage.tsx
    • crates/agent-gui/test/chat/rendering-perf-locks.test.mjs(新增)

documentVisibility / intlFormatters 都在 @liveagent/ui 内,WebUI 通过同一 workspace 包消费,
不涉及额外镜像改动;StatusDashboardPage(WebUI 专属)单独同步。

Screenshots / preview

无像素级 before/after:本次改的是渲染成本与隐藏窗口的空转,界面观感不变。治理检查会因改动
命中前端路径而要求配图,处理沿用 #730 / #796 的先例(加 governance-exempt 后重新 Ready)。

线格式层面的前后对照——同一段渲染路径里 formatter 的构造次数(新增用例实测):

修复前:formatStatTokens/formatStatCount 各调用一次 → 每次一次 ICU 初始化
        状态栏运行中每秒重渲染 → 每秒数十次构造
修复后:cachedNumberFormat(locale, variant) 命中缓存 → 25 次调用 / 1 次构造(测试断言 built === 3,三档)

修复前:ConversationStatsBar / AssistantWorkTrace / BackgroundTasksPanel
        窗口隐藏时仍每秒重渲染
修复后:隐藏时不启动心跳,回到前台由 visibilitychange 重启并立刻补齐读数

Verification

  • cd crates/agent-gui && pnpm test:frontend3095/3095 通过(改动前 3088,新增 7 条:
    6 条渲染性能反漂移锁 + 1 条本 PR 关联的既有断言;无快照更新)。
  • cd crates/agent-gui && pnpm build(tsc + vite)通过;pnpm lint(biome,333 文件)无问题。
  • cd crates/agent-gateway/web && pnpm build && pnpm lint && pnpm test → build/lint 通过、
    718/718 通过(WebUI 侧含 StatusDashboardPage 与共享包)。
  • cd crates/agent-ui && pnpm exec biome check <本次改动的 14 个文件> → 无问题
    (其余报错是本 PR 未触及的既有问题:ProviderModal.tsx 的 useTemplate、transcript.css 的 specificity)。
  • 新增 test/chat/rendering-perf-locks.test.mjs:用包装 Intl.NumberFormat 计数的方式断言
    "同一 (variant, locale) 只构造一次",并断言 isDocumentHidden()hidden
    visibilityState 两个信号都成立——这两条正是本次修复的核心契约,避免以后改动悄悄退化。

Pre-submit checklist

现场:3.7 小时会话把 WebKit WebContent 顶到 88~124% CPU、RSS 涨到 1.28GB。
sample 主线程全在 run loop 内,主导栈是定时器驱动的
`constructIntlDateTimeFormat → udat_open → icu::SimpleDateFormat`。

- 新增 lib/shared/intlFormatters.ts:按 `${variant}|${locale}` 缓存
  Intl.NumberFormat / DateTimeFormat / RelativeTimeFormat。构造 formatter 会走
  ICU 初始化,在渲染体或每 tick 路径里反复 new 等价于每 tick 一次 ICU 初始化。
  键用调用点给定的稳定 variant 名而不是序列化 options,避免把省下的 ICU 开销
  换成每次调用的字符串开销。
- 换掉 stats / presentation / 搜索弹窗 / 共享历史 / 记忆面板 / 用户消息 / WebUI
  状态面板里所有 per-call 构造;WebUI 状态面板此前是每行每秒一次。
- 新增 lib/shared/documentVisibility.ts(合并设置页原有的 hidden 判定),
  ConversationStatsBar / AssistantWorkTrace / BackgroundTasksPanel 的 1s 心跳
  在窗口不可见时不再重渲染。
- agent-ui 的 rebalanceHostedSearchTextBoundaries 补早退:没有 hosted-search 块
  时返回同一数组,避免每次文本增量重建整个块数组(桌面端副本早有该早退)。
- Markdown 表格滚动容器加 `contain: layout paint style`,把表格重排/重绘
  (RenderTableSection::paintObject)限制在自身子树内。
- 新增 test/chat/rendering-perf-locks.test.mjs 锁住 formatter 复用与可见性判定。
@StackCairn
StackCairn marked this pull request as draft September 11, 2026 17:38
@github-actions

Copy link
Copy Markdown
Contributor

PR governance checks failed — this PR has been converted to draft.

  • UI change without screenshots: this PR modifies frontend code. Please add before/after screenshots or a recording under "Screenshots / preview" in the PR body.

Fix the items above, then click Ready for review to re-run the checks.

@coder-hhx coder-hhx added the governance-exempt Skip PR governance checks label Sep 11, 2026
@coder-hhx
coder-hhx marked this pull request as ready for review September 11, 2026 17:39
@coder-hhx
coder-hhx merged commit dd2762c into main Sep 11, 2026
9 of 10 checks passed
@coder-hhx
coder-hhx deleted the fix/perf-frontend-render branch September 11, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

governance-exempt Skip PR governance checks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Performance] 长会话渲染进程 CPU 尖峰至 124%、内存涨到 1.3GB:定时器每 tick 重建 Intl formatter + 表格重绘

1 participant