Skip to main content

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.

SurfaceInteractive targetBoundary
Chat continuous scrolling60 FPS / 16.67 msFixed transcript, view, and input trace
Brain publication-driven rendering50 FPS / 20 msFixed active workload and natural paints
Renderer certification gate≤40 ms p95Existing 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

  1. Reproduce: choose one stable workload and record the environment.
  2. Measure release: establish p50, p95, maximum, work count, and dropped work with an optimized executable.
  3. Classify: locate the cost in worker, state projection, view render, layout/text, prepaint, scene recording, GPU presentation, or I/O.
  4. Change one cause: make the smallest change that tests the hypothesis.
  5. Compare: repeat the same workload and retain before/after evidence.
  6. Regress: verify input, focus, accessibility, visual output, lifecycle, security boundaries, and inactive behavior.
  7. 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:

FieldBaseline 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

SymptomMeasure firstLikely owner
Worker FPS is low while paint is readyWorker step and publication durationSimulation or worker-side allocation
Snapshot/build time spikes after camera or resizeCache key, projection, and static buildBrain render cache
Draw time is high with cache hitsPrimitive count and draw_canvas CPU profileGPUI scene recording
Paint cadence is low but draw is smallRoot render, layout, prepaint, text, or schedulingGPUI view tree/invalidation
Wheel input hitchesParent notifications, realized rows, identity allocation, text/action constructionTranscript view and row projection
Memory grows during long useRetained rows, logs, caches, images, channels, or task historyOwnership and eviction policy
Idle app keeps consuming CPUTimers, animation frames, polls, worker wakes, or notificationsLifecycle 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

MeasurementBeforeAfterDeltaAcceptance
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 clean as 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.