Skip to content

perf(chat): preload chart helpers for inline visualizations - #1867

Open
zszz3 wants to merge 4 commits into
vastsa:mainfrom
zszz3:perf/gen-ui-kit-helpers
Open

zszz3 wants to merge 4 commits into
vastsa:mainfrom
zszz3:perf/gen-ui-kit-helpers

Conversation

@zszz3

@zszz3 zszz3 commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

背景

#1826 合入的 inline 可视化出图慢:模型每次都要手写 resize/主题重绘/坐标轴/刻度防重叠代码和 D3 标签,skill 还要求写完回读,经常演变成跑 shell、开浏览器、截图验证,一次出图 1~3 分钟。

怎么改的

  1. 预加载图表层:外壳在片段之前注入内置 D3 7.9.0 和新的 viz 辅助层(apps/desktop/resources/gen-ui/visualize-helpers.js):
    • viz.chart / plot / axes / extent / pruneOverlaps / series / color / duration / regression:负责容器尺寸、resize 和主题重绘、坐标轴、标签防重叠等样板代码;
    • viz.render(el, spec):声明式出常见图表(bar 含横向/分组/堆叠、line、area、point 含趋势线、heatmap、arc 环形),自带坐标轴、图例、数值标签和 tooltip,update() 支持交互更新。
  2. skill 改写(visualize.md,11988 B,仍在 12 KiB 上限内):删掉“写完回读/Review”,新增 Hard rules(一次 Write 后直接回复,不跑 shell、不开浏览器、不截图、不翻源码);Charts 一节改成以 viz.render 为主,viz.chart 用于自定义图。
  3. 兼容:旧片段里固定版本的 D3 标签会被移除,避免覆盖预加载的全局 d3;旧片段照常渲染。CSP、沙箱和协议都没有改动。
  4. visualize.css 增加坐标轴/图例/数值标签样式;ADR、spec(中英文)、E2E 场景文档、NOTICE、unreleased 同步更新。

耗时对比

测试方法:用真实构建的 PI-Desktop,通过 renderer IPC(CDP)发 prompt,经本地代理转发到 DeepSeek 官方 API。模型 deepseek-flash(以自定义 OpenAI 兼容 provider 接入,会话 thinking 设为 off,权限 auto),每个 query 跑 1 次(n=1),计时从发出 prompt 到 agent_end。四个版本的条件完全相同。

指标 原版 ① ② ③ 本 PR ③ 对比原版
总耗时(10 个 query 合计) 1004.6 s 642.9 s 604.4 s 498.0 s −50%
中位数 83.9 s 55.0 s 50.6 s 42.3 s −50%
最慢一次 190.3 s 125.7 s 160.3 s 151.0 s −21%
模型往返轮次合计 95 87 32 33 −65%
片段体积合计 99.6 KB 61.8 KB 59.4 KB 47.8 KB −52%
请求体积合计 10.95 MB 10.15 MB 2.03 MB 1.97 MB −82%
回读了自己写的文件 10/10 3/10 0/10 0/10
回复里带出卡片 10/10 10/10 10/10 10/10
# 场景 原版 ① ② ③ 本 PR ③ 对比原版 ③ 的写法
1 PM2.5 折线图 72.0 s · 13 轮 125.7 s · 45 轮 51.3 s · 3 轮 27.0 s · 3 轮 −62% viz.render
2 工时柱状图 + 滑块 81.0 s · 5 轮 54.2 s · 3 轮 31.0 s · 3 轮 57.8 s · 3 轮 −29% viz.chart
3 二分查找步骤 103.6 s · 5 轮 55.8 s · 3 轮 50.0 s · 3 轮 52.1 s · 3 轮 −50% 手写 DOM
4 家庭开支环形图 86.8 s · 4 轮 65.6 s · 9 轮 93.0 s · 4 轮 54.3 s · 3 轮 −37% viz.chart
5 正态分布 + 直方图 89.5 s · 8 轮 43.7 s · 7 轮 62.7 s · 3 轮 43.0 s · 3 轮 −52% viz.chart
6 24h 渠道堆叠面积图 72.2 s · 4 轮 37.1 s · 3 轮 41.6 s · 4 轮 21.3 s · 3 轮 −70% viz.render
7 排序算法耗时柱状图 165.7 s · 8 轮 63.5 s · 7 轮 27.4 s · 3 轮 41.6 s · 6 轮 −75% viz.render
8 电梯状态机 190.3 s · 33 轮 119.1 s · 3 轮 160.3 s · 3 轮 151.0 s · 3 轮 −21% 手写 DOM
9 身高体重散点 + 回归线 78.1 s · 11 轮 35.9 s · 4 轮 30.3 s · 3 轮 28.2 s · 3 轮 −64% viz.render
10 地铁客流热力图 65.4 s · 4 轮 42.2 s · 3 轮 57.0 s · 3 轮 21.8 s · 3 轮 −67% viz.render

按写法拆分(③ 对比原版):

  • 用了 viz.render 的 5 个:453.4 s → 139.9 s(−69%),平均片段 9.5 KB → 2.3 KB,平均每个 28.0 s
  • 没用的 5 个:551.3 s → 358.1 s(−35%),平均片段 10.4 KB → 7.2 KB,平均每个 71.6 s

原版慢的典型路径:q1 Skill → Bash → Write → Read → ToolSearch → BrowserPreview → Read → Edit ×3 → Read → Edit;q8 调用 Playwright 截图,共 33 轮。

使用的 query

  1. 用折线图对比北京和上海过去 12 个月的 PM2.5 月均值(数据你自拟合理值),并给出可视化。
  2. 用可交互的柱状图展示我一周七天的工作时长,带一个滑块能整体调整本周总工时。
  3. 画出示意图:在有序数组里二分查找目标值的每一步比较过程,要能逐步切换。
  4. 用环形图展示一个家庭一个月的开支构成,点击扇区能看到该类的明细。
  5. 画出正态分布曲线和样本直方图叠加的图,带一个样本量滑块,重新抽样时直方图跟着变。
  6. 用堆叠面积图展示过去 24 小时网站三个渠道(直接访问、搜索、社交)的流量占比变化,数据你自拟。
  7. 用柱状图对比冒泡排序、插入排序、快速排序在 100、1000、10000 个元素时的耗时,能切换数据规模。
  8. 画一个电梯状态机:空闲、上行、下行、开门四个状态,点击状态之间的转换能看到说明。
  9. 用散点图展示身高和体重的样本关系,带一条回归线和相关系数,鼠标悬停能看到具体数值。
  10. 用热力图展示一周七天 × 24 小时的地铁客流量,颜色越深人越多,悬停显示具体人数。

验证

  • pnpm build:js、pnpm --filter @pi-desktop/desktop typecheck、pnpm lint、node scripts/check-architecture.mjs:通过
  • gen-ui 相关单测(visualize-skill / gen-ui-protocol / gen-ui-runtime / gen-ui-file-safety / gen-ui-card)53/53 通过;新增断言:D3 和 viz 在 <head> 中先于片段加载、片段自带的固定版本 D3 标签被移除、skill 里提到的 viz.* 都真实存在
  • pnpm test:e2e:gen-ui:通过,10 项检查;新增 viz.chart 尺寸探针和 viz.render 出图探针
  • desktop 全量单测 4400 项:4396 通过、3 跳过、1 失败。失败的是 live-voice-gemini-adapter,原因是本机会 kill 命令行中带 "gemini" 的进程,与本 PR 无关;换别名路径单独运行可以通过
  • 测速时生成的 10 个 ③ 片段离线渲染了一遍:9 个无报错,1 个(老哥 再提个优化建议 #7)因为模型传错了容器 id 而显示空白卡片(见后续方向 2)

已知限制 / 后续方向

  1. 只有 5/10 的标准图表用了 viz.render,另外 3 个仍然手写 viz.chart,可以在 Hard rules 里要求标准图表必须用 viz.render。
  2. 片段脚本报错(例如容器 id 写错)只会出现在 console 里,卡片显示空白。应该在卡片内显示错误。
  3. viz.render 还不支持多图层叠加(例如直方图 + 曲线)。
  4. 非图表类的交互演示(状态机、步骤演示)仍然慢(我记得明明有回复的 为啥 过了一段时间 没了 #8 在四个版本里都是 119~190 s),可以考虑提供现成组件。
  5. 每个 query 只跑了 1 次,方差较大(例如 feat(plan): land complete plan mode (original PR: PI-Desktop-Legacy#116, by @Tioit-Wang) #2 在 ② 和 ③ 之间反而变慢了),需要多跑几轮再看。
  6. 流式预览收益有限:按事件间隔估算,写 HTML 那次调用里约 88% 的时间花在开始输出 HTML 之前。

Inline visualizations were slow to author. Every model-written fragment
carried its own resize, theme-redraw, axis, and tick-overlap code plus a
pinned D3 tag, and the visualize skill asked the model to read the file
back, which often grew into shell, browser, and screenshot checks.

The visualization shell now preloads the bundled D3 and a small `viz`
helper layer (container sizing and redraw, axes, label overlap, and a
declarative `viz.render` for common charts), and the skill asks for one
write followed by the reply. Fragments written under the earlier skill
keep rendering; their pinned D3 tag is dropped so it cannot replace the
preloaded global.

Measured in the app with deepseek-flash on ten queries, total time went
from 1004.6 s to 498.0 s and model round trips from 95 to 33.

@vastsa vastsa left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

图表辅助层解决了重复样板代码,改动也覆盖了 D3 预加载和旧片段兼容。暂不能合入:这个新硬规则禁止对生成结果做任何验证,而 PR 自己的 10 个样例中已经有 1 个因为容器 ID 写错而显示空白卡片;当前脚本错误也只写入 console。请保留一个轻量的结果检查路径,或让运行时把渲染错误呈现在卡片里,再用错误样例验证用户能发现并修正空白图。感谢整理性能数据和兼容性处理。


## Hard rules

- One `Write`, then the reply. Do not verify the fragment: no Bash or shell

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这条规则把验证完全禁掉了,但 PR 的 n=1 实测已有一个片段因传错容器 ID 而成为空白卡片;当前运行时只把这类异常写入 console。模型在“一次 Write 后立即回复”的约束下无法发现这个用户可见故障。请允许一次低成本的结果检查,或先在卡片内呈现渲染错误,并为错误容器 ID 增加覆盖。

zszz3 added 2 commits October 26, 2026 00:42
Review of the preload change pointed out that the skill now forbids any
verification, yet one of ten live fragments rendered a blank card
because it targeted a missing container id, and the error only reached
the guest console.

The Write result for a fragment in the session visualization directory
now carries a static check: inline scripts are compiled (never run) and
literal ids passed to viz.render, viz.chart, getElementById, or
querySelector must exist in the fragment. Problems are listed so the
model can fix them with one Edit; a clean write keeps the plain result.
The guest runtime also reports its first uncaught error to the card,
which keeps the frame and shows the message as bounded plain text.
The skill keeps its ban on self-verification, because a live run
without it brought back Bash, browser, and read-back detours (82 vs
30 round trips over ten queries).

The viz helpers also get review fixes: date-only strings parse as
local dates, year-like values skip thousands separators, re-rendering a
container replaces the previous chart, destroy is idempotent, and arc
and heatmap specs report missing channels.
@zszz3

zszz3 commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

感谢 review,两点都按建议补上了(084818cb7,并已合入最新 main):

1. 低成本结果检查:Write 回执自带静态检查

  • 只对写进本会话可视化目录的 .html 生效(packages/agent-runtime/src/gen-ui-fragment-check.ts)。
  • 内联脚本用 vm.Script 只编译不执行,查语法错误;viz.render / viz.chart / getElementById / querySelector("#…") 里写死的 id 必须在片段里存在。
  • 有问题时回执末尾列出问题(附片段里实际存在的 id),模型用一次 Edit 修;没问题时回执与原来完全一致,不多一轮。
  • 用之前保存的 15 个真实片段验证:只报出 review 里那张空白卡片(viz.render("sb-bar"),实际容器是 sb-chart),0 误报。

2. 运行时错误显示在卡片里

  • iframe 运行时捕获第一个未处理的 error / unhandledrejection,通过 pi-genui:script-error 报给宿主;卡片保留 frame,在上方用纯文本显示提示和错误(上限 300 字符),不会渲染成 HTML。
  • E2E 新增 “fragment script errors show in the card”:容器 id 写错的片段会在卡片上显示 viz: no element missing-chart。

3. skill 的禁令为什么保留
试过去掉 “no Bash / browser / screenshot / read back”,只保留 “Write 回执就是检查”:10 条里 5 条又开始跑 node、回读、开 Playwright,总计 82 轮 / 692 s。保留禁令 + 回执检查后 10 条都是 3 轮(Skill → Write → 回复),0 次额外工具。

4. 另外修了 helper review 里的 9 处问题:日期字符串按本地日期解析、年份不加千分位、同一容器重复渲染会替换旧图、destroy 幂等、arc / heatmap 缺通道时报错、y 轴标题避让刻度、堆叠柱支持负值等。

最新实测(deepseek-flash,同样 10 条 query,n=1)

版本 总耗时 往返轮次 额外工具
原版 skill 1004.6 s 95 10/10 回读
PR 首版 498.0 s 33 3/10
本次(+ 回执检查 + 卡片报错) 660.1 s 30 0/10

本次比首版慢主要是模型选择的写法不同:只有 4/10 用了 viz.render(首版 5/10),排序和热力图改成手写,电梯状态机写了 12.7 KB、单条 214 s。n=1 的单条波动较大,轮次已经稳定在最低的 3 轮。

验证:gen-ui E2E 11 项通过;desktop 单测 4400/4404(唯一失败是本机会 kill 参数含 gemini 的进程导致的 live-voice-gemini-adapter,与本 PR 无关);agent-runtime 片段检查 + runtime 测试 354/354;lint、两个包 typecheck、架构检查通过。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants