文档

性能报告

NSH 应用的实测开销。NSH 如何保持高速、如何测量你自己的应用,见性能。

#基线

来自 NSH 自己的基线(除特别注明外均为 Apple silicon):

场景数据
计数器应用,无界面(debug)首帧 ~0.5–0.65 ms(58 ops / 49 mount items);一次点击 ~0.15 ms
计数器应用,Mac Catalyst(release)首帧 ~1.05–1.15 ms(其中约 0.7 ms 用于首次用 CoreText 布局 10 段文本);一次点击 ~0.12 ms
Gallery 照片列表,无界面(debug)首帧 653 ops / 224 nodes,~2.7 ms
滚动照片列表,Catalyst(release)慢速滚动时重新绑定一行 ~0.4 ms;快速甩动时一整屏约 32 行 0.9–1.6 ms
滚动照片列表,Android 模拟器(arm64,release)重新绑定一行 ~0.3–0.4 ms(JS ~60 µs)
引擎启动(release)~75–155 µs;全局 API(fetch、URL 等)在首次使用前没有任何开销,首次使用后 ~90 µs
热重载从保存文件到新版本运行 ~120 ms;应用更新 0.2–0.5 ms

#Linux(GTK 4)

测量环境:Linux 桌面(x86_64,24 线程,NVIDIA GPU),release 构建,gallery 运行在 120 Hz 显示器上一个 1896 × 1030 的窗口里, 所有读数都由 examples/gallery/tools/perf.py 自动采集(nsh test --target linux --fps,每秒一行;CPU 指整个进程,以单核的百分比计)。 帧 p95 是主线程绘制一帧所用时间的第 95 百分位:低于 8.3 ms,一帧就能放进 120 Hz 的一次刷新。

#动画演示

演示FPS帧 p95CPU
Stocks:22 条实时走势线、分时图、盘口、逐笔成交117–1205–6 ms25–29%(1200 × 800)、28–32%(1896 × 1030)
Stocks,窗口铺满 4K 144 Hz 显示器(1.6 倍缩放)143–1454.1–4.4 ms26–29%
Canvas:仪表盘、时钟、点阵1202.6–4.4 ms5–10%
Animations:三个原生循环、弹簧、序列1202.0 ms12–13%

#每个页面

gallery 的每个页面,分别在刚打开时和测试滚动它时测量,窗口 1200 × 800,显示器 144 Hz(同一台机器)。NSH 只在屏幕上有内容变化时才绘制一帧, 所以这里的 FPS 是画面在动时的帧率:按相邻两帧间隔的中位数计算(perf.py 的 moving);等于显示器的 144 时,动画或滚动的每一帧都紧接着上一次刷新。 超过一次刷新 是耗时超过一次刷新(6.9 ms)的帧所占的比例。Images 页的 GIF 和 WebP 按它们自己的帧率播放(这里约 50 FPS),视频也一样。Physics、Overlays 和 Menus 页面很短:在 1200 × 800 下滚动几帧就到底了,所以它们在 900 × 500 的窗口里测量,那里有足够的内容可以滚动。

页面帧的来源FPS帧 p95超过一次刷新CPU
Stocks实时数据、原生循环1444.1 ms0%26%
Stocks,滚动实时数据、滚动1444.1 ms0%25%
Photos,滚动滚动1442.8 ms2%9%
Lists,滚动滚动1431.7 ms0%7%
Scrolling,滚动滚动1441.6 ms1%4%
Navigation,滚动滚动1431.7 ms0%2%
Form,滚动滚动1441.3 ms0%4%
Resizable,滚动滚动1430.6 ms0%1%
Dock,滚动滚动1420.5 ms1%1%
Gestures弹簧、脉动的圆环1441.6 ms0%1–5%
Physics,滚动滚动(900 × 500 窗口)1441.6 ms1%1%
Animations原生循环1442.6 ms0–1%5%
Canvas,滚动滚动、原生循环1444.2 ms0%1–4%
Styles,滚动滚动1442.3 ms2%3%
Tailwindanimate-* 工具类1441.7 ms0%3–8%
Text,滚动滚动1441.9 ms0%4%
Text View,滚动滚动1411.8 ms2%7%
Images,滚动滚动、GIF 和 WebP 的帧1432.9 ms2%8%
Components,滚动滚动1441.4 ms0%3%
Overlays,滚动滚动(900 × 500 窗口)1441.7 ms0%2%
Menus,滚动滚动(900 × 500 窗口)1441.9 ms0%2%
Controls过渡动画1440.9 ms0%1–4%
Web and video视频帧、网页135–1421.0 ms0%7–8%

#内存

启动 3 秒后的静止内存(release 运行器,480 × 640 窗口)。私有内存是只属于这个进程的内存(USS);常驻内存还包含共享库的页面 (GTK、GPU 驱动),这些页面由机器上所有 GTK 应用共享:

应用渲染器私有内存(USS)常驻内存(RSS)
Rust 应用(nsh init --template rust)GL(ngl)~49 MB~142 MB
Rust 应用Cairo(GSK_RENDERER=cairo)~28 MB~95 MB
TSX 计数器(examples/counter)GL~79 MB~166 MB
只有一个 label 的 GTK 4 窗口,作为参照Vulkan / Cairo~62 / ~40 MB~132 / ~109 MB

再次访问同一页面不会增加内存;第一次访问某个页面时,进程增长的是它有意保留的缓存(解码后的图片,上限 128 MB)以及首次使用时加载的库(网页视图用的 WebKitGTK)。

#应用体积

用 nsh init 的每种模板生成的 hello world 应用,以及两个实际在用的应用,由 nsh package --target linux 为 x86_64 构建。运行器是应用唯一的可执行文件; 压缩包是它发布用的 .tar.gz。hello world 都以链接时优化构建并去除符号(CARGO_PROFILE_RELEASE_LTO=fat、 CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1、CARGO_PROFILE_RELEASE_STRIP=true):

应用编写语言运行器压缩包
Hello world(--template rust)Rust16.5 MB7.5 MB
Hello world(--template ts)TSX,运行在 QuickJS 上25.7 MB11.6 MB
Hello world(--template mixed)TSX 加一个 Rust crate25.8 MB11.7 MB
sysmon 0.1.3Rust20.9 MB(去除符号后)9.7 MB
Omasend 0.2.0Rust22.3 MB9.8 MB

TSX 运行器包含 QuickJS 运行时、原生模块和渲染器;应用自己的代码是运行器旁边的 bundle,hello world 的不到 1 KB。

这些数字来自:

  • 渲染器:在 NVIDIA 驱动下,NSH 使用 GSK 的 GL 渲染器绘制,在那里它每帧的开销只是 Vulkan 渲染器的一小部分(Animations 演示:每帧绘制 ~1 ms,Vulkan 下约 9.5 ms)。仍可用 GSK_RENDERER 指定渲染器。
  • Canvas 栅格:每个 canvas 只栅格化一次,分成保存在 GPU 上的 256 px 图块。动画值变化时,只重绘读取它的操作覆盖的区域,并叠加在绘制中静态部分保留下来的像素上;看不见的 canvas 会等到滚入视野再绘制。
  • 路径只暂存一次:长路径之后的 fillRect 和 fillText 会把路径留在原处,直到再次用到它(一条 400 点的折线加 240 根柱子:0.63 ms)。

#macOS(AppKit)

测量环境:MacBook Pro(Apple M3,24 GB)的内置 120 Hz ProMotion 显示屏,release 构建,gallery 运行在 1200 × 800 的窗口里, 所有读数都由 examples/gallery/tools/perf.py 自动采集(nsh test --target macos --fps,每秒一行;CPU 指整个进程,以单核的百分比计, GPU 是它占用 GPU 的比例)。阶段是主线程每帧的开销:tick(display link 上的原生动画)和 canvas(重放绘制)。

#动画演示

演示FPS帧 p95CPUGPU
Stocks:22 条实时走势线、分时图、盘口、逐笔成交1202.7–3.7 ms33–36%18–22%
Canvas:仪表盘、时钟、点阵119–1200.6–0.7 ms4–7%0%
Animations:三个原生循环、弹簧、序列119–1200.3–1.6 ms5–8%0%

Stocks 上一帧的主线程时间约 1.6 ms 用于 tick,其中约 0.45 ms 重放 canvas 中的动画部分;每个 canvas 的其余部分由 Core Animation 在自己的线程上绘制。

#每个页面

gallery 的每个页面,打开时以及测试滚动它时,同样在 1200 × 800 的窗口里。与 Linux 一样,FPS 是有东西在动时的速率(每个相邻绘制帧间隔的中位数一帧); 达到显示器的 120 时,动画或滚动的每一帧都在上一帧之后的下一次刷新到来。超过一次刷新是耗时超过一次刷新(8.3 ms)的帧所占的比例。 静止的页面不绘制帧,CPU 为 2–5%。图片的 GIF 和 WebP 由 Core Animation 播放,视频由 AVFoundation 播放,按各自的帧率。

页面帧的来源FPS帧 p95超过一次刷新CPU
Stocks实时数据、原生循环1203.2 ms0%35%
Stocks,滚动中实时数据、滚动1203.5 ms0%36%
AI Chat,滚动中滚动1202.4 ms1%5%
Photos,滚动中滚动1201.4 ms0%10%
Lists,滚动中滚动1201.6 ms0%8%
Scrolling,滚动中滚动1202.7 ms0%6%
Navigation,滚动中滚动1200.2 ms0%4%
Form,滚动中滚动1200.6 ms0%5%
Resizable,滚动中滚动1200.3 ms0%4%
Dock,滚动中滚动1200.3 ms1%5%
Gestures弹簧、脉动的圆环1200.4 ms0%4%
Physics,滚动中滚动1200.3 ms0%4%
Animations原生循环1200.3 ms0%5%
Canvas,滚动中滚动、原生循环1201.2 ms0%7%
Styles,滚动中滚动、摆动的变换1201.0 ms0%6%
Tailwindanimate-* 工具类1200.5 ms0%4%
Text,滚动中滚动1200.3 ms0%5%
Text View,滚动中滚动1200.7 ms0%5%
Images,滚动中滚动1200.4 ms0%5%
Components,滚动中滚动1200.3 ms0%5%
Overlays,滚动中滚动1200.3 ms0%4%
Menus,滚动中滚动1200.3 ms0%5%
Controls过渡1200.3 ms0%4%
Web and video,滚动中滚动1200.2 ms0%6%
SystemrequestAnimationFrame、定时器1200.2 ms0%3%
Standard APIs,滚动中滚动1200.3 ms0%5%
Storage,滚动中滚动1200.3 ms0%5%

#内存

启动 3 秒后的静止内存(release 运行器)。*占用(footprint)*是 macOS 计入这个进程的内存(活动监视器显示的值);常驻内存还包含系统共享库的页面:

应用窗口占用常驻内存(RSS)
Rust 应用(nsh init --template rust)1200 × 800~19 MB~86 MB
TSX 计数器(examples/counter)1200 × 800~20 MB~89 MB
只有一个 label 的 AppKit 窗口,作为参照480 × 640~17 MB~80 MB

浏览 gallery 的过程中,进程在 Canvas 页之前占用 84–230 MB,从 Text View 页起约 350–360 MB,增长的是它有意保留的缓存(解码后的图片, 上限 128 MB;Text View 页的文档)以及首次使用时加载的框架(网页视图用的 WebKit);Canvas 页的栅格在三个 canvas 都可见时最多约 310 MB,离开页面即释放。

#应用体积

用 nsh init 的每种模板生成的 hello world 应用,由 nsh package --target macos 为 Apple 芯片(arm64)构建,与 Linux 一样以链接时优化构建并去除符号;以及上面两个应用为 Apple 芯片发布的版本。 可执行文件是 .app 里的运行器;zip 是压缩后的 .app(ditto -c -k):

应用编写语言可执行文件压缩后的 .app
Hello world(--template rust)Rust12.4 MB6.0 MB
Hello world(--template ts)TSX,运行在 QuickJS 上19.8 MB9.5 MB
Hello world(--template mixed)TSX 加一个 Rust crate19.8 MB9.5 MB
sysmon 0.1.3Rust21.6 MB8.4 MB(它发布的 .tar.gz)
Omasend 0.2.0Rust18.4 MB9.7 MB(它发布的 .tar.gz)

这些数字来自:

  • 只为变化绘制帧:display link 在原生动画运行时跳动,并随动画一起停止;动画随页面一起停止(视图删除时它的关键帧动画结束,循环在组件的 cleanup 中停止)。
  • Canvas 在开销最小的地方绘制:频繁重绘的 canvas(动画、实时数据)在 Core Animation 的线程上异步绘制(drawsAsynchronously),不占用主线程;偶尔才重绘的 canvas 直接在主线程上绘制,省下异步绘制占用的显存。动画值变化时只重绘读取它的操作覆盖的区域。
  • 保留 canvas 的静态部分:绘制内容稳定后,第一个动画操作之前较长的部分保留为一个图层;每帧绘制这个图层,只重放动画的其余部分,并直接在主线程上完成(Canvas 页的仪表盘、时钟和点阵:不占 GPU,内存减半)。
  • 内容变化时才绘制:文本和 canvas 绘制在各自的 layer 里,内容或尺寸变化时才重绘,移动或滚动时不重绘。

#Windows(Win32、Direct2D)

测量环境:一台 Windows 11 台式机(Intel Core i7-13700KF,32 GB,NVIDIA GeForce RTX 3060 Ti),4K 144 Hz 显示器、150% 缩放, release 构建,gallery 运行在 1200 × 800 的窗口里(1800 × 1200 像素),所有读数都由 examples/gallery/tools/perf.py 自动采集 (nsh test --target windows --fps,每秒一行;CPU 指整个进程,以单核的百分比计)。阶段是主线程每帧的开销:paint(Direct2D 绘制窗口)以及其中的 canvas(录制和重放绘制)。

#动画演示

演示FPS帧 p95每帧 paint / canvasCPU
Stocks:22 条实时走势线、分时图、盘口、逐笔成交1443.7–4.3 ms1.4 / 0.13 ms36–43%
Canvas:仪表盘、时钟、点阵1441.0–2.0 ms0.8–1.4 / 0.05 ms21–35%
Animations:三个原生循环、弹簧、序列1441.0–3.6 ms0.9–1.9 ms20–41%

Stocks 每秒 commit 16–17 次,每次在引擎线程上用时最多 2.1 ms 的 JS 和 1.7 ms 的 core(p95)。

#每个页面

gallery 的每个页面,打开时以及测试滚动它时,同样在 1200 × 800 的窗口里。与 Linux 和 macOS 一样,FPS 是有东西在动时的速率(每个相邻绘制帧间隔的中位数一帧); 达到显示器的 144 时,动画或滚动的每一帧都在上一帧之后的下一次刷新到来。超过一次刷新是耗时超过一次刷新(6.9 ms)的帧所占的比例。 静止的页面不绘制帧,CPU 为 0–2%。图片的 GIF 和 WebP 按各自的帧率播放(这里约 40 FPS)。Physics、Overlays、Menus 和 Storage 在 900 × 500 的窗口里测量,在那里它们有足够的内容可以滚动。

页面帧的来源FPS帧 p95超过一次刷新CPU
Stocks实时数据、原生循环1443.9 ms0%36%
Stocks,滚动中实时数据、滚动1444.3 ms0%43%
Photos,滚动中滚动1432.1 ms0%9%
Lists,滚动中滚动1442.0 ms1%19%
Scrolling,滚动中滚动1432.1 ms0%7%
Navigation,滚动中滚动1440.8 ms0%9%
Form,滚动中滚动1440.8 ms0%12%
Resizable,滚动中滚动1430.4 ms0%4%
Dock,滚动中滚动1440.5 ms1%5%
Gestures弹簧、脉动的圆环1441.8 ms0%36%
Physics,滚动中滚动(900 × 500 窗口)1440.8 ms0%4%
Animations原生循环1441.0 ms0%28%
Canvas,滚动中滚动、原生循环1442.0 ms0%35%
Styles,滚动中滚动、摇摆的变换1442.1 ms0%39%
Tailwindanimate-* 工具类1440.6 ms0%19%
Text,滚动中滚动1441.6 ms0%8%
Text View,滚动中滚动1441.1 ms0%10%
Images,滚动中滚动、GIF 和 WebP 的帧1434.8 ms2%23%
Components,滚动中滚动1441.0 ms1%11%
Overlays,滚动中滚动(900 × 500 窗口)1440.6 ms0%1%
Menus,滚动中滚动(900 × 500 窗口)1440.6 ms0%2%
Controls过渡117–1441.5 ms0%22%
Web and video,滚动中滚动1441.6 ms0%15%
SystemrequestAnimationFrame、定时器144< 0.1 ms0%1%
Storage,滚动中滚动(900 × 500 窗口)1440.5 ms0%1%

#内存

启动 3 秒后的静止内存(release 运行器,1200 × 800 的窗口)。专用工作集是只有这个进程使用的内存(任务管理器“内存”一列的值); 专用字节是它提交的内存;工作集还包含它与其他进程共享的系统库的页面(Direct3D、Direct2D、DirectWrite、GPU 驱动):

应用专用工作集专用字节工作集
Rust 应用(nsh init --template rust)~14 MB~60 MB~53–71 MB
TSX 计数器(examples/counter)~15 MB~61 MB~56–71 MB

浏览 gallery 的过程中,工作集在 Stocks 页约 108–115 MB,从 Canvas 页起约 230 MB,从 Text View 页起约 320–340 MB, Web and video 页打开 WebView2 之后约 390–405 MB(网页运行在 WebView2 自己的进程里),增长的是它有意保留的缓存(解码后的图片, 上限 128 MB;Text View 页的文档)。

#应用体积

用 nsh init 的每种模板生成的 hello world 应用,由 nsh package --target windows 为 x86_64 构建(Rust 运行器与 Linux 一样以链接时优化构建并去除符号)。 可执行文件是应用的 <Name>.exe;zip 是写在它的目录旁边的 .zip:

应用编写语言可执行文件Zip
Hello world(--template rust)Rust15.0 MB6.7 MB
Hello world(--template ts)TSX,运行在 QuickJS 上28.6 MB12.4 MB
Hello world(--template mixed)TSX 加一个 Rust crate26.3 MB11.5 MB

这些数字来自:

  • 只为变化绘制帧,只画变化的地方:按合成器节奏运行的 ticker 只在原生动画、过渡或帧回调运行时跳动。一帧只在窗口保留的像素上 重画变化视图的区域(它原来的位置和现在的位置),并且只把这些区域交给合成器(Stocks:每帧约 34 个视图)。
  • Canvas 只录制动画部分:绘制被录制为 Direct2D 的 command list,是矢量的,任意缩放都清晰。动画值变化时,只重新录制从第一个读取动画值的操作开始的部分, 之前的部分保留(Stocks:22 条脉动的走势线和分时图,每秒约 26 ms 的录制)。
  • 只绘制看得见的部分:滚动视图或裁剪视图中不在视野内的子视图不进入这一帧,没有自身内容的视图只绘制背景和边框。

#iOS(UIKit)

测量环境:iOS 模拟器(iPhone 18 Pro Max,运行在上面那台 MacBook Pro 上),release 构建,与上面相同的浏览流程(perf.py --target ios: nsh test --target ios --fps,以 UIKit 收到的手指触摸操作)。模拟器以 60 Hz 呈现;在配有 ProMotion 屏幕的 iPhone 上,NSH 的 display link 以屏幕的 120 Hz 运行(nsh package 构建的应用和开发宿主通过 CADisableMinimumFrameDurationOnPhone 请求它)。阶段是 tick(原生动画)、 drain(应用引擎的批次)和 scroll(跟随滚动视图的工作:列表、吸顶单元格、绑定的值)。

页面帧的来源FPS帧 p95CPU
Home,滚动中滚动600.1 ms6%
Stocks实时数据、原生循环600.3 ms22%
Stocks,滚动中实时数据、滚动600.3 ms25%
Photos,滚动中滚动601.2 ms6%
Lists,滚动中滚动601.1 ms8%
Scrolling,滚动中滚动600.1 ms7%
Navigation,滚动中滚动600.2 ms7%
Form,滚动中滚动600.3 ms7%
Gestures弹簧、脉动的圆环600.3 ms3%
Animations原生循环600.7 ms6%
Canvas原生循环600.4 ms16%
Styles摆动的变换600.4 ms4%
Tailwind,滚动中滚动600.2 ms7%
Text,滚动中滚动600.1 ms7%
Text View,滚动中滚动600.1 ms6%
Images,滚动中滚动600.1 ms5%
Components,滚动中滚动600.2 ms6%
Overlays,滚动中滚动600.1 ms6%
Menus,滚动中滚动600.2 ms7%
Controls,滚动中滚动、过渡600.2 ms5%
Web and video,滚动中滚动600.1 ms8%
SystemrequestAnimationFrame、定时器600.3 ms4%
Standard APIs,滚动中滚动600.1 ms7%
Storage,滚动中滚动600.1 ms8%

FPS 与上面一样是有东西在动时的速率:达到模拟器的 60 时,每一帧都在上一帧之后的下一次刷新到来。没有任何东西在动的页面不绘制帧, 静止时 CPU 为 1–2%。进程在 gallery 首页占用 40 MB,浏览完全部页面后约 180 MB(有意缓存的图片、Text View 页的文档)。

在 iPhone 17 Pro(ProMotion,120 Hz)上,以开发宿主单独运行一个 demo,动画页面每次刷新都绘制一帧:

页面帧的来源FPS帧 p95超过一次刷新CPU内存
Stocks实时数据、原生循环1200.2–0.3 ms0%46–70%41–43 MB
Canvas原生循环1200.1 ms0%25–36%37–38 MB
Animations原生循环1200.2 ms0%3–11%37 MB

Stocks 每秒提交 20 次(每 50 ms 一次行情),每次在引擎线程上耗时 JS 1.5 ms、core 1.4 ms(p95 为 2.2 和 2.0 ms), 引擎线程的忙碌占比为 4–6%。

#测量应用

用同样的方式测量一个应用,在 --target linux、macos、windows 或 ios 上带上 --fps 和 -v 运行它的测试或步骤:

cargo build --release -p nsh-cli
python3 examples/gallery/tools/perf.py --target macos --only Stocks,Canvas   # gallery,逐页测量
echo 'await app.wait(5000)' | nsh -v test App.tsx --target linux --fps        # 任意应用