运行时诊断
import { RenderRuntimeDiagnosticsPanel, type AppHost } from 'ds-ui'
运行时诊断用于排查 DirectSurface UI 的布局、绘制、重绘请求、焦点、弹窗、窗口和性能问题。它面向复杂业务应用开发者和现场问题复现人员。
这篇是操作手册:重点说明如何打开工具、如何看指标、如何根据现象定位到代码。
诊断工具链
| 工具 | 用途 | 适合问题 |
|---|---|---|
| 性能浮层 | 看当前帧耗时、layout/paint 趋势和是否持续刷新。 | 页面卡顿、持续重绘、输入延迟。 |
| DevTools Runtime tab | 看 paint/layout request 历史、热点聚合、调用栈和 dirty target。 | “谁一直请求刷新”。 |
| DevTools Layout tab | 看 render tree、节点 rect、命中区域、app context 和选中对象状态。 | 布局错位、命中异常、上下文读错。 |
| ObjectInspector | 展开对象、复制值、查看 debugState 和上下文值。 | 状态异常、对象结构不清楚。 |
| 浏览器 Performance | 看 JS、rAF、事件处理、样式和浏览器主线程耗时。 | 框架日志很快但体感仍卡。 |
| Heap Snapshot | 看关闭页面后对象是否仍被引用。 | 内存泄露、大文档关闭后内存不降。 |
打开入口
demo 中通常通过顶部菜单的“调试”打开性能浮层、paint request debug、DevTools 和诊断快照。业务应用可以用自己的菜单或快捷键接入这些能力。
推荐通过 AppOverlayService 打开 DevTools:
function openDiagnostics(host: AppHost): void { const uiServices = host.uiServices uiServices.showDevTools({ activeTab: 'layout-inspector' }) uiServices.showDevTools({ activeTab: 'runtime' })}
如果直接组合诊断面板,最小结构如下:
const panel = new RenderRuntimeDiagnosticsPanel({ state: { paintRequestDebugEnabled: true, paintRequests: [], },})
真实应用中,state 应来自运行时 host 或应用服务,不应长期手工维护空数组。
标准排查流程
- 打开性能浮层,确认是 layout、paint、输入事件还是浏览器主线程问题。
- 打开 paint request debug。
- 打开 Runtime Diagnostics Panel。
- 复现问题,只做一个动作,例如点击一次、输入一个字、滚动一次。
- 查看 request 列表,确认是否持续增长。
- 切到 hotspots,看最高频 target、kind、source 和 reason。
- 点开详情,用 ObjectInspector 查看 stack、dirty layers、layoutTargets、paintTargets。
- 回到对应组件,检查 setter、layout、paint、事件处理和数据同步。
- 修复后关闭 debug,再用正常交互验证。
判断问题在哪一层
| 现象 | 优先看 | 判断依据 |
|---|---|---|
| 点击后光标迟迟不出现 | Performance + 输入组件 debugState | pointer 事件很快但 rAF 晚,可能是隐藏输入、浏览器主线程或焦点同步。 |
| hover 卡顿 | Runtime Diagnostics hotspots | hover 是否触发 layout,命中测试是否扫描全量树。 |
| 输入中文卡顿 | 输入事件、composition、layout 日志 | composition update 是否重复排版,hidden textarea 是否保留大 value。 |
| 页面空闲仍高频绘制 | paint request 列表 | 是否有光标、动画、sidebar、tabs 等组件反复 markNeedsPaint。 |
| 页面空闲仍高频 layout | layout request stack | 是否在 performLayout() 中反复 setItems() 或写同样状态。 |
| 滚动卡顿 | 可视数量和 paint 耗时 | 是否绘制不可见节点,滚动容器是否嵌套。 |
| resize 卡顿 | layout targets | 是否窗口缩放时全量测量大文档、大树、大表格。 |
| 关闭后内存不降 | Heap Snapshot | 页面 root、controller、context、popup、cache 是否仍被引用。 |
Runtime Diagnostics Panel 怎么看
Requests 视图
Requests 视图按时间列出刷新请求。重点看:
seq:请求顺序。kind:frame或layout。source:来自 pipeline、overlay、runtime、window 还是 debug。target:发起请求的 RenderObject。reason:请求原因。alreadyScheduled:当时是否已有帧被调度。layoutNeeded:是否已经存在 layout 需求。
如果同一个 target 在空闲状态下不断出现,优先查它的 setter、定时器、动画或 layout 同步逻辑。
Hotspots 视图
Hotspots 会按 target/kind/source/reason 聚合。它适合回答:
- 哪个组件请求最多。
- 是 layout 多还是 paint 多。
- 是同一个 reason 反复触发,还是多个来源混杂。
- 是否存在已经 scheduled 仍持续请求的情况。
修复高频刷新时,先处理 count 最高且业务上不应持续变化的项。
详情面板
详情面板由 ObjectInspector 组成,可以展开:
- request 原始记录。
- stack。
- dirty layers。
- pipeline 状态。
- layout targets。
- paint targets。
- transient paint targets。
stack 最有价值。它能直接告诉你是哪一行代码触发了 markNeedsLayout() 或 markNeedsPaint()。
常见案例
layout 中反复 setItems
现象:
RenderReviewSidebar.setItems
RenderMedicalRecordEditor.syncReviewSidebarLayout
RenderMedicalRecordEditor.performLayout
判断:layout 阶段每次都给子组件写入 items,即使内容没变,也会触发子组件继续 layout。
修复方向:
- 给 items 做签名比较。
- 内容没变时不调用 setter。
- 避免在
performLayout()中构造新数组并写入子组件。
hover 触发布局
现象:鼠标移动时 Runtime Diagnostics 不断出现 kind: layout。
判断:hover 应只影响视觉状态,通常只需要 markNeedsPaint()。
修复方向:
- hover setter 只改绘制状态。
- 不在 hover 时重建模板或子树,除非组件明确需要。
- 需要重建模板时,限制在当前可视项。
光标闪烁导致整页重绘
现象:存在输入光标时持续高频 paint,且 target 是页面根或大容器。
判断:光标闪烁应尽量是局部绘制,不应让大文档整页重绘。
修复方向:
- 光标只请求局部 dirty rect。
- 不因光标闪烁触发 layout。
- 检查父组件是否把局部 paint 放大成整页 dirty。
大对象查看器展开卡顿
现象:打开诊断面板或 ObjectInspector 后点击展开大对象卡顿。
判断:一次性枚举太多属性或复制太大值。
修复方向:
- 降低
maxProperties。 - 设置
propertyGroupSize。 - 设置
expansionBatchSize。 - 对业务敏感或超大字段先脱敏或摘要化。
诊断快照
诊断快照用于离线分析。它适合现场复现后导出,再给开发者分析。
快照应包含:
- 运行时状态。
- 最近 paint/layout request。
- 性能 samples。
- render tree 摘要。
- window/popup 状态。
- app context 摘要。
快照不应包含未脱敏的患者信息、账号 token、完整病历内容或大体积业务数据。
诊断结束后的检查
修复后至少确认:
- 性能浮层不再显示异常高频 layout/paint。
- Runtime Diagnostics hotspots 中目标 count 不再持续增长。
- 关闭 debug 后正常交互仍然流畅。
- 相关组件的
debugState()不再显示异常状态。 - 如果是内存问题,关闭页面后重新生成 Heap Snapshot 验证 retained object。
常见错误
长期开启无限请求历史
诊断工具本身会持有 request 和 stack。只在复现问题时打开,结束后清空或关闭。
只看 paintMs,不看 paint request 来源
单次 paint 很快也可能体感卡,因为请求频率过高。需要同时看频率、来源和浏览器主线程。
把诊断输出当业务审计
诊断数据服务开发和排障,不是业务审计日志。业务审计应在命令、服务或业务流程层记录。
没有给复杂组件提供 debugState
复杂组件应提供 debugState(),让诊断面板和 Layout Inspector 能看到内部状态,例如 visible row count、selected key、scroll offset、painted page count。