From 4ed2d638196cfc0b21703eefa78a083b51ec21c2 Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:08:52 +0100 Subject: [PATCH 01/29] std.Progress: display nodes in a tree form --- lib/std/Progress.zig | 283 +++++++++++++++++++++++-------------------- 1 file changed, 154 insertions(+), 129 deletions(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index fef89a228fde..41d728181acc 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -43,7 +43,10 @@ prev_refresh_timestamp: u64 = undefined, /// This buffer represents the maximum number of bytes written to the terminal /// with each refresh. -output_buffer: [100]u8 = undefined, +output_buffer: [400]u8 = undefined, + +/// This symbol will be used as a bullet in the tree listing. +bullet: u8 = '-', /// How many nanoseconds between writing updates to the terminal. refresh_rate_ns: u64 = 50 * std.time.ns_per_ms, @@ -53,14 +56,14 @@ initial_delay_ns: u64 = 500 * std.time.ns_per_ms, done: bool = true, -/// Protects the `refresh` function, as well as `node.recently_updated_child`. +/// Protects the `refresh` function, as well as `Node` attributes. /// Without this, callsites would call `Node.end` and then free `Node` memory /// while it was still being accessed by the `refresh` function. update_mutex: std.Thread.Mutex = .{}, -/// Keeps track of how many columns in the terminal have been output, so that +/// Keeps track of how many rows in the terminal have been output, so that /// we can move the cursor back later. -columns_written: usize = undefined, +rows_written: usize = undefined, /// Represents one unit of progress. Each node can have children nodes, or /// one can use integers with `update`. @@ -69,17 +72,45 @@ pub const Node = struct { parent: ?*Node, name: []const u8, unit: []const u8 = "", - /// Must be handled atomically to be thread-safe. - recently_updated_child: ?*Node = null, + /// Depth of the Node within the tree + node_tree_depth: usize = undefined, + /// Must be handled using `update_mutex.lock` to be thread-safe. + children: [presentable_children]?*Node = [1]?*Node{null} ** presentable_children, /// Must be handled atomically to be thread-safe. 0 means null. unprotected_estimated_total_items: usize, /// Must be handled atomically to be thread-safe. unprotected_completed_items: usize, + const presentable_children = 10; + + /// Push this `Node` to the `parent.children` stack of the provided `Node` (insert at first index). Thread-safe + /// Returns void if `parent` is null + fn tryPushToParentStack(self: *Node, target_node: *Node) void { + if (target_node.parent) |parent| { + inline for (parent.children) |child| if (child == self) return; + self.context.update_mutex.lock(); // lock below existence check for slight performance reasons + defer self.context.update_mutex.unlock(); // (downside: less precision, but not noticeable) + std.mem.copyBackwards(?*Node, parent.children[1..], parent.children[0 .. parent.children.len - 1]); + parent.children[0] = self; + } + } + + /// Remove this `Node` from the `parent.children` stack of the provided `Node`. Thread-safe + /// Returns void if `parent` is null + fn tryRemoveFromParentStack(self: *Node, target_node: *Node) void { + if (target_node.parent) |parent| { + self.context.update_mutex.lock(); + defer self.context.update_mutex.unlock(); + const index = std.mem.indexOfScalar(?*Node, parent.children[0..], self) orelse return; + std.mem.copyBackwards(?*Node, parent.children[index..], parent.children[index + 1 ..]); + parent.children[parent.children.len - 1] = null; + } + } + /// Create a new child progress node. Thread-safe. /// Call `Node.end` when done. /// TODO solve https://github.com/ziglang/zig/issues/2765 and then change this - /// API to set `self.parent.recently_updated_child` with the return value. + /// API to set `self.children` with the return value. /// Until that is fixed you probably want to call `activate` on the return value. /// Passing 0 for `estimated_total_items` means unknown. pub fn start(self: *Node, name: []const u8, estimated_total_items: usize) Node { @@ -87,6 +118,7 @@ pub const Node = struct { .context = self.context, .parent = self, .name = name, + .node_tree_depth = self.node_tree_depth + 1, .unprotected_estimated_total_items = estimated_total_items, .unprotected_completed_items = 0, }; @@ -94,9 +126,6 @@ pub const Node = struct { /// This is the same as calling `start` and then `end` on the returned `Node`. Thread-safe. pub fn completeOne(self: *Node) void { - if (self.parent) |parent| { - @atomicStore(?*Node, &parent.recently_updated_child, self, .Release); - } _ = @atomicRmw(usize, &self.unprotected_completed_items, .Add, 1, .Monotonic); self.context.maybeRefresh(); } @@ -105,11 +134,7 @@ pub const Node = struct { pub fn end(self: *Node) void { self.context.maybeRefresh(); if (self.parent) |parent| { - { - self.context.update_mutex.lock(); - defer self.context.update_mutex.unlock(); - _ = @cmpxchgStrong(?*Node, &parent.recently_updated_child, self, null, .Monotonic, .Monotonic); - } + self.tryRemoveFromParentStack(self); parent.completeOne(); } else { self.context.update_mutex.lock(); @@ -121,40 +146,28 @@ pub const Node = struct { /// Tell the parent node that this node is actively being worked on. Thread-safe. pub fn activate(self: *Node) void { - if (self.parent) |parent| { - @atomicStore(?*Node, &parent.recently_updated_child, self, .Release); - self.context.maybeRefresh(); - } + self.tryPushToParentStack(self); + self.context.maybeRefresh(); } - /// Thread-safe. + /// Will also tell the parent node that this node is actively being worked on. Thread-safe. pub fn setName(self: *Node, name: []const u8) void { + self.tryPushToParentStack(self); const progress = self.context; progress.update_mutex.lock(); defer progress.update_mutex.unlock(); self.name = name; - if (self.parent) |parent| { - @atomicStore(?*Node, &parent.recently_updated_child, self, .Release); - if (parent.parent) |grand_parent| { - @atomicStore(?*Node, &grand_parent.recently_updated_child, parent, .Release); - } - if (progress.timer) |*timer| progress.maybeRefreshWithHeldLock(timer); - } + if (progress.timer) |*timer| progress.maybeRefreshWithHeldLock(timer); } - /// Thread-safe. + /// Will also tell the parent node that this node is actively being worked on. Thread-safe. pub fn setUnit(self: *Node, unit: []const u8) void { + self.tryPushToParentStack(self); const progress = self.context; progress.update_mutex.lock(); defer progress.update_mutex.unlock(); self.unit = unit; - if (self.parent) |parent| { - @atomicStore(?*Node, &parent.recently_updated_child, self, .Release); - if (parent.parent) |grand_parent| { - @atomicStore(?*Node, &grand_parent.recently_updated_child, parent, .Release); - } - if (progress.timer) |*timer| progress.maybeRefreshWithHeldLock(timer); - } + if (progress.timer) |*timer| progress.maybeRefreshWithHeldLock(timer); } /// Thread-safe. 0 means unknown. @@ -190,10 +203,11 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N .context = self, .parent = null, .name = name, + .node_tree_depth = 0, .unprotected_estimated_total_items = estimated_total_items, .unprotected_completed_items = 0, }; - self.columns_written = 0; + self.rows_written = 0; self.prev_refresh_timestamp = 0; self.timer = std.time.Timer.start() catch null; self.done = false; @@ -230,69 +244,68 @@ pub fn refresh(self: *Progress) void { fn clearWithHeldLock(p: *Progress, end_ptr: *usize) void { const file = p.terminal orelse return; var end = end_ptr.*; - if (p.columns_written > 0) { - // restore the cursor position by moving the cursor - // `columns_written` cells to the left, then clear the rest of the - // line - if (p.supports_ansi_escape_codes) { - end += (std.fmt.bufPrint(p.output_buffer[end..], "\x1b[{d}D", .{p.columns_written}) catch unreachable).len; - end += (std.fmt.bufPrint(p.output_buffer[end..], "\x1b[0K", .{}) catch unreachable).len; - } else if (builtin.os.tag == .windows) winapi: { - std.debug.assert(p.is_windows_terminal); - - var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; - if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE) { - // stop trying to write to this file - p.terminal = null; - break :winapi; - } - - var cursor_pos = windows.COORD{ - .X = info.dwCursorPosition.X - @as(windows.SHORT, @intCast(p.columns_written)), - .Y = info.dwCursorPosition.Y, - }; - - if (cursor_pos.X < 0) - cursor_pos.X = 0; - - const fill_chars = @as(windows.DWORD, @intCast(info.dwSize.X - cursor_pos.X)); - - var written: windows.DWORD = undefined; - if (windows.kernel32.FillConsoleOutputAttribute( - file.handle, - info.wAttributes, - fill_chars, - cursor_pos, - &written, - ) != windows.TRUE) { - // stop trying to write to this file - p.terminal = null; - break :winapi; - } - if (windows.kernel32.FillConsoleOutputCharacterW( - file.handle, - ' ', - fill_chars, - cursor_pos, - &written, - ) != windows.TRUE) { - // stop trying to write to this file - p.terminal = null; - break :winapi; - } - if (windows.kernel32.SetConsoleCursorPosition(file.handle, cursor_pos) != windows.TRUE) { - // stop trying to write to this file - p.terminal = null; - break :winapi; - } + + // restore the cursor position by moving the cursor + // `rows_written` cells up, beginning of line, + // then clear to the end of the screen + if (p.supports_ansi_escape_codes) { + if (p.rows_written == 0) { + end += (std.fmt.bufPrint(p.output_buffer[end..], "\x1b[0G", .{}) catch unreachable).len; // beginning of line + end += (std.fmt.bufPrint(p.output_buffer[end..], "\x1b[0K", .{}) catch unreachable).len; // clear till end of line } else { - // we are in a "dumb" terminal like in acme or writing to a file - p.output_buffer[end] = '\n'; - end += 1; + end += (std.fmt.bufPrint(p.output_buffer[end..], "\x1b[{d}F", .{p.rows_written}) catch unreachable).len; // move up and to start + end += (std.fmt.bufPrint(p.output_buffer[end..], "\x1b[0J", .{}) catch unreachable).len; // clear till end of screen } + } else if (builtin.os.tag == .windows) winapi: { + std.debug.assert(p.is_windows_terminal); - p.columns_written = 0; + var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; + if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE) { + // stop trying to write to this file + p.terminal = null; + break :winapi; + } + var cursor_pos = windows.COORD{ + .X = 0, + .Y = info.dwCursorPosition.Y - @as(windows.SHORT, @intCast(p.rows_written)), + }; + + const fill_chars = @as(windows.DWORD, @intCast(info.dwSize.Y - cursor_pos.Y)); + + var written: windows.DWORD = undefined; + if (windows.kernel32.FillConsoleOutputAttribute( + file.handle, + info.wAttributes, + fill_chars, + cursor_pos, + &written, + ) != windows.TRUE) { + // stop trying to write to this file + p.terminal = null; + break :winapi; + } + if (windows.kernel32.FillConsoleOutputCharacterW( + file.handle, + ' ', + fill_chars, + cursor_pos, + &written, + ) != windows.TRUE) { + // stop trying to write to this file + p.terminal = null; + break :winapi; + } + if (windows.kernel32.SetConsoleCursorPosition(file.handle, cursor_pos) != windows.TRUE) { + // stop trying to write to this file + p.terminal = null; + break :winapi; + } + } else { + // we are in a "dumb" terminal like in acme or writing to a file + p.output_buffer[end] = '\n'; + end += 1; } + p.rows_written = 0; end_ptr.* = end; } @@ -306,36 +319,7 @@ fn refreshWithHeldLock(self: *Progress) void { clearWithHeldLock(self, &end); if (!self.done) { - var need_ellipse = false; - var maybe_node: ?*Node = &self.root; - while (maybe_node) |node| { - if (need_ellipse) { - self.bufWrite(&end, "... ", .{}); - } - need_ellipse = false; - const eti = @atomicLoad(usize, &node.unprotected_estimated_total_items, .Monotonic); - const completed_items = @atomicLoad(usize, &node.unprotected_completed_items, .Monotonic); - const current_item = completed_items + 1; - if (node.name.len != 0 or eti > 0) { - if (node.name.len != 0) { - self.bufWrite(&end, "{s}", .{node.name}); - need_ellipse = true; - } - if (eti > 0) { - if (need_ellipse) self.bufWrite(&end, " ", .{}); - self.bufWrite(&end, "[{d}/{d}{s}] ", .{ current_item, eti, node.unit }); - need_ellipse = false; - } else if (completed_items != 0) { - if (need_ellipse) self.bufWrite(&end, " ", .{}); - self.bufWrite(&end, "[{d}{s}] ", .{ current_item, node.unit }); - need_ellipse = false; - } - } - maybe_node = @atomicLoad(?*Node, &node.recently_updated_child, .Acquire); - } - if (need_ellipse) { - self.bufWrite(&end, "... ", .{}); - } + refreshOutputBufWithHeldLock(self, &self.root, &end); } _ = file.write(self.output_buffer[0..end]) catch { @@ -347,6 +331,48 @@ fn refreshWithHeldLock(self: *Progress) void { } } +fn refreshOutputBufWithHeldLock(self: *Progress, node: *Node, end_ptr: *usize) void { + var end = end_ptr.*; + var need_ellipse = false; + + const eti = @atomicLoad(usize, &node.unprotected_estimated_total_items, .Monotonic); + const completed_items = @atomicLoad(usize, &node.unprotected_completed_items, .Monotonic); + const current_item = completed_items + 1; + + if (node.name.len != 0 or eti > 0) { + if (node.node_tree_depth > 0) { + const depth: usize = @min(10, node.node_tree_depth); + const whitespace_length: usize = if (depth > 1) (depth - 1) * 2 else 0; + const indentation_buffer = [1]u8{' '} ** 18; // fits indentation up to depth 10 + self.bufWrite(&end, "\n{s}{s}{c} ", .{ + indentation_buffer[0..whitespace_length], + if (node.node_tree_depth > 10) "_ " else "", + self.bullet, + }); + } + if (node.name.len != 0) { + self.bufWrite(&end, "{s} ", .{node.name}); + need_ellipse = true; + } + if (eti > 0) { + self.bufWrite(&end, "[{d}/{d}{s}]", .{ current_item, eti, node.unit }); + need_ellipse = false; + } else if (completed_items != 0) { + self.bufWrite(&end, "[{d}{s}]", .{ current_item, node.unit }); + need_ellipse = false; + } + if (need_ellipse) { + self.bufWrite(&end, "...", .{}); + } + } + + for (node.children) |maybe_child| { + if (maybe_child) |child| refreshOutputBufWithHeldLock(self, child, &end); + } + + end_ptr.* = end; +} + pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void { const file = self.terminal orelse { std.debug.print(format, args); @@ -357,7 +383,7 @@ pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void { self.terminal = null; return; }; - self.columns_written = 0; + if (self.rows_written > 0) self.rows_written -= 1; } /// Allows the caller to freely write to stderr until unlock_stderr() is called. @@ -382,14 +408,13 @@ pub fn unlock_stderr(p: *Progress) void { fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void { if (std.fmt.bufPrint(self.output_buffer[end.*..], format, args)) |written| { + self.rows_written += std.mem.count(u8, format, "\n"); const amt = written.len; end.* += amt; - self.columns_written += amt; } else |err| switch (err) { error.NoSpaceLeft => { - self.columns_written += self.output_buffer.len - end.*; end.* = self.output_buffer.len; - const suffix = "... "; + const suffix = "..."; @memcpy(self.output_buffer[self.output_buffer.len - suffix.len ..], suffix); }, } From 1ec1bf3c44e4ea3671788fede51bf0b31863fe0f Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:08:52 +0100 Subject: [PATCH 02/29] std.Progress: truncate content to terminal width --- lib/std/Progress.zig | 67 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 41d728181acc..188a885ab815 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -65,6 +65,14 @@ update_mutex: std.Thread.Mutex = .{}, /// we can move the cursor back later. rows_written: usize = undefined, +/// Keeps track of how many cols in the terminal have been output, so that +/// we can apply truncation if needed. +columns_written: usize = undefined, + +/// Stores the current max width of the terminal. +/// If not available then 0. +max_columns: usize = undefined, + /// Represents one unit of progress. Each node can have children nodes, or /// one can use integers with `update`. pub const Node = struct { @@ -208,6 +216,8 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N .unprotected_completed_items = 0, }; self.rows_written = 0; + self.columns_written = 0; + self.max_columns = determineTerminalWidth(self) orelse 0; self.prev_refresh_timestamp = 0; self.timer = std.time.Timer.start() catch null; self.done = false; @@ -241,6 +251,28 @@ pub fn refresh(self: *Progress) void { return self.refreshWithHeldLock(); } +/// Determine the terminal window width in columns +fn determineTerminalWidth(self: *Progress) ?usize { + if (self.terminal == null) return null; + switch (builtin.os.tag) { + .linux => { + var window_size: std.os.linux.winsize = undefined; + const exit_code = std.os.linux.ioctl(self.terminal.?.handle, std.os.linux.T.IOCGWINSZ, @intFromPtr(&window_size)); + if (exit_code < 0) return null; + return @intCast(window_size.ws_col); + }, + .windows => { + std.debug.assert(self.is_windows_terminal); + var screen_buffer_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; + const exit_code = windows.kernel32.GetConsoleScreenBufferInfo(self.terminal.?.handle, &screen_buffer_info); + if (exit_code != windows.TRUE) return null; + return @intCast(screen_buffer_info.dwSize.X - 1); + }, + else => return null, + } + return null; +} + fn clearWithHeldLock(p: *Progress, end_ptr: *usize) void { const file = p.terminal orelse return; var end = end_ptr.*; @@ -316,6 +348,7 @@ fn refreshWithHeldLock(self: *Progress) void { const file = self.terminal orelse return; var end: usize = 0; + self.columns_written = 0; clearWithHeldLock(self, &end); if (!self.done) { @@ -344,25 +377,26 @@ fn refreshOutputBufWithHeldLock(self: *Progress, node: *Node, end_ptr: *usize) v const depth: usize = @min(10, node.node_tree_depth); const whitespace_length: usize = if (depth > 1) (depth - 1) * 2 else 0; const indentation_buffer = [1]u8{' '} ** 18; // fits indentation up to depth 10 - self.bufWrite(&end, "\n{s}{s}{c} ", .{ + self.bufWriteInsertNewline(&end); + self.bufWriteLineTruncate(&end, "{s}{s}{c} ", .{ indentation_buffer[0..whitespace_length], if (node.node_tree_depth > 10) "_ " else "", self.bullet, }); } if (node.name.len != 0) { - self.bufWrite(&end, "{s} ", .{node.name}); + self.bufWriteLineTruncate(&end, "{s} ", .{node.name}); need_ellipse = true; } if (eti > 0) { - self.bufWrite(&end, "[{d}/{d}{s}]", .{ current_item, eti, node.unit }); + self.bufWriteLineTruncate(&end, "[{d}/{d}{s}]", .{ current_item, eti, node.unit }); need_ellipse = false; } else if (completed_items != 0) { - self.bufWrite(&end, "[{d}{s}]", .{ current_item, node.unit }); + self.bufWriteLineTruncate(&end, "[{d}{s}]", .{ current_item, node.unit }); need_ellipse = false; } if (need_ellipse) { - self.bufWrite(&end, "...", .{}); + self.bufWriteLineTruncate(&end, "...", .{}); } } @@ -406,9 +440,30 @@ pub fn unlock_stderr(p: *Progress) void { p.update_mutex.unlock(); } +/// Wrapping function around `bufWrite` that cares about the terminal width +fn bufWriteLineTruncate(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void { + comptime assert(std.mem.count(u8, format, "\n") == 0); + const ellipse = "..."; + const start_index = end.*; + if (self.max_columns <= 0 or self.columns_written < self.max_columns) self.bufWrite(end, format, args); + const actual_columns_written = end.* - start_index; + const truncated_end_index = start_index + @min(self.max_columns - self.columns_written, actual_columns_written); + if (self.max_columns > 0) end.* = truncated_end_index; + self.columns_written += truncated_end_index - start_index; + if (self.max_columns > 0 and actual_columns_written + self.columns_written > self.max_columns) { + @memcpy(self.output_buffer[end.* - ellipse.len .. end.*], ellipse); + } +} + +/// Wrapping function around `bufWrite` that inserts a new line +fn bufWriteInsertNewline(self: *Progress, end: *usize) void { + self.bufWrite(end, "\n", .{}); + self.columns_written = 0; +} + fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void { if (std.fmt.bufPrint(self.output_buffer[end.*..], format, args)) |written| { - self.rows_written += std.mem.count(u8, format, "\n"); + self.rows_written += comptime std.mem.count(u8, format, "\n"); const amt = written.len; end.* += amt; } else |err| switch (err) { From ae732bade0514ed0a3675cfeb8ba9a5e3665ca7e Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:08:52 +0100 Subject: [PATCH 03/29] std.Progress: make the output_buffer end index a field This is useful for a lower-level interaction with the module. An example would be the post-processing of the buffer before writing to `terminal`. --- lib/std/Progress.zig | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 188a885ab815..cd9467b06bc4 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -45,6 +45,10 @@ prev_refresh_timestamp: u64 = undefined, /// with each refresh. output_buffer: [400]u8 = undefined, +/// This is the end index for the `output_buffer` +/// that will be used for printing to the terminal. +end: usize = undefined, + /// This symbol will be used as a bullet in the tree listing. bullet: u8 = '-', @@ -215,6 +219,7 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N .unprotected_estimated_total_items = estimated_total_items, .unprotected_completed_items = 0, }; + self.end = 0; self.rows_written = 0; self.columns_written = 0; self.max_columns = determineTerminalWidth(self) orelse 0; @@ -347,15 +352,15 @@ fn refreshWithHeldLock(self: *Progress) void { const file = self.terminal orelse return; - var end: usize = 0; + self.end = 0; self.columns_written = 0; - clearWithHeldLock(self, &end); + clearWithHeldLock(self, &self.end); if (!self.done) { - refreshOutputBufWithHeldLock(self, &self.root, &end); + refreshOutputBufWithHeldLock(self, &self.root, &self.end); } - _ = file.write(self.output_buffer[0..end]) catch { + _ = file.write(self.output_buffer[0..self.end]) catch { // stop trying to write to this file self.terminal = null; }; From 215c40ddfb353082f6efbf3e1a0c97a6258616ee Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:08:52 +0100 Subject: [PATCH 04/29] frontend: migrate to the new API of std.Progress --- src/main.zig | 45 +++++---------------------------------------- 1 file changed, 5 insertions(+), 40 deletions(-) diff --git a/src/main.zig b/src/main.zig index bcc7860e357a..5d41cb560f3c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3807,7 +3807,7 @@ fn serve( switch (hdr.tag) { .exit => return, .update => { - assert(main_progress_node.recently_updated_child == null); + for (main_progress_node.children) |child| assert(child == null); tracy.frameMark(); if (arg_mode == .translate_c) { @@ -3872,7 +3872,7 @@ fn serve( }, .hot_update => { tracy.frameMark(); - assert(main_progress_node.recently_updated_child == null); + for (main_progress_node.children) |child| assert(child == null); if (child_pid) |pid| { try comp.hotCodeSwap(main_progress_node, pid); try serveUpdateResults(&server, comp); @@ -3911,48 +3911,13 @@ fn progressThread(progress: *std.Progress, server: *const Server, reset: *std.Th error.Timeout => {}, } - var buf: std.BoundedArray(u8, 160) = .{}; - - { - progress.update_mutex.lock(); - defer progress.update_mutex.unlock(); - - var need_ellipse = false; - var maybe_node: ?*std.Progress.Node = &progress.root; - while (maybe_node) |node| { - if (need_ellipse) { - buf.appendSlice("... ") catch {}; - } - need_ellipse = false; - const eti = @atomicLoad(usize, &node.unprotected_estimated_total_items, .Monotonic); - const completed_items = @atomicLoad(usize, &node.unprotected_completed_items, .Monotonic); - const current_item = completed_items + 1; - if (node.name.len != 0 or eti > 0) { - if (node.name.len != 0) { - buf.appendSlice(node.name) catch {}; - need_ellipse = true; - } - if (eti > 0) { - if (need_ellipse) buf.appendSlice(" ") catch {}; - buf.writer().print("[{d}/{d}] ", .{ current_item, eti }) catch {}; - need_ellipse = false; - } else if (completed_items != 0) { - if (need_ellipse) buf.appendSlice(" ") catch {}; - buf.writer().print("[{d}] ", .{current_item}) catch {}; - need_ellipse = false; - } - } - maybe_node = @atomicLoad(?*std.Progress.Node, &node.recently_updated_child, .Acquire); - } - } - - const progress_string = buf.slice(); + progress.refresh(); server.serveMessage(.{ .tag = .progress, - .bytes_len = @as(u32, @intCast(progress_string.len)), + .bytes_len = @as(u32, @intCast(progress.end)), }, &.{ - progress_string, + progress.output_buffer[0..progress.end], }) catch |err| { fatal("unable to write to client: {s}", .{@errorName(err)}); }; From d421c7829365d531a5bac0f6e4c924df95c251b1 Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:08:52 +0100 Subject: [PATCH 05/29] std.Progress: always write the output_buffer Following the commit that made the output_buffer end index become its own field, it's also reasonable to write to the output_buffer even if the terminal field is null. --- lib/std/Progress.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index cd9467b06bc4..71488d28bd59 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -14,7 +14,7 @@ const assert = std.debug.assert; const Progress = @This(); /// `null` if the current node (and its children) should -/// not print on update() +/// not print on update(), refreshes the `output_buffer` nonetheless terminal: ?std.fs.File = undefined, /// Is this a windows API terminal (note: this is not the same as being run on windows @@ -350,8 +350,6 @@ fn refreshWithHeldLock(self: *Progress) void { const is_dumb = !self.supports_ansi_escape_codes and !self.is_windows_terminal; if (is_dumb and self.dont_print_on_dumb) return; - const file = self.terminal orelse return; - self.end = 0; self.columns_written = 0; clearWithHeldLock(self, &self.end); @@ -360,6 +358,8 @@ fn refreshWithHeldLock(self: *Progress) void { refreshOutputBufWithHeldLock(self, &self.root, &self.end); } + const file = self.terminal orelse return; + _ = file.write(self.output_buffer[0..self.end]) catch { // stop trying to write to this file self.terminal = null; From 64138fb9bcf04b89412b331f30f6413ab9b68793 Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:08:52 +0100 Subject: [PATCH 06/29] docgen: add support for more ansi escape sequences --- tools/docgen.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/docgen.zig b/tools/docgen.zig index 669f6bca8d5d..3ed0ed9f728f 100644 --- a/tools/docgen.zig +++ b/tools/docgen.zig @@ -875,7 +875,11 @@ fn termColor(allocator: Allocator, input: []const u8) ![]u8 { }, .after_number => switch (c) { ';' => state = .arg, - 'D' => state = .start, + 'A'...'G' => state = .start, + 'J' => { + buf.items.len = last_new_line; + state = .start; + }, 'K' => { buf.items.len = last_new_line; state = .start; From 4d0574bd5db0d103ab241db9bb8005788087604b Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:08:53 +0100 Subject: [PATCH 07/29] std.Progress: use fill option to replace indentation_buffer Co-authored-by: Veikka Tuominen --- lib/std/Progress.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 71488d28bd59..72ca0f5ce8d5 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -381,12 +381,12 @@ fn refreshOutputBufWithHeldLock(self: *Progress, node: *Node, end_ptr: *usize) v if (node.node_tree_depth > 0) { const depth: usize = @min(10, node.node_tree_depth); const whitespace_length: usize = if (depth > 1) (depth - 1) * 2 else 0; - const indentation_buffer = [1]u8{' '} ** 18; // fits indentation up to depth 10 self.bufWriteInsertNewline(&end); - self.bufWriteLineTruncate(&end, "{s}{s}{c} ", .{ - indentation_buffer[0..whitespace_length], + self.bufWriteLineTruncate(&end, "{s: <[3]}{s}{c} ", .{ + "", if (node.node_tree_depth > 10) "_ " else "", self.bullet, + whitespace_length, }); } if (node.name.len != 0) { From 9f9c17b54d2f23e67be57d5e7daf9e101986781c Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:08:53 +0100 Subject: [PATCH 08/29] std.Progress: remove confusing comment Co-authored-by: Veikka Tuominen --- lib/std/Progress.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 72ca0f5ce8d5..1b4ec3ffa0f7 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -96,9 +96,8 @@ pub const Node = struct { const presentable_children = 10; /// Push this `Node` to the `parent.children` stack of the provided `Node` (insert at first index). Thread-safe - /// Returns void if `parent` is null fn tryPushToParentStack(self: *Node, target_node: *Node) void { - if (target_node.parent) |parent| { + const parent = target_node.parent orelse return; inline for (parent.children) |child| if (child == self) return; self.context.update_mutex.lock(); // lock below existence check for slight performance reasons defer self.context.update_mutex.unlock(); // (downside: less precision, but not noticeable) From 10f04b6523c6ca0c2f26e9ff42d500f37a673d15 Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:08:53 +0100 Subject: [PATCH 09/29] std.Progress: remove confusing comment Co-authored-by: Veikka Tuominen --- lib/std/Progress.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 1b4ec3ffa0f7..facdba390bb0 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -107,9 +107,8 @@ pub const Node = struct { } /// Remove this `Node` from the `parent.children` stack of the provided `Node`. Thread-safe - /// Returns void if `parent` is null fn tryRemoveFromParentStack(self: *Node, target_node: *Node) void { - if (target_node.parent) |parent| { + const parent = target_node.parent orelse return; self.context.update_mutex.lock(); defer self.context.update_mutex.unlock(); const index = std.mem.indexOfScalar(?*Node, parent.children[0..], self) orelse return; From b8bbf9bd56593cfc9092d7b670926cffaa29d432 Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:08:53 +0100 Subject: [PATCH 10/29] std.Progress: fix trailing bracket --- lib/std/Progress.zig | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index facdba390bb0..08ec268a44b2 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -98,23 +98,21 @@ pub const Node = struct { /// Push this `Node` to the `parent.children` stack of the provided `Node` (insert at first index). Thread-safe fn tryPushToParentStack(self: *Node, target_node: *Node) void { const parent = target_node.parent orelse return; - inline for (parent.children) |child| if (child == self) return; - self.context.update_mutex.lock(); // lock below existence check for slight performance reasons - defer self.context.update_mutex.unlock(); // (downside: less precision, but not noticeable) - std.mem.copyBackwards(?*Node, parent.children[1..], parent.children[0 .. parent.children.len - 1]); - parent.children[0] = self; - } + inline for (parent.children) |child| if (child == self) return; + self.context.update_mutex.lock(); // lock below existence check for slight performance reasons + defer self.context.update_mutex.unlock(); // (downside: less precision, but not noticeable) + std.mem.copyBackwards(?*Node, parent.children[1..], parent.children[0 .. parent.children.len - 1]); + parent.children[0] = self; } /// Remove this `Node` from the `parent.children` stack of the provided `Node`. Thread-safe fn tryRemoveFromParentStack(self: *Node, target_node: *Node) void { const parent = target_node.parent orelse return; - self.context.update_mutex.lock(); - defer self.context.update_mutex.unlock(); - const index = std.mem.indexOfScalar(?*Node, parent.children[0..], self) orelse return; - std.mem.copyBackwards(?*Node, parent.children[index..], parent.children[index + 1 ..]); - parent.children[parent.children.len - 1] = null; - } + self.context.update_mutex.lock(); + defer self.context.update_mutex.unlock(); + const index = std.mem.indexOfScalar(?*Node, parent.children[0..], self) orelse return; + std.mem.copyBackwards(?*Node, parent.children[index..], parent.children[index + 1 ..]); + parent.children[parent.children.len - 1] = null; } /// Create a new child progress node. Thread-safe. From 1ba94ea1767ff62b1a0b4f431b5df5af21986995 Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:08:53 +0100 Subject: [PATCH 11/29] std.Progress: redundant mutability --- lib/std/Progress.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 08ec268a44b2..3ad0eb84ab33 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -298,7 +298,7 @@ fn clearWithHeldLock(p: *Progress, end_ptr: *usize) void { p.terminal = null; break :winapi; } - var cursor_pos = windows.COORD{ + const cursor_pos = windows.COORD{ .X = 0, .Y = info.dwCursorPosition.Y - @as(windows.SHORT, @intCast(p.rows_written)), }; From 72cc1788f38ea660941218caa04cb285878aeb7e Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:08:53 +0100 Subject: [PATCH 12/29] std.Progress: add truncation support for macos --- lib/std/Progress.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 3ad0eb84ab33..1fd7f1ed5773 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -262,6 +262,12 @@ fn determineTerminalWidth(self: *Progress) ?usize { if (exit_code < 0) return null; return @intCast(window_size.ws_col); }, + .macos => { + var window_size: std.c.winsize = undefined; + const exit_code = std.c.ioctl(self.terminal.?.handle, std.c.T.IOCGWINSZ, @intFromPtr(&window_size)); + if (exit_code < 0) return null; + return @intCast(window_size.ws_col); + }, .windows => { std.debug.assert(self.is_windows_terminal); var screen_buffer_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; From 20cb751c6f71bef5fa2b64762eed32c8484411b3 Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:26:57 +0200 Subject: [PATCH 13/29] std.Progress: update casing of AtomicOrder field identifiers --- lib/std/Progress.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index c41afe6a88e9..a9ad8d00e3fa 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -375,8 +375,8 @@ fn refreshOutputBufWithHeldLock(self: *Progress, node: *Node, end_ptr: *usize) v var end = end_ptr.*; var need_ellipse = false; - const eti = @atomicLoad(usize, &node.unprotected_estimated_total_items, .Monotonic); - const completed_items = @atomicLoad(usize, &node.unprotected_completed_items, .Monotonic); + const eti = @atomicLoad(usize, &node.unprotected_estimated_total_items, .monotonic); + const completed_items = @atomicLoad(usize, &node.unprotected_completed_items, .monotonic); const current_item = completed_items + 1; if (node.name.len != 0 or eti > 0) { From e5209b92787b40cbfcff55f99aca0724e8eb4fb0 Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:58:30 +0200 Subject: [PATCH 14/29] std.Progress: do not copy ellipse if not memory-safe --- lib/std/Progress.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index a9ad8d00e3fa..5c5971e42b8b 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -457,7 +457,7 @@ fn bufWriteLineTruncate(self: *Progress, end: *usize, comptime format: []const u const truncated_end_index = start_index + @min(self.max_columns - self.columns_written, actual_columns_written); if (self.max_columns > 0) end.* = truncated_end_index; self.columns_written += truncated_end_index - start_index; - if (self.max_columns > 0 and actual_columns_written + self.columns_written > self.max_columns) { + if (self.max_columns > 0 and actual_columns_written + self.columns_written > self.max_columns and end.* >= ellipse.len) { @memcpy(self.output_buffer[end.* - ellipse.len .. end.*], ellipse); } } From aaa13b01ace4c4be6800f6af7004e90f8d954082 Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:59:59 +0200 Subject: [PATCH 15/29] std.Progress: continously update max_columns --- lib/std/Progress.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 5c5971e42b8b..9148404699d7 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -228,6 +228,7 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N /// Updates the terminal if enough time has passed since last update. Thread-safe. pub fn maybeRefresh(self: *Progress) void { if (self.timer) |*timer| { + self.max_columns = determineTerminalWidth(self) orelse 0; if (!self.update_mutex.tryLock()) return; defer self.update_mutex.unlock(); maybeRefreshWithHeldLock(self, timer); From aa1c0ccd7f0f9eb7d80828d534792a19510f82ca Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:43:17 +0200 Subject: [PATCH 16/29] std.Progress: `output_buffer` as two-dimensional This refactor should improve readability by utilizing a nested buffer instead of a one-dimensional buffer. The upside is better maintainability. Before this commit, the idea was to keep track of LFs in a single buffer. --- lib/std/Progress.zig | 161 ++++++++++++++++++++++--------------------- 1 file changed, 84 insertions(+), 77 deletions(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 9148404699d7..0bdfad28fba7 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -13,6 +13,9 @@ const testing = std.testing; const assert = std.debug.assert; const Progress = @This(); +const output_buffer_rows = 10; +const output_buffer_cols = 200; + /// `null` if the current node (and its children) should /// not print on update(), refreshes the `output_buffer` nonetheless terminal: ?std.fs.File = undefined, @@ -41,13 +44,9 @@ timer: ?std.time.Timer = null, /// Used to compare with `refresh_rate_ms`. prev_refresh_timestamp: u64 = undefined, -/// This buffer represents the maximum number of bytes written to the terminal -/// with each refresh. -output_buffer: [400]u8 = undefined, - -/// This is the end index for the `output_buffer` -/// that will be used for printing to the terminal. -end: usize = undefined, +/// This buffer represents the maximum number of rows and columns +/// written to the terminal with each refresh. +output_buffer: [output_buffer_rows][output_buffer_cols]u8 = undefined, /// This symbol will be used as a bullet in the tree listing. bullet: u8 = '-', @@ -69,9 +68,8 @@ update_mutex: std.Thread.Mutex = .{}, /// we can move the cursor back later. rows_written: usize = undefined, -/// Keeps track of how many cols in the terminal have been output, so that -/// we can apply truncation if needed. -columns_written: usize = undefined, +/// Keeps track of how many cols in the terminal should be output for each row +columns_written: [output_buffer_rows]usize = undefined, /// Stores the current max width of the terminal. /// If not available then 0. @@ -215,9 +213,8 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N .unprotected_estimated_total_items = estimated_total_items, .unprotected_completed_items = 0, }; - self.end = 0; self.rows_written = 0; - self.columns_written = 0; + self.columns_written[0] = 0; self.max_columns = determineTerminalWidth(self) orelse 0; self.prev_refresh_timestamp = 0; self.timer = std.time.Timer.start() catch null; @@ -281,21 +278,29 @@ fn determineTerminalWidth(self: *Progress) ?usize { return null; } -fn clearWithHeldLock(p: *Progress, end_ptr: *usize) void { +/// Clear previously written data and empty the `output_buffer` +fn clearWithHeldLock(p: *Progress) void { const file = p.terminal orelse return; - var end = end_ptr.*; // restore the cursor position by moving the cursor // `rows_written` cells up, beginning of line, // then clear to the end of the screen if (p.supports_ansi_escape_codes) { + var buffer: [20]u8 = undefined; + var end: usize = 0; + if (p.rows_written == 0) { - end += (std.fmt.bufPrint(p.output_buffer[end..], "\x1b[0G", .{}) catch unreachable).len; // beginning of line - end += (std.fmt.bufPrint(p.output_buffer[end..], "\x1b[0K", .{}) catch unreachable).len; // clear till end of line + end += (std.fmt.bufPrint(buffer[end..], "\x1b[0G", .{}) catch unreachable).len; // beginning of line + end += (std.fmt.bufPrint(buffer[end..], "\x1b[0K", .{}) catch unreachable).len; // clear till end of line } else { - end += (std.fmt.bufPrint(p.output_buffer[end..], "\x1b[{d}F", .{p.rows_written}) catch unreachable).len; // move up and to start - end += (std.fmt.bufPrint(p.output_buffer[end..], "\x1b[0J", .{}) catch unreachable).len; // clear till end of screen + end += (std.fmt.bufPrint(buffer[end..], "\x1b[{d}F", .{p.rows_written}) catch unreachable).len; // move up and to start + end += (std.fmt.bufPrint(buffer[end..], "\x1b[0J", .{}) catch unreachable).len; // clear till end of screen } + + _ = file.write(&buffer) catch { + // stop trying to write to this file + p.terminal = null; + }; } else if (builtin.os.tag == .windows) winapi: { std.debug.assert(p.is_windows_terminal); @@ -342,38 +347,53 @@ fn clearWithHeldLock(p: *Progress, end_ptr: *usize) void { } } else { // we are in a "dumb" terminal like in acme or writing to a file - p.output_buffer[end] = '\n'; - end += 1; + _ = file.write("\n") catch { + // stop trying to write to this file + p.terminal = null; + }; } p.rows_written = 0; - end_ptr.* = end; + p.columns_written[0] = 0; +} + +/// Write the `output_buffer` to the terminal +/// Together with `clearWithHeldLock` this method flushes the buffer +fn writeOutputBufferToFile(p: *Progress) void { + const file = p.terminal orelse return; + + for (p.output_buffer[0 .. p.rows_written + 1], 0..) |output_row, row_index| { + // Join the rows with LFs without requiring a large buffer + if (row_index != 0) { + _ = file.write("\n") catch { + p.terminal = null; + break; + }; + } + _ = file.write(output_row[0..p.columns_written[row_index]]) catch { + // stop trying to write to this file + p.terminal = null; + break; + }; + } } fn refreshWithHeldLock(self: *Progress) void { const is_dumb = !self.supports_ansi_escape_codes and !self.is_windows_terminal; if (is_dumb and self.dont_print_on_dumb) return; - self.end = 0; - self.columns_written = 0; - clearWithHeldLock(self, &self.end); + clearWithHeldLock(self); if (!self.done) { - refreshOutputBufWithHeldLock(self, &self.root, &self.end); + refreshOutputBufWithHeldLock(self, &self.root); } - const file = self.terminal orelse return; - - _ = file.write(self.output_buffer[0..self.end]) catch { - // stop trying to write to this file - self.terminal = null; - }; + writeOutputBufferToFile(self); if (self.timer) |*timer| { self.prev_refresh_timestamp = timer.read(); } } -fn refreshOutputBufWithHeldLock(self: *Progress, node: *Node, end_ptr: *usize) void { - var end = end_ptr.*; +fn refreshOutputBufWithHeldLock(self: *Progress, node: *Node) void { var need_ellipse = false; const eti = @atomicLoad(usize, &node.unprotected_estimated_total_items, .monotonic); @@ -384,8 +404,8 @@ fn refreshOutputBufWithHeldLock(self: *Progress, node: *Node, end_ptr: *usize) v if (node.node_tree_depth > 0) { const depth: usize = @min(10, node.node_tree_depth); const whitespace_length: usize = if (depth > 1) (depth - 1) * 2 else 0; - self.bufWriteInsertNewline(&end); - self.bufWriteLineTruncate(&end, "{s: <[3]}{s}{c} ", .{ + self.bufWriteLineFeed(); + self.bufWrite("{s: <[3]}{s}{c} ", .{ "", if (node.node_tree_depth > 10) "_ " else "", self.bullet, @@ -393,53 +413,46 @@ fn refreshOutputBufWithHeldLock(self: *Progress, node: *Node, end_ptr: *usize) v }); } if (node.name.len != 0) { - self.bufWriteLineTruncate(&end, "{s} ", .{node.name}); + self.bufWrite("{s} ", .{node.name}); need_ellipse = true; } if (eti > 0) { - self.bufWriteLineTruncate(&end, "[{d}/{d}{s}]", .{ current_item, eti, node.unit }); + self.bufWrite("[{d}/{d}{s}]", .{ current_item, eti, node.unit }); need_ellipse = false; } else if (completed_items != 0) { - self.bufWriteLineTruncate(&end, "[{d}{s}]", .{ current_item, node.unit }); + self.bufWrite("[{d}{s}]", .{ current_item, node.unit }); need_ellipse = false; } if (need_ellipse) { - self.bufWriteLineTruncate(&end, "...", .{}); + self.bufWrite("...", .{}); } } for (node.children) |maybe_child| { - if (maybe_child) |child| refreshOutputBufWithHeldLock(self, child, &end); + if (maybe_child) |child| refreshOutputBufWithHeldLock(self, child); } - - end_ptr.* = end; } +/// Print to the terminal, temporarily stopping the progress bar from flushing the buffer pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void { + self.lock_stderr(); + defer self.unlock_stderr(); const file = self.terminal orelse { std.debug.print(format, args); return; }; - self.refresh(); file.writer().print(format, args) catch { self.terminal = null; return; }; - if (self.rows_written > 0) self.rows_written -= 1; } /// Allows the caller to freely write to stderr until unlock_stderr() is called. /// During the lock, the progress information is cleared from the terminal. pub fn lock_stderr(p: *Progress) void { p.update_mutex.lock(); - if (p.terminal) |file| { - var end: usize = 0; - clearWithHeldLock(p, &end); - _ = file.write(p.output_buffer[0..end]) catch { - // stop trying to write to this file - p.terminal = null; - }; - } + clearWithHeldLock(p); + writeOutputBufferToFile(p); std.debug.getStderrMutex().lock(); } @@ -448,39 +461,33 @@ pub fn unlock_stderr(p: *Progress) void { p.update_mutex.unlock(); } -/// Wrapping function around `bufWrite` that cares about the terminal width -fn bufWriteLineTruncate(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void { - comptime assert(std.mem.count(u8, format, "\n") == 0); - const ellipse = "..."; - const start_index = end.*; - if (self.max_columns <= 0 or self.columns_written < self.max_columns) self.bufWrite(end, format, args); - const actual_columns_written = end.* - start_index; - const truncated_end_index = start_index + @min(self.max_columns - self.columns_written, actual_columns_written); - if (self.max_columns > 0) end.* = truncated_end_index; - self.columns_written += truncated_end_index - start_index; - if (self.max_columns > 0 and actual_columns_written + self.columns_written > self.max_columns and end.* >= ellipse.len) { - @memcpy(self.output_buffer[end.* - ellipse.len .. end.*], ellipse); - } +/// Move to the next row in the buffer and reset the cursor position to 0 +fn bufWriteLineFeed(self: *Progress) void { + self.rows_written += 1; + self.columns_written[self.rows_written] = 0; } -/// Wrapping function around `bufWrite` that inserts a new line -fn bufWriteInsertNewline(self: *Progress, end: *usize) void { - self.bufWrite(end, "\n", .{}); - self.columns_written = 0; -} +/// Append to the current row stored in the buffer +fn bufWrite(self: *Progress, comptime format: []const u8, args: anytype) void { + comptime std.debug.assert(std.mem.count(u8, format, "\n") == 0); + const output_row = &self.output_buffer[self.rows_written]; + const columns_written = &self.columns_written[self.rows_written]; -fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void { - if (std.fmt.bufPrint(self.output_buffer[end.*..], format, args)) |written| { - self.rows_written += comptime std.mem.count(u8, format, "\n"); - const amt = written.len; - end.* += amt; + if (std.fmt.bufPrint(output_row[columns_written.*..], format, args)) |written| { + columns_written.* += written.len; } else |err| switch (err) { error.NoSpaceLeft => { - end.* = self.output_buffer.len; + columns_written.* = output_row.len; const suffix = "..."; - @memcpy(self.output_buffer[self.output_buffer.len - suffix.len ..], suffix); + @memcpy(output_row.*[output_row.*.len - suffix.len ..], suffix); }, } + + if (columns_written.* > self.max_columns) { + const ellipse = "..."; + columns_written.* = self.max_columns; + if (columns_written.* >= ellipse.len) @memcpy(output_row.*[columns_written.* - ellipse.len .. columns_written.*], ellipse); + } } test "basic functionality" { From 623b0681444d7fd420a0ace2ff773e0bee7cd04f Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:49:36 +0200 Subject: [PATCH 17/29] std.Progress: add method for deactivating node --- lib/std/Progress.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 0bdfad28fba7..83c5ece0f18d 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -156,6 +156,12 @@ pub const Node = struct { self.context.maybeRefresh(); } + /// Tell the parent node that this node is not being worked on anymore. Thread-safe. + pub fn deactivate(self: *Node) void { + self.tryRemoveFromParentStack(self); + self.context.maybeRefresh(); + } + /// Will also tell the parent node that this node is actively being worked on. Thread-safe. pub fn setName(self: *Node, name: []const u8) void { self.tryPushToParentStack(self); From 7fcfe7948017d4904261fbc6d3203ae4ac59d08d Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Mon, 29 Apr 2024 18:01:00 +0200 Subject: [PATCH 18/29] std.Progress: prevent unnecessary iterations --- lib/std/Progress.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 83c5ece0f18d..da0eb888394f 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -435,7 +435,7 @@ fn refreshOutputBufWithHeldLock(self: *Progress, node: *Node) void { } for (node.children) |maybe_child| { - if (maybe_child) |child| refreshOutputBufWithHeldLock(self, child); + if (maybe_child) |child| refreshOutputBufWithHeldLock(self, child) else break; } } From 0cbc6012e9e2d256db4edce72b102b19723359ff Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Tue, 30 Apr 2024 03:42:37 +0200 Subject: [PATCH 19/29] doctest: add support for more ansi escape sequences --- tools/doctest.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/doctest.zig b/tools/doctest.zig index 6be9d59ffbd0..b63796e1620f 100644 --- a/tools/doctest.zig +++ b/tools/doctest.zig @@ -1011,7 +1011,11 @@ fn termColor(allocator: Allocator, input: []const u8) ![]u8 { }, .after_number => switch (c) { ';' => state = .arg, - 'D' => state = .start, + 'A'...'G' => state = .start, + 'J' => { + buf.items.len = last_new_line; + state = .start; + }, 'K' => { buf.items.len = last_new_line; state = .start; From acb5067f260249a56c39ce5a1e8c1830d4f429b6 Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Tue, 30 Apr 2024 03:44:25 +0200 Subject: [PATCH 20/29] std.Progress: missing check for `max_columns` is 0 --- lib/std/Progress.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index da0eb888394f..7bba6c86a2dc 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -489,7 +489,7 @@ fn bufWrite(self: *Progress, comptime format: []const u8, args: anytype) void { }, } - if (columns_written.* > self.max_columns) { + if (self.max_columns != 0 and columns_written.* > self.max_columns) { const ellipse = "..."; columns_written.* = self.max_columns; if (columns_written.* >= ellipse.len) @memcpy(output_row.*[columns_written.* - ellipse.len .. columns_written.*], ellipse); From f4258a22543de3f013325c98c8e86fa02e385a96 Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Tue, 30 Apr 2024 03:46:10 +0200 Subject: [PATCH 21/29] std.Progress: double `output_buffer` row limit --- lib/std/Progress.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 7bba6c86a2dc..28d9938bffe2 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -13,7 +13,7 @@ const testing = std.testing; const assert = std.debug.assert; const Progress = @This(); -const output_buffer_rows = 10; +const output_buffer_rows = 20; const output_buffer_cols = 200; /// `null` if the current node (and its children) should From d29ca27748f8252a132cd044a6a6d73a8eae173b Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Tue, 30 Apr 2024 03:47:49 +0200 Subject: [PATCH 22/29] frontend: migrate to new backend of std.Progress The internal structure of `std.Progress` changed and due to the low-level approach of `main.zig` this leads to issues. This commit makes use of methods provided by `std.Progress` and `serveStringMessage` to resolve these issues. --- lib/std/zig/Server.zig | 2 +- src/main.zig | 35 +++++++++++++---------------------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/lib/std/zig/Server.zig b/lib/std/zig/Server.zig index 10e14a55fc68..8f93d9b8cafa 100644 --- a/lib/std/zig/Server.zig +++ b/lib/std/zig/Server.zig @@ -134,7 +134,7 @@ pub fn receiveBody_u32(s: *Server) !u32 { return bswap(result); } -pub fn serveStringMessage(s: *Server, tag: OutMessage.Tag, msg: []const u8) !void { +pub fn serveStringMessage(s: *const Server, tag: OutMessage.Tag, msg: []const u8) !void { return s.serveMessage(.{ .tag = tag, .bytes_len = @as(u32, @intCast(msg.len)), diff --git a/src/main.zig b/src/main.zig index 3b7233c61dfd..8f6f269a317a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4024,20 +4024,9 @@ fn serve( var progress: std.Progress = .{ .terminal = null, - .root = .{ - .context = undefined, - .parent = null, - .name = "", - .unprotected_estimated_total_items = 0, - .unprotected_completed_items = 0, - }, - .columns_written = 0, - .prev_refresh_timestamp = 0, - .timer = null, - .done = false, }; - const main_progress_node = &progress.root; - main_progress_node.context = &progress; + const main_progress_node = progress.start("", 0); + progress.timer = null; while (true) { const hdr = try server.receiveMessage(); @@ -4151,14 +4140,17 @@ fn progressThread(progress: *std.Progress, server: *const Server, reset: *std.Th progress.refresh(); - server.serveMessage(.{ - .tag = .progress, - .bytes_len = @as(u32, @intCast(progress.end)), - }, &.{ - progress.output_buffer[0..progress.end], - }) catch |err| { - fatal("unable to write to client: {s}", .{@errorName(err)}); - }; + // TODO: Provisional. This should be better integrated. + for (progress.output_buffer[0 .. progress.rows_written + 1], 0..) |output_row, row_index| { + if (row_index != 0) { + server.serveStringMessage(.progress, "\n") catch |err| { + fatal("unable to write to client: {s}", .{@errorName(err)}); + }; + } + server.serveStringMessage(.progress, output_row[0..progress.columns_written[row_index]]) catch |err| { + fatal("unable to write to client: {s}", .{@errorName(err)}); + }; + } } } @@ -5440,7 +5432,6 @@ fn jitCmd( .unprotected_estimated_total_items = 0, .unprotected_completed_items = 0, }, - .columns_written = 0, .prev_refresh_timestamp = 0, .timer = null, .done = false, From b5510ce0bf4d415b66204c1ac2ec18586e37825e Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:42:24 +0200 Subject: [PATCH 23/29] std.Progress: skip newline on node without info This commit addresses the issue of nodes without name or eta causing a newline. The resulting output is unexpected. --- lib/std/Progress.zig | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 28d9938bffe2..7b623716bd05 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -390,7 +390,8 @@ fn refreshWithHeldLock(self: *Progress) void { clearWithHeldLock(self); if (!self.done) { - refreshOutputBufWithHeldLock(self, &self.root); + var need_newline = false; + refreshOutputBufWithHeldLock(self, &self.root, &need_newline); } writeOutputBufferToFile(self); @@ -399,7 +400,7 @@ fn refreshWithHeldLock(self: *Progress) void { } } -fn refreshOutputBufWithHeldLock(self: *Progress, node: *Node) void { +fn refreshOutputBufWithHeldLock(self: *Progress, node: *Node, need_newline: *bool) void { var need_ellipse = false; const eti = @atomicLoad(usize, &node.unprotected_estimated_total_items, .monotonic); @@ -407,10 +408,13 @@ fn refreshOutputBufWithHeldLock(self: *Progress, node: *Node) void { const current_item = completed_items + 1; if (node.name.len != 0 or eti > 0) { + if (need_newline.*) { + self.bufWriteLineFeed(); + need_newline.* = false; + } if (node.node_tree_depth > 0) { const depth: usize = @min(10, node.node_tree_depth); const whitespace_length: usize = if (depth > 1) (depth - 1) * 2 else 0; - self.bufWriteLineFeed(); self.bufWrite("{s: <[3]}{s}{c} ", .{ "", if (node.node_tree_depth > 10) "_ " else "", @@ -432,10 +436,12 @@ fn refreshOutputBufWithHeldLock(self: *Progress, node: *Node) void { if (need_ellipse) { self.bufWrite("...", .{}); } + + need_newline.* = true; } for (node.children) |maybe_child| { - if (maybe_child) |child| refreshOutputBufWithHeldLock(self, child) else break; + if (maybe_child) |child| refreshOutputBufWithHeldLock(self, child, need_newline) else break; } } From 7bc2e15a165e4734ca69097c47329d7deed77dd7 Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Tue, 30 Apr 2024 17:02:09 +0200 Subject: [PATCH 24/29] std.Progress: option to emulate old progress bar This commit serves as a preparation for restoring the original CI compilation output since the frontend is not making use of the new behavior yet. --- lib/std/Progress.zig | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 7b623716bd05..ec7d6dccb273 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -75,6 +75,9 @@ columns_written: [output_buffer_rows]usize = undefined, /// If not available then 0. max_columns: usize = undefined, +/// Replicate the old one-line style progress bar +emulate_one_line_bar: bool = false, + /// Represents one unit of progress. Each node can have children nodes, or /// one can use integers with `update`. pub const Node = struct { @@ -96,10 +99,14 @@ pub const Node = struct { /// Push this `Node` to the `parent.children` stack of the provided `Node` (insert at first index). Thread-safe fn tryPushToParentStack(self: *Node, target_node: *Node) void { const parent = target_node.parent orelse return; - inline for (parent.children) |child| if (child == self) return; + if (self.context.emulate_one_line_bar) { + if (parent.children[0] == self) return; + } else { + inline for (parent.children) |child| if (child == self) return; + } self.context.update_mutex.lock(); // lock below existence check for slight performance reasons defer self.context.update_mutex.unlock(); // (downside: less precision, but not noticeable) - std.mem.copyBackwards(?*Node, parent.children[1..], parent.children[0 .. parent.children.len - 1]); + if (!self.context.emulate_one_line_bar) std.mem.copyBackwards(?*Node, parent.children[1..], parent.children[0 .. parent.children.len - 1]); parent.children[0] = self; } @@ -108,9 +115,13 @@ pub const Node = struct { const parent = target_node.parent orelse return; self.context.update_mutex.lock(); defer self.context.update_mutex.unlock(); - const index = std.mem.indexOfScalar(?*Node, parent.children[0..], self) orelse return; - std.mem.copyBackwards(?*Node, parent.children[index..], parent.children[index + 1 ..]); - parent.children[parent.children.len - 1] = null; + if (self.context.emulate_one_line_bar) { + parent.children[0] = null; + } else { + const index = std.mem.indexOfScalar(?*Node, parent.children[0..], self) orelse return; + std.mem.copyBackwards(?*Node, parent.children[index..], parent.children[index + 1 ..]); + parent.children[parent.children.len - 1] = null; + } } /// Create a new child progress node. Thread-safe. @@ -354,7 +365,6 @@ fn clearWithHeldLock(p: *Progress) void { } else { // we are in a "dumb" terminal like in acme or writing to a file _ = file.write("\n") catch { - // stop trying to write to this file p.terminal = null; }; } @@ -409,10 +419,14 @@ fn refreshOutputBufWithHeldLock(self: *Progress, node: *Node, need_newline: *boo if (node.name.len != 0 or eti > 0) { if (need_newline.*) { - self.bufWriteLineFeed(); + if (self.emulate_one_line_bar) { + self.bufWrite(" ", .{}); + } else { + self.bufWriteLineFeed(); + } need_newline.* = false; } - if (node.node_tree_depth > 0) { + if (node.node_tree_depth > 0 and !self.emulate_one_line_bar) { const depth: usize = @min(10, node.node_tree_depth); const whitespace_length: usize = if (depth > 1) (depth - 1) * 2 else 0; self.bufWrite("{s: <[3]}{s}{c} ", .{ @@ -440,7 +454,7 @@ fn refreshOutputBufWithHeldLock(self: *Progress, node: *Node, need_newline: *boo need_newline.* = true; } - for (node.children) |maybe_child| { + for (node.children[0..if (self.emulate_one_line_bar) 1 else node.children.len]) |maybe_child| { if (maybe_child) |child| refreshOutputBufWithHeldLock(self, child, need_newline) else break; } } From 6b037fc317b2d01bce59704894a99c684524af5a Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Tue, 30 Apr 2024 20:41:10 +0200 Subject: [PATCH 25/29] std.Progress: ansi buffer write to end --- lib/std/Progress.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index ec7d6dccb273..7c91c0608ae7 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -314,7 +314,7 @@ fn clearWithHeldLock(p: *Progress) void { end += (std.fmt.bufPrint(buffer[end..], "\x1b[0J", .{}) catch unreachable).len; // clear till end of screen } - _ = file.write(&buffer) catch { + _ = file.write(buffer[0..end]) catch { // stop trying to write to this file p.terminal = null; }; From 59e3c2dc2068590f5c8793d030f55ace3494e8c2 Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Tue, 30 Apr 2024 20:43:02 +0200 Subject: [PATCH 26/29] frontend: enable one-line progress bar everywhere Emulate the old progress bar everywhere to provide a decently seamless migration to the new progress bar. In addition, this commit makes it easier to manually initiate a progress bar without using the `start` method. --- lib/compiler/test_runner.zig | 1 + lib/std/Progress.zig | 9 +++------ src/main.zig | 21 ++++++++++++++++----- test/src/Cases.zig | 2 +- tools/update_cpu_features.zig | 2 +- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig index 0b9a060fb573..9e08932bbb5f 100644 --- a/lib/compiler/test_runner.zig +++ b/lib/compiler/test_runner.zig @@ -129,6 +129,7 @@ fn mainTerminal() void { var fail_count: usize = 0; var progress = std.Progress{ .dont_print_on_dumb = true, + .emulate_one_line_bar = true, }; const root_node = progress.start("Test", test_fn_list.len); const have_tty = progress.terminal != null and diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 7c91c0608ae7..e71a77d3610a 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -42,7 +42,7 @@ timer: ?std.time.Timer = null, /// When the previous refresh was written to the terminal. /// Used to compare with `refresh_rate_ms`. -prev_refresh_timestamp: u64 = undefined, +prev_refresh_timestamp: u64 = 0, /// This buffer represents the maximum number of rows and columns /// written to the terminal with each refresh. @@ -66,10 +66,10 @@ update_mutex: std.Thread.Mutex = .{}, /// Keeps track of how many rows in the terminal have been output, so that /// we can move the cursor back later. -rows_written: usize = undefined, +rows_written: usize = 0, /// Keeps track of how many cols in the terminal should be output for each row -columns_written: [output_buffer_rows]usize = undefined, +columns_written: [output_buffer_rows]usize = [_]usize{0} ** output_buffer_rows, /// Stores the current max width of the terminal. /// If not available then 0. @@ -230,10 +230,7 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N .unprotected_estimated_total_items = estimated_total_items, .unprotected_completed_items = 0, }; - self.rows_written = 0; - self.columns_written[0] = 0; self.max_columns = determineTerminalWidth(self) orelse 0; - self.prev_refresh_timestamp = 0; self.timer = std.time.Timer.start() catch null; self.done = false; return &self.root; diff --git a/src/main.zig b/src/main.zig index 8f6f269a317a..bf47565f2ee8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4024,9 +4024,19 @@ fn serve( var progress: std.Progress = .{ .terminal = null, + .root = .{ + .context = undefined, + .parent = null, + .name = "", + .unprotected_estimated_total_items = 0, + .unprotected_completed_items = 0, + }, + .timer = null, + .done = false, + .emulate_one_line_bar = true, }; - const main_progress_node = progress.start("", 0); - progress.timer = null; + const main_progress_node = &progress.root; + main_progress_node.context = &progress; while (true) { const hdr = try server.receiveMessage(); @@ -4423,7 +4433,7 @@ fn runOrTestHotSwap( fn updateModule(comp: *Compilation, color: Color) !void { { // If the terminal is dumb, we dont want to show the user all the output. - var progress: std.Progress = .{ .dont_print_on_dumb = true }; + var progress: std.Progress = .{ .dont_print_on_dumb = true, .emulate_one_line_bar = true }; const main_progress_node = progress.start("", 0); defer main_progress_node.end(); switch (color) { @@ -4687,7 +4697,7 @@ const usage_build = ; fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { - var progress: std.Progress = .{ .dont_print_on_dumb = true }; + var progress: std.Progress = .{ .dont_print_on_dumb = true, .emulate_one_line_bar = true }; var build_file: ?[]const u8 = null; var override_lib_dir: ?[]const u8 = try EnvVar.ZIG_LIB_DIR.get(arena); @@ -5435,6 +5445,7 @@ fn jitCmd( .prev_refresh_timestamp = 0, .timer = null, .done = false, + .emulate_one_line_bar = true, }; const main_progress_node = &progress.root; main_progress_node.context = &progress; @@ -6768,7 +6779,7 @@ fn cmdFetch( try http_client.initDefaultProxies(arena); - var progress: std.Progress = .{ .dont_print_on_dumb = true }; + var progress: std.Progress = .{ .dont_print_on_dumb = true, .emulate_one_line_bar = true }; const root_prog_node = progress.start("Fetch", 0); defer root_prog_node.end(); diff --git a/test/src/Cases.zig b/test/src/Cases.zig index f84d0d61dbf5..3fae06e0432b 100644 --- a/test/src/Cases.zig +++ b/test/src/Cases.zig @@ -1438,7 +1438,7 @@ fn resolveTargetQuery(query: std.Target.Query) std.Build.ResolvedTarget { fn runCases(self: *Cases, zig_exe_path: []const u8) !void { const host = try std.zig.system.resolveTargetQuery(.{}); - var progress = std.Progress{}; + var progress = std.Progress{ .emulate_one_line_bar = true }; const root_node = progress.start("compiler", self.cases.items.len); progress.terminal = null; defer root_node.end(); diff --git a/tools/update_cpu_features.zig b/tools/update_cpu_features.zig index 662f28c6f75d..fe6efdebfe34 100644 --- a/tools/update_cpu_features.zig +++ b/tools/update_cpu_features.zig @@ -1022,7 +1022,7 @@ pub fn main() anyerror!void { var zig_src_dir = try fs.cwd().openDir(zig_src_root, .{}); defer zig_src_dir.close(); - var progress = std.Progress{}; + var progress = std.Progress{ .emulate_one_line_bar = true }; const root_progress = progress.start("", llvm_targets.len); defer root_progress.end(); From 13c3f6954de774cd7eb79e3b09ebf65439c3942e Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Tue, 30 Apr 2024 21:22:10 +0200 Subject: [PATCH 27/29] std.Progress: ignore newline if buffer is full --- lib/std/Progress.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index e71a77d3610a..6fe0c8b92dc0 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -485,7 +485,9 @@ pub fn unlock_stderr(p: *Progress) void { } /// Move to the next row in the buffer and reset the cursor position to 0 +/// Ignores request if buffer is full fn bufWriteLineFeed(self: *Progress) void { + if (self.rows_written + 1 >= self.columns_written.len) return; self.rows_written += 1; self.columns_written[self.rows_written] = 0; } From 41f3adcdd008724e182715f6257a1ae51c88cc0e Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Wed, 1 May 2024 16:41:25 +0200 Subject: [PATCH 28/29] std.Progress: option to disregard terminal width --- lib/std/Progress.zig | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 6fe0c8b92dc0..89ed2f8c0c5a 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -73,7 +73,10 @@ columns_written: [output_buffer_rows]usize = [_]usize{0} ** output_buffer_rows, /// Stores the current max width of the terminal. /// If not available then 0. -max_columns: usize = undefined, +max_columns: usize = 0, + +/// Disable or enable truncating the buffer to the terminal width +respect_terminal_width: bool = true, /// Replicate the old one-line style progress bar emulate_one_line_bar: bool = false, @@ -230,7 +233,7 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N .unprotected_estimated_total_items = estimated_total_items, .unprotected_completed_items = 0, }; - self.max_columns = determineTerminalWidth(self) orelse 0; + if (self.respect_terminal_width) self.max_columns = determineTerminalWidth(self) orelse 0; self.timer = std.time.Timer.start() catch null; self.done = false; return &self.root; @@ -239,7 +242,7 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N /// Updates the terminal if enough time has passed since last update. Thread-safe. pub fn maybeRefresh(self: *Progress) void { if (self.timer) |*timer| { - self.max_columns = determineTerminalWidth(self) orelse 0; + if (self.respect_terminal_width) self.max_columns = determineTerminalWidth(self) orelse 0; if (!self.update_mutex.tryLock()) return; defer self.update_mutex.unlock(); maybeRefreshWithHeldLock(self, timer); From 3cb4e80f3406e54daf2b102579815249a8a8e2ed Mon Sep 17 00:00:00 2001 From: DISTREAT <99132213+DISTREAT@users.noreply.github.com> Date: Wed, 1 May 2024 16:41:48 +0200 Subject: [PATCH 29/29] std.Progress: do not assume windows is always tty This patch removes assert statements in windows that were causing issues due to the assumption that every `terminal` is a TTY. --- lib/std/Progress.zig | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 89ed2f8c0c5a..2039f3e8f2f1 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -284,7 +284,7 @@ fn determineTerminalWidth(self: *Progress) ?usize { return @intCast(window_size.ws_col); }, .windows => { - std.debug.assert(self.is_windows_terminal); + if (!self.is_windows_terminal) return null; var screen_buffer_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; const exit_code = windows.kernel32.GetConsoleScreenBufferInfo(self.terminal.?.handle, &screen_buffer_info); if (exit_code != windows.TRUE) return null; @@ -318,9 +318,7 @@ fn clearWithHeldLock(p: *Progress) void { // stop trying to write to this file p.terminal = null; }; - } else if (builtin.os.tag == .windows) winapi: { - std.debug.assert(p.is_windows_terminal); - + } else if (builtin.os.tag == .windows and p.is_windows_terminal) winapi: { var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE) { // stop trying to write to this file