Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions crates/libghostty-vt/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,46 @@ impl<'alloc: 'cb, 'cb> Terminal<'alloc, 'cb> {
Ok(CompressionActivity(value))
}

/// How much memory the terminal's screens hold.
///
/// Most of it is the pages that store the screen and its scrollback,
/// colours, styles and hyperlinks included; images have their own
/// figure. Small structures outside the pages, such as the title, are not
/// counted. Budget against [`ScreenMemory::resident_bytes`].
///
/// This never decompresses scrollback, but it looks at every page, so
/// avoid reading it after every write.
pub fn memory_usage(&self) -> Result<MemoryUsage> {
let mut value = ffi::sized!(ffi::TerminalMemoryUsage);
let result = unsafe {
ffi::ghostty_terminal_get(
self.inner.as_raw(),
Data::MEMORY_USAGE,
(&raw mut value).cast(),
)
};
from_result(result)?;
Ok(MemoryUsage {
compression_supported: value.compression_supported,
primary: ScreenMemory {
pages: value.primary_pages,
virtual_bytes: value.primary_virtual_bytes,
resident_bytes: value.primary_resident_bytes,
compressed_pages: value.primary_compressed_pages,
compressed_bytes: value.primary_compressed_bytes,
image_bytes: value.primary_image_bytes,
},
alternate: ScreenMemory {
pages: value.alternate_pages,
virtual_bytes: value.alternate_virtual_bytes,
resident_bytes: value.alternate_resident_bytes,
compressed_pages: value.alternate_compressed_pages,
compressed_bytes: value.alternate_compressed_bytes,
image_bytes: value.alternate_image_bytes,
},
})
}

/// The configured maximum retained VT continuation size in bytes.
///
/// A value of zero means continuation tracking is disabled. This reports
Expand Down Expand Up @@ -1698,6 +1738,48 @@ pub enum CompressionResult {
Complete = ffi::TerminalCompressionResult::COMPLETE,
}

/// Memory held by a terminal, from [`Terminal::memory_usage`].
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MemoryUsage {
/// Whether compressing scrollback can free memory on this platform. When
/// false, [`Terminal::compress`] reports [`CompressionResult::Unsupported`]
/// and the compressed figures are always zero.
pub compression_supported: bool,
/// The primary screen, which holds all of the scrollback.
pub primary: ScreenMemory,
/// The alternate screen: all zero until a program first switches to it.
pub alternate: ScreenMemory,
}

impl MemoryUsage {
/// Physical memory both screens' pages use, the figure to budget against.
#[must_use]
pub const fn resident_bytes(&self) -> u64 {
self.primary.resident_bytes.saturating_add(self.alternate.resident_bytes)
}
}

/// Memory held by one screen's pages.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ScreenMemory {
/// Pages, compressed ones included.
pub pages: u64,
/// Address space reserved for the pages, compressed and spare ones
/// included. Always at least [`resident_bytes`](Self::resident_bytes).
pub virtual_bytes: u64,
/// Physical memory the pages use; a compressed page counts only its
/// compressed size.
pub resident_bytes: u64,
/// Pages that are compressed.
pub compressed_pages: u64,
/// Compressed data held for them, already part of
/// [`resident_bytes`](Self::resident_bytes).
pub compressed_bytes: u64,
/// Image data stored through the Kitty graphics protocol, not part of
/// [`resident_bytes`](Self::resident_bytes).
pub image_bytes: u64,
}

/// Opaque token representing a terminal's current compression activity.
///
/// The token is opaque and only equality comparisons are meaningful.
Expand Down Expand Up @@ -4369,6 +4451,39 @@ mod tests {
assert_eq!(terminal.mouse_shape().unwrap(), mouse::Shape::Text);
}

#[test]
fn memory_usage_grows_with_scrollback_and_compression_lowers_it() {
let mut terminal = Terminal::new(80, 24).unwrap();
let fresh = terminal.memory_usage().unwrap();
assert!(fresh.primary.pages >= 1);
assert!(fresh.primary.resident_bytes > 0);
assert!(fresh.primary.resident_bytes <= fresh.primary.virtual_bytes);
assert_eq!(fresh.alternate, ScreenMemory::default());

for i in 0..20_000 {
terminal.vt_write(format!("line {i} of plain output\r\n").as_bytes());
}
let full = terminal.memory_usage().unwrap();
assert!(full.primary.pages > fresh.primary.pages);
assert!(full.primary.resident_bytes > fresh.primary.resident_bytes);
assert_eq!(full.resident_bytes(), full.primary.resident_bytes);

let result = terminal.compress(CompressionMode::Full).unwrap();
let compressed = terminal.memory_usage().unwrap();
if full.compression_supported {
assert_eq!(result, CompressionResult::Complete);
assert!(compressed.primary.compressed_pages > 0);
assert!(compressed.primary.compressed_bytes > 0);
assert!(compressed.primary.resident_bytes < full.primary.resident_bytes);
} else {
assert_eq!(result, CompressionResult::Unsupported);
assert_eq!(compressed.primary.compressed_pages, 0);
}

terminal.vt_write(b"\x1b[?1049h");
assert!(terminal.memory_usage().unwrap().alternate.pages >= 1);
}

fn tiny_terminal() -> Terminal<'static, 'static> {
Terminal::new(8, 3).expect("terminal should initialize")
}
Expand Down
Loading