Skip to content

Commit b462e01

Browse files
authored
Reduce TUI zero-state animation CPU usage (#14604)
## Description Reduces the CPU cost of the TUI zero-state animation while preserving its original 66 ms (~15 FPS) visual cadence. - Suspends animation repaint scheduling while the terminal is unfocused and resumes immediately on focus gain. - Caches logo geometry, reuses projection buffers, and paints the starfield directly. - Replaces generic stack composition in the zero state with a specialized direct compositor while preserving opaque-overlay semantics. - Adds production-shaped Criterion benchmarks and rendering/focus regression coverage. At 120×40, the retained-frame benchmarks improved by approximately 68% for the built-in logo and 73% for the ASCII logo. Larger terminal sizes showed greater savings. Implementation plan: https://staging.warp.dev/drive/notebook/NHie0i2bcU6ge5gHne0EEC ## Linked Issue None. ## Testing - [x] Manually tested the TUI locally with `./script/run-tui` - [x] `cargo nextest run -p warpui_core --features tui --no-fail-fast` (559 passed, 7 skipped) - [x] `cargo nextest run -p warp_tui --features test-util --no-fail-fast` (947 passed) - [x] `cargo clippy --workspace --exclude warp_completer --all-targets --tests -- -D warnings` - [x] `cargo clippy -p warp --all-targets --tests -- -D warnings` - [x] `cargo clippy -p warp_completer --all-targets --tests -- -D warnings` - [x] `./script/format --check` - [x] `git diff --check` ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode CHANGELOG-TUI: Reduced CPU usage while the zero-state animation is active.
1 parent a24741b commit b462e01

17 files changed

Lines changed: 970 additions & 49 deletions

crates/warp_tui/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ name = "transcript_bench"
123123
harness = false
124124
required-features = ["test-util"]
125125

126+
[[bench]]
127+
name = "zero_state_bench"
128+
harness = false
129+
required-features = ["test-util"]
130+
126131
[features]
127132
# Exposes deterministic, production-shaped transcript fixtures to benchmarks.
128133
test-util = ["warp/test-util", "warp_core/test-util"]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use std::hint::black_box;
2+
use std::time::Duration;
3+
4+
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
5+
use warp_tui::benchmark_support::{
6+
ZeroStateBenchmark, ZeroStateBenchmarkShape, ZeroStateProjectionBenchmark,
7+
};
8+
9+
fn benchmark_zero_state_frame(criterion: &mut Criterion) {
10+
let mut group = criterion.benchmark_group("tui_zero_state/retained_frame");
11+
for (width, height) in [(80, 24), (120, 40), (240, 80)] {
12+
for shape in [
13+
ZeroStateBenchmarkShape::BuiltIn,
14+
ZeroStateBenchmarkShape::Ascii,
15+
] {
16+
let mut benchmark = ZeroStateBenchmark::new(shape, width, height);
17+
group.bench_with_input(
18+
BenchmarkId::new(format!("{shape:?}"), format!("{width}x{height}")),
19+
&(width, height),
20+
|b, _| b.iter(|| black_box(benchmark.present())),
21+
);
22+
}
23+
}
24+
group.finish();
25+
}
26+
27+
fn benchmark_logo_projection(criterion: &mut Criterion) {
28+
let mut group = criterion.benchmark_group("tui_zero_state/logo_projection");
29+
for (width, height) in [(32, 24), (32, 40), (32, 80)] {
30+
for shape in [
31+
ZeroStateBenchmarkShape::BuiltIn,
32+
ZeroStateBenchmarkShape::Ascii,
33+
] {
34+
let mut benchmark = ZeroStateProjectionBenchmark::new(shape, width, height);
35+
group.bench_with_input(
36+
BenchmarkId::new(format!("{shape:?}"), format!("{width}x{height}")),
37+
&(width, height),
38+
|b, _| b.iter(|| black_box(benchmark.project())),
39+
);
40+
}
41+
}
42+
group.finish();
43+
}
44+
45+
criterion_group! {
46+
name = benches;
47+
config = Criterion::default()
48+
.sample_size(10)
49+
.warm_up_time(Duration::from_millis(500))
50+
.measurement_time(Duration::from_secs(1));
51+
targets = benchmark_zero_state_frame, benchmark_logo_projection
52+
}
53+
criterion_main!(benches);

crates/warp_tui/src/benchmark_support.rs

Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::cell::RefCell;
44
use std::collections::HashMap;
55
use std::rc::Rc;
66
use std::sync::Arc;
7+
use std::time::Duration;
78

89
use parking_lot::FairMutex;
910
use warp::tui_export::{
@@ -17,9 +18,10 @@ use warpui::{
1718
AddWindowOptions, App, AppContext, Entity, EntityId, EntityIdSet, TuiView, TypedActionView,
1819
ViewContext, ViewHandle, WindowInvalidation,
1920
};
21+
use warpui_core::elements::animation::AnimationClock;
2022
use warpui_core::elements::tui::{
21-
TuiClipped, TuiElement, TuiRect, TuiViewportPosition, TuiViewportVerticalAlignment,
22-
TuiViewportedList, TuiViewportedListState,
23+
TuiClipped, TuiElement, TuiRect, TuiSize, TuiStyle, TuiText, TuiViewportPosition,
24+
TuiViewportVerticalAlignment, TuiViewportedList, TuiViewportedListState,
2325
};
2426
use warpui_core::presenter::tui::TuiPresenter;
2527

@@ -31,6 +33,158 @@ use crate::tui_block_list_viewport_source::{
3133
TuiBlockListViewportSource,
3234
};
3335
use crate::tui_builder::TuiUiBuilder;
36+
use crate::zero_state::build_zero_state_layout;
37+
use crate::zero_state_animation::{
38+
LogoProjector, WarpLogoStyles, ZeroStateAnimationConfig, ZeroStateAnimationElement,
39+
ZeroStateInteractionHandle, ZeroStateStarfieldElement, benchmark_logo_projection,
40+
};
41+
42+
const ZERO_STATE_COPY_COLS: u16 = 48;
43+
const ZERO_STATE_ANIMATION_COLS: u16 = 32;
44+
45+
#[derive(Clone, Copy, Debug)]
46+
pub enum ZeroStateBenchmarkShape {
47+
BuiltIn,
48+
Ascii,
49+
}
50+
51+
impl ZeroStateBenchmarkShape {
52+
fn config(self) -> ZeroStateAnimationConfig {
53+
match self {
54+
Self::BuiltIn => ZeroStateAnimationConfig::default(),
55+
Self::Ascii => ZeroStateAnimationConfig::benchmark_ascii(),
56+
}
57+
}
58+
}
59+
60+
pub struct ZeroStateBenchmark {
61+
app: App,
62+
root: ViewHandle<BenchmarkZeroStateView>,
63+
presenter: TuiPresenter,
64+
area: TuiRect,
65+
}
66+
67+
impl ZeroStateBenchmark {
68+
pub fn new(shape: ZeroStateBenchmarkShape, width: u16, height: u16) -> Self {
69+
let config = Arc::new(shape.config());
70+
App::test((), move |mut app| async move {
71+
let (_, root) = app.update(|ctx| {
72+
ctx.add_tui_window(
73+
AddWindowOptions {
74+
window_style: WindowStyle::NotStealFocus,
75+
..Default::default()
76+
},
77+
move |_| BenchmarkZeroStateView {
78+
clock: AnimationClock::starting_at(Duration::ZERO),
79+
config,
80+
interaction: ZeroStateInteractionHandle::default(),
81+
},
82+
)
83+
});
84+
let mut benchmark = Self {
85+
app,
86+
root,
87+
presenter: TuiPresenter::new(),
88+
area: TuiRect::new(0, 0, width, height),
89+
};
90+
benchmark.invalidate();
91+
benchmark.present();
92+
benchmark
93+
})
94+
}
95+
96+
pub fn present(&mut self) -> u64 {
97+
let frame = self
98+
.app
99+
.update(|ctx| self.presenter.present(ctx, &self.root, self.area));
100+
frame.buffer.content.iter().fold(0u64, |checksum, cell| {
101+
checksum.wrapping_add(cell.symbol().len() as u64)
102+
})
103+
}
104+
105+
fn invalidate(&mut self) {
106+
let invalidation = WindowInvalidation {
107+
updated: EntityIdSet::from_iter([self.root.id()]),
108+
..Default::default()
109+
};
110+
self.app.read(|ctx| {
111+
self.presenter
112+
.invalidate(&invalidation, ctx, self.root.window_id(ctx));
113+
});
114+
}
115+
}
116+
117+
struct BenchmarkZeroStateView {
118+
clock: AnimationClock,
119+
config: Arc<ZeroStateAnimationConfig>,
120+
interaction: ZeroStateInteractionHandle,
121+
}
122+
123+
impl Entity for BenchmarkZeroStateView {
124+
type Event = ();
125+
}
126+
127+
impl TypedActionView for BenchmarkZeroStateView {
128+
type Action = ();
129+
}
130+
131+
impl TuiView for BenchmarkZeroStateView {
132+
fn ui_name() -> &'static str {
133+
"BenchmarkZeroStateView"
134+
}
135+
136+
fn render(&self, _app: &AppContext) -> Box<dyn TuiElement> {
137+
let style = TuiStyle::default();
138+
let starfield = ZeroStateStarfieldElement::new(
139+
self.clock,
140+
style,
141+
ZERO_STATE_COPY_COLS,
142+
ZERO_STATE_ANIMATION_COLS,
143+
)
144+
.finish();
145+
let animation = ZeroStateAnimationElement::new(
146+
self.clock,
147+
self.config.clone(),
148+
self.interaction.clone(),
149+
WarpLogoStyles {
150+
front: style,
151+
back: style,
152+
side: style,
153+
background: style,
154+
},
155+
)
156+
.without_background_stars()
157+
.finish();
158+
let overlay = TuiText::new(
159+
"Warp Agent\nv0.0.0\n\nWhat's new\n• benchmark\n\nProject\nbenchmark fixture",
160+
)
161+
.finish();
162+
build_zero_state_layout(starfield, animation, overlay)
163+
}
164+
}
165+
166+
pub struct ZeroStateProjectionBenchmark {
167+
elapsed: Duration,
168+
size: TuiSize,
169+
config: ZeroStateAnimationConfig,
170+
projector: LogoProjector,
171+
}
172+
173+
impl ZeroStateProjectionBenchmark {
174+
pub fn new(shape: ZeroStateBenchmarkShape, width: u16, height: u16) -> Self {
175+
Self {
176+
elapsed: Duration::ZERO,
177+
size: TuiSize::new(width, height),
178+
config: shape.config(),
179+
projector: LogoProjector::default(),
180+
}
181+
}
182+
183+
pub fn project(&mut self) -> u64 {
184+
self.elapsed += Duration::from_millis(66);
185+
benchmark_logo_projection(self.elapsed, self.size, &self.config, &mut self.projector)
186+
}
187+
}
34188

35189
/// Shape of the retained transcript fixture.
36190
#[derive(Clone, Copy, Debug)]

crates/warp_tui/src/editor_element.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -890,7 +890,9 @@ impl TuiElement for TuiEditorElement {
890890
handler(TuiEditorAction::PasteText(text.clone()), event_ctx);
891891
return true;
892892
}
893-
TuiEvent::ModifierKeyChanged { .. }
893+
TuiEvent::FocusGained
894+
| TuiEvent::FocusLost
895+
| TuiEvent::ModifierKeyChanged { .. }
894896
| TuiEvent::ScrollWheel { .. }
895897
| TuiEvent::LeftMouseDown { .. }
896898
| TuiEvent::LeftMouseUp { .. }

crates/warp_tui/src/option_selector.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1711,7 +1711,9 @@ impl TuiElement for SelectorInputElement {
17111711
true
17121712
}
17131713
TuiEvent::Paste { .. } => false,
1714-
TuiEvent::ModifierKeyChanged { .. }
1714+
TuiEvent::FocusGained
1715+
| TuiEvent::FocusLost
1716+
| TuiEvent::ModifierKeyChanged { .. }
17151717
| TuiEvent::LeftMouseDown { .. }
17161718
| TuiEvent::LeftMouseUp { .. }
17171719
| TuiEvent::LeftMouseDragged { .. }

crates/warp_tui/src/terminal_content_element.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ impl TuiElement for TuiTerminalContentElement {
176176
return true;
177177
}
178178
}
179+
TuiEvent::FocusGained | TuiEvent::FocusLost => {}
179180
TuiEvent::ScrollWheel { .. }
180181
| TuiEvent::LeftMouseDown { .. }
181182
| TuiEvent::LeftMouseUp { .. }
@@ -257,7 +258,9 @@ fn forwarded_pty_input_for_event<'a>(
257258
possible_typeahead: Some(Cow::Owned(normalized)),
258259
})
259260
}
260-
TuiEvent::KeyDown {
261+
TuiEvent::FocusGained
262+
| TuiEvent::FocusLost
263+
| TuiEvent::KeyDown {
261264
is_composing: true, ..
262265
}
263266
| TuiEvent::ModifierKeyChanged { .. }

0 commit comments

Comments
 (0)