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 {
713713
714714 processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File,
715715 processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize,
716 lockStderr: *const fn (?*anyopaque, buffer: []u8, ?Terminal.Mode) Cancelable!LockedStderr,
717 tryLockStderr: *const fn (?*anyopaque, buffer: []u8, ?Terminal.Mode) Cancelable!?LockedStderr,
716 lockStderr: *const fn (?*anyopaque, ?Terminal.Mode) Cancelable!LockedStderr,
717 tryLockStderr: *const fn (?*anyopaque, ?Terminal.Mode) Cancelable!?LockedStderr,
718718 unlockStderr: *const fn (?*anyopaque) void,
719719 processSetCurrentDir: *const fn (?*anyopaque, Dir) std.process.SetCurrentDirError!void,
720720
......@@ -2190,6 +2190,23 @@ pub const LockedStderr = struct {
21902190 .mode = ls.terminal_mode,
21912191 };
21922192 }
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 }
21932210};
21942211
21952212/// For doing application-level writes to the standard error stream.
......@@ -2200,12 +2217,16 @@ pub const LockedStderr = struct {
22002217/// See also:
22012218/// * `tryLockStderr`
22022219pub 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;
22042223}
22052224
22062225/// Same as `lockStderr` but non-blocking.
22072226pub 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;
22092230}
22102231
22112232pub fn unlockStderr(io: Io) void {
lib/std/Io/Threaded.zig+5-30
......@@ -10864,33 +10864,21 @@ fn netLookupFallible(
1086410864 return error.OptionUnsupported;
1086510865}
1086610866
10867fn lockStderr(
10868 userdata: ?*anyopaque,
10869 buffer: []u8,
10870 terminal_mode: ?Io.Terminal.Mode,
10871) Io.Cancelable!Io.LockedStderr {
10867fn lockStderr(userdata: ?*anyopaque, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!Io.LockedStderr {
1087210868 const t: *Threaded = @ptrCast(@alignCast(userdata));
1087310869 // Only global mutex since this is Threaded.
1087410870 std.process.stderr_thread_mutex.lock();
10875 return initLockedStderr(t, buffer, terminal_mode);
10871 return initLockedStderr(t, terminal_mode);
1087610872}
1087710873
10878fn tryLockStderr(
10879 userdata: ?*anyopaque,
10880 buffer: []u8,
10881 terminal_mode: ?Io.Terminal.Mode,
10882) Io.Cancelable!?Io.LockedStderr {
10874fn tryLockStderr(userdata: ?*anyopaque, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!?Io.LockedStderr {
1088310875 const t: *Threaded = @ptrCast(@alignCast(userdata));
1088410876 // Only global mutex since this is Threaded.
1088510877 if (!std.process.stderr_thread_mutex.tryLock()) return null;
10886 return try initLockedStderr(t, buffer, terminal_mode);
10878 return try initLockedStderr(t, terminal_mode);
1088710879}
1088810880
10889fn initLockedStderr(
10890 t: *Threaded,
10891 buffer: []u8,
10892 terminal_mode: ?Io.Terminal.Mode,
10893) Io.Cancelable!Io.LockedStderr {
10881fn initLockedStderr(t: *Threaded, terminal_mode: ?Io.Terminal.Mode) Io.Cancelable!Io.LockedStderr {
1089410882 if (!t.stderr_writer_initialized) {
1089510883 const io_t = ioBasic(t);
1089610884 if (is_windows) t.stderr_writer.file = .stderr();
......@@ -10901,19 +10889,6 @@ fn initLockedStderr(
1090110889 const CLICOLOR_FORCE = t.environ.exist.CLICOLOR_FORCE;
1090210890 t.stderr_mode = terminal_mode orelse try .detect(io_t, t.stderr_writer.file, NO_COLOR, CLICOLOR_FORCE);
1090310891 }
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;
1091710892 return .{
1091810893 .file_writer = &t.stderr_writer,
1091910894 .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 {
525525
526526 if (switch (global_progress.terminal_mode) {
527527 .off => unreachable, // handled a few lines above
528 .ansi_escape_codes => io.concurrent(updateThreadRun, .{io}),
529 .windows_api => if (is_windows) io.concurrent(windowsApiUpdateThreadRun, .{io}) else unreachable,
528 .ansi_escape_codes => io.concurrent(updateTask, .{io}),
529 .windows_api => if (is_windows) io.concurrent(windowsApiUpdateTask, .{io}) else unreachable,
530530 }) |future| {
531531 global_progress.update_worker = future;
532532 } else |err| {
......@@ -561,18 +561,23 @@ fn wait(io: Io, timeout_ns: u64) bool {
561561 return resize_flag or (global_progress.cols == 0);
562562}
563563
564fn updateThreadRun(io: Io) void {
564fn updateTask(io: Io) void {
565565 // Store this data in the thread so that it does not need to be part of the
566566 // linker data of the main executable.
567567 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
569574 {
570575 const resize_flag = wait(io, global_progress.initial_delay_ns);
571576 if (@atomicLoad(bool, &global_progress.done, .monotonic)) return;
572577 maybeUpdateSize(resize_flag);
573578
574579 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| {
576581 defer io.unlockStderr();
577582 global_progress.need_clear = true;
578583 locked_stderr.file_writer.interface.writeAll(buffer) catch return;
......@@ -583,7 +588,7 @@ fn updateThreadRun(io: Io) void {
583588 const resize_flag = wait(io, global_progress.refresh_rate_ns);
584589
585590 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;
587592 defer io.unlockStderr();
588593 return clearWrittenWithEscapeCodes(stderr.file_writer) catch {};
589594 }
......@@ -591,7 +596,7 @@ fn updateThreadRun(io: Io) void {
591596 maybeUpdateSize(resize_flag);
592597
593598 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| {
595600 defer io.unlockStderr();
596601 global_progress.need_clear = true;
597602 locked_stderr.file_writer.interface.writeAll(buffer) catch return;
......@@ -607,16 +612,21 @@ fn windowsApiWriteMarker() void {
607612 _ = windows.kernel32.WriteConsoleW(handle, &[_]u16{windows_api_start_marker}, 1, &num_chars_written, null);
608613}
609614
610fn windowsApiUpdateThreadRun(io: Io) void {
615fn windowsApiUpdateTask(io: Io) void {
611616 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
613623 {
614624 const resize_flag = wait(io, global_progress.initial_delay_ns);
615625 if (@atomicLoad(bool, &global_progress.done, .monotonic)) return;
616626 maybeUpdateSize(resize_flag);
617627
618628 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| {
620630 defer io.unlockStderr();
621631 windowsApiWriteMarker();
622632 global_progress.need_clear = true;
......@@ -629,7 +639,7 @@ fn windowsApiUpdateThreadRun(io: Io) void {
629639 const resize_flag = wait(io, global_progress.refresh_rate_ns);
630640
631641 if (@atomicLoad(bool, &global_progress.done, .monotonic)) {
632 _ = io.lockStderr(&.{}, null) catch return;
642 _ = io.vtable.lockStderr(io.userdata, null) catch return;
633643 defer io.unlockStderr();
634644 return clearWrittenWindowsApi() catch {};
635645 }
......@@ -637,7 +647,7 @@ fn windowsApiUpdateThreadRun(io: Io) void {
637647 maybeUpdateSize(resize_flag);
638648
639649 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| {
641651 defer io.unlockStderr();
642652 clearWrittenWindowsApi() catch return;
643653 windowsApiWriteMarker();