NativeDesktop

Performance

The frame budgets nd apps hold, the numbers measured so far on e1504g, g815 and the macbook, and how to measure your own app.

The target is idle at 0% CPU, work that scales with the dirty view, and a frame on time at every display's refresh rate: 16.7 ms at 60 Hz, 6.9 ms at 144 Hz, 4.2 ms at 240 Hz. The slowest test machine is e1504g (Intel i3-N305, integrated GPU, 60 Hz), so it sets the budgets.

Budgets

Measured on e1504g for examples/hello and examples/notes with demo data:

MeasureBudget
Cold start to first frameunder 300 ms
Idle CPU over 60 s, focused and unfocused0.0%
RSSunder 80 MB with the GL path patched out
Key to present, p95under 16.7 ms
Scrolling 10k rowsno missed frames
100k rowswithin budget

Measured so far

A raw GPUI window on e1504g with no nd code, as the floor (2026-10-13): first frame 106 ms median, idle CPU 0.000%, RSS 177 MB (over budget by about 100 MB, the GL path is still linked), frame interval p95 about 17.0 ms with 0 missed of about 600. The numbers and the rerun steps are in baselines.md.

The 100k-row scroll of examples/scroll on e1504g at 60 Hz, build 3bebe63b (2026-10-14): p50 16.65 to 16.67 ms, p95 17.00 to 17.05 ms, 1 to 2 of about 600 frames missed (the first, while rows and glyphs are built). Before two fixes it missed 153 to 253 frames at p95 52 to 100 ms. The first fix was the flight recorder, which serialized the whole semantic tree to JSON every frame and now keeps typed records. The second was the GPUI presenter, which laid out overscan rows every frame and now lays out visible and unmeasured rows only. On g815, headless and in release, the same list costs p95 0.9 ms of work per frame at 40 points per frame and 1.7 ms at 400.

g815 drives 240 Hz and 144 Hz monitors and the per-monitor pacing patch was checked on both (STATUS.md). Windows pacing is written and has not run on a Windows machine. The macbook has no recorded frame budget yet; its measured number is the dev build loop: an incremental hello build takes about 1.25 s, so the dev profile stays on LLVM instead of Cranelift.

Scroll

nd diag scroll launches the scroll-bench example, scrolls a virtualized list of variable-height rows every display frame, and reads the host's frame line back.

nd diag scroll --rows 100000 --seconds 10 --step 40

It passes when no frame is missed beyond a warm-up allowance and p95 stays within a quarter period of the display's rate. --bin or ND_SCROLL_BIN names the binary when it is not next to nd. The list itself is explained in virtualization.

Example: Scroll. A virtualized list of 100 000 rows with heights from 28 to 124 points, the list `nd diag scroll` measures. Source in examples/scroll. It also runs live in the docs app.
Scroll on macOS

Captured on macOS 27.0, 2026-10-14.

Environment knobs

The GPUI host prints nd-budget: lines when these are set:

nd diag budget is not implemented; use these directly or nd diag scroll.

Per-display pacing

Each window has its own clock at the refresh rate of the display it is on, and an idle window asks for no frames. Move the two windows of examples/pacing to monitors with different rates and each shows its own. ND_PACING=1 prints the measured rate per window.

// Rendering again next frame keeps the window animating at its display's rate.
cx.notify();
let time = window.frame_time();
let hz = if time.interval() > 0. { 1. / time.interval() } else { 0. };
v_flex()
    .size_full()
    .items_center()
    .justify_center()
    .gap_1()
    .child(text(self.name).text_size(px(20.)))
    .child(text(format!("frame {}", time.frame)).tabular_nums())
    .child(text(format!("{hz:.0} Hz display")).tabular_nums())

From examples/pacing/src/lib.rs, line 14.

The fast feature

fast is a feature of the nd crate (and of nd-gpui). It builds the GPUI backend on gpui-fast instead of gpui-pre and is off by default. To compare when a Linux or Windows app misses frames, enable it in the app's dependency:

nd = { version = "*", features = ["fast"] }

The comparison against gpui-pre is tracked in docs/plan/baselines.md; no result is recorded there yet.

Profiling

The frame timing histograms in every incident bundle's profile.json (diagnostics) are always on. For a timeline, build nd-diag with the tracy feature and attach Tracy. The nd::trace! macros and the in-app HUD described in ARCHITECTURE.md 9.1 are not shipped yet.

Rules that keep an app inside the budget

agents/performance holds the rules for agents. The short version: virtualize long lists, keep views small so a change repaints only its own, and never block the main thread.