From 0fb89f4ffebabd7ea868f75a93f14a41ff65764a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 9 Jul 2026 09:38:43 -0700 Subject: [PATCH] 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. --- src/config/Config.zig | 33 +++++++++++++++++++++++++++++---- src/renderer/Thread.zig | 20 ++++++++++++++++++-- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 681dfe6f0..d6a038d9d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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. /// diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index da8fefc09..7f93e9493 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -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();