From a82637b53aa434fa5c8bc8360c58561d7d48a8e1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 9 Aug 2026 15:38:23 -0700 Subject: [PATCH] crash: resolve sentry directories on the init thread Sentry initialization already ran on a separate thread, but the cache and state directory resolution happened on the main thread before spawning it. On macOS the cache dir resolution calls NSFileManager URLForDirectory:inDomain:appropriateForURL:create:error: which takes multiple milliseconds and was the single largest cost in global.init. All directory resolution now happens on the init thread. before: 2967us-4018us after: 30us-70us (env map snapshot + thread spawn) global.init total drops from ~3.4-5.0ms to ~0.4-1.0ms. --- src/crash/sentry.zig | 97 +++++++++++++++++++++++++++++--------------- src/global.zig | 8 ++-- 2 files changed, 68 insertions(+), 37 deletions(-) diff --git a/src/crash/sentry.zig b/src/crash/sentry.zig index 8b6471f16..4181b5c1d 100644 --- a/src/crash/sentry.zig +++ b/src/crash/sentry.zig @@ -56,52 +56,55 @@ pub threadlocal var thread_state: ?ThreadState = null; /// crash reports and logs, but we only store them locally (see Transport). /// It is up to the user to grab the logs and manually send them to us /// (or to their own Sentry instance) if they want to. -pub fn init(gpa: Allocator, environ_map: *const std.process.Environ.Map) !void { - if (comptime !build_options.sentry) return; +pub fn init(gpa: Allocator, environ_map: std.process.Environ.Map) !void { + if (comptime !build_options.sentry) { + var map = environ_map; + map.deinit(); + return; + } // Not supported on Windows currently, doesn't build. - if (comptime builtin.os.tag == .windows) return; + if (comptime builtin.os.tag == .windows) { + var map = environ_map; + map.deinit(); + return; + } // Must only start once assert(init_thread == null); - // Get our directories. - var single_threaded: std.Io.Threaded = .init_single_threaded; - defer single_threaded.deinit(); - var fba: std.heap.FixedBufferAllocator = .init(&dir_mem); - - state_dir_ = state_dir: { - const dir = try crash.defaultDir(single_threaded.io(), gpa, environ_map); - defer gpa.free(dir.path); - break :state_dir try fba.allocator().dupe(u8, dir.path); - }; - errdefer state_dir_ = null; - - const cache_dir = cache_dir: { - const dir = try cacheDir(single_threaded.io(), gpa, environ_map); - defer gpa.free(dir); - break :cache_dir try fba.allocator().dupe(u8, dir); - }; - cache_dir_ = cache_dir; - errdefer cache_dir_ = null; - - // We use a thread for initializing Sentry because initialization takes - // ~2k ns on my M3 Max. That's not a LOT of time but it's enough to be - // 90% of our pre-App startup time. Everything Sentry is doing initially - // is safe to do on a separate thread and fast enough that its very - // likely to be done before a crash occurs. - const thr = try std.Thread.spawn( + // We use a thread for initializing Sentry because initialization is + // slow enough to matter for process startup: resolving our directories + // can take multiple milliseconds on macOS (Apple APIs) and Sentry's + // own init does disk I/O. Everything Sentry is doing initially is safe + // to do on a separate thread and fast enough that its very likely to + // be done before a crash occurs. + // + // The environ map is a snapshot owned by the thread (and freed there), + // so it is safe against concurrent mutations of the process environment + // (e.g. ensureLocale on the main thread). + const thr = std.Thread.spawn( .{}, initThread, - .{cache_dir}, - ); + .{ gpa, environ_map }, + ) catch |err| { + var map = environ_map; + map.deinit(); + return err; + }; + // Naming the thread from here only works on some platforms (e.g. + // Linux). On Darwin the thread names itself in initThread. + var single_threaded: std.Io.Threaded = .init_single_threaded; + defer single_threaded.deinit(); thr.setName(single_threaded.io(), "sentry-init") catch {}; + init_thread = thr; } -fn initThread(cache_dir: []const u8) !void { - if (comptime !build_options.sentry) return; +fn initThread(gpa: Allocator, environ_map_: std.process.Environ.Map) !void { + var environ_map = environ_map_; + defer environ_map.deinit(); // Right now, on Darwin, `std.Thread.setName` can only name the current // thread, and we have no way to get the current thread from within it, @@ -110,6 +113,34 @@ fn initThread(cache_dir: []const u8) !void { internal_os.macos.pthread_setname_np(&"sentry-init".*); } + // Get our directories. + var single_threaded: std.Io.Threaded = .init_single_threaded; + defer single_threaded.deinit(); + var fba: std.heap.FixedBufferAllocator = .init(&dir_mem); + + state_dir_ = state_dir: { + const dir = try crash.defaultDir( + single_threaded.io(), + gpa, + &environ_map, + ); + defer gpa.free(dir.path); + break :state_dir try fba.allocator().dupe(u8, dir.path); + }; + errdefer state_dir_ = null; + + const cache_dir = cache_dir: { + const dir = try cacheDir( + single_threaded.io(), + gpa, + &environ_map, + ); + defer gpa.free(dir); + break :cache_dir try fba.allocator().dupe(u8, dir); + }; + cache_dir_ = cache_dir; + errdefer cache_dir_ = null; + const transport = sentry.Transport.init(&Transport.send); // This will crash if the transport was never used so we avoid // that for now. This probably leaks some memory but it'd be very diff --git a/src/global.zig b/src/global.zig index 11aa17b3a..3312039a9 100644 --- a/src/global.zig +++ b/src/global.zig @@ -181,10 +181,10 @@ pub fn init(opts: InitOpts) !void { self.rlimits = .init(); if (build_options.sentry) { - // Initialize our crash reporting. - var environ_map = try self.environ.createMap(self.alloc); - defer environ_map.deinit(); - crash.init(self.alloc, &environ_map) catch |err| { + // Initialize our crash reporting. The environ map snapshot is + // owned by crash.init (it is freed by the init thread). + const environ_map = try self.environ.createMap(self.alloc); + crash.init(self.alloc, environ_map) catch |err| { std.log.warn( "sentry init failed, no crash capture available err={}", .{err},