terminal: configure scrollback compression

Idle compression was always enabled on supported renderer-backed
surfaces and the default logical scrollback limit remained sized for
fully resident history.

Add a default-on scrollback-compression option and make renderer
scheduling honor it across config reloads. Existing compressed pages
remain encoded when the option is disabled, while reenabling it starts a
fresh idle pass.

Raise the default logical scrollback limit from 10 MB to 50 MB and
document typical physical-memory savings, content-dependent behavior,
and retained virtual address usage.
This commit is contained in:
Mitchell Hashimoto
2026-07-09 09:38:43 -07:00
parent 95685afd26
commit 0fb89f4ffe
2 changed files with 47 additions and 6 deletions

View File

@@ -1375,9 +1375,10 @@ input: RepeatableReadableIO = .{},
///
/// Scrollback is stored in memory and allocated lazily up to this limit, so
/// setting a very large limit does not immediately consume that amount of
/// memory. On supported systems, Ghostty attempts to compress fully historical
/// pages which are not currently visible while the terminal is idle. This can
/// reduce physical memory usage, depending on the contents of the scrollback.
/// memory. On supported systems with scrollback compression enabled, Ghostty
/// attempts to compress fully historical pages which are not currently visible
/// while the terminal is idle. This can reduce physical memory usage, depending
/// on the contents of the scrollback.
///
/// This limit always measures the uncompressed logical size of the terminal
/// pages. Compression does not allow Ghostty to retain more history than the
@@ -1390,7 +1391,31 @@ input: RepeatableReadableIO = .{},
/// This is a future planned feature.
///
/// This can be changed at runtime but will only affect new terminal surfaces.
@"scrollback-limit": usize = 10_000_000, // 10MB
@"scrollback-limit": usize = 50_000_000, // 50MB
/// Whether to compress scrollback pages while the terminal is idle.
///
/// Ghostty does its best to only compress when idle and decompress
/// as needed. This means that compression doesn't lower IO throughput.
/// We recommend you keep it on.
///
/// The scrollback limit remains an uncompressed logical limit regardless of
/// this setting, so disabling compression can increase physical memory usage
/// but does not change how much history is retained.
///
/// Text-heavy terminal history generally compresses to approximately 10% to
/// 30% of its uncompressed page memory, corresponding to a 70% to 90% reduction
/// in physical memory for pages which are compressed. Compression savings are
/// content-dependent.
///
/// Note that the way Ghostty works is that we compress and discard the
/// physical/resident memory but we retain virtual mappings. You will not
/// see a decrease in virtual memory usage, but you will see a decrease
/// in physical/memory usage.
///
/// Changing this at runtime affects future compression work. Pages which are
/// already compressed remain compressed until their contents are accessed.
@"scrollback-compression": bool = true,
/// Control when the scrollbar is shown to scroll the scrollback buffer.
///

View File

@@ -115,10 +115,12 @@ flags: packed struct {
pub const DerivedConfig = struct {
custom_shader_animation: configpkg.CustomShaderAnimation,
scrollback_compression: bool,
pub fn init(config: *const configpkg.Config) DerivedConfig {
return .{
.custom_shader_animation = config.@"custom-shader-animation",
.scrollback_compression = config.@"scrollback-compression",
};
}
};
@@ -506,6 +508,16 @@ fn drainMailbox(self: *Thread) !void {
}
fn changeConfig(self: *Thread, config: *const DerivedConfig) !void {
// A newly enabled scheduler must reconsider existing history even when no
// terminal activity occurred while compression was disabled.
if (comptime terminalpkg.compression_enabled) {
if (!self.config.scrollback_compression and
config.scrollback_compression)
{
self.compression.activity = null;
}
}
self.config = config.*;
}
@@ -769,6 +781,7 @@ const Compression = struct {
fn wake(self: *Compression, thread: *Thread) void {
// If we have no compression then don't do anything.
if (comptime !terminalpkg.compression_enabled) return;
if (!thread.config.scrollback_compression) return;
// PageList activity, rather than a generic renderer wake, restarts the
// idle interval. In particular, the inspector wakes the renderer every
@@ -816,13 +829,16 @@ const Compression = struct {
const thread = thread_ orelse return .disarm;
const self = &thread.compression;
if (self.step(thread.state)) |delay| self.schedule(thread, delay);
if (self.step(thread)) |delay| self.schedule(thread, delay);
return .disarm;
}
/// Try one bounded step without waiting for the terminal lock. The return
/// value is the delay before another attempt, or null when work is done.
fn step(self: *Compression, state: *rendererpkg.State) ?u64 {
fn step(self: *Compression, thread: *Thread) ?u64 {
if (!thread.config.scrollback_compression) return null;
const state = thread.state;
if (!state.mutex.tryLock()) return idle_interval;
defer state.mutex.unlock();