From f862762f091637ad0d17c4735226c0c477c2cb3e Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 29 Dec 2025 12:49:43 -0800 Subject: [PATCH] std: fix Progress flickering move some of the clearing logic from std.Io.Threaded to the Io interface layer, thus allowing it to be skipped by advanced usage code via calling the vtable functions directly. then take advantage of this in std.Progress to avoid clearing the terminal twice. closes #30611 --- lib/std/Io.zig | 29 +++++++++++++++++++++++++---- lib/std/Io/Threaded.zig | 35 +++++------------------------------ lib/std/Progress.zig | 30 ++++++++++++++++++++---------- 3 files changed, 50 insertions(+), 44 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 77eedccb743b5f836804e7a73c94c17fd5672e5e..1e73f0e012efb47e7c6a706538fc0515a986f236 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -713,8 +713,8 @@ pub const VTable = struct { processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File, processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize, - lockStderr: *const fn (?*anyopaque, buffer: []u8, ?Terminal.Mode) Cancelable!LockedStderr, - tryLockStderr: *const fn (?*anyopaque, buffer: []u8, ?Terminal.Mode) Cancelable!?LockedStderr, + lockStderr: *const fn (?*anyopaque, ?Terminal.Mode) Cancelable!LockedStderr, + tryLockStderr: *const fn (?*anyopaque, ?Terminal.Mode) Cancelable!?LockedStderr, unlockStderr: *const fn (?*anyopaque) void, processSetCurrentDir: *const fn (?*anyopaque, Dir) std.process.SetCurrentDirError!void, @@ -2190,6 +2190,23 @@ pub const LockedStderr = struct { .mode = ls.terminal_mode, }; } + + pub fn clear(ls: LockedStderr, buffer: []u8) Cancelable!void { + const fw = ls.file_writer; + std.Progress.clearWrittenWithEscapeCodes(fw) catch |err| switch (err) { + error.WriteFailed => switch (fw.err.?) { + error.Canceled => |e| return e, + else => {}, + }, + }; + fw.interface.flush() catch |err| switch (err) { + error.WriteFailed => switch (fw.err.?) { + error.Canceled => |e| return e, + else => {}, + }, + }; + fw.interface.buffer = buffer; + } }; /// For doing application-level writes to the standard error stream. @@ -2200,12 +2217,16 @@ pub const LockedStderr = struct { /// See also: /// * `tryLockStderr` pub fn lockStderr(io: Io, buffer: []u8, terminal_mode: ?Terminal.Mode) Cancelable!LockedStderr { - return io.vtable.lockStderr(io.userdata, buffer, terminal_mode); + const ls = try io.vtable.lockStderr(io.userdata, terminal_mode); + try ls.clear(buffer); + return ls; } /// Same as `lockStderr` but non-blocking. pub fn tryLockStderr(io: Io, buffer: []u8, terminal_mode: ?Terminal.Mode) Cancelable!?LockedStderr { - return io.vtable.tryLockStderr(io.userdata, buffer, terminal_mode); + const ls = (try io.vtable.tryLockStderr(io.userdata, buffer, terminal_mode)) orelse return null; + try ls.clear(buffer); + return ls; } pub fn unlockStderr(io: Io) void { diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 3a1b5145610c9042fe8413a4283457be195cf7a5..444ca491a040ce028d7a4fb055863f89b951f0f3 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -10864,33 +10864,21 @@ fn netLookupFallible( return error.OptionUnsupported; } -fn lockStderr( - userdata: ?*anyopaque, - buffer: []u8, - terminal_mode: ?Io.Terminal.Mode, -) Io.Cancelable!Io.LockedStderr { +fn lockStderr(userdata: ?*anyopaque, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!Io.LockedStderr { const t: *Threaded = @ptrCast(@alignCast(userdata)); // Only global mutex since this is Threaded. std.process.stderr_thread_mutex.lock(); - return initLockedStderr(t, buffer, terminal_mode); + return initLockedStderr(t, terminal_mode); } -fn tryLockStderr( - userdata: ?*anyopaque, - buffer: []u8, - terminal_mode: ?Io.Terminal.Mode, -) Io.Cancelable!?Io.LockedStderr { +fn tryLockStderr(userdata: ?*anyopaque, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!?Io.LockedStderr { const t: *Threaded = @ptrCast(@alignCast(userdata)); // Only global mutex since this is Threaded. if (!std.process.stderr_thread_mutex.tryLock()) return null; - return try initLockedStderr(t, buffer, terminal_mode); + return try initLockedStderr(t, terminal_mode); } -fn initLockedStderr( - t: *Threaded, - buffer: []u8, - terminal_mode: ?Io.Terminal.Mode, -) Io.Cancelable!Io.LockedStderr { +fn initLockedStderr(t: *Threaded, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!Io.LockedStderr { if (!t.stderr_writer_initialized) { const io_t = ioBasic(t); if (is_windows) t.stderr_writer.file = .stderr(); @@ -10901,19 +10889,6 @@ fn initLockedStderr( const CLICOLOR_FORCE = t.environ.exist.CLICOLOR_FORCE; t.stderr_mode = terminal_mode orelse try .detect(io_t, t.stderr_writer.file, NO_COLOR, CLICOLOR_FORCE); } - std.Progress.clearWrittenWithEscapeCodes(&t.stderr_writer) catch |err| switch (err) { - error.WriteFailed => switch (t.stderr_writer.err.?) { - error.Canceled => |e| return e, - else => {}, - }, - }; - t.stderr_writer.interface.flush() catch |err| switch (err) { - error.WriteFailed => switch (t.stderr_writer.err.?) { - error.Canceled => |e| return e, - else => {}, - }, - }; - t.stderr_writer.interface.buffer = buffer; return .{ .file_writer = &t.stderr_writer, .terminal_mode = terminal_mode orelse t.stderr_mode, diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 6baa24d246f72f550f5f667109abb920a048a14c..f0baca2784f0ae912488d1ed4a0a1dad28bf1406 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -525,8 +525,8 @@ pub fn start(io: Io, options: Options) Node { if (switch (global_progress.terminal_mode) { .off => unreachable, // handled a few lines above - .ansi_escape_codes => io.concurrent(updateThreadRun, .{io}), - .windows_api => if (is_windows) io.concurrent(windowsApiUpdateThreadRun, .{io}) else unreachable, + .ansi_escape_codes => io.concurrent(updateTask, .{io}), + .windows_api => if (is_windows) io.concurrent(windowsApiUpdateTask, .{io}) else unreachable, }) |future| { global_progress.update_worker = future; } else |err| { @@ -561,18 +561,23 @@ fn wait(io: Io, timeout_ns: u64) bool { return resize_flag or (global_progress.cols == 0); } -fn updateThreadRun(io: Io) void { +fn updateTask(io: Io) void { // Store this data in the thread so that it does not need to be part of the // linker data of the main executable. var serialized_buffer: Serialized.Buffer = undefined; + // In this function we bypass the wrapper code inside `Io.lockStderr` / + // `Io.tryLockStderr` in order to avoid clearing the terminal twice. + // We still want to go through the `Io` instance however in case it uses a + // task-switching mutex. + { const resize_flag = wait(io, global_progress.initial_delay_ns); if (@atomicLoad(bool, &global_progress.done, .monotonic)) return; maybeUpdateSize(resize_flag); const buffer, _ = computeRedraw(&serialized_buffer); - if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| { + if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { defer io.unlockStderr(); global_progress.need_clear = true; locked_stderr.file_writer.interface.writeAll(buffer) catch return; @@ -583,7 +588,7 @@ fn updateThreadRun(io: Io) void { const resize_flag = wait(io, global_progress.refresh_rate_ns); if (@atomicLoad(bool, &global_progress.done, .monotonic)) { - const stderr = io.lockStderr(&.{}, null) catch return; + const stderr = io.vtable.lockStderr(io.userdata, null) catch return; defer io.unlockStderr(); return clearWrittenWithEscapeCodes(stderr.file_writer) catch {}; } @@ -591,7 +596,7 @@ fn updateThreadRun(io: Io) void { maybeUpdateSize(resize_flag); const buffer, _ = computeRedraw(&serialized_buffer); - if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| { + if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { defer io.unlockStderr(); global_progress.need_clear = true; locked_stderr.file_writer.interface.writeAll(buffer) catch return; @@ -607,16 +612,21 @@ fn windowsApiWriteMarker() void { _ = windows.kernel32.WriteConsoleW(handle, &[_]u16{windows_api_start_marker}, 1, &num_chars_written, null); } -fn windowsApiUpdateThreadRun(io: Io) void { +fn windowsApiUpdateTask(io: Io) void { var serialized_buffer: Serialized.Buffer = undefined; + // In this function we bypass the wrapper code inside `Io.lockStderr` / + // `Io.tryLockStderr` in order to avoid clearing the terminal twice. + // We still want to go through the `Io` instance however in case it uses a + // task-switching mutex. + { const resize_flag = wait(io, global_progress.initial_delay_ns); if (@atomicLoad(bool, &global_progress.done, .monotonic)) return; maybeUpdateSize(resize_flag); const buffer, const nl_n = computeRedraw(&serialized_buffer); - if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| { + if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { defer io.unlockStderr(); windowsApiWriteMarker(); global_progress.need_clear = true; @@ -629,7 +639,7 @@ fn windowsApiUpdateThreadRun(io: Io) void { const resize_flag = wait(io, global_progress.refresh_rate_ns); if (@atomicLoad(bool, &global_progress.done, .monotonic)) { - _ = io.lockStderr(&.{}, null) catch return; + _ = io.vtable.lockStderr(io.userdata, null) catch return; defer io.unlockStderr(); return clearWrittenWindowsApi() catch {}; } @@ -637,7 +647,7 @@ fn windowsApiUpdateThreadRun(io: Io) void { maybeUpdateSize(resize_flag); const buffer, const nl_n = computeRedraw(&serialized_buffer); - if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| { + if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { defer io.unlockStderr(); clearWrittenWindowsApi() catch return; windowsApiWriteMarker(); -- 2.54.0