termio: coalesce resize events

On macOS, we were seeing resize events dropped by child processes if
too many SIGWNCH events were generated.
This commit is contained in:
Mitchell Hashimoto
2023-03-08 08:43:42 -08:00
parent a754fe8c30
commit 15b7e7fcd7
2 changed files with 72 additions and 4 deletions

View File

@ -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,

View File

@ -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 {