Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Performance

AZUREAL is designed to feel instant. The performance budget is tight: CPU must stay below 5% during scrolling, input must be reflected in under 1ms on macOS, and the application must remain responsive even while agents stream thousands of events per second. This page documents the performance rules and invariants that make this possible.


The Core Rule

Never create expensive objects in the render path.

The render path is everything between “an event arrives” and “pixels appear on screen.” Any operation in this path directly impacts perceived responsiveness. The single most important rule is that expensive initialization – syntax highlighter creation (100ms+), file I/O, network calls, process spawning – must happen outside the render path, either at startup or on a background thread.


Performance Invariants

These are not guidelines. They are invariants that the codebase enforces:

1. Cache Rendered Output

The rendered_lines_cache stores the fully styled, wrapped output of the session pane. Drawing reads from this cache. Only new events trigger render work; previously rendered content is never re-rendered unless the terminal width changes.

2. Decouple Animation from Content Cache

Animations (streaming cursor blink, progress indicators) are patched into the viewport at draw time without invalidating the content cache. The cursor blink does not trigger a re-render of the entire conversation – it patches a single cell in the viewport slice.

3. Skip Redraw When Nothing Changed

Scroll operations return a boolean indicating whether the scroll position actually changed. If the user is already at the bottom and presses Down, scroll returns false and no redraw occurs. This prevents wasted frames on no-op inputs.

4. Pre-Format Expensive Data at Load Time

Data that is expensive to format (file tree entries, session list items, status bar components) is formatted once when loaded or changed, not on every draw call. The draw path reads pre-formatted strings.

5. Never Use .wrap() on Pre-Wrapped Content

Text wrapping is performed once during rendering. The ratatui Paragraph widget is given pre-wrapped lines and is not configured with .wrap(), which would perform a redundant wrapping pass on already-wrapped content.

6. Cache Edit Mode Highlighting Per Version

When the user edits a file in edit mode, syntax highlighting is cached per edit version (a monotonically increasing counter). Highlighting is recomputed only when the content changes, not on every cursor movement or draw cycle.

7. File I/O is Safe on the Render Thread

The render thread may read files from disk (e.g., for syntax detection or grammar loading), but file I/O is never performed on the draw path. The distinction matters: the render thread runs in the background and does not block frame output. The draw path runs on the main thread and must complete within the frame budget.


CPU Budget

ScenarioTarget CPUNotes
Idle (no input, no streaming)<1%100ms poll timeout, no draw calls
Scrolling<5%Viewport cache hit, no re-render
Active typing<3%fast_draw_input() on macOS, deferred full draw
Agent streaming<8%5fps draw throttle, incremental render
Agent streaming + user scrolling<12%30fps draw, viewport cache rebuild

These targets assume a modern machine (2020+ CPU). The primary lever for CPU reduction is draw throttling – the adaptive 5fps/30fps system described in Event Loop is the single largest contributor to low idle CPU usage.


What To Watch For

Common performance mistakes and how AZUREAL avoids them:

SyntaxHighlighter::new() in a Loop

Creating a syntax highlighter loads grammar definitions and compiles them. This takes 100ms+ and must happen once, at startup. The highlighter is stored as a long-lived value and reused across all renders.

Allocating in the Draw Path

The draw path should allocate as little as possible. Pre-rendered lines are stored as Vec<Line> and sliced by reference for the viewport. No new String or Vec allocations occur per frame for content that has not changed.

Unnecessary Full Redraws

A full terminal.draw() call costs ~18ms. The fast-path input optimization (~0.1ms) and the skip-redraw-on-no-change logic exist specifically to avoid paying this cost when it is not needed. Every code path that might trigger a redraw must justify why a full draw is necessary rather than a partial update or no update at all.

Blocking the Main Loop

The main loop must never block. All I/O (file reads, process spawning, git commands) runs on background threads or is dispatched asynchronously. A blocked main loop means frozen input handling, which is the most user-visible performance failure.