Improve terminal rendering performance (#33345)

Closes #18263

Improvements:

• **Batch text rendering** - Combine adjacent cells with identical
styling into single text runs to reduce draw calls
• **Throttle hyperlink searches** - Limit hyperlink detection to every
100ms or when mouse moves >5px to reduce CPU usage
• **Pre-allocate collections** - Use `Vec::with_capacity()` for cells,
runs, and regions to minimize reallocations
• **Optimize background regions** - Merge adjacent background rectangles
to reduce number of draw operations
• **Cache selection text** - Only compute terminal selection string when
selection exists

Release Notes:

- Improved terminal rendering performance.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Alisina Bahadori 2025-07-08 11:05:01 -04:00 committed by GitHub
parent bcac748c2b
commit 925464cfc6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 609 additions and 177 deletions

View file

@ -58,7 +58,7 @@ use std::{
path::PathBuf,
process::ExitStatus,
sync::Arc,
time::Duration,
time::{Duration, Instant},
};
use thiserror::Error;
@ -501,6 +501,8 @@ impl TerminalBuilder {
vi_mode_enabled: false,
is_ssh_terminal,
python_venv_directory,
last_mouse_move_time: Instant::now(),
last_hyperlink_search_position: None,
};
Ok(TerminalBuilder {
@ -659,6 +661,8 @@ pub struct Terminal {
task: Option<TaskState>,
vi_mode_enabled: bool,
is_ssh_terminal: bool,
last_mouse_move_time: Instant,
last_hyperlink_search_position: Option<Point<Pixels>>,
}
pub struct TaskState {
@ -1307,24 +1311,27 @@ impl Terminal {
fn make_content(term: &Term<ZedListener>, last_content: &TerminalContent) -> TerminalContent {
let content = term.renderable_content();
// Pre-allocate with estimated size to reduce reallocations
let estimated_size = content.display_iter.size_hint().0;
let mut cells = Vec::with_capacity(estimated_size);
cells.extend(content.display_iter.map(|ic| IndexedCell {
point: ic.point,
cell: ic.cell.clone(),
}));
let selection_text = if content.selection.is_some() {
term.selection_to_string()
} else {
None
};
TerminalContent {
cells: content
.display_iter
//TODO: Add this once there's a way to retain empty lines
// .filter(|ic| {
// !ic.flags.contains(Flags::HIDDEN)
// && !(ic.bg == Named(NamedColor::Background)
// && ic.c == ' '
// && !ic.flags.contains(Flags::INVERSE))
// })
.map(|ic| IndexedCell {
point: ic.point,
cell: ic.cell.clone(),
})
.collect::<Vec<IndexedCell>>(),
cells,
mode: content.mode,
display_offset: content.display_offset,
selection_text: term.selection_to_string(),
selection_text,
selection: content.selection,
cursor: content.cursor,
cursor_char: term.grid()[content.cursor.point].c,
@ -1457,10 +1464,26 @@ impl Terminal {
if self.selection_phase == SelectionPhase::Selecting {
self.last_content.last_hovered_word = None;
} else if self.last_content.terminal_bounds.bounds.contains(&position) {
self.events.push_back(InternalEvent::FindHyperlink(
position - self.last_content.terminal_bounds.bounds.origin,
false,
));
// Throttle hyperlink searches to avoid excessive processing
let now = Instant::now();
let should_search = if let Some(last_pos) = self.last_hyperlink_search_position {
// Only search if mouse moved significantly or enough time passed
let distance_moved =
((position.x - last_pos.x).abs() + (position.y - last_pos.y).abs()) > px(5.0);
let time_elapsed = now.duration_since(self.last_mouse_move_time).as_millis() > 100;
distance_moved || time_elapsed
} else {
true
};
if should_search {
self.last_mouse_move_time = now;
self.last_hyperlink_search_position = Some(position);
self.events.push_back(InternalEvent::FindHyperlink(
position - self.last_content.terminal_bounds.bounds.origin,
false,
));
}
} else {
self.last_content.last_hovered_word = None;
}