From 15b7e7fcd783459834015597aa651232631c8a7f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Mar 2023 08:43:42 -0800 Subject: [PATCH] termio: coalesce resize events On macOS, we were seeing resize events dropped by child processes if too many SIGWNCH events were generated. --- src/termio/Thread.zig | 68 +++++++++++++++++++++++++++++++++++++++++- src/termio/message.zig | 8 +++-- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index 49ae661cc..b1c03cc6e 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -18,6 +18,15 @@ const log = std.log.scoped(.io_thread); /// the future if we want it configurable. pub const Mailbox = BlockingQueue(termio.Message, 64); +/// This stores the information that is coalesced. +const Coalesce = struct { + /// The number of milliseconds to coalesce certain messages like resize for. + /// Not all message types are coalesced. + const min_ms = 25; + + resize: ?termio.Message.Resize = null, +}; + /// Allocator used for some state alloc: std.mem.Allocator, @@ -34,6 +43,12 @@ wakeup_c: xev.Completion = .{}, stop: xev.Async, stop_c: xev.Completion = .{}, +/// This is used to coalesce resize events. +coalesce: xev.Timer, +coalesce_c: xev.Completion = .{}, +coalesce_cancel_c: xev.Completion = .{}, +coalesce_data: Coalesce = .{}, + /// The underlying IO implementation. impl: *termio.Impl, @@ -60,6 +75,10 @@ pub fn init( var stop_h = try xev.Async.init(); errdefer stop_h.deinit(); + // This timer is used to coalesce resize events. + var coalesce_h = try xev.Timer.init(); + errdefer coalesce_h.deinit(); + // The mailbox for messaging this thread var mailbox = try Mailbox.create(alloc); errdefer mailbox.destroy(alloc); @@ -69,6 +88,7 @@ pub fn init( .loop = loop, .wakeup = wakeup_h, .stop = stop_h, + .coalesce = coalesce_h, .impl = impl, .mailbox = mailbox, }; @@ -77,6 +97,7 @@ pub fn init( /// Clean up the thread. This is only safe to call once the thread /// completes executing; the caller must join prior to this. pub fn deinit(self: *Thread) void { + self.coalesce.deinit(); self.stop.deinit(); self.wakeup.deinit(); self.loop.deinit(); @@ -129,7 +150,7 @@ fn drainMailbox(self: *Thread) !void { log.debug("mailbox message={}", .{message}); switch (message) { - .resize => |v| try self.impl.resize(v.grid_size, v.screen_size, v.padding), + .resize => |v| self.handleResize(v), .clear_screen => |v| try self.impl.clearScreen(v.history), .write_small => |v| try self.impl.queueWrite(v.data[0..v.len]), .write_stable => |v| try self.impl.queueWrite(v), @@ -147,6 +168,51 @@ fn drainMailbox(self: *Thread) !void { } } +fn handleResize(self: *Thread, resize: termio.Message.Resize) void { + self.coalesce_data.resize = resize; + + // If the timer is already active we just return. In the future we want + // to reset the timer up to a maximum wait time but for now this ensures + // relatively smooth resizing. + if (self.coalesce_c.state() == .active) return; + + self.coalesce.reset( + &self.loop, + &self.coalesce_c, + &self.coalesce_cancel_c, + Coalesce.min_ms, + Thread, + self, + coalesceCallback, + ); +} + +fn coalesceCallback( + self_: ?*Thread, + _: *xev.Loop, + _: *xev.Completion, + r: xev.Timer.RunError!void, +) xev.CallbackAction { + _ = r catch |err| switch (err) { + error.Canceled => {}, + else => { + log.warn("error during coalesce callback err={}", .{err}); + return .disarm; + }, + }; + + const self = self_ orelse return .disarm; + + if (self.coalesce_data.resize) |v| { + self.coalesce_data.resize = null; + self.impl.resize(v.grid_size, v.screen_size, v.padding) catch |err| { + log.warn("error during resize err={}", .{err}); + }; + } + + return .disarm; +} + fn wakeupCallback( self_: ?*Thread, _: *xev.Loop, diff --git a/src/termio/message.zig b/src/termio/message.zig index a9f9a9006..d1a75bc01 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -15,8 +15,7 @@ pub const Message = union(enum) { /// in the future. pub const WriteReq = MessageData(u8, 38); - /// Resize the window. - resize: struct { + pub const Resize = struct { /// The grid size for the given screen size with padding applied. grid_size: renderer.GridSize, @@ -27,7 +26,10 @@ pub const Message = union(enum) { /// The padding, so that the terminal implementation can subtract /// this to send to the pty. padding: renderer.Padding, - }, + }; + + /// Resize the window. + resize: Resize, /// Clear the screen. clear_screen: struct {