From 20b39924ceabc13c1c72fbeccdcf7f3d490228db Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 21 Sep 2026 23:05:00 -0300 Subject: Measure startup, and find there is nothing in it worth optimizing A --startup case in the perf harness times Pardes.init and the first frame for each boot layout, with the same --json and --base shape as the others, so these are a baseline rather than a one-off reading. The core boots and paints in 0.22 ms (tty) to 0.30 ms (classic): about one percent of what starting the editor costs, and nothing to win. The other 99% is one thing, found with perf record rather than guessed at. Zig's start.main allocates through the Debug-mode DebugAllocator, which captures a six-frame stack trace per allocation, and the first capture parses and sorts the DWARF unwind tables of an 800 MB binary -- 22% of all samples sit in mem.swap under that pdq sort. It is not the dynamic loader (25 us), not static initializers (the binary has no .init_array), not paging (435 page faults), and not lockStderr. std/start.zig:694 hardcodes DebugAllocator(.{}), so there is no knob short of the build mode. Controls that pin it down: a bare std.process.Init hello-world starts in 3 ms, the ReleaseFast pardes-perf binary in 4 ms, /bin/true in 1 ms, and a Debug pardes in 22 ms. The gui binary's 107 ms first run was cold page cache; warm it matches the tty one. So the conclusion recorded in features.txt is to leave it alone. Twenty-two milliseconds is imperceptible for an editor, and the only lever is switching the default build mode, which would multiply an already ten-minute build to save eighteen milliseconds of startup. Co-Authored-By: Claude Opus 5 (1M context) --- test/perf.zig | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) (limited to 'test') diff --git a/test/perf.zig b/test/perf.zig index 5aa5c4d4..8f463204 100644 --- a/test/perf.zig +++ b/test/perf.zig @@ -151,6 +151,86 @@ const Allocations = struct { } }; +/// What a session costs before it can show anything: the core's own boot, then +/// the first frame built from it. The host's share — process spawn, dynamic +/// linking, GPU and font setup — is not in here and has to be timed around the +/// real binary; this is the half that a change to the editor can move. +fn measureStartup(io: std.Io, reps: usize, json: bool) !void { + if (reps == 0 or screen_cols == 0 or screen_rows == 0) return error.InvalidMeasurement; + const init_samples = try gpa.alloc(u64, reps); + defer gpa.free(init_samples); + const frame_samples = try gpa.alloc(u64, reps); + defer gpa.free(frame_samples); + + const cases = [_]struct { name: []const u8, boot: pardes.Boot }{ + .{ .name = "startup-tty", .boot = .tty }, + .{ .name = "startup-tty-shell", .boot = .tty_shell }, + .{ .name = "startup-shell", .boot = .shell }, + .{ .name = "startup-classic", .boot = .classic }, + }; + for (cases) |case| { + if (!pardes.terminal_panes) continue; + var cold_ns: u64 = 0; + var calls: usize = 0; + var bytes: usize = 0; + var peak: usize = 0; + for (0..reps + 1) |index| { + var counter: Allocations = .{}; + const allocator = counter.allocator(); + const begin = nowNs(); + const core = try pardes.Pardes.init(allocator, .{ + .tty_only = case.boot == .tty or case.boot == .tty_shell, + .boot = case.boot, + .shells = if (case.boot == .classic) 3 else 1, + .cols = screen_cols, + .rows = screen_rows, + }); + const booted = nowNs(); + var paint = std.heap.ArenaAllocator.init(allocator); + _ = try core.render(paint.allocator()); + const painted = nowNs(); + paint.deinit(); + core.deinit(); + if (index == 0) { + cold_ns = booted - begin; + } else { + init_samples[index - 1] = booted - begin; + frame_samples[index - 1] = painted - booted; + calls += counter.calls; + bytes += counter.bytes; + peak = @max(peak, counter.peak); + } + } + std.mem.sort(u64, init_samples, {}, std.sort.asc(u64)); + std.mem.sort(u64, frame_samples, {}, std.sort.asc(u64)); + const result = .{ + .build = build_identity, + .harness = harness_id, + .case = case.name, + .cols = screen_cols, + .rows = screen_rows, + .reps = reps, + .cold_ns = cold_ns, + .init_median_ns = init_samples[reps / 2], + .init_p95_ns = init_samples[@min(reps - 1, reps * 95 / 100)], + .frame_median_ns = frame_samples[reps / 2], + .allocations = calls / reps, + .allocated_bytes = bytes / reps, + .peak_bytes = peak, + }; + const text = if (json) + try std.json.Stringify.valueAlloc(gpa, result, .{}) + else + try std.fmt.allocPrint(gpa, "{s}: boot {d} ns (cold {d}), first frame {d} ns, {d} allocations / {d} B, peak {d} B", .{ + result.case, result.init_median_ns, result.cold_ns, result.frame_median_ns, + result.allocations, result.allocated_bytes, result.peak_bytes, + }); + defer gpa.free(text); + try std.Io.File.stdout().writeStreamingAll(io, text); + try std.Io.File.stdout().writeStreamingAll(io, "\n"); + } +} + fn measureCreation(io: std.Io, reps: usize, json: bool) !void { if (reps == 0 or screen_cols == 0 or screen_rows == 0) return error.InvalidMeasurement; const samples = try gpa.alloc(u64, reps); @@ -232,6 +312,7 @@ const Options = struct { base: ?[]const u8 = null, only: ?[]const u8 = null, creation: bool = false, + startup: bool = false, mini: bool = false, terminal_mib: ?usize = null, self_test: bool = false, @@ -242,11 +323,12 @@ const Options = struct { while (i < args.len) : (i += 1) { const arg = args[i]; if (!std.mem.startsWith(u8, arg, "--")) return error.UnknownOption; - const option = std.meta.stringToEnum(enum { json, creation, mini, @"terminal-mib", @"self-test", reps, cols, rows, base, only }, arg[2..]) orelse + const option = std.meta.stringToEnum(enum { json, creation, startup, mini, @"terminal-mib", @"self-test", reps, cols, rows, base, only }, arg[2..]) orelse return error.UnknownOption; switch (option) { .json => opts.json = true, .creation => opts.creation = true, + .startup => opts.startup = true, .mini => opts.mini = true, .@"self-test" => opts.self_test = true, else => { @@ -295,6 +377,7 @@ pub fn main(init: std.process.Init) !void { screen_rows = opts.rows; const reps = opts.reps; const only = opts.only; + if (opts.startup) return measureStartup(init.io, reps, opts.json); if (opts.creation) return measureCreation(init.io, reps, opts.json); if (opts.terminal_mib) |mib| return measureTerminalSession(init.io, mib, reps, opts.json); if (opts.mini) return measureMini(init.io, init.gpa, reps, opts.json); -- cgit v1.3