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.
This commit is contained in:
Mitchell Hashimoto
2026-08-09 15:38:23 -07:00
parent fea378e565
commit a82637b53a
2 changed files with 68 additions and 37 deletions

View File

@@ -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

View File

@@ -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},