(macOS) Memory Leak Fixes (#7770)

This PR contains fixes for 4 different memory leaks that affected
Ghostty on macOS.

1. (whenever a font is loaded) CoreText font features dict list wasn't
properly released. Fixed by releasing.
2. (whenever a font is searched for) CoreText discovery iterator
descriptors weren't properly released. Fixed by releasing.
3. (during resize) Metal texture descriptors were not properly released.
Fixed by releasing.
4. (every frame) Objective-C runtime blocks for buffer completion
handler and IOSurfaceLayer set surface were not properly deallocated due
to issues with the internal implementation in `zig-objc`. Fixed in
`zig-objc`, dependency hash updated with fix.

A handful of small apparent leaks remain but their cause is not clear
and they're all static (not increasing over time, seemingly).

### Xcode memory graph "leaks" comparison
|Before (main)|After (this PR)|
|-|-|
|<img width="445" alt="image"
src="https://github.com/user-attachments/assets/d1c89918-8ab2-4201-bf1e-9b3a519a85a8"
/>|<img width="445" alt="image"
src="https://github.com/user-attachments/assets/88c60807-756e-48d8-9918-2a52d6556035"/>|

<sup>Images taken after launching Ghostty, creating 4 tabs, and rapidly
switching between them to force render many frames.</sup>

---

Hopefully this fixes the occasional OOM issues some users have reported.
This commit is contained in:
Qwerasd
2025-07-02 17:08:34 -06:00
committed by GitHub
11 changed files with 37 additions and 40 deletions

View File

@ -26,8 +26,8 @@
}, },
.zig_objc = .{ .zig_objc = .{
// mitchellh/zig-objc // mitchellh/zig-objc
.url = "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz", .url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
.hash = "zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt", .hash = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk",
.lazy = true, .lazy = true,
}, },
.zig_js = .{ .zig_js = .{

6
build.zig.zon.json generated
View File

@ -144,10 +144,10 @@
"url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz", "url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz",
"hash": "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0=" "hash": "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0="
}, },
"zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt": { "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk": {
"name": "zig_objc", "name": "zig_objc",
"url": "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz", "url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
"hash": "sha256-zn1tR6xhSmDla4UJ3t+Gni4Ni3R8deSK3tEe7DGzNXw=" "hash": "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw="
}, },
"wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy": { "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy": {
"name": "zig_wayland", "name": "zig_wayland",

6
build.zig.zon.nix generated
View File

@ -314,11 +314,11 @@ in
}; };
} }
{ {
name = "zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt"; name = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk";
path = fetchZigArtifact { path = fetchZigArtifact {
name = "zig_objc"; name = "zig_objc";
url = "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz"; url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz";
hash = "sha256-zn1tR6xhSmDla4UJ3t+Gni4Ni3R8deSK3tEe7DGzNXw="; hash = "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw=";
}; };
} }
{ {

2
build.zig.zon.txt generated
View File

@ -29,6 +29,6 @@ https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.ta
https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz
https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz
https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz
https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz

View File

@ -175,9 +175,9 @@
}, },
{ {
"type": "archive", "type": "archive",
"url": "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz", "url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
"dest": "vendor/p/zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt", "dest": "vendor/p/zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk",
"sha256": "ce7d6d47ac614a60e56b8509dedf869e2e0d8b747c75e48aded11eec31b3357c" "sha256": "a37be5eea7e44a2d1b2976ba256b85f76a8c1063fc01ffec85c8a9e67468e4dc"
}, },
{ {
"type": "archive", "type": "archive",

View File

@ -831,6 +831,9 @@ pub const CoreText = struct {
i: usize, i: usize,
pub fn deinit(self: *DiscoverIterator) void { pub fn deinit(self: *DiscoverIterator) void {
for (self.list) |desc| {
desc.release();
}
self.alloc.free(self.list); self.alloc.free(self.list);
self.* = undefined; self.* = undefined;
} }

View File

@ -109,7 +109,8 @@ pub const Shaper = struct {
/// settings the font features of a CoreText font. /// settings the font features of a CoreText font.
fn makeFeaturesDict(feats: []const Feature) !*macos.foundation.Dictionary { fn makeFeaturesDict(feats: []const Feature) !*macos.foundation.Dictionary {
const list = try macos.foundation.MutableArray.create(); const list = try macos.foundation.MutableArray.create();
errdefer list.release(); // The list will be retained by the dict once we add it to it.
defer list.release();
for (feats) |feat| { for (feats) |feat| {
const value_num: c_int = @intCast(feat.value); const value_num: c_int = @intCast(feat.value);

View File

@ -28,7 +28,7 @@ pub const Options = struct {
/// MTLCommandBuffer /// MTLCommandBuffer
buffer: objc.Object, buffer: objc.Object,
block: CompletionBlock, block: CompletionBlock.Context,
/// Begin encoding a frame. /// Begin encoding a frame.
pub fn begin( pub fn begin(
@ -47,7 +47,7 @@ pub fn begin(
// Create our block to register for completion updates. // Create our block to register for completion updates.
// The block is deallocated by the objC runtime on success. // The block is deallocated by the objC runtime on success.
const block = try CompletionBlock.init( const block = CompletionBlock.init(
.{ .{
.renderer = renderer, .renderer = renderer,
.target = target, .target = target,
@ -55,7 +55,6 @@ pub fn begin(
}, },
&bufferCompleted, &bufferCompleted,
); );
errdefer block.deinit();
return .{ .buffer = buffer, .block = block }; return .{ .buffer = buffer, .block = block };
} }
@ -114,24 +113,23 @@ pub inline fn complete(self: *Self, sync: bool) void {
// If we don't need to complete synchronously, // If we don't need to complete synchronously,
// we add our block as a completion handler. // we add our block as a completion handler.
// //
// It will be deallocated by the objc runtime on success. // It will be copied when we add the handler, and then the
// copy will be deallocated by the objc runtime on success.
if (!sync) { if (!sync) {
self.buffer.msgSend( self.buffer.msgSend(
void, void,
objc.sel("addCompletedHandler:"), objc.sel("addCompletedHandler:"),
.{self.block.context}, .{&self.block},
); );
} }
self.buffer.msgSend(void, objc.sel("commit"), .{}); self.buffer.msgSend(void, objc.sel("commit"), .{});
// If we need to complete synchronously, we wait until // If we need to complete synchronously, we wait until
// the buffer is completed and call the callback directly, // the buffer is completed and invoke the block directly.
// deiniting the block after we're done.
if (sync) { if (sync) {
self.buffer.msgSend(void, "waitUntilCompleted", .{}); self.buffer.msgSend(void, "waitUntilCompleted", .{});
self.block.context.sync = true; self.block.sync = true;
bufferCompleted(self.block.context, self.buffer.value); CompletionBlock.invoke(&self.block, .{self.buffer.value});
self.block.deinit();
} }
} }

View File

@ -54,13 +54,11 @@ pub inline fn setSurface(self: *IOSurfaceLayer, surface: *IOSurface) !void {
// //
// We release in the callback after setting the contents. // We release in the callback after setting the contents.
surface.retain(); surface.retain();
// We also need to retain the layer itself to make sure it // NOTE: Since `self.layer` is passed as an `objc.c.id`, it's
// isn't destroyed before the callback completes, since if // automatically retained when the block is copied, so we
// that happens it will try to interact with a deallocated // don't need to retain it ourselves like with the surface.
// object.
_ = self.layer.retain();
var block = try SetSurfaceBlock.init(.{ var block = SetSurfaceBlock.init(.{
.layer = self.layer.value, .layer = self.layer.value,
.surface = surface, .surface = surface,
}, &setSurfaceCallback); }, &setSurfaceCallback);
@ -68,15 +66,15 @@ pub inline fn setSurface(self: *IOSurfaceLayer, surface: *IOSurface) !void {
// We check if we're on the main thread and run the block directly if so. // We check if we're on the main thread and run the block directly if so.
const NSThread = objc.getClass("NSThread").?; const NSThread = objc.getClass("NSThread").?;
if (NSThread.msgSend(bool, "isMainThread", .{})) { if (NSThread.msgSend(bool, "isMainThread", .{})) {
setSurfaceCallback(block.context); setSurfaceCallback(&block);
block.deinit();
} else { } else {
// NOTE: The block will automatically be deallocated by the objc // NOTE: The block will be copied when we pass it to dispatch_async,
// runtime once it's executed, so there's no need to deinit it. // and then automatically be deallocated by the objc runtime
// once it's executed.
macos.dispatch.dispatch_async( macos.dispatch.dispatch_async(
@ptrCast(macos.dispatch.queue.getMain()), @ptrCast(macos.dispatch.queue.getMain()),
@ptrCast(block.context), @ptrCast(&block),
); );
} }
} }
@ -100,10 +98,7 @@ fn setSurfaceCallback(
const surface: *IOSurface = block.surface; const surface: *IOSurface = block.surface;
// See explanation of why we retain and release in `setSurface`. // See explanation of why we retain and release in `setSurface`.
defer { defer surface.release();
surface.release();
layer.release();
}
// We check to see if the surface is the appropriate size for // We check to see if the surface is the appropriate size for
// the layer, if it's not then we discard it. This is because // the layer, if it's not then we discard it. This is because

View File

@ -68,7 +68,7 @@ pub fn init(opts: Options) !Self {
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
break :init id_init; break :init id_init;
}; };
errdefer desc.msgSend(void, objc.sel("release"), .{}); defer desc.release();
// Set our properties // Set our properties
desc.setProperty("width", @as(c_ulong, @intCast(opts.width))); desc.setProperty("width", @as(c_ulong, @intCast(opts.width)));

View File

@ -50,7 +50,7 @@ pub fn init(
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
break :init id_init; break :init id_init;
}; };
errdefer desc.msgSend(void, objc.sel("release"), .{}); defer desc.release();
// Set our properties // Set our properties
desc.setProperty("pixelFormat", @intFromEnum(opts.pixel_format)); desc.setProperty("pixelFormat", @intFromEnum(opts.pixel_format));