f754bb6b
Add performance benchmarking instrumentation
a73x 2026-04-10 10:17
Commit message
.gitignore
| Old | New | ||
|---|---|---|---|
| @@ -3,3 +3,6 @@ zig-out/ | |||
| 3 | *.o | 3 | *.o |
| 4 | *.swp | 4 | *.swp |
| 5 | .worktrees/ | 5 | .worktrees/ |
| 6 | bench.log | ||
| 7 | perf.data | ||
| 8 | flamegraph.svg | ||
Makefile
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,36 @@ | |||
| 1 | ZIG ?= zig | ||
| 2 | FLAMEGRAPH ?= flamegraph.pl | ||
| 3 | STACKCOLLAPSE ?= stackcollapse-perf.pl | ||
| 4 | |||
| 5 | .PHONY: build run test bench profile clean | ||
| 6 | |||
| 7 | build: | ||
| 8 | $(ZIG) build | ||
| 9 | |||
| 10 | run: build | ||
| 11 | $(ZIG) build run | ||
| 12 | |||
| 13 | test: | ||
| 14 | $(ZIG) build test | ||
| 15 | |||
| 16 | zig-out/bin/waystty: $(wildcard src/*.zig) $(wildcard shaders/*) | ||
| 17 | $(ZIG) build | ||
| 18 | |||
| 19 | bench: zig-out/bin/waystty | ||
| 20 | WAYSTTY_BENCH=1 ./zig-out/bin/waystty 2>bench.log || true | ||
| 21 | @echo "--- frame timing ---" | ||
| 22 | @grep -A 12 "waystty frame timing" bench.log || echo "(no timing data found)" | ||
| 23 | |||
| 24 | profile: | ||
| 25 | $(ZIG) build -Doptimize=ReleaseSafe | ||
| 26 | perf record -g -F 999 --no-inherit -o perf.data -- \ | ||
| 27 | sh -c 'WAYSTTY_BENCH=1 ./zig-out/bin/waystty 2>bench.log' | ||
| 28 | perf script -i perf.data \ | ||
| 29 | | $(STACKCOLLAPSE) \ | ||
| 30 | | $(FLAMEGRAPH) > flamegraph.svg | ||
| 31 | @echo "--- frame timing ---" | ||
| 32 | @grep -A 12 "waystty frame timing" bench.log || echo "(no timing data found)" | ||
| 33 | xdg-open flamegraph.svg | ||
| 34 | |||
| 35 | clean: | ||
| 36 | rm -rf zig-out .zig-cache perf.data bench.log flamegraph.svg | ||
src/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -168,8 +168,18 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 168 | var atlas = try font.Atlas.init(alloc, 1024, 1024); | 168 | var atlas = try font.Atlas.init(alloc, 1024, 1024); |
| 169 | defer atlas.deinit(); | 169 | defer atlas.deinit(); |
| 170 | 170 | ||
| 171 | // Upload empty atlas first (so descriptor set is valid) | 171 | // Precompute printable ASCII glyphs (32-126) into atlas |
| 172 | for (32..127) |cp| { | ||
| 173 | _ = atlas.getOrInsert(&face, @intCast(cp)) catch |err| switch (err) { | ||
| 174 | error.AtlasFull => break, | ||
| 175 | else => return err, | ||
| 176 | }; | ||
| 177 | } | ||
| 178 | // Upload warm atlas (full upload — descriptor set needs valid data) | ||
| 172 | try ctx.uploadAtlas(atlas.pixels); | 179 | try ctx.uploadAtlas(atlas.pixels); |
| 180 | atlas.last_uploaded_y = atlas.cursor_y; | ||
| 181 | atlas.needs_full_upload = false; | ||
| 182 | atlas.dirty = false; | ||
| 173 | 183 | ||
| 174 | // === terminal === | 184 | // === terminal === |
| 175 | var term = try vt.Terminal.init(alloc, .{ | 185 | var term = try vt.Terminal.init(alloc, .{ |
| @@ -188,12 +198,25 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 188 | 198 | ||
| 189 | // === pty === | 199 | // === pty === |
| 190 | const shell: [:0]const u8 = blk: { | 200 | const shell: [:0]const u8 = blk: { |
| 201 | if (std.posix.getenv("WAYSTTY_BENCH") != null) { | ||
| 202 | break :blk try alloc.dupeZ(u8, "/bin/sh"); | ||
| 203 | } | ||
| 191 | const shell_env = std.posix.getenv("SHELL") orelse "/bin/sh"; | 204 | const shell_env = std.posix.getenv("SHELL") orelse "/bin/sh"; |
| 192 | break :blk try alloc.dupeZ(u8, shell_env); | 205 | break :blk try alloc.dupeZ(u8, shell_env); |
| 193 | }; | 206 | }; |
| 194 | defer alloc.free(shell); | 207 | defer alloc.free(shell); |
| 195 | 208 | ||
| 196 | var p = try pty.Pty.spawn(.{ .cols = cols, .rows = rows, .shell = shell }); | 209 | const bench_script: ?[:0]const u8 = if (std.posix.getenv("WAYSTTY_BENCH") != null) |
| 210 | "echo warmup; sleep 0.2; seq 1 50000; find /usr/lib -name '*.so' 2>/dev/null | head -500; yes 'hello world' | head -2000; exit 0" | ||
| 211 | else | ||
| 212 | null; | ||
| 213 | |||
| 214 | var p = try pty.Pty.spawn(.{ | ||
| 215 | .cols = cols, | ||
| 216 | .rows = rows, | ||
| 217 | .shell = shell, | ||
| 218 | .shell_args = if (bench_script) |script| &.{ "-c", script } else null, | ||
| 219 | }); | ||
| 197 | defer p.deinit(); | 220 | defer p.deinit(); |
| 198 | term.setWritePtyCallback(&p, &writePtyFromTerminal); | 221 | term.setWritePtyCallback(&p, &writePtyFromTerminal); |
| 199 | 222 | ||
| @@ -202,6 +225,10 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 202 | defer render_cache.deinit(alloc); | 225 | defer render_cache.deinit(alloc); |
| 203 | try render_cache.resizeRows(alloc, rows); | 226 | try render_cache.resizeRows(alloc, rows); |
| 204 | 227 | ||
| 228 | // === frame timing === | ||
| 229 | var frame_ring = FrameTimingRing{}; | ||
| 230 | installSigusr1Handler(); | ||
| 231 | |||
| 205 | // === main loop === | 232 | // === main loop === |
| 206 | const wl_fd = conn.display.getFd(); | 233 | const wl_fd = conn.display.getFd(); |
| 207 | var pollfds = [_]std.posix.pollfd{ | 234 | var pollfds = [_]std.posix.pollfd{ |
| @@ -354,10 +381,15 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 354 | 381 | ||
| 355 | if (!shouldRenderFrame(render_pending, false, false)) continue; | 382 | if (!shouldRenderFrame(render_pending, false, false)) continue; |
| 356 | 383 | ||
| 384 | var frame_timing: FrameTiming = .{}; | ||
| 385 | |||
| 357 | // === render === | 386 | // === render === |
| 358 | const previous_cursor = term.render_state.cursor; | 387 | const previous_cursor = term.render_state.cursor; |
| 388 | var section_timer = std.time.Timer.start() catch unreachable; | ||
| 359 | try term.snapshot(); | 389 | try term.snapshot(); |
| 390 | frame_timing.snapshot_us = usFromTimer(§ion_timer); | ||
| 360 | 391 | ||
| 392 | section_timer = std.time.Timer.start() catch unreachable; | ||
| 361 | const default_bg = term.backgroundColor(); | 393 | const default_bg = term.backgroundColor(); |
| 362 | const bg_uv = atlas.cursorUV(); | 394 | const bg_uv = atlas.cursorUV(); |
| 363 | 395 | ||
| @@ -449,13 +481,29 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 449 | cursor_rebuilt = true; | 481 | cursor_rebuilt = true; |
| 450 | } | 482 | } |
| 451 | 483 | ||
| 452 | // Re-upload atlas if new glyphs were added | 484 | frame_timing.row_rebuild_us = usFromTimer(§ion_timer); |
| 485 | |||
| 486 | section_timer = std.time.Timer.start() catch unreachable; | ||
| 487 | // Re-upload atlas if new glyphs were added (incremental) | ||
| 453 | if (atlas.dirty) { | 488 | if (atlas.dirty) { |
| 454 | try ctx.uploadAtlas(atlas.pixels); | 489 | const y_start = atlas.last_uploaded_y; |
| 490 | const y_end = atlas.cursor_y + atlas.row_height; | ||
| 491 | if (y_start < y_end) { | ||
| 492 | try ctx.uploadAtlasRegion( | ||
| 493 | atlas.pixels, | ||
| 494 | y_start, | ||
| 495 | y_end, | ||
| 496 | atlas.needs_full_upload, | ||
| 497 | ); | ||
| 498 | atlas.last_uploaded_y = atlas.cursor_y; | ||
| 499 | atlas.needs_full_upload = false; | ||
| 500 | render_cache.layout_dirty = true; | ||
| 501 | } | ||
| 455 | atlas.dirty = false; | 502 | atlas.dirty = false; |
| 456 | render_cache.layout_dirty = true; | ||
| 457 | } | 503 | } |
| 504 | frame_timing.atlas_upload_us = usFromTimer(§ion_timer); | ||
| 458 | 505 | ||
| 506 | section_timer = std.time.Timer.start() catch unreachable; | ||
| 459 | const upload_plan = applyRenderPlan(.{ | 507 | const upload_plan = applyRenderPlan(.{ |
| 460 | .layout_dirty = render_cache.layout_dirty, | 508 | .layout_dirty = render_cache.layout_dirty, |
| 461 | .rows_rebuilt = rows_rebuilt, | 509 | .rows_rebuilt = rows_rebuilt, |
| @@ -514,6 +562,9 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 514 | } | 562 | } |
| 515 | } | 563 | } |
| 516 | 564 | ||
| 565 | frame_timing.instance_upload_us = usFromTimer(§ion_timer); | ||
| 566 | |||
| 567 | section_timer = std.time.Timer.start() catch unreachable; | ||
| 517 | const baseline_coverage = renderer.coverageVariantParams(.baseline); | 568 | const baseline_coverage = renderer.coverageVariantParams(.baseline); |
| 518 | ctx.drawCells( | 569 | ctx.drawCells( |
| 519 | render_cache.total_instance_count, | 570 | render_cache.total_instance_count, |
| @@ -531,10 +582,22 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 531 | }, | 582 | }, |
| 532 | else => return err, | 583 | else => return err, |
| 533 | }; | 584 | }; |
| 585 | frame_timing.gpu_submit_us = usFromTimer(§ion_timer); | ||
| 586 | |||
| 587 | frame_ring.push(frame_timing); | ||
| 588 | |||
| 589 | // Check for SIGUSR1 stats dump request | ||
| 590 | if (sigusr1_received.swap(false, .acq_rel)) { | ||
| 591 | printFrameStats(computeFrameStats(&frame_ring)); | ||
| 592 | } | ||
| 593 | |||
| 534 | clearConsumedDirtyFlags(&term.render_state.dirty, dirty_rows, refresh_plan); | 594 | clearConsumedDirtyFlags(&term.render_state.dirty, dirty_rows, refresh_plan); |
| 535 | render_pending = false; | 595 | render_pending = false; |
| 536 | } | 596 | } |
| 537 | 597 | ||
| 598 | // Dump timing stats on exit | ||
| 599 | printFrameStats(computeFrameStats(&frame_ring)); | ||
| 600 | |||
| 538 | _ = try ctx.vkd.deviceWaitIdle(ctx.device); | 601 | _ = try ctx.vkd.deviceWaitIdle(ctx.device); |
| 539 | } | 602 | } |
| 540 | 603 | ||
| @@ -855,6 +918,152 @@ fn clampSelectionSpan(span: SelectionSpan, cols: u16, rows: u16) ?SelectionSpan | |||
| 855 | } else null; | 918 | } else null; |
| 856 | } | 919 | } |
| 857 | 920 | ||
| 921 | const FrameTiming = struct { | ||
| 922 | snapshot_us: u32 = 0, | ||
| 923 | row_rebuild_us: u32 = 0, | ||
| 924 | atlas_upload_us: u32 = 0, | ||
| 925 | instance_upload_us: u32 = 0, | ||
| 926 | gpu_submit_us: u32 = 0, | ||
| 927 | |||
| 928 | fn total(self: FrameTiming) u32 { | ||
| 929 | return self.snapshot_us + | ||
| 930 | self.row_rebuild_us + | ||
| 931 | self.atlas_upload_us + | ||
| 932 | self.instance_upload_us + | ||
| 933 | self.gpu_submit_us; | ||
| 934 | } | ||
| 935 | }; | ||
| 936 | |||
| 937 | const FrameTimingRing = struct { | ||
| 938 | const capacity = 256; | ||
| 939 | |||
| 940 | entries: [capacity]FrameTiming = [_]FrameTiming{.{}} ** capacity, | ||
| 941 | head: usize = 0, | ||
| 942 | count: usize = 0, | ||
| 943 | |||
| 944 | fn push(self: *FrameTimingRing, timing: FrameTiming) void { | ||
| 945 | const idx = if (self.count < capacity) self.count else self.head; | ||
| 946 | self.entries[idx] = timing; | ||
| 947 | if (self.count < capacity) { | ||
| 948 | self.count += 1; | ||
| 949 | } else { | ||
| 950 | self.head = (self.head + 1) % capacity; | ||
| 951 | } | ||
| 952 | } | ||
| 953 | |||
| 954 | /// Return a slice of valid entries in insertion order. | ||
| 955 | /// Caller must provide a scratch buffer of `capacity` entries. | ||
| 956 | fn orderedSlice(self: *const FrameTimingRing, buf: *[capacity]FrameTiming) []const FrameTiming { | ||
| 957 | if (self.count < capacity) { | ||
| 958 | return self.entries[0..self.count]; | ||
| 959 | } | ||
| 960 | // Ring has wrapped — copy from head..end then 0..head | ||
| 961 | const tail_len = capacity - self.head; | ||
| 962 | @memcpy(buf[0..tail_len], self.entries[self.head..capacity]); | ||
| 963 | @memcpy(buf[tail_len..capacity], self.entries[0..self.head]); | ||
| 964 | return buf[0..capacity]; | ||
| 965 | } | ||
| 966 | }; | ||
| 967 | |||
| 968 | const SectionStats = struct { | ||
| 969 | min: u32 = 0, | ||
| 970 | avg: u32 = 0, | ||
| 971 | p99: u32 = 0, | ||
| 972 | max: u32 = 0, | ||
| 973 | }; | ||
| 974 | |||
| 975 | const FrameTimingStats = struct { | ||
| 976 | snapshot: SectionStats = .{}, | ||
| 977 | row_rebuild: SectionStats = .{}, | ||
| 978 | atlas_upload: SectionStats = .{}, | ||
| 979 | instance_upload: SectionStats = .{}, | ||
| 980 | gpu_submit: SectionStats = .{}, | ||
| 981 | total: SectionStats = .{}, | ||
| 982 | frame_count: usize = 0, | ||
| 983 | }; | ||
| 984 | |||
| 985 | fn computeSectionStats(values: []u32) SectionStats { | ||
| 986 | if (values.len == 0) return .{}; | ||
| 987 | std.mem.sort(u32, values, {}, std.sort.asc(u32)); | ||
| 988 | var sum: u64 = 0; | ||
| 989 | for (values) |v| sum += v; | ||
| 990 | const p99_idx = if (values.len <= 1) 0 else ((values.len - 1) * 99) / 100; | ||
| 991 | return .{ | ||
| 992 | .min = values[0], | ||
| 993 | .avg = @intCast(sum / values.len), | ||
| 994 | .p99 = values[p99_idx], | ||
| 995 | .max = values[values.len - 1], | ||
| 996 | }; | ||
| 997 | } | ||
| 998 | |||
| 999 | fn computeFrameStats(ring: *const FrameTimingRing) FrameTimingStats { | ||
| 1000 | if (ring.count == 0) return .{}; | ||
| 1001 | |||
| 1002 | var ordered_buf: [FrameTimingRing.capacity]FrameTiming = undefined; | ||
| 1003 | const entries = ring.orderedSlice(&ordered_buf); | ||
| 1004 | const n = entries.len; | ||
| 1005 | |||
| 1006 | var snapshot_vals: [FrameTimingRing.capacity]u32 = undefined; | ||
| 1007 | var row_rebuild_vals: [FrameTimingRing.capacity]u32 = undefined; | ||
| 1008 | var atlas_upload_vals: [FrameTimingRing.capacity]u32 = undefined; | ||
| 1009 | var instance_upload_vals: [FrameTimingRing.capacity]u32 = undefined; | ||
| 1010 | var gpu_submit_vals: [FrameTimingRing.capacity]u32 = undefined; | ||
| 1011 | var total_vals: [FrameTimingRing.capacity]u32 = undefined; | ||
| 1012 | |||
| 1013 | for (entries, 0..) |e, i| { | ||
| 1014 | snapshot_vals[i] = e.snapshot_us; | ||
| 1015 | row_rebuild_vals[i] = e.row_rebuild_us; | ||
| 1016 | atlas_upload_vals[i] = e.atlas_upload_us; | ||
| 1017 | instance_upload_vals[i] = e.instance_upload_us; | ||
| 1018 | gpu_submit_vals[i] = e.gpu_submit_us; | ||
| 1019 | total_vals[i] = e.total(); | ||
| 1020 | } | ||
| 1021 | |||
| 1022 | return .{ | ||
| 1023 | .snapshot = computeSectionStats(snapshot_vals[0..n]), | ||
| 1024 | .row_rebuild = computeSectionStats(row_rebuild_vals[0..n]), | ||
| 1025 | .atlas_upload = computeSectionStats(atlas_upload_vals[0..n]), | ||
| 1026 | .instance_upload = computeSectionStats(instance_upload_vals[0..n]), | ||
| 1027 | .gpu_submit = computeSectionStats(gpu_submit_vals[0..n]), | ||
| 1028 | .total = computeSectionStats(total_vals[0..n]), | ||
| 1029 | .frame_count = n, | ||
| 1030 | }; | ||
| 1031 | } | ||
| 1032 | |||
| 1033 | fn printFrameStats(stats: FrameTimingStats) void { | ||
| 1034 | const row_fmt = "{s:<20}{d:>6}{d:>6}{d:>6}{d:>6}\n"; | ||
| 1035 | std.debug.print("\n=== waystty frame timing ({d} frames) ===\n", .{stats.frame_count}); | ||
| 1036 | std.debug.print("{s:<20}{s:>6}{s:>6}{s:>6}{s:>6} (us)\n", .{ "section", "min", "avg", "p99", "max" }); | ||
| 1037 | std.debug.print(row_fmt, .{ "snapshot", stats.snapshot.min, stats.snapshot.avg, stats.snapshot.p99, stats.snapshot.max }); | ||
| 1038 | std.debug.print(row_fmt, .{ "row_rebuild", stats.row_rebuild.min, stats.row_rebuild.avg, stats.row_rebuild.p99, stats.row_rebuild.max }); | ||
| 1039 | std.debug.print(row_fmt, .{ "atlas_upload", stats.atlas_upload.min, stats.atlas_upload.avg, stats.atlas_upload.p99, stats.atlas_upload.max }); | ||
| 1040 | std.debug.print(row_fmt, .{ "instance_upload", stats.instance_upload.min, stats.instance_upload.avg, stats.instance_upload.p99, stats.instance_upload.max }); | ||
| 1041 | std.debug.print(row_fmt, .{ "gpu_submit", stats.gpu_submit.min, stats.gpu_submit.avg, stats.gpu_submit.p99, stats.gpu_submit.max }); | ||
| 1042 | std.debug.print("----------------------------------------------------\n", .{}); | ||
| 1043 | std.debug.print(row_fmt, .{ "total", stats.total.min, stats.total.avg, stats.total.p99, stats.total.max }); | ||
| 1044 | } | ||
| 1045 | |||
| 1046 | var sigusr1_received: std.atomic.Value(bool) = std.atomic.Value(bool).init(false); | ||
| 1047 | |||
| 1048 | fn sigusr1Handler(_: c_int) callconv(.c) void { | ||
| 1049 | sigusr1_received.store(true, .release); | ||
| 1050 | } | ||
| 1051 | |||
| 1052 | fn installSigusr1Handler() void { | ||
| 1053 | const act = std.posix.Sigaction{ | ||
| 1054 | .handler = .{ .handler = sigusr1Handler }, | ||
| 1055 | .mask = std.posix.sigemptyset(), | ||
| 1056 | .flags = std.posix.SA.RESTART, | ||
| 1057 | }; | ||
| 1058 | std.posix.sigaction(std.posix.SIG.USR1, &act, null); | ||
| 1059 | } | ||
| 1060 | |||
| 1061 | fn usFromTimer(timer: *std.time.Timer) u32 { | ||
| 1062 | const ns = timer.read(); | ||
| 1063 | const us = ns / std.time.ns_per_us; | ||
| 1064 | return std.math.cast(u32, us) orelse std.math.maxInt(u32); | ||
| 1065 | } | ||
| 1066 | |||
| 858 | test "SelectionSpan.normalized orders endpoints in reading order" { | 1067 | test "SelectionSpan.normalized orders endpoints in reading order" { |
| 859 | const span = (SelectionSpan{ | 1068 | const span = (SelectionSpan{ |
| 860 | .start = .{ .col = 7, .row = 4 }, | 1069 | .start = .{ .col = 7, .row = 4 }, |
| @@ -2914,6 +3123,82 @@ test "buildTextCoverageCompareScene repeats the same specimen in four panels" { | |||
| 2914 | ); | 3123 | ); |
| 2915 | } | 3124 | } |
| 2916 | 3125 | ||
| 3126 | test "FrameTiming.total sums all sections" { | ||
| 3127 | const ft: FrameTiming = .{ | ||
| 3128 | .snapshot_us = 10, | ||
| 3129 | .row_rebuild_us = 20, | ||
| 3130 | .atlas_upload_us = 30, | ||
| 3131 | .instance_upload_us = 40, | ||
| 3132 | .gpu_submit_us = 50, | ||
| 3133 | }; | ||
| 3134 | try std.testing.expectEqual(@as(u32, 150), ft.total()); | ||
| 3135 | } | ||
| 3136 | |||
| 3137 | test "FrameTimingRing records and wraps correctly" { | ||
| 3138 | var ring = FrameTimingRing{}; | ||
| 3139 | try std.testing.expectEqual(@as(usize, 0), ring.count); | ||
| 3140 | |||
| 3141 | ring.push(.{ .snapshot_us = 1, .row_rebuild_us = 2, .atlas_upload_us = 3, .instance_upload_us = 4, .gpu_submit_us = 5 }); | ||
| 3142 | try std.testing.expectEqual(@as(usize, 1), ring.count); | ||
| 3143 | try std.testing.expectEqual(@as(u32, 1), ring.entries[0].snapshot_us); | ||
| 3144 | |||
| 3145 | // Fill to capacity | ||
| 3146 | for (1..FrameTimingRing.capacity) |i| { | ||
| 3147 | ring.push(.{ .snapshot_us = @intCast(i + 1), .row_rebuild_us = 0, .atlas_upload_us = 0, .instance_upload_us = 0, .gpu_submit_us = 0 }); | ||
| 3148 | } | ||
| 3149 | try std.testing.expectEqual(FrameTimingRing.capacity, ring.count); | ||
| 3150 | |||
| 3151 | // One more wraps around — overwrites entries[0], head advances to 1 | ||
| 3152 | ring.push(.{ .snapshot_us = 999, .row_rebuild_us = 0, .atlas_upload_us = 0, .instance_upload_us = 0, .gpu_submit_us = 0 }); | ||
| 3153 | try std.testing.expectEqual(FrameTimingRing.capacity, ring.count); | ||
| 3154 | // Newest entry is at (head + capacity - 1) % capacity = 0 | ||
| 3155 | try std.testing.expectEqual(@as(u32, 999), ring.entries[0].snapshot_us); | ||
| 3156 | // head has advanced past the overwritten slot | ||
| 3157 | try std.testing.expectEqual(@as(usize, 1), ring.head); | ||
| 3158 | } | ||
| 3159 | |||
| 3160 | test "FrameTimingRing.orderedSlice returns entries in insertion order after wrap" { | ||
| 3161 | var ring = FrameTimingRing{}; | ||
| 3162 | // Push capacity + 3 entries so the ring wraps | ||
| 3163 | for (0..FrameTimingRing.capacity + 3) |i| { | ||
| 3164 | ring.push(.{ .snapshot_us = @intCast(i), .row_rebuild_us = 0, .atlas_upload_us = 0, .instance_upload_us = 0, .gpu_submit_us = 0 }); | ||
| 3165 | } | ||
| 3166 | var buf: [FrameTimingRing.capacity]FrameTiming = undefined; | ||
| 3167 | const ordered = ring.orderedSlice(&buf); | ||
| 3168 | try std.testing.expectEqual(FrameTimingRing.capacity, ordered.len); | ||
| 3169 | // First entry should be the 4th pushed (index 3), last should be capacity+2 | ||
| 3170 | try std.testing.expectEqual(@as(u32, 3), ordered[0].snapshot_us); | ||
| 3171 | try std.testing.expectEqual(@as(u32, FrameTimingRing.capacity + 2), ordered[ordered.len - 1].snapshot_us); | ||
| 3172 | } | ||
| 3173 | |||
| 3174 | test "FrameTimingStats computes min/avg/p99/max correctly" { | ||
| 3175 | var ring = FrameTimingRing{}; | ||
| 3176 | // Push 100 frames with snapshot_us = 1..100 | ||
| 3177 | for (0..100) |i| { | ||
| 3178 | ring.push(.{ | ||
| 3179 | .snapshot_us = @intCast(i + 1), | ||
| 3180 | .row_rebuild_us = 0, | ||
| 3181 | .atlas_upload_us = 0, | ||
| 3182 | .instance_upload_us = 0, | ||
| 3183 | .gpu_submit_us = 0, | ||
| 3184 | }); | ||
| 3185 | } | ||
| 3186 | const stats = computeFrameStats(&ring); | ||
| 3187 | try std.testing.expectEqual(@as(u32, 1), stats.snapshot.min); | ||
| 3188 | try std.testing.expectEqual(@as(u32, 100), stats.snapshot.max); | ||
| 3189 | try std.testing.expectEqual(@as(u32, 50), stats.snapshot.avg); | ||
| 3190 | // p99 of 1..100 = value at index 98 (0-based) = 99 | ||
| 3191 | try std.testing.expectEqual(@as(u32, 99), stats.snapshot.p99); | ||
| 3192 | try std.testing.expectEqual(@as(usize, 100), stats.frame_count); | ||
| 3193 | } | ||
| 3194 | |||
| 3195 | test "FrameTimingStats handles empty ring" { | ||
| 3196 | var ring = FrameTimingRing{}; | ||
| 3197 | const stats = computeFrameStats(&ring); | ||
| 3198 | try std.testing.expectEqual(@as(usize, 0), stats.frame_count); | ||
| 3199 | try std.testing.expectEqual(@as(u32, 0), stats.snapshot.min); | ||
| 3200 | } | ||
| 3201 | |||
| 2917 | fn runRenderSmokeTest(alloc: std.mem.Allocator) !void { | 3202 | fn runRenderSmokeTest(alloc: std.mem.Allocator) !void { |
| 2918 | const conn = try wayland_client.Connection.init(alloc); | 3203 | const conn = try wayland_client.Connection.init(alloc); |
| 2919 | defer conn.deinit(); | 3204 | defer conn.deinit(); |
src/pty.zig
| Old | New | ||
|---|---|---|---|
| @@ -19,6 +19,7 @@ pub const Pty = struct { | |||
| 19 | cols: u16, | 19 | cols: u16, |
| 20 | rows: u16, | 20 | rows: u16, |
| 21 | shell: [:0]const u8, | 21 | shell: [:0]const u8, |
| 22 | shell_args: ?[]const [:0]const u8 = null, | ||
| 22 | }; | 23 | }; |
| 23 | 24 | ||
| 24 | pub fn spawn(opts: SpawnOptions) !Pty { | 25 | pub fn spawn(opts: SpawnOptions) !Pty { |
| @@ -37,8 +38,18 @@ pub const Pty = struct { | |||
| 37 | // Child process | 38 | // Child process |
| 38 | _ = c.setenv("TERM", "xterm-256color", 1); | 39 | _ = c.setenv("TERM", "xterm-256color", 1); |
| 39 | 40 | ||
| 40 | var argv = [_:null]?[*:0]const u8{ opts.shell.ptr, null }; | 41 | if (opts.shell_args) |args| { |
| 41 | std.posix.execveZ(opts.shell.ptr, &argv, std.c.environ) catch {}; | 42 | std.debug.assert(args.len < 15); // argv[0] = shell, must fit in 16-slot buffer |
| 43 | var argv_buf: [16:null]?[*:0]const u8 = .{null} ** 16; | ||
| 44 | argv_buf[0] = opts.shell.ptr; | ||
| 45 | for (args, 1..) |arg, i| { | ||
| 46 | argv_buf[i] = arg.ptr; | ||
| 47 | } | ||
| 48 | std.posix.execveZ(opts.shell.ptr, &argv_buf, std.c.environ) catch {}; | ||
| 49 | } else { | ||
| 50 | var argv = [_:null]?[*:0]const u8{ opts.shell.ptr, null }; | ||
| 51 | std.posix.execveZ(opts.shell.ptr, &argv, std.c.environ) catch {}; | ||
| 52 | } | ||
| 42 | std.process.exit(1); | 53 | std.process.exit(1); |
| 43 | } | 54 | } |
| 44 | 55 | ||