Commit aa6ef10cc6
Changed files (6)
lib/std/special/test_runner.zig
@@ -36,7 +36,7 @@ pub fn main() anyerror!void {
}
std.testing.log_level = .warn;
- var test_node = root_node.start(test_fn.name, null);
+ var test_node = root_node.start(test_fn.name, 0);
test_node.activate();
progress.refresh();
if (progress.terminal == null) {
lib/std/Progress.zig
@@ -3,263 +3,298 @@
// This file is part of [zig](https://ziglang.org/), which is MIT licensed.
// The MIT license requires this copyright notice to be included in all copies
// and substantial portions of the software.
+
+//! This API non-allocating, non-fallible, and thread-safe.
+//! The tradeoff is that users of this API must provide the storage
+//! for each `Progress.Node`.
+//!
+//! Initialize the struct directly, overriding these fields as desired:
+//! * `refresh_rate_ms`
+//! * `initial_delay_ms`
+
const std = @import("std");
const windows = std.os.windows;
const testing = std.testing;
const assert = std.debug.assert;
+const Progress = @This();
-/// This API is non-allocating and non-fallible. The tradeoff is that users of
-/// this API must provide the storage for each `Progress.Node`.
-/// Initialize the struct directly, overriding these fields as desired:
-/// * `refresh_rate_ms`
-/// * `initial_delay_ms`
-pub const Progress = struct {
- /// `null` if the current node (and its children) should
- /// not print on update()
- terminal: ?std.fs.File = undefined,
-
- /// Whether the terminal supports ANSI escape codes.
- supports_ansi_escape_codes: bool = false,
-
- root: Node = undefined,
-
- /// Keeps track of how much time has passed since the beginning.
- /// Used to compare with `initial_delay_ms` and `refresh_rate_ms`.
- timer: std.time.Timer = undefined,
-
- /// When the previous refresh was written to the terminal.
- /// 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: [100]u8 = undefined,
-
- /// How many nanoseconds between writing updates to the terminal.
- refresh_rate_ns: u64 = 50 * std.time.ns_per_ms,
-
- /// How many nanoseconds to keep the output hidden
- initial_delay_ns: u64 = 500 * std.time.ns_per_ms,
-
- done: bool = true,
-
- /// Keeps track of how many columns in the terminal have been output, so that
- /// we can move the cursor back later.
- columns_written: usize = undefined,
-
- /// Represents one unit of progress. Each node can have children nodes, or
- /// one can use integers with `update`.
- pub const Node = struct {
- context: *Progress,
- parent: ?*Node,
- completed_items: usize,
- name: []const u8,
- recently_updated_child: ?*Node = null,
-
- /// This field may be updated freely.
- estimated_total_items: ?usize,
-
- /// Create a new child progress node.
- /// 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.
- /// Until that is fixed you probably want to call `activate` on the return value.
- pub fn start(self: *Node, name: []const u8, estimated_total_items: ?usize) Node {
- return Node{
- .context = self.context,
- .parent = self,
- .completed_items = 0,
- .name = name,
- .estimated_total_items = estimated_total_items,
- };
- }
+/// `null` if the current node (and its children) should
+/// not print on update()
+terminal: ?std.fs.File = undefined,
- /// This is the same as calling `start` and then `end` on the returned `Node`.
- pub fn completeOne(self: *Node) void {
- if (self.parent) |parent| parent.recently_updated_child = self;
- self.completed_items += 1;
- self.context.maybeRefresh();
- }
+/// Whether the terminal supports ANSI escape codes.
+supports_ansi_escape_codes: bool = false,
- pub fn end(self: *Node) void {
- self.context.maybeRefresh();
- if (self.parent) |parent| {
- if (parent.recently_updated_child) |parent_child| {
- if (parent_child == self) {
- parent.recently_updated_child = null;
- }
- }
- parent.completeOne();
- } else {
- self.context.done = true;
- self.context.refresh();
- }
- }
+root: Node = undefined,
- /// Tell the parent node that this node is actively being worked on.
- pub fn activate(self: *Node) void {
- if (self.parent) |parent| parent.recently_updated_child = self;
- }
- };
+/// Keeps track of how much time has passed since the beginning.
+/// Used to compare with `initial_delay_ms` and `refresh_rate_ms`.
+timer: std.time.Timer = undefined,
+
+/// When the previous refresh was written to the terminal.
+/// 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: [100]u8 = undefined,
+
+/// How many nanoseconds between writing updates to the terminal.
+refresh_rate_ns: u64 = 50 * std.time.ns_per_ms,
+
+/// How many nanoseconds to keep the output hidden
+initial_delay_ns: u64 = 500 * std.time.ns_per_ms,
+
+done: bool = true,
+
+/// Protects the `refresh` function, as well as `node.recently_updated_child`.
+/// Without this, callsites would call `Node.end` and then free `Node` memory
+/// while it was still being accessed by the `refresh` function.
+update_lock: std.Mutex = .{},
+
+/// Keeps track of how many columns in the terminal have been output, so that
+/// we can move the cursor back later.
+columns_written: usize = undefined,
- /// Create a new progress node.
+/// Represents one unit of progress. Each node can have children nodes, or
+/// one can use integers with `update`.
+pub const Node = struct {
+ context: *Progress,
+ parent: ?*Node,
+ name: []const u8,
+ /// Must be handled atomically to be thread-safe.
+ recently_updated_child: ?*Node = null,
+ /// 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,
+
+ /// 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 return Progress rather than accept it as a parameter.
- pub fn start(self: *Progress, name: []const u8, estimated_total_items: ?usize) !*Node {
- const stderr = std.io.getStdErr();
- self.terminal = null;
- if (stderr.supportsAnsiEscapeCodes()) {
- self.terminal = stderr;
- self.supports_ansi_escape_codes = true;
- } else if (std.builtin.os.tag == .windows and stderr.isTty()) {
- self.terminal = stderr;
- }
- self.root = Node{
- .context = self,
- .parent = null,
- .completed_items = 0,
+ /// API to set `self.parent.recently_updated_child` 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 {
+ return Node{
+ .context = self.context,
+ .parent = self,
.name = name,
- .estimated_total_items = estimated_total_items,
+ .unprotected_estimated_total_items = estimated_total_items,
+ .unprotected_completed_items = 0,
};
- self.columns_written = 0;
- self.prev_refresh_timestamp = 0;
- self.timer = try std.time.Timer.start();
- self.done = false;
- return &self.root;
}
- /// Updates the terminal if enough time has passed since last update.
- pub fn maybeRefresh(self: *Progress) void {
- const now = self.timer.read();
- if (now < self.initial_delay_ns) return;
- if (now - self.prev_refresh_timestamp < self.refresh_rate_ns) return;
- self.refresh();
+ /// This is the same as calling `start` and then `end` on the returned `Node`. Thread-safe.
+ pub fn completeOne(self: *Node) void {
+ self.activate();
+ _ = @atomicRmw(usize, &self.unprotected_completed_items, .Add, 1, .Monotonic);
+ self.context.maybeRefresh();
}
- /// Updates the terminal and resets `self.next_refresh_timestamp`.
- pub fn refresh(self: *Progress) void {
- const file = self.terminal orelse return;
-
- const prev_columns_written = self.columns_written;
- var end: usize = 0;
- if (self.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 (self.supports_ansi_escape_codes) {
- end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[{}D", .{self.columns_written}) catch unreachable).len;
- end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[0K", .{}) catch unreachable).len;
- } else if (std.builtin.os.tag == .windows) winapi: {
- var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
- if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE)
- unreachable;
-
- var cursor_pos = windows.COORD{
- .X = info.dwCursorPosition.X - @intCast(windows.SHORT, self.columns_written),
- .Y = info.dwCursorPosition.Y,
- };
-
- if (cursor_pos.X < 0)
- cursor_pos.X = 0;
-
- const fill_chars = @intCast(windows.DWORD, 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.
- self.terminal = null;
- break :winapi;
- }
- if (windows.kernel32.FillConsoleOutputCharacterA(
- file.handle,
- ' ',
- fill_chars,
- cursor_pos,
- &written,
- ) != windows.TRUE) unreachable;
-
- if (windows.kernel32.SetConsoleCursorPosition(file.handle, cursor_pos) != windows.TRUE)
- unreachable;
- } else unreachable;
-
- self.columns_written = 0;
+ /// Finish a started `Node`. Thread-safe.
+ pub fn end(self: *Node) void {
+ self.context.maybeRefresh();
+ if (self.parent) |parent| {
+ {
+ const held = self.context.update_lock.acquire();
+ defer held.release();
+ _ = @cmpxchgStrong(?*Node, &parent.recently_updated_child, self, null, .Monotonic, .Monotonic);
+ }
+ parent.completeOne();
+ } else {
+ self.context.done = true;
+ self.context.refresh();
}
+ }
- 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;
- if (node.name.len != 0 or node.estimated_total_items != null) {
- if (node.name.len != 0) {
- self.bufWrite(&end, "{}", .{node.name});
- need_ellipse = true;
- }
- if (node.estimated_total_items) |total| {
- if (need_ellipse) self.bufWrite(&end, " ", .{});
- self.bufWrite(&end, "[{}/{}] ", .{ node.completed_items + 1, total });
- need_ellipse = false;
- } else if (node.completed_items != 0) {
- if (need_ellipse) self.bufWrite(&end, " ", .{});
- self.bufWrite(&end, "[{}] ", .{node.completed_items + 1});
- need_ellipse = false;
- }
- }
- maybe_node = node.recently_updated_child;
- }
- if (need_ellipse) {
- self.bufWrite(&end, "... ", .{});
- }
+ /// 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, .Monotonic);
}
+ }
- _ = file.write(self.output_buffer[0..end]) catch |e| {
- // Stop trying to write to this file once it errors.
- self.terminal = null;
- };
- self.prev_refresh_timestamp = self.timer.read();
+ /// Thread-safe. 0 means unknown.
+ pub fn setEstimatedTotalItems(self: *Node, count: usize) void {
+ @atomicStore(usize, &self.unprotected_estimated_total_items, count, .Monotonic);
}
- pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void {
- const file = self.terminal orelse return;
- self.refresh();
- file.outStream().print(format, args) catch {
- self.terminal = null;
- return;
- };
+ /// Thread-safe.
+ pub fn setCompletedItems(self: *Node, completed_items: usize) void {
+ @atomicStore(usize, &self.unprotected_completed_items, completed_items, .Monotonic);
+ }
+};
+
+/// Create a new progress node.
+/// Call `Node.end` when done.
+/// TODO solve https://github.com/ziglang/zig/issues/2765 and then change this
+/// API to return Progress rather than accept it as a parameter.
+/// `estimated_total_items` value of 0 means unknown.
+pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) !*Node {
+ const stderr = std.io.getStdErr();
+ self.terminal = null;
+ if (stderr.supportsAnsiEscapeCodes()) {
+ self.terminal = stderr;
+ self.supports_ansi_escape_codes = true;
+ } else if (std.builtin.os.tag == .windows and stderr.isTty()) {
+ self.terminal = stderr;
+ }
+ self.root = Node{
+ .context = self,
+ .parent = null,
+ .name = name,
+ .unprotected_estimated_total_items = estimated_total_items,
+ .unprotected_completed_items = 0,
+ };
+ self.columns_written = 0;
+ self.prev_refresh_timestamp = 0;
+ self.timer = try std.time.Timer.start();
+ self.done = false;
+ return &self.root;
+}
+
+/// Updates the terminal if enough time has passed since last update. Thread-safe.
+pub fn maybeRefresh(self: *Progress) void {
+ const now = self.timer.read();
+ if (now < self.initial_delay_ns) return;
+ const held = self.update_lock.tryAcquire() orelse return;
+ defer held.release();
+ if (now - self.prev_refresh_timestamp < self.refresh_rate_ns) return;
+ return self.refreshWithHeldLock();
+}
+
+/// Updates the terminal and resets `self.next_refresh_timestamp`. Thread-safe.
+pub fn refresh(self: *Progress) void {
+ const held = self.update_lock.tryAcquire() orelse return;
+ defer held.release();
+
+ return self.refreshWithHeldLock();
+}
+
+fn refreshWithHeldLock(self: *Progress) void {
+ const file = self.terminal orelse return;
+
+ const prev_columns_written = self.columns_written;
+ var end: usize = 0;
+ if (self.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 (self.supports_ansi_escape_codes) {
+ end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[{d}D", .{self.columns_written}) catch unreachable).len;
+ end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[0K", .{}) catch unreachable).len;
+ } else if (std.builtin.os.tag == .windows) winapi: {
+ var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
+ if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE)
+ unreachable;
+
+ var cursor_pos = windows.COORD{
+ .X = info.dwCursorPosition.X - @intCast(windows.SHORT, self.columns_written),
+ .Y = info.dwCursorPosition.Y,
+ };
+
+ if (cursor_pos.X < 0)
+ cursor_pos.X = 0;
+
+ const fill_chars = @intCast(windows.DWORD, 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.
+ self.terminal = null;
+ break :winapi;
+ }
+ if (windows.kernel32.FillConsoleOutputCharacterA(
+ file.handle,
+ ' ',
+ fill_chars,
+ cursor_pos,
+ &written,
+ ) != windows.TRUE) unreachable;
+
+ if (windows.kernel32.SetConsoleCursorPosition(file.handle, cursor_pos) != windows.TRUE)
+ unreachable;
+ } else unreachable;
+
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| {
- 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;
- },
+ 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);
+ 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}] ", .{ completed_items + 1, eti });
+ need_ellipse = false;
+ } else if (completed_items != 0) {
+ if (need_ellipse) self.bufWrite(&end, " ", .{});
+ self.bufWrite(&end, "[{d}] ", .{completed_items + 1});
+ need_ellipse = false;
+ }
+ }
+ maybe_node = @atomicLoad(?*Node, &node.recently_updated_child, .Monotonic);
}
- const bytes_needed_for_esc_codes_at_end = if (std.builtin.os.tag == .windows) 0 else 11;
- const max_end = self.output_buffer.len - bytes_needed_for_esc_codes_at_end;
- if (end.* > max_end) {
- const suffix = "... ";
- self.columns_written = self.columns_written - (end.* - max_end) + suffix.len;
- std.mem.copy(u8, self.output_buffer[max_end..], suffix);
- end.* = max_end + suffix.len;
+ if (need_ellipse) {
+ self.bufWrite(&end, "... ", .{});
}
}
-};
+
+ _ = file.write(self.output_buffer[0..end]) catch |e| {
+ // Stop trying to write to this file once it errors.
+ self.terminal = null;
+ };
+ self.prev_refresh_timestamp = self.timer.read();
+}
+
+pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void {
+ const file = self.terminal orelse return;
+ self.refresh();
+ file.outStream().print(format, args) catch {
+ self.terminal = null;
+ return;
+ };
+ 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| {
+ 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 bytes_needed_for_esc_codes_at_end = if (std.builtin.os.tag == .windows) 0 else 11;
+ const max_end = self.output_buffer.len - bytes_needed_for_esc_codes_at_end;
+ if (end.* > max_end) {
+ const suffix = "... ";
+ self.columns_written = self.columns_written - (end.* - max_end) + suffix.len;
+ std.mem.copy(u8, self.output_buffer[max_end..], suffix);
+ end.* = max_end + suffix.len;
+ }
+}
test "basic functionality" {
var disable = true;
@@ -300,7 +335,7 @@ test "basic functionality" {
std.time.sleep(5 * std.time.ns_per_ms);
}
{
- var node = root_node.start("this is a really long name designed to activate the truncation code. let's find out if it works", null);
+ var node = root_node.start("this is a really long name designed to activate the truncation code. let's find out if it works", 0);
node.activate();
std.time.sleep(10 * std.time.ns_per_ms);
progress.refresh();
lib/std/std.zig
@@ -29,7 +29,7 @@ pub const PackedIntArrayEndian = @import("packed_int_array.zig").PackedIntArrayE
pub const PackedIntSlice = @import("packed_int_array.zig").PackedIntSlice;
pub const PackedIntSliceEndian = @import("packed_int_array.zig").PackedIntSliceEndian;
pub const PriorityQueue = @import("priority_queue.zig").PriorityQueue;
-pub const Progress = @import("progress.zig").Progress;
+pub const Progress = @import("Progress.zig");
pub const ResetEvent = @import("reset_event.zig").ResetEvent;
pub const SemanticVersion = @import("SemanticVersion.zig");
pub const SinglyLinkedList = @import("linked_list.zig").SinglyLinkedList;
src/Compilation.zig
@@ -1378,7 +1378,7 @@ pub fn getAllErrorsAlloc(self: *Compilation) !AllErrors {
pub fn performAllTheWork(self: *Compilation) error{ TimerUnsupported, OutOfMemory }!void {
var progress: std.Progress = .{};
- var main_progress_node = try progress.start("", null);
+ var main_progress_node = try progress.start("", 0);
defer main_progress_node.end();
if (self.color == .off) progress.terminal = null;
@@ -1811,7 +1811,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_comp_progress_node: *
const c_source_basename = std.fs.path.basename(c_object.src.src_path);
c_comp_progress_node.activate();
- var child_progress_node = c_comp_progress_node.start(c_source_basename, null);
+ var child_progress_node = c_comp_progress_node.start(c_source_basename, 0);
child_progress_node.activate();
defer child_progress_node.end();
src/stage1.zig
@@ -293,7 +293,7 @@ export fn stage2_progress_start_root(
) *std.Progress.Node {
return progress.start(
name_ptr[0..name_len],
- if (estimated_total_items == 0) null else estimated_total_items,
+ estimated_total_items,
) catch @panic("timer unsupported");
}
@@ -312,7 +312,7 @@ export fn stage2_progress_start(
const child_node = std.heap.c_allocator.create(std.Progress.Node) catch @panic("out of memory");
child_node.* = node.start(
name_ptr[0..name_len],
- if (estimated_total_items == 0) null else estimated_total_items,
+ estimated_total_items,
);
child_node.activate();
return child_node;
@@ -333,8 +333,8 @@ export fn stage2_progress_complete_one(node: *std.Progress.Node) void {
// ABI warning
export fn stage2_progress_update_node(node: *std.Progress.Node, done_count: usize, total_count: usize) void {
- node.completed_items = done_count;
- node.estimated_total_items = total_count;
+ node.setCompletedItems(done_count);
+ node.setEstimatedTotalItems(total_count);
node.activate();
node.context.maybeRefresh();
}
CMakeLists.txt
@@ -410,7 +410,7 @@ set(ZIG_STAGE2_SOURCES
"${CMAKE_SOURCE_DIR}/lib/std/os/windows/win32error.zig"
"${CMAKE_SOURCE_DIR}/lib/std/pdb.zig"
"${CMAKE_SOURCE_DIR}/lib/std/process.zig"
- "${CMAKE_SOURCE_DIR}/lib/std/progress.zig"
+ "${CMAKE_SOURCE_DIR}/lib/std/Progress.zig"
"${CMAKE_SOURCE_DIR}/lib/std/rand.zig"
"${CMAKE_SOURCE_DIR}/lib/std/reset_event.zig"
"${CMAKE_SOURCE_DIR}/lib/std/sort.zig"