mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
(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:
@ -26,8 +26,8 @@
|
||||
},
|
||||
.zig_objc = .{
|
||||
// mitchellh/zig-objc
|
||||
.url = "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz",
|
||||
.hash = "zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt",
|
||||
.url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
|
||||
.hash = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk",
|
||||
.lazy = true,
|
||||
},
|
||||
.zig_js = .{
|
||||
|
6
build.zig.zon.json
generated
6
build.zig.zon.json
generated
@ -144,10 +144,10 @@
|
||||
"url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz",
|
||||
"hash": "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0="
|
||||
},
|
||||
"zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt": {
|
||||
"zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk": {
|
||||
"name": "zig_objc",
|
||||
"url": "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz",
|
||||
"hash": "sha256-zn1tR6xhSmDla4UJ3t+Gni4Ni3R8deSK3tEe7DGzNXw="
|
||||
"url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
|
||||
"hash": "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw="
|
||||
},
|
||||
"wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy": {
|
||||
"name": "zig_wayland",
|
||||
|
6
build.zig.zon.nix
generated
6
build.zig.zon.nix
generated
@ -314,11 +314,11 @@ in
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt";
|
||||
name = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk";
|
||||
path = fetchZigArtifact {
|
||||
name = "zig_objc";
|
||||
url = "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz";
|
||||
hash = "sha256-zn1tR6xhSmDla4UJ3t+Gni4Ni3R8deSK3tEe7DGzNXw=";
|
||||
url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz";
|
||||
hash = "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
2
build.zig.zon.txt
generated
2
build.zig.zon.txt
generated
@ -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/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.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/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz
|
||||
|
@ -175,9 +175,9 @@
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz",
|
||||
"dest": "vendor/p/zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt",
|
||||
"sha256": "ce7d6d47ac614a60e56b8509dedf869e2e0d8b747c75e48aded11eec31b3357c"
|
||||
"url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
|
||||
"dest": "vendor/p/zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk",
|
||||
"sha256": "a37be5eea7e44a2d1b2976ba256b85f76a8c1063fc01ffec85c8a9e67468e4dc"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
@ -831,6 +831,9 @@ pub const CoreText = struct {
|
||||
i: usize,
|
||||
|
||||
pub fn deinit(self: *DiscoverIterator) void {
|
||||
for (self.list) |desc| {
|
||||
desc.release();
|
||||
}
|
||||
self.alloc.free(self.list);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
@ -109,7 +109,8 @@ pub const Shaper = struct {
|
||||
/// settings the font features of a CoreText font.
|
||||
fn makeFeaturesDict(feats: []const Feature) !*macos.foundation.Dictionary {
|
||||
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| {
|
||||
const value_num: c_int = @intCast(feat.value);
|
||||
|
@ -28,7 +28,7 @@ pub const Options = struct {
|
||||
/// MTLCommandBuffer
|
||||
buffer: objc.Object,
|
||||
|
||||
block: CompletionBlock,
|
||||
block: CompletionBlock.Context,
|
||||
|
||||
/// Begin encoding a frame.
|
||||
pub fn begin(
|
||||
@ -47,7 +47,7 @@ pub fn begin(
|
||||
|
||||
// Create our block to register for completion updates.
|
||||
// The block is deallocated by the objC runtime on success.
|
||||
const block = try CompletionBlock.init(
|
||||
const block = CompletionBlock.init(
|
||||
.{
|
||||
.renderer = renderer,
|
||||
.target = target,
|
||||
@ -55,7 +55,6 @@ pub fn begin(
|
||||
},
|
||||
&bufferCompleted,
|
||||
);
|
||||
errdefer block.deinit();
|
||||
|
||||
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,
|
||||
// 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) {
|
||||
self.buffer.msgSend(
|
||||
void,
|
||||
objc.sel("addCompletedHandler:"),
|
||||
.{self.block.context},
|
||||
.{&self.block},
|
||||
);
|
||||
}
|
||||
|
||||
self.buffer.msgSend(void, objc.sel("commit"), .{});
|
||||
|
||||
// If we need to complete synchronously, we wait until
|
||||
// the buffer is completed and call the callback directly,
|
||||
// deiniting the block after we're done.
|
||||
// the buffer is completed and invoke the block directly.
|
||||
if (sync) {
|
||||
self.buffer.msgSend(void, "waitUntilCompleted", .{});
|
||||
self.block.context.sync = true;
|
||||
bufferCompleted(self.block.context, self.buffer.value);
|
||||
self.block.deinit();
|
||||
self.block.sync = true;
|
||||
CompletionBlock.invoke(&self.block, .{self.buffer.value});
|
||||
}
|
||||
}
|
||||
|
@ -54,13 +54,11 @@ pub inline fn setSurface(self: *IOSurfaceLayer, surface: *IOSurface) !void {
|
||||
//
|
||||
// We release in the callback after setting the contents.
|
||||
surface.retain();
|
||||
// We also need to retain the layer itself to make sure it
|
||||
// isn't destroyed before the callback completes, since if
|
||||
// that happens it will try to interact with a deallocated
|
||||
// object.
|
||||
_ = self.layer.retain();
|
||||
// NOTE: Since `self.layer` is passed as an `objc.c.id`, it's
|
||||
// automatically retained when the block is copied, so we
|
||||
// don't need to retain it ourselves like with the surface.
|
||||
|
||||
var block = try SetSurfaceBlock.init(.{
|
||||
var block = SetSurfaceBlock.init(.{
|
||||
.layer = self.layer.value,
|
||||
.surface = surface,
|
||||
}, &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.
|
||||
const NSThread = objc.getClass("NSThread").?;
|
||||
if (NSThread.msgSend(bool, "isMainThread", .{})) {
|
||||
setSurfaceCallback(block.context);
|
||||
block.deinit();
|
||||
setSurfaceCallback(&block);
|
||||
} else {
|
||||
// NOTE: The block will automatically be deallocated by the objc
|
||||
// runtime once it's executed, so there's no need to deinit it.
|
||||
// NOTE: The block will be copied when we pass it to dispatch_async,
|
||||
// and then automatically be deallocated by the objc runtime
|
||||
// once it's executed.
|
||||
|
||||
macos.dispatch.dispatch_async(
|
||||
@ptrCast(macos.dispatch.queue.getMain()),
|
||||
@ptrCast(block.context),
|
||||
@ptrCast(&block),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -100,10 +98,7 @@ fn setSurfaceCallback(
|
||||
const surface: *IOSurface = block.surface;
|
||||
|
||||
// See explanation of why we retain and release in `setSurface`.
|
||||
defer {
|
||||
surface.release();
|
||||
layer.release();
|
||||
}
|
||||
defer surface.release();
|
||||
|
||||
// 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
|
||||
|
@ -68,7 +68,7 @@ pub fn init(opts: Options) !Self {
|
||||
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
|
||||
break :init id_init;
|
||||
};
|
||||
errdefer desc.msgSend(void, objc.sel("release"), .{});
|
||||
defer desc.release();
|
||||
|
||||
// Set our properties
|
||||
desc.setProperty("width", @as(c_ulong, @intCast(opts.width)));
|
||||
|
@ -50,7 +50,7 @@ pub fn init(
|
||||
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
|
||||
break :init id_init;
|
||||
};
|
||||
errdefer desc.msgSend(void, objc.sel("release"), .{});
|
||||
defer desc.release();
|
||||
|
||||
// Set our properties
|
||||
desc.setProperty("pixelFormat", @intFromEnum(opts.pixel_format));
|
||||
|
Reference in New Issue
Block a user