Rust and GPUI optimization playbook
Use this playbook whenever native desktop work affects frame cadence, input latency, scrolling, CPU time, memory, worker throughput, or GPUI invalidation. It turns the current performance analysis into a repeatable engineering method.
The goal is not to make code look low-level. The goal is to spend less time and allocate less data on the path between an input or worker frame and the pixels the user sees, while preserving correctness, security, accessibility, and the existing benchmark boundaries.
Proposed performance budgets
The Chat and Brain values below are user-approved optimization targets, not current automated gates. The renderer certification threshold is the only existing locked gate in this table.
| Surface | Interactive target | Boundary |
|---|---|---|
| Chat continuous scrolling | 60 FPS / 16.67 ms | Fixed transcript, view, and input trace |
| Brain publication-driven rendering | 50 FPS / 20 ms | Fixed active workload and natural paints |
| Renderer certification gate | ≤40 ms p95 | Existing locked gate; do not relax or reinterpret it |
Treat these as separate workloads. Report the frame-time distribution, not only an average FPS number.
The optimization loop
- Reproduce: choose one stable workload and record the environment.
- Measure release: establish p50, p95, maximum, work count, and dropped work with an optimized executable.
- Classify: locate the cost in worker, state projection, view render, layout/text, prepaint, scene recording, GPU presentation, or I/O.
- Change one cause: make the smallest change that tests the hypothesis.
- Compare: repeat the same workload and retain before/after evidence.
- Regress: verify input, focus, accessibility, visual output, lifecycle, security boundaries, and inactive behavior.
- Record: document what improved, what did not, and what remains unknown.
Do not combine multiple speculative optimizations into one measurement. A good result should explain why it became faster.
Build the executable you intend to measure
desktop/scripts/run-dev.ps1 is the normal correctness-development loop. It
uses Cargo's development profile and must not be treated as release performance
evidence.
Open Developer PowerShell for VS 2022 so the MSVC linker and Windows SDK are available. From the repository root, build and launch the explicit optimized target:
$env:Path = "$env:USERPROFILE\.cargo\bin;$env:Path"
Push-Location -LiteralPath '.\desktop'
& "$env:USERPROFILE\.cargo\bin\cargo.exe" `
+1.96.0-x86_64-pc-windows-msvc `
build `
-p exepert-desktop `
--target x86_64-pc-windows-msvc `
--release `
--locked
& '.\target\x86_64-pc-windows-msvc\release\exepert-desktop.exe'
Pop-Location
Record the literal executable path and source commit. A stale executable from a different target directory is not comparable evidence.
cargo clean is not a performance technique. Use it only for disk recovery or
suspected corrupt/stale artifacts; it forces a rebuild but does not improve the
generated release code.
Record a reproducible baseline
Before changing source, fill in this record:
| Field | Baseline value |
|---|---|
| Date and commit | |
| Executable path and profile | |
| Windows version, CPU, GPU, memory, power mode | |
| Window client size and DPI | |
| Surface and presentation | |
| Brain preset/signals or Chat task/row count | |
| Input device and trace | |
| Inspector, Runtime Log, telemetry, and side panels | |
| Warm-up and sample duration | |
| p50 / p95 / max frame time | |
| Worker, draw, layout, or row-realization timing | |
| Dropped/coalesced work and memory |
Use at least three warmed runs. Keep the transcript, Brain workload, window, DPI, diagnostics, and input trace constant. Report run-to-run variance instead of selecting only the fastest run.
Classify the bottleneck before changing code
| Symptom | Measure first | Likely owner |
|---|---|---|
| Worker FPS is low while paint is ready | Worker step and publication duration | Simulation or worker-side allocation |
| Snapshot/build time spikes after camera or resize | Cache key, projection, and static build | Brain render cache |
| Draw time is high with cache hits | Primitive count and draw_canvas CPU profile | GPUI scene recording |
| Paint cadence is low but draw is small | Root render, layout, prepaint, text, or scheduling | GPUI view tree/invalidation |
| Wheel input hitches | Parent notifications, realized rows, identity allocation, text/action construction | Transcript view and row projection |
| Memory grows during long use | Retained rows, logs, caches, images, channels, or task history | Ownership and eviction policy |
| Idle app keeps consuming CPU | Timers, animation frames, polls, worker wakes, or notifications | Lifecycle cancellation |
Use the built-in Brain telemetry and renderer artifacts first. Add narrowly
scoped Instant measurements around suspected Rust stages when the existing
telemetry cannot separate them. Use a sampling CPU profiler such as Windows
Performance Recorder/Analyzer or Visual Studio CPU Usage when instrumentation
would distort the hot path or the owner remains unclear.
Rust hot-path methods
Keep the GPUI thread non-blocking
- Move simulation, network, process supervision, file work, and expensive data conversion to owned background tasks or workers.
- Never wait synchronously for a channel, process, network request, or thread join from a GPUI event or render callback.
- Return bounded results to GPUI and apply them in bounded batches.
- Coalesce redundant wakeups; one pending notification is enough when only the latest snapshot matters.
Bound queues and retained data
- Prefer a one-frame or otherwise explicit bounded channel for replaceable visual state.
- Define maximum transcript windows, diagnostics entries, payload sizes, and image caches with visible older-history or paging behavior.
- Track dropped or coalesced work so backpressure remains observable.
- Cancel producers when their view, generation, process, or window is no longer active.
Reuse allocations and stable data
- Reuse
Vec,String, map, and scratch-buffer capacity across frames when ownership is clear. - Store stable row identities once per reconciliation instead of rebuilding a vector and cloning every key during wheel and animation frames.
- Prefer cloning
Arc,Rc, entity handles, or compact keys over cloning message bodies, payloads, and complete state collections. - Derive display data once per content revision and retain it until the revision changes.
- Separate static and dynamic data so a signal update does not clone or rebuild static geometry, controls, or metadata.
Optimize only measured computation
- Remove repeated parsing, formatting, sorting, and projection from the frame path before considering unsafe code, SIMD, or custom allocators.
- Preserve readable ownership and bounded lifetimes; a fast unbounded cache is still a memory leak.
- Do not replace a correct bounded channel with shared mutable state unless the profile proves channel overhead is material and the new synchronization is simpler to validate.
GPUI rendering methods
Invalidate the smallest owner
- Call
cx.notify()only when observable state changed. - Notify the transcript entity for transcript scroll state; avoid notifying the root application merely to advance a child animation.
- Isolate fast telemetry, progress, cursor, or animation state in small entities so unchanged panels can reuse cached paint.
- Close popovers or cancel tail-follow only when they are actually active, then notify only the entity that owns the changed state.
Broad invalidation is often more expensive than the state mutation that caused it because GPUI must revisit view construction, layout, prepaint, text, and scene recording before GPU submission.
Preserve stable identity and paint reuse
- Keep view entities, element IDs, list keys, focus handles, and content revisions stable across frames.
- Reconcile rows only when their key, order, or content revision changes.
- Store row focus handles by stable key and discard only keys that leave the retained window.
- Use stable text/Markdown views so unchanged content reuses its parsed and shaped representation.
- Avoid closure-captured copies of large row or task state when a compact key or retained row view model is sufficient.
Virtualize deliberately
- Use GPUI's variable-height list for unbounded or long transcript/history surfaces.
- Measure realized row count, row render time, and layout time at the target viewport before changing overdraw.
- Keep enough overdraw to avoid blank edges during fast input, but do not treat a large buffer as free.
- Preserve scroll bookmarks by stable key and offset when older rows are inserted, removed, or remeasured.
- Keep one scrolling owner; nested popovers and option lists must stop wheel propagation when they own scrolling.
Bound animation and recurring work
- Request the next animation frame only while interpolation is active.
- Cancel animation generations on direct pointer input, task/source changes, minimize, inactivity, reduced motion, or completed transitions.
- Keep precision touchpad input direct and verify mouse-wheel easing separately.
- Stop timers, polls, worker wakes, and diagnostics refreshes when their owner is inactive.
- Measure idle CPU and paint counts after every recurring-work change.
Preserve accessibility while optimizing
- Retain semantic roles, accessible names, selected/toggled state, focus rings, tooltips, tab order, keyboard navigation, and UI Automation exposure.
- Cache accessible presentation by the same stable revision as visible presentation; do not remove labels or focusable controls to reduce element count.
- Verify pointer, keyboard, reduced-motion, high-DPI, and assistive-technology behavior after a view reuse or virtualization change.
Brain canvas checklist
Static work
- Key cached geometry by every input that changes projection: fixture, camera, viewport, DPI, and relevant visual style.
- Rebuild static axon/neuron geometry only when that key changes.
- Record cache hit/miss and static-build time; a cache without evidence is hard to trust.
- Reuse GPUI prepaint/paint ranges or retained scene data where the framework's ownership rules permit it.
Per-frame work
- Count axon paths, neuron quads, signal projections, signal quads, and inspector primitives separately.
- Batch or retain unchanged primitives before reducing visual fidelity.
- Keep dynamic signal projection contiguous and allocation-free when the profile shows it is material.
- Use level of detail only as an explicit product behavior with visual and interaction acceptance, not as a hidden performance shortcut.
- Keep telemetry collection lightweight and publish it through a store that does not invalidate the canvas.
Worker/render coordination
- Keep the latest useful frame rather than queuing stale visual snapshots.
- Distinguish worker step, snapshot copy, wake latency, draw duration, and inter-paint interval.
- Track published and dropped snapshots at the same measurement boundaries.
- Do not increase the 50 Hz worker cadence until the current 20 ms budget is sustained and a product requirement justifies more novel frames.
Chat transcript checklist
- Project protocol state into compact row view models only when task/session or row revisions change.
- Retain the identity vector used by scroll reconciliation and animation.
- Cache Markdown, formatted labels, action metadata, tooltips, and accessible names by content revision.
- Keep wheel animation local to the transcript and avoid parent/root renders when menu or tail-follow state is already inactive.
- Measure viewport rows and 512 px overdraw rows separately.
- Test line-wheel, precision-touchpad, reduced-motion, scrollbar, keyboard, tail-follow, jump-to-bottom, and history-prepend paths.
- Verify EXEPERT, Codex, and DSH independently; shared scrolling code does not imply identical parent invalidation or row construction.
- Preserve stable IDs, focus restoration, action discoverability, selection, links, copy behavior, and variable-height remeasurement.
Acceptance after a performance change
Automated checks
Run formatting and focused contracts for the Rust/GPUI surface that changed, then the locked desktop suite when the risk crosses shared transcript, worker, renderer, or accessibility boundaries. Build the explicit Windows release target before collecting timing evidence.
For renderer work, keep the focused probes and full certification gate separate. A short probe, software-ready result, or non-floor host cannot be reported as a certifying PASS.
Native interaction checks
- Repeat the exact baseline workload at the same view size and DPI.
- Confirm frame p95 meets the workload-specific budget across three warmed runs without a correctness or memory regression.
- Exercise both wide and stacked layouts.
- Verify focus, keyboard, pointer, scrolling, reduced motion, and UIA semantics.
- Confirm minimized, idle, hidden, and inactive surfaces stop recurring work.
- Test task/source switching, cancellation, recovery, and diagnostics state.
Before/after record
| Measurement | Before | After | Delta | Acceptance |
|---|---|---|---|---|
| Frame p50 | ||||
| Frame p95 | ||||
| Maximum frame | ||||
| Worker or draw duration | ||||
| Realized rows or primitives | ||||
| Dropped/coalesced work | ||||
| Peak RSS / retained entries | ||||
| Idle paints / CPU |
Record the failed hypothesis too. Knowing that an optimization did not move the target metric prevents future engineers from repeating the same experiment.
Performance anti-patterns
- Judging shipping performance from
run-dev.ps1. - Treating
cargo cleanas an optimization. - Assuming GPU acceleration removes Rust render, layout, text, or scene costs.
- Reporting average FPS without p95, maximum, workload, window, and sample duration.
- Raising worker frequency to hide a slow consumer.
- Broadly calling
cx.notify()because it is simpler than tracking ownership. - Rebuilding row identities, formatted text, or static geometry every frame.
- Increasing overdraw or cache size without a bound and eviction rule.
- Disabling animations, accessibility, focus, tooltips, or visual fidelity without explicit product acceptance.
- Calling a short or non-certifying renderer probe PASS.
- Optimizing multiple stages at once and losing causal evidence.