diff options
Diffstat (limited to 'src/tty/tty.zig')
| -rw-r--r-- | src/tty/tty.zig | 149 |
1 files changed, 107 insertions, 42 deletions
diff --git a/src/tty/tty.zig b/src/tty/tty.zig index 14417cd0..822c2d7f 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -76,7 +76,8 @@ pub const Command = struct { pub var value: union(enum) { nop, quit, - tick, + /// A timed wait ran out: the core's next wake has come. + wake, key_press: vaxis.Key, pty_read: struct { id: usize, gen: u32, bytes: []u8 }, pty_eof: struct { id: usize, gen: u32 }, @@ -737,6 +738,7 @@ fn localSession( .paste_buf = paste_buf, .inotify_fd = file_watch.init(false), .fs = fs, + .test_clock = host_io.testClock(), }; pardes.lsp.setStatusSink(&sh, lspStatusSink); host_io.setExitWake(loop, wakeForExit); @@ -786,7 +788,7 @@ fn localSession( try startInput(loop, input_cache); defer if (attached.* == null) stopInput(loop); (try std.Thread.spawn(.{}, winchWatch, .{ loop, vx, tty })).detach(); - (try std.Thread.spawn(.{}, tickWatch, .{ io, loop })).detach(); + (try std.Thread.spawn(.{}, wakeWatch, .{ io, loop })).detach(); if (fs) |f| try f.watch(loop, wakeFs); try vx.queryTerminalSend(tty.writer()); @@ -808,7 +810,6 @@ fn localSession( frames: while (!core.quit) { pardes.turn.restoreSettled(); try core.pump(host); - sh.takeExits(); if (core.takeRestore()) |rp| blk: { const bytes = filesystem.readRestore(gpa, rp, core.settings.dump_dir.get()) catch |err| { core.reportError(core.active, "Restore", err); @@ -912,7 +913,11 @@ const Shell = struct { caps_pending: bool = true, check_files: bool = false, in_paste: bool = false, - tracks: []const pardes.layout.Track = &.{}, + tracks: []const pardes.animation.Track = &.{}, + /// PARDES_TEST_CLOCK's virtual time (host_io.testClock), and where the + /// running-out wait moves it: the core's next wake. + test_clock: ?u64 = null, + wake_to: u64 = 0, fn of(ctx: ?*anyopaque) *Shell { return @ptrCast(@alignCast(ctx.?)); @@ -924,12 +929,14 @@ const Shell = struct { const isolated_vtable: host_io.Host.VTable = .{ .wait_input = waitInput, + .now = clockNow, .present = present, .post_present = postPresent, }; const vtable: host_io.Host.VTable = .{ .wait_input = waitInput, + .now = clockNow, .present = present, .post_present = postPresent, .poll_frame = pollFrame, @@ -952,12 +959,20 @@ const Shell = struct { .pipe = pipe, }; + fn clockNow(ctx: ?*anyopaque) u64 { + const s = of(ctx); + return s.test_clock orelse host_io.monotonicNs(); + } + fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void { const s = of(ctx); var batch: usize = 0; - // The wait is the 9P connections' turn with the core. Animating, the - // frame's tick arrives through the same queue (`tickWatch`). - if (timeout_ms != 0) tick_request.set(s.io); + // The wait is the 9P connections' turn with the core. A timeout (the + // core's next wake) arrives through the same queue (`wakeWatch`). + if (timeout_ms != 0) { + s.wake_to = s.core.nextWake() orelse 0; + requestWake(s.io, timeout_ms); + } pardes.turn.rest(); const first = s.loop.nextEvent() catch { pardes.turn.wake(); @@ -988,6 +1003,9 @@ const Shell = struct { batch += 1; } if (motion) |m| _ = s.apply(m); + // An exit's wake (a .nop) or output an exit waited behind: told + // here, inside the step, so the frame after it shows it. + s.takeExits(); s.reloadWatched(); } @@ -997,7 +1015,9 @@ const Shell = struct { defer tz_event.end(); switch (event) { .nop => {}, - .tick => core.update(.tick), + .wake => if (s.test_clock) |*virtual| { + virtual.* = @max(virtual.*, s.wake_to); + }, .quit => { core.quit = true; return true; @@ -1015,6 +1035,8 @@ const Shell = struct { if (s.gens[pr.id] == pr.gen) core.update(.{ .output = .{ .pane = @intCast(pr.id), .bytes = pr.bytes } }); s.gpa.free(pr.bytes); + // An exit waiting behind this output may be told now. + s.takeExits(); return true; }, .pty_eof => |e| if (s.gens[e.id] == e.gen) { @@ -1024,9 +1046,7 @@ const Shell = struct { // closing it would hang up one that runs on without it. // Its exit, if it came first, is told now its output is in. if (pt.cmd.watched) { - pt.cmd.eof = true; - core.update(.{ .eof = .{ .pane = @intCast(e.id) } }); - s.takeExits(); + host_io.commandEof(core, &s.ptys, e.id, s, closeWatched); return true; } _ = libc.close(pt.file.handle); @@ -1225,7 +1245,7 @@ const Shell = struct { s.gens[pane] +%= 1; const child = host_io.forkShell(s.core, pane, s.prompt_rcs, s.core.shellBin(), cwd, s.core.screen_h, s.core.screen_w, s.fs) catch |err| return s.core.reportError(pane, "shell", err); const command = if (s.core.panes[pane]) |pn| pn.command != null else false; - s.ptys[pane] = .{ .file = child.file, .pid = child.pid, .reader = .{ .any_future = null, .result = {} }, .cmd = .{ .watched = command and host_io.watchExit(child.pid) } }; + s.ptys[pane] = .{ .file = child.file, .pid = child.pid, .reader = .{ .any_future = null, .result = {} }, .cmd = .{ .watched = command and host_io.watchExit(child.pid), .fd = child.file.handle } }; var lbuf: [pardes.memory.limits.host_path_cap + 1]u8 = undefined; if (host_io.shellCwd(child.pid, &lbuf)) |wd| s.core.setCwd(pane, wd); if (s.threads_ok) { @@ -1274,26 +1294,15 @@ const Shell = struct { return host_io.ttyTaken(pt.pid, pt.file.handle); } - /// Each watched child that exited: reaped, the core told, and its pty - /// closed if its end of file came first. + /// The command panes' exits, told once their output is in (host_io). fn takeExits(s: *@This()) void { - while (host_io.takeExited()) |pid| for (&s.ptys) |*slot| { - const pt = if (slot.*) |*pt| pt else continue; - if (pt.pid != pid or pt.cmd.exited) continue; - pt.cmd.exit((host_io.reapExited(pid) orelse break).status); - pt.pid = 0; // reaped: no signal or retire may reach whoever gets it next - break; - }; - for (&s.ptys, 0..) |*slot, id| { - const pt = if (slot.*) |*pt| pt else continue; - if (!pt.cmd.due()) continue; - pt.cmd.told = true; - s.core.update(.{ .exited = .{ .pane = @intCast(id), .status = pt.cmd.status } }); - if (pt.cmd.eof) { - _ = libc.close(pt.file.handle); - slot.* = null; - } - } + host_io.takeExits(s.core, &s.ptys, s, closeWatched); + } + + fn closeWatched(s: *@This(), id: usize) void { + const pt = &(s.ptys[id] orelse return); + _ = libc.close(pt.file.handle); + s.ptys[id] = null; } fn killJob(ctx: ?*anyopaque, pane: u8) bool { @@ -1574,21 +1583,77 @@ fn winchWatch(loop: *Loop, vx: *vaxis.Vaxis, tty: *vaxis.Tty) void { } } -/// Set by an animating frame's wait: post one `.tick` a frame from now. The -/// tick joins the input queue, so a key or a click that lands inside the -/// frame is handled at once instead of after a blind sleep. -var tick_request: std.Io.Event = .unset; +/// A wait with a timeout: post one `.wake` when it runs out. The wake joins +/// the input queue, so a key or a click that lands first is handled at once +/// instead of after a blind sleep; vaxis's queue has no timed wait. Each +/// request bumps `wake_gen`, which cuts short a sleep still timing an +/// earlier, longer request (an 800 ms linger interrupted by a key that +/// starts a 16 ms fall) and times the new one instead. +var wake_gen: std.atomic.Value(u32) = .init(0); +var wake_ms: std.atomic.Value(u32) = .init(0); -fn tickWatch(io: std.Io, loop: *Loop) void { +fn requestWake(io: std.Io, ms: u32) void { + wake_ms.store(ms, .release); + _ = wake_gen.fetchAdd(1, .acq_rel); + io.futexWake(u32, &wake_gen.raw, 1); +} + +fn wakeWatch(io: std.Io, loop: *Loop) void { + var seen = wake_gen.load(.acquire); while (true) { - tick_request.waitUncancelable(io); - tick_request.reset(); - var ts: libc.timespec = .{ .sec = 0, .nsec = @as(isize, pardes.layout.Animation.frame_ms) * std.time.ns_per_ms }; - _ = libc.nanosleep(&ts, null); - loop.postEvent(.tick) catch {}; + // Asleep until a request. + while (wake_gen.load(.acquire) == seen) io.futexWaitUncancelable(u32, &wake_gen.raw, seen); + seen = wake_gen.load(.acquire); + if (timeWake(io, &seen)) loop.postEvent(.wake) catch {}; + } +} + +/// Sleep out the latest request; true when it ran its course, false when +/// none is pending. A newer request met on the way restarts the timing. +fn timeWake(io: std.Io, seen: *u32) bool { + while (true) { + const ms = wake_ms.load(.acquire); + const deadline = host_io.monotonicNs() + @as(u64, ms) * std.time.ns_per_ms; + while (true) { + const now = host_io.monotonicNs(); + if (now >= deadline) return true; + io.futexWaitTimeout(u32, &wake_gen.raw, seen.*, .{ .duration = .{ + .raw = .fromNanoseconds(@intCast(deadline - now)), + .clock = .awake, + } }) catch {}; + const gen = wake_gen.load(.acquire); + if (gen != seen.*) { + seen.* = gen; + break; + } + } } } +test "a short wake request cuts short a long one still sleeping" { + const io = std.testing.io; + var seen = wake_gen.load(.acquire); + requestWake(io, 800); + seen = wake_gen.load(.acquire); + const Timer = struct { + fn run(timer_io: std.Io, start_seen: u32, done: *std.atomic.Value(u64)) void { + var local = start_seen; + _ = timeWake(timer_io, &local); + done.store(host_io.monotonicNs(), .release); + } + }; + var done: std.atomic.Value(u64) = .init(0); + const start = host_io.monotonicNs(); + const thread = try std.Thread.spawn(.{}, Timer.run, .{ io, seen, &done }); + var pause: std.c.timespec = .{ .sec = 0, .nsec = 50 * std.time.ns_per_ms }; + _ = std.c.nanosleep(&pause, null); + requestWake(io, 16); + thread.join(); + // The 16 ms request lands about 66 ms in: well before the 800 ms one + // would have, however loaded the machine. + try std.testing.expect(done.load(.acquire) - start < 700 * std.time.ns_per_ms); +} + fn wakeFs(ctx: ?*anyopaque) void { const loop: *Loop = @ptrCast(@alignCast(ctx.?)); _ = loop.tryPostEvent(.fs_ready) catch {}; @@ -1873,7 +1938,7 @@ const Attach = struct { fn apply(a: *Attach, event: @TypeOf(Command.value)) ?AttachEnd { switch (event) { - .nop, .tick, .fs_ready, .lsp_done, .lsp_status, .pipe_done, .pty_read, .pty_eof, .files_changed => {}, + .nop, .wake, .fs_ready, .lsp_done, .lsp_status, .pipe_done, .pty_read, .pty_eof, .files_changed => {}, .quit => return .none, .focus_in => {}, .focus_out => return a.send(.pointer_leave), |
