authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-29 12:49:43-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-29 12:49:43-08:00
logf862762f091637ad0d17c4735226c0c477c2cb3e
tree48264a8d8bf0e5e461182edd84f7b08239845858
parentec13e8eaaafc34193f245e593333f512681f168f

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

3 files changed, 50 insertions(+), 44 deletions(-)

lib/std/Io.zig+25-4
...@@ -713,8 +713,8 @@ pub const VTable = struct {...@@ -713,8 +713,8 @@ pub const VTable = struct {
713713
714 processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File,714 processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File,
715 processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize,715 processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize,
716 lockStderr: *const fn (?*anyopaque, buffer: []u8, ?Terminal.Mode) Cancelable!LockedStderr,716 lockStderr: *const fn (?*anyopaque, ?Terminal.Mode) Cancelable!LockedStderr,
717 tryLockStderr: *const fn (?*anyopaque, buffer: []u8, ?Terminal.Mode) Cancelable!?LockedStderr,717 tryLockStderr: *const fn (?*anyopaque, ?Terminal.Mode) Cancelable!?LockedStderr,
718 unlockStderr: *const fn (?*anyopaque) void,718 unlockStderr: *const fn (?*anyopaque) void,
719 processSetCurrentDir: *const fn (?*anyopaque, Dir) std.process.SetCurrentDirError!void,719 processSetCurrentDir: *const fn (?*anyopaque, Dir) std.process.SetCurrentDirError!void,
720720
...@@ -2190,6 +2190,23 @@ pub const LockedStderr = struct {...@@ -2190,6 +2190,23 @@ pub const LockedStderr = struct {
2190 .mode = ls.terminal_mode,2190 .mode = ls.terminal_mode,
2191 };2191 };
2192 }2192 }
2193
2194 pub fn clear(ls: LockedStderr, buffer: []u8) Cancelable!void {
2195 const fw = ls.file_writer;
2196 std.Progress.clearWrittenWithEscapeCodes(fw) catch |err| switch (err) {
2197 error.WriteFailed => switch (fw.err.?) {
2198 error.Canceled => |e| return e,
2199 else => {},
2200 },
2201 };
2202 fw.interface.flush() catch |err| switch (err) {
2203 error.WriteFailed => switch (fw.err.?) {
2204 error.Canceled => |e| return e,
2205 else => {},
2206 },
2207 };
2208 fw.interface.buffer = buffer;
2209 }
2193};2210};
21942211
2195/// For doing application-level writes to the standard error stream.2212/// For doing application-level writes to the standard error stream.
...@@ -2200,12 +2217,16 @@ pub const LockedStderr = struct {...@@ -2200,12 +2217,16 @@ pub const LockedStderr = struct {
2200/// See also:2217/// See also:
2201/// * `tryLockStderr`2218/// * `tryLockStderr`
2202pub fn lockStderr(io: Io, buffer: []u8, terminal_mode: ?Terminal.Mode) Cancelable!LockedStderr {2219pub fn lockStderr(io: Io, buffer: []u8, terminal_mode: ?Terminal.Mode) Cancelable!LockedStderr {
2203 return io.vtable.lockStderr(io.userdata, buffer, terminal_mode);2220 const ls = try io.vtable.lockStderr(io.userdata, terminal_mode);
2221 try ls.clear(buffer);
2222 return ls;
2204}2223}
22052224
2206/// Same as `lockStderr` but non-blocking.2225/// Same as `lockStderr` but non-blocking.
2207pub fn tryLockStderr(io: Io, buffer: []u8, terminal_mode: ?Terminal.Mode) Cancelable!?LockedStderr {2226pub fn tryLockStderr(io: Io, buffer: []u8, terminal_mode: ?Terminal.Mode) Cancelable!?LockedStderr {
2208 return io.vtable.tryLockStderr(io.userdata, buffer, terminal_mode);2227 const ls = (try io.vtable.tryLockStderr(io.userdata, buffer, terminal_mode)) orelse return null;
2228 try ls.clear(buffer);
2229 return ls;
2209}2230}
22102231
2211pub fn unlockStderr(io: Io) void {2232pub fn unlockStderr(io: Io) void {
lib/std/Io/Threaded.zig+5-30
...@@ -10864,33 +10864,21 @@ fn netLookupFallible(...@@ -10864,33 +10864,21 @@ fn netLookupFallible(
10864 return error.OptionUnsupported;10864 return error.OptionUnsupported;
10865}10865}
1086610866
10867fn lockStderr(10867fn lockStderr(userdata: ?*anyopaque, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!Io.LockedStderr {
10868 userdata: ?*anyopaque,
10869 buffer: []u8,
10870 terminal_mode: ?Io.Terminal.Mode,
10871) Io.Cancelable!Io.LockedStderr {
10872 const t: *Threaded = @ptrCast(@alignCast(userdata));10868 const t: *Threaded = @ptrCast(@alignCast(userdata));
10873 // Only global mutex since this is Threaded.10869 // Only global mutex since this is Threaded.
10874 std.process.stderr_thread_mutex.lock();10870 std.process.stderr_thread_mutex.lock();
10875 return initLockedStderr(t, buffer, terminal_mode);10871 return initLockedStderr(t, terminal_mode);
10876}10872}
1087710873
10878fn tryLockStderr(10874fn tryLockStderr(userdata: ?*anyopaque, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!?Io.LockedStderr {
10879 userdata: ?*anyopaque,
10880 buffer: []u8,
10881 terminal_mode: ?Io.Terminal.Mode,
10882) Io.Cancelable!?Io.LockedStderr {
10883 const t: *Threaded = @ptrCast(@alignCast(userdata));10875 const t: *Threaded = @ptrCast(@alignCast(userdata));
10884 // Only global mutex since this is Threaded.10876 // Only global mutex since this is Threaded.
10885 if (!std.process.stderr_thread_mutex.tryLock()) return null;10877 if (!std.process.stderr_thread_mutex.tryLock()) return null;
10886 return try initLockedStderr(t, buffer, terminal_mode);10878 return try initLockedStderr(t, terminal_mode);
10887}10879}
1088810880
10889fn initLockedStderr(10881fn initLockedStderr(t: *Threaded, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!Io.LockedStderr {
10890 t: *Threaded,
10891 buffer: []u8,
10892 terminal_mode: ?Io.Terminal.Mode,
10893) Io.Cancelable!Io.LockedStderr {
10894 if (!t.stderr_writer_initialized) {10882 if (!t.stderr_writer_initialized) {
10895 const io_t = ioBasic(t);10883 const io_t = ioBasic(t);
10896 if (is_windows) t.stderr_writer.file = .stderr();10884 if (is_windows) t.stderr_writer.file = .stderr();
...@@ -10901,19 +10889,6 @@ fn initLockedStderr(...@@ -10901,19 +10889,6 @@ fn initLockedStderr(
10901 const CLICOLOR_FORCE = t.environ.exist.CLICOLOR_FORCE;10889 const CLICOLOR_FORCE = t.environ.exist.CLICOLOR_FORCE;
10902 t.stderr_mode = terminal_mode orelse try .detect(io_t, t.stderr_writer.file, NO_COLOR, CLICOLOR_FORCE);10890 t.stderr_mode = terminal_mode orelse try .detect(io_t, t.stderr_writer.file, NO_COLOR, CLICOLOR_FORCE);
10903 }10891 }
10904 std.Progress.clearWrittenWithEscapeCodes(&t.stderr_writer) catch |err| switch (err) {
10905 error.WriteFailed => switch (t.stderr_writer.err.?) {
10906 error.Canceled => |e| return e,
10907 else => {},
10908 },
10909 };
10910 t.stderr_writer.interface.flush() catch |err| switch (err) {
10911 error.WriteFailed => switch (t.stderr_writer.err.?) {
10912 error.Canceled => |e| return e,
10913 else => {},
10914 },
10915 };
10916 t.stderr_writer.interface.buffer = buffer;
10917 return .{10892 return .{
10918 .file_writer = &t.stderr_writer,10893 .file_writer = &t.stderr_writer,
10919 .terminal_mode = terminal_mode orelse t.stderr_mode,10894 .terminal_mode = terminal_mode orelse t.stderr_mode,
lib/std/Progress.zig+20-10
...@@ -525,8 +525,8 @@ pub fn start(io: Io, options: Options) Node {...@@ -525,8 +525,8 @@ pub fn start(io: Io, options: Options) Node {
525525
526 if (switch (global_progress.terminal_mode) {526 if (switch (global_progress.terminal_mode) {
527 .off => unreachable, // handled a few lines above527 .off => unreachable, // handled a few lines above
528 .ansi_escape_codes => io.concurrent(updateThreadRun, .{io}),528 .ansi_escape_codes => io.concurrent(updateTask, .{io}),
529 .windows_api => if (is_windows) io.concurrent(windowsApiUpdateThreadRun, .{io}) else unreachable,529 .windows_api => if (is_windows) io.concurrent(windowsApiUpdateTask, .{io}) else unreachable,
530 }) |future| {530 }) |future| {
531 global_progress.update_worker = future;531 global_progress.update_worker = future;
532 } else |err| {532 } else |err| {
...@@ -561,18 +561,23 @@ fn wait(io: Io, timeout_ns: u64) bool {...@@ -561,18 +561,23 @@ fn wait(io: Io, timeout_ns: u64) bool {
561 return resize_flag or (global_progress.cols == 0);561 return resize_flag or (global_progress.cols == 0);
562}562}
563563
564fn updateThreadRun(io: Io) void {564fn updateTask(io: Io) void {
565 // Store this data in the thread so that it does not need to be part of the565 // Store this data in the thread so that it does not need to be part of the
566 // linker data of the main executable.566 // linker data of the main executable.
567 var serialized_buffer: Serialized.Buffer = undefined;567 var serialized_buffer: Serialized.Buffer = undefined;
568568
569 // In this function we bypass the wrapper code inside `Io.lockStderr` /
570 // `Io.tryLockStderr` in order to avoid clearing the terminal twice.
571 // We still want to go through the `Io` instance however in case it uses a
572 // task-switching mutex.
573
569 {574 {
570 const resize_flag = wait(io, global_progress.initial_delay_ns);575 const resize_flag = wait(io, global_progress.initial_delay_ns);
571 if (@atomicLoad(bool, &global_progress.done, .monotonic)) return;576 if (@atomicLoad(bool, &global_progress.done, .monotonic)) return;
572 maybeUpdateSize(resize_flag);577 maybeUpdateSize(resize_flag);
573578
574 const buffer, _ = computeRedraw(&serialized_buffer);579 const buffer, _ = computeRedraw(&serialized_buffer);
575 if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| {580 if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| {
576 defer io.unlockStderr();581 defer io.unlockStderr();
577 global_progress.need_clear = true;582 global_progress.need_clear = true;
578 locked_stderr.file_writer.interface.writeAll(buffer) catch return;583 locked_stderr.file_writer.interface.writeAll(buffer) catch return;
...@@ -583,7 +588,7 @@ fn updateThreadRun(io: Io) void {...@@ -583,7 +588,7 @@ fn updateThreadRun(io: Io) void {
583 const resize_flag = wait(io, global_progress.refresh_rate_ns);588 const resize_flag = wait(io, global_progress.refresh_rate_ns);
584589
585 if (@atomicLoad(bool, &global_progress.done, .monotonic)) {590 if (@atomicLoad(bool, &global_progress.done, .monotonic)) {
586 const stderr = io.lockStderr(&.{}, null) catch return;591 const stderr = io.vtable.lockStderr(io.userdata, null) catch return;
587 defer io.unlockStderr();592 defer io.unlockStderr();
588 return clearWrittenWithEscapeCodes(stderr.file_writer) catch {};593 return clearWrittenWithEscapeCodes(stderr.file_writer) catch {};
589 }594 }
...@@ -591,7 +596,7 @@ fn updateThreadRun(io: Io) void {...@@ -591,7 +596,7 @@ fn updateThreadRun(io: Io) void {
591 maybeUpdateSize(resize_flag);596 maybeUpdateSize(resize_flag);
592597
593 const buffer, _ = computeRedraw(&serialized_buffer);598 const buffer, _ = computeRedraw(&serialized_buffer);
594 if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| {599 if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| {
595 defer io.unlockStderr();600 defer io.unlockStderr();
596 global_progress.need_clear = true;601 global_progress.need_clear = true;
597 locked_stderr.file_writer.interface.writeAll(buffer) catch return;602 locked_stderr.file_writer.interface.writeAll(buffer) catch return;
...@@ -607,16 +612,21 @@ fn windowsApiWriteMarker() void {...@@ -607,16 +612,21 @@ fn windowsApiWriteMarker() void {
607 _ = windows.kernel32.WriteConsoleW(handle, &[_]u16{windows_api_start_marker}, 1, &num_chars_written, null);612 _ = windows.kernel32.WriteConsoleW(handle, &[_]u16{windows_api_start_marker}, 1, &num_chars_written, null);
608}613}
609614
610fn windowsApiUpdateThreadRun(io: Io) void {615fn windowsApiUpdateTask(io: Io) void {
611 var serialized_buffer: Serialized.Buffer = undefined;616 var serialized_buffer: Serialized.Buffer = undefined;
612617
618 // In this function we bypass the wrapper code inside `Io.lockStderr` /
619 // `Io.tryLockStderr` in order to avoid clearing the terminal twice.
620 // We still want to go through the `Io` instance however in case it uses a
621 // task-switching mutex.
622
613 {623 {
614 const resize_flag = wait(io, global_progress.initial_delay_ns);624 const resize_flag = wait(io, global_progress.initial_delay_ns);
615 if (@atomicLoad(bool, &global_progress.done, .monotonic)) return;625 if (@atomicLoad(bool, &global_progress.done, .monotonic)) return;
616 maybeUpdateSize(resize_flag);626 maybeUpdateSize(resize_flag);
617627
618 const buffer, const nl_n = computeRedraw(&serialized_buffer);628 const buffer, const nl_n = computeRedraw(&serialized_buffer);
619 if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| {629 if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| {
620 defer io.unlockStderr();630 defer io.unlockStderr();
621 windowsApiWriteMarker();631 windowsApiWriteMarker();
622 global_progress.need_clear = true;632 global_progress.need_clear = true;
...@@ -629,7 +639,7 @@ fn windowsApiUpdateThreadRun(io: Io) void {...@@ -629,7 +639,7 @@ fn windowsApiUpdateThreadRun(io: Io) void {
629 const resize_flag = wait(io, global_progress.refresh_rate_ns);639 const resize_flag = wait(io, global_progress.refresh_rate_ns);
630640
631 if (@atomicLoad(bool, &global_progress.done, .monotonic)) {641 if (@atomicLoad(bool, &global_progress.done, .monotonic)) {
632 _ = io.lockStderr(&.{}, null) catch return;642 _ = io.vtable.lockStderr(io.userdata, null) catch return;
633 defer io.unlockStderr();643 defer io.unlockStderr();
634 return clearWrittenWindowsApi() catch {};644 return clearWrittenWindowsApi() catch {};
635 }645 }
...@@ -637,7 +647,7 @@ fn windowsApiUpdateThreadRun(io: Io) void {...@@ -637,7 +647,7 @@ fn windowsApiUpdateThreadRun(io: Io) void {
637 maybeUpdateSize(resize_flag);647 maybeUpdateSize(resize_flag);
638648
639 const buffer, const nl_n = computeRedraw(&serialized_buffer);649 const buffer, const nl_n = computeRedraw(&serialized_buffer);
640 if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| {650 if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| {
641 defer io.unlockStderr();651 defer io.unlockStderr();
642 clearWrittenWindowsApi() catch return;652 clearWrittenWindowsApi() catch return;
643 windowsApiWriteMarker();653 windowsApiWriteMarker();