summaryrefslogtreecommitdiff
path: root/src/tty/tty.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/tty/tty.zig')
-rw-r--r--src/tty/tty.zig149
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),