perf(chat): 长会话渲染进程 CPU 尖峰与隐藏窗口空转 - #800
Merged
Merged
Conversation
现场: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
marked this pull request as draft
September 11, 2026 17:38
Contributor
|
PR governance checks failed — this PR has been converted to draft.
Fix the items above, then click Ready for review to re-run the checks. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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那类热点)。trajectory/stats.ts(状态栏渲染体逐项调用)、trajectory/presentation.ts(含toLocaleTimeString内部构造)、ConversationSearchDialog、SharedHistoryManagerModal、memory/panelModel、chat/userMessageContent,以及 WebUI 的
StatusDashboardPage(此前每行每秒构造一次)。lib/shared/documentVisibility.ts(新增):合并设置页原有的 hidden 判定(
document.hidden与visibilityState都算隐藏,WKWebView 后台启动出现过两者不同步)。ConversationStatsBar/AssistantWorkTrace/BackgroundTasksPanel的 1s 心跳在窗口不可见时不再重渲染——这些帧用户看不到,代价却是每秒重排 + 重建 formatter。
agent-ui/lib/chat/uiMessages.ts的rebalanceHostedSearchTextBoundaries在没有任何 hosted-search 块时返回同一数组(桌面端副本早有这个早退),避免每个文本增量
都重建整个块数组。
Markdown.tsx的表格滚动容器加contain: layout paint style,把
RenderTableSection::paintObject那类重排/重绘限制在自身子树内。刻意没做:行级
contain(虚拟列表靠ResizeObserver量高,需要真机验证)、回合内全量重新测量与轨迹账本每秒重建(见 #797 的"未覆盖"一节,风险与前缀收益不成比例)。
Change scope
crates/agent-ui/src/lib/shared/intlFormatters.ts(新增)crates/agent-ui/src/lib/shared/documentVisibility.ts(新增)crates/agent-ui/src/lib/trajectory/{stats,presentation}.tscrates/agent-ui/src/components/chat/{ConversationStatsBar,AssistantWorkTrace,ConversationSearchDialog,SharedHistoryManagerModal}.tsxcrates/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.tsxcrates/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 的构造次数(新增用例实测):
Verification
cd crates/agent-gui && pnpm test:frontend→ 3095/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
desktop-release.yml的既有本地改动未纳入)