summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 14:25:52 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit1d0f8cd158bb47941c0187d32422787c92e8feeb (patch)
treeb7f6806cc431c5e2339cdd4887405275a6f2c635
parent69c5e62fe45a3768edff14993789f2aadfafadec (diff)
downloadpardes-1d0f8cd158bb47941c0187d32422787c92e8feeb.tar.gz
pardes-1d0f8cd158bb47941c0187d32422787c92e8feeb.zip
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 <[email protected]>
-rw-r--r--src/detached/server.zig10
-rw-r--r--src/host_io.zig4
-rw-r--r--src/pardes.zig30
3 files changed, 44 insertions, 0 deletions
diff --git a/src/detached/server.zig b/src/detached/server.zig
index d8379793..001acc47 100644
--- a/src/detached/server.zig
+++ b/src/detached/server.zig
@@ -376,6 +376,7 @@ pub const Session = struct {
.now = clockNow,
.present = present,
.poll_frame = pollFrame,
+ .watched = watched,
.spawn = spawn,
.pty_write = ptyWrite,
.pty_resize = ptyResize,
@@ -641,6 +642,12 @@ pub const Session = struct {
s.mailbox.signal();
}
+ fn watched(ctx: ?*anyopaque) bool {
+ const s = of(ctx);
+ for (&s.clients) |*c| if (c.attached) return true;
+ return false;
+ }
+
fn pollFrame(ctx: ?*anyopaque) void {
const s = of(ctx);
if (s.ninep) |l| s.ninep_pending = l.tick().pending;
@@ -1052,6 +1059,9 @@ pub const Session = struct {
c.attached = true;
c.need_full = true;
c.greet = true;
+ // Unwatched until now, it may have drawn nothing: a frame
+ // for the one attaching (watched).
+ s.core.needs_frame = true;
},
.bye => s.close(c, .bye),
.event => |ev| {
diff --git a/src/host_io.zig b/src/host_io.zig
index ac25e63c..c8c537b9 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -19,6 +19,10 @@ pub const Host = struct {
present: ?*const fn (ctx: ?*anyopaque, surface: *const pardes.Surface) void = null,
post_present: ?*const fn (ctx: ?*anyopaque) void = null,
poll_frame: ?*const fn (ctx: ?*anyopaque) void = null,
+ /// Whether anyone sees a frame drawn now. A detached session with no
+ /// client attached draws none (a /screen read draws its own), and
+ /// the frame owed is drawn when one attaches. Absent: always.
+ watched: ?*const fn (ctx: ?*anyopaque) bool = null,
detach: ?*const fn (ctx: ?*anyopaque) void = null,
spawn: ?*const fn (ctx: ?*anyopaque, pane: u8, cwd: []const u8) void = null,
pty_write: ?*const fn (ctx: ?*anyopaque, pane: u8, bytes: []const u8) void = null,
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);