// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! How long starting up took, phase by phase, measured from when the //! process began: printed to standard error when `HARRIER_STARTUP_TIMES` is //! set, and nothing otherwise. What every change to the startup path is //! measured with. The time since the last mark is since the last on any //! thread: the fonts load on one of their own. const std = @import("std"); const builtin = @import("builtin"); const linux = std.os.linux; const win32 = if (builtin.os.tag == .windows) @import("win32").everything else struct {}; var enabled: bool = false; /// Marks come from more than one thread: the fonts load on their own. var last_ns: std.atomic.Value(u64) = .init(0); /// Turns the marks on, if the environment asks for them. pub fn init(env: *const std.process.Environ.Map) void { enabled = env.get("HARRIER_STARTUP_TIMES") != null; } /// Notes that `phase` is done: how long since the process started, and /// how long since the last mark. pub fn mark(phase: []const u8) void { if (!enabled) return; const now = sinceStart() orelse return; var buf: [128]u8 = undefined; const last = last_ns.swap(now, .acq_rel); const line = std.fmt.bufPrint(&buf, "startup: {d:>7.1} ms (+{d:>6.1}) {s}\n", .{ @as(f64, @floatFromInt(now)) / 1e6, @as(f64, @floatFromInt(now -| last)) / 1e6, phase, }) catch return; switch (builtin.os.tag) { .windows => { var written: u32 = 0; _ = win32.WriteFile(win32.GetStdHandle(win32.STD_ERROR_HANDLE), line.ptr, @intCast(line.len), &written, null); }, else => _ = linux.write(2, line.ptr, line.len), } } var once: std.StaticBitSet(8) = .empty; /// `mark`, the first time `which` comes up only. pub fn markOnce(comptime which: enum { first_draw, first_frame }, phase: []const u8) void { if (!enabled or once.isSet(@backingInt(which))) return; once.set(@backingInt(which)); mark(phase); } /// Nanoseconds since the process started, by the kernel's account. fn sinceStart() ?u64 { return switch (builtin.os.tag) { .windows => sinceStartWindows(), else => sinceStartLinux(), }; } /// The time now less the process's creation time, both in the 100 ns ticks /// Windows keeps time in. fn sinceStartWindows() ?u64 { var created: win32.FILETIME = undefined; var exited: win32.FILETIME = undefined; var kernel: win32.FILETIME = undefined; var user: win32.FILETIME = undefined; if (win32.GetProcessTimes(win32.GetCurrentProcess(), &created, &exited, &kernel, &user) == 0) return null; var now: win32.FILETIME = undefined; win32.GetSystemTimePreciseAsFileTime(&now); return (fileTicks(now) -| fileTicks(created)) * 100; } fn fileTicks(t: win32.FILETIME) u64 { return @as(u64, t.dwHighDateTime) << 32 | t.dwLowDateTime; } /// The clock now, less the process's start time from /proc/self/stat. fn sinceStartLinux() ?u64 { var ts: linux.timespec = undefined; if (linux.errno(linux.clock_gettime(.BOOTTIME, &ts)) != .SUCCESS) return null; const now: u64 = @as(u64, @intCast(ts.sec)) * std.time.ns_per_s + @as(u64, @intCast(ts.nsec)); const start = startTime() orelse return null; return now -| start; } var start_cache: ?u64 = null; fn startTime() ?u64 { if (start_cache) |s| return s; const fd = linux.open("/proc/self/stat", .{ .ACCMODE = .RDONLY, .CLOEXEC = true }, 0); if (linux.errno(fd) != .SUCCESS) return null; defer _ = linux.close(@intCast(fd)); var buf: [1024]u8 = undefined; const n = linux.read(@intCast(fd), &buf, buf.len); if (linux.errno(n) != .SUCCESS) return null; // The 22nd field, in clock ticks since boot; the command name, the 2nd, // is in parentheses and may hold spaces, so counting starts after it. const text = buf[0..n]; const close = std.mem.findScalarLast(u8, text, ')') orelse return null; var fields = std.mem.tokenizeScalar(u8, text[close + 1 ..], ' '); var i: usize = 3; while (fields.next()) |f| : (i += 1) { if (i == 22) { const ticks = std.fmt.parseInt(u64, f, 10) catch return null; // USER_HZ is 100 on every Linux harrier runs on. start_cache = ticks * (std.time.ns_per_s / 100); return start_cache; } } return null; }