指南
用 nsh trace 测试
nsh trace 以 无界面(headless) 方式运行应用:使用真实的运行时(与设备上相同的 QuickJS VM、响应式图、渲染器、
组件、布局和列表虚拟化),但不需要模拟器、设备或窗口。它不进行绘制,而是打印运行时本应发送给原生视图的每一个操作,
并附带耗时。只要 CLI 能运行的地方它都能运行,包括 Linux CI 机器。
nsh trace App.tsx
它的用途:
- 检查应用(或某个 demo)能否无错误地构建、执行和渲染;
- 准确查看一次改动创建、移动或更新了哪些原生视图,以及数量;
- 测量首帧、一次点击以及每一帧滚动的开销;
- 在 CI 中发现回归。
#它做了什么
- 打包入口(开发配置,不压缩)并编译为字节码。
- 以 390×844 的视口启动引擎,渲染首帧。
- 可选地滚动第一个滚动视图或列表。
- 在应用空闲时等待(默认 1.2 s,让引擎的空闲垃圾回收先运行,从而暴露对象保留方面的 bug),同时打印应用在此期间渲染 的任何内容(定时器、fetch、动画)。
- 对第一个可按压视图(第一个带
onPress的视图)点击若干次,并打印每次点击带来的变化。
| 参数 | 默认值 | 说明 |
|---|---|---|
--taps <n> | 2 | 对第一个可按压视图发送的点击次数。 |
--idle-ms <ms> | 1200 | 点击前的空闲时间。 |
--scroll <points> | 无 | 将第一个滚动视图或列表滚动这么多点,每帧 10 pt,并打印每一帧的批次。 |
--width <points> | 390 | 视口宽度。 |
--height <points> | 844 | 视口高度。 |
#读懂输出
下面是计数器示例(examples/counter/App.tsx)的输出,有删节:
bundled in 14.3 ms (3255 B)
info nsh::perf commit: 56 ops, 395µs (list js 6µs), 49 mount items, 21 nodes
info nsh::perf first frame: eval 271µs, total 1235µs
── initial render: 49 items
LayoutDirection { rtl: false }
Create { id: 2, kind: View }
Create { id: 4, kind: Text }
Insert { parent: 2, child: 4, before: None }
...
Events { id: 20, events: EventMask(1) }
Frame { id: 2, x: 0, y: 0, w: 390, h: 844 }
Frame { id: 4, x: 24, y: 64, w: 342, h: 34 }
...
Text { id: 4, text: "nsh counter", content: 342x34 @ 0,0 }
Text { id: 14, text: "0", content: 36x77 @ 0,0 }
info nsh::perf event Press on view 20
info nsh::perf event js: 196µs
info nsh::perf commit: 10 ops, 248µs (list js 6µs), 8 mount items, 24 nodes
── tap #1 on view 20: 8 items
Create { id: 42, kind: Text }
Insert { parent: 32, child: 42, before: None }
Frame { id: 14, x: 136, y: 49, w: 71, h: 77 }
Frame { id: 32, x: 24, y: 366, w: 342, h: 43 }
Frame { id: 42, x: 0, y: 23, w: 342, h: 20 }
Text { id: 14, text: "-1", content: 71x77 @ 0,0 }
Text { id: 42, text: "• -1", content: 342x20 @ 0,0 }
Text { id: 34, text: "Recent values: 1", content: 342x17 @ 0,0 }
#批次与 mount item
每个 ── title: N items 块是一个 mount 批次(mount batch):平台在一帧内应用到原生视图上的全部内容,一次性切换到
主线程完成。其中的每一行是一个 mount item:
| Item | 含义 |
|---|---|
Create { id, kind } | 创建一个原生视图(View、Text、Image、Scroll 等)。 |
Insert { parent, child, before } | 把视图放进父视图,位于某个兄弟视图之前(None:放在末尾)。 |
Remove / Delete | 将视图从父视图中移除 / 释放视图。 |
ViewStyle { id, style } | 视图的装饰:背景、边框、圆角、阴影、不透明度、变换等。 |
ViewProps { id, props } | 触摸和无障碍设置、testID。 |
Events { id, events } | 视图上报哪些事件(EventMask(1) 表示按压)。 |
Frame { id, x, y, w, h } | 它在父视图中的位置和大小,单位为点。 |
Text { id, text, content } | 文本视图排版后的文本。 |
Image { id, source, ... } | 要加载的图片。 |
只发送发生变化的内容:一次只改变一个数字的点击只发送一个 Text item;视图不会收到默认样式,也不会收到空的事件掩码;
未变化的 frame 不会发送。一帧滚动如果没有露出新行,根本不会产生批次。
#nsh::perf 日志行
| 行 | 含义 |
|---|---|
bundled in 14.3 ms (3255 B) | 打包耗时和 bundle 大小(开发配置)。 |
commit: 56 ops, 395µs (list js 6µs), 49 mount items, 21 nodes | 一次提交:JS 排队的操作数、核心花费的时间(渲染组件、布局、diff)、绑定列表行花费的 JS 时间、产生的 mount item 数,以及存活的节点数。 |
first frame: eval 271µs, total 1235µs | 执行 bundle 的时间,以及从启动到第一个批次的时间。 |
event Press on view 20 / event js: 196µs | 分发了一个事件,以及它的处理函数(及其引起的一切)在 JS 中花费的时间。 |
单次运行的耗时很大程度上取决于 CPU 的频率状态(刚从空闲中唤醒的机器会比较慢)。请在同一台机器上比较多次运行,取多次 运行中的最小值,并且更多地相信操作数和 item 数,而不是微秒数。
#滚动
nsh trace examples/gallery/Bench.tsx --scroll 3000 --taps 0
以每帧 10 点的速度滚动第一个 <Scroll> 或 List,并打印每一帧的批次:
info nsh::perf commit: 26 ops, 297µs (list js 111µs), 22 mount items, 233 nodes
── scroll to 20: 22 items
Create { id: 448, kind: View }
...
Text { id: 456, text: "Photo #25", content: 298x20 @ 0,0 }
对于列表,这能展示虚拟化的实际效果:哪些帧绑定了新行、一行需要多少操作,以及没有露出新行的帧不产生任何
内容。要得到有意义的耗时,请使用 release 构建的 CLI(在源码检出中为 ./target/release/nsh)。
#点击
nsh trace 点击首帧中 第一个 带 onPress 的视图。要设计便于 trace 的入口,可以把要测量的交互放在最前面,或者为它
单独写一个小的入口文件:
// trace/AddItem.tsx: renders the screen and puts the button under test first
import { mount } from "nsh";
import { Cart } from "../src/Cart";
mount(() => <Cart initialItems={[]} />);
nsh trace trace/AddItem.tsx --taps 5
如果某次点击没有产生任何更新,nsh trace 会以 Error: tap #1 produced no update 停止,并返回非零退出码:它正是靠这一点
发现失效的处理函数,例如应用的节点被垃圾回收的情况。
#在 CI 中
nsh trace 只需要 CLI,因此可以在 Linux runner 上运行。应用中的错误(bundle 执行失败、未捕获的异常)会打印为 error
行;构建失败、应用什么都没渲染,或者某次点击没有带来任何变化,都会使它以非零退出码退出。两者都要检查:
#!/bin/sh
set -e
for entry in App.tsx trace/*.tsx; do
out=$(nsh trace "$entry" --idle-ms 300 2>&1) || { echo "$out"; exit 1; }
if echo "$out" | grep -E '^ *error' >/dev/null; then
echo "$out"
echo "errors in $entry"
exit 1
fi
done
# .github/workflows/trace.yml
jobs:
trace:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./scripts/trace.sh
(在 runner 上安装 nsh CLI 的方式与本地安装相同;参见安装。)
要守住性能,可以记录几个交互的 item 数(── … : N items 行以及 ops / mount items 的数字),在它们增长时让 CI 失败。
这些计数是确定的;共享 CI 机器上的耗时则不是。
#用于 UI 测试的 testID
nsh trace 检查的是运行时渲染出的内容。如果要用平台的测试工具(XCUITest、Espresso / UI Automator、Appium、Maestro)
驱动真实的原生 UI,请给视图设置 testID:
<View testID="add-to-cart" onPress={add} style={{ padding: 12 }}>
<Text testID="cart-count">{`${count()} items`}</Text>
</View>
| 平台 | testID 对应到哪里 |
|---|---|
| iOS | 视图的 accessibilityIdentifier(在 XCUITest 中为 app.otherElements["add-to-cart"])。 |
| Android | 无障碍节点的 view id 资源名(在 UI Automator 中为 By.res("add-to-cart"))。 |
testID 是每个元素以及 nsh/ui/mobile 中各组件都有的 prop。它不会改变屏幕阅读器读出的内容;那应该用
accessibilityLabel(参见无障碍)。在 nsh trace 的输出中,它出现在视图的 ViewProps item 里,
形如 test_id: Some("add-to-cart")。