From 1d0f8cd158bb47941c0187d32422787c92e8feeb Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 14:25:52 -0300 Subject: A detached session no client watches draws no frames: yes | head -c 50M through pty/run goes from over 30 s to 11 s Profiling the 50 MB run showed 70% of its time in draw.render, recolorAnsi alone 43%, walking cells through PageList.pin. The detached session drew a frame after every pty read, with no client attached to see it. A host now says whether anyone watches (Host.VTable.watched). The detached server answers whether a client is attached, and while none is the core keeps the frame owed instead of drawing it. A /screen read draws its own, and a client attaching gets one (hello sets needs_frame). Debug, 80x24: 10 MB went from 7.36 s to 2.10 s, and 50 MB from over the 30 s client timeout (about 37 s by rate) to 11.08 s. What is left is the terminal's own parsing and scrollback growth. A session a frontend shows still draws after every read. Pacing those frames is the next step if that case matters. Co-Authored-By: Claude Opus 5.5 --- src/pardes.zig | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'src/pardes.zig') diff --git a/src/pardes.zig b/src/pardes.zig index 841faf7a..b16d39b7 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1112,6 +1112,32 @@ test "first document fallback splits like Tty from the originating pane" { }; } +test "a frame no one watches is not drawn, and is drawn once someone does" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const Watch = struct { + var presents: usize = 0; + var seen = false; + const vtable: Host.VTable = .{ .present = present, .watched = watched }; + fn present(_: ?*anyopaque, _: *const Surface) void { + presents += 1; + } + fn watched(_: ?*anyopaque) bool { + return seen; + } + }; + Watch.presents = 0; + Watch.seen = false; + p.needs_frame = true; + try p.pump(.{ .vtable = &Watch.vtable }); + try std.testing.expectEqual(@as(usize, 0), Watch.presents); + try std.testing.expect(p.needs_frame); // still owed + Watch.seen = true; + try p.pump(.{ .vtable = &Watch.vtable }); + try std.testing.expectEqual(@as(usize, 1), Watch.presents); +} + test "Lspwhy after a Hover asks to explain that hover, in its file at its place, run from any pane" { if (!lsp.supports.contains(.hover)) return error.SkipZigTest; const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); @@ -5625,6 +5651,10 @@ pub const Pardes = struct { // A shell with a clock draws what `advance` stepped; one without // steps its own `.tick`s and draws while anything animates. if (!p.needs_frame and !p.continuous() and (v.now != null or !p.animationActive())) return; + // No one to see it: the frame stays owed (needs_frame), not drawn. + // A pty streaming into an unwatched session spent most of its time + // drawing what nobody looked at. + if (v.watched) |f| if (!f(h.ctx)) return; _ = p.frame_arena.reset(.retain_capacity); const surface = try p.render(p.frame_arena.allocator()); if (v.present) |f| f(h.ctx, surface); -- cgit v1.3