| ... | @@ -22,14 +22,10 @@ is_windows_terminal: bool, | ... | @@ -22,14 +22,10 @@ is_windows_terminal: bool, |
| 22 | /// Whether the terminal supports ANSI escape codes. | 22 | /// Whether the terminal supports ANSI escape codes. |
| 23 | supports_ansi_escape_codes: bool, | 23 | supports_ansi_escape_codes: bool, |
| 24 | | 24 | |
| 25 | root: Node, | | |
| 26 | | | |
| 27 | update_thread: ?std.Thread, | 25 | update_thread: ?std.Thread, |
| 28 | | 26 | |
| 29 | /// Atomically set by SIGWINCH as well as the root done() function. | 27 | /// Atomically set by SIGWINCH as well as the root done() function. |
| 30 | redraw_event: std.Thread.ResetEvent, | 28 | redraw_event: std.Thread.ResetEvent, |
| 31 | /// Ensure there is only 1 global Progress object. | | |
| 32 | initialized: bool, | | |
| 33 | /// Indicates a request to shut down and reset global state. | 29 | /// Indicates a request to shut down and reset global state. |
| 34 | /// Accessed atomically. | 30 | /// Accessed atomically. |
| 35 | done: bool, | 31 | done: bool, |
| ... | @@ -43,13 +39,22 @@ cols: u16, | ... | @@ -43,13 +39,22 @@ cols: u16, |
| 43 | /// Accessed only by the update thread. | 39 | /// Accessed only by the update thread. |
| 44 | draw_buffer: []u8, | 40 | draw_buffer: []u8, |
| 45 | | 41 | |
| | 42 | /// This is in a separate array from `node_storage` but with the same length so |
| | 43 | /// that it can be iterated over efficiently without trashing too much of the |
| | 44 | /// CPU cache. |
| | 45 | node_parents: []Node.Parent, |
| | 46 | node_storage: []Node.Storage, |
| | 47 | node_freelist: []Node.OptionalIndex, |
| | 48 | node_freelist_first: Node.OptionalIndex, |
| | 49 | node_end_index: u32, |
| | 50 | |
| 46 | pub const Options = struct { | 51 | pub const Options = struct { |
| 47 | /// User-provided buffer with static lifetime. | 52 | /// User-provided buffer with static lifetime. |
| 48 | /// | 53 | /// |
| 49 | /// Used to store the entire write buffer sent to the terminal. Progress output will be truncated if it | 54 | /// Used to store the entire write buffer sent to the terminal. Progress output will be truncated if it |
| 50 | /// cannot fit into this buffer which will look bad but not cause any malfunctions. | 55 | /// cannot fit into this buffer which will look bad but not cause any malfunctions. |
| 51 | /// | 56 | /// |
| 52 | /// Must be at least 100 bytes. | 57 | /// Must be at least 200 bytes. |
| 53 | draw_buffer: []u8, | 58 | draw_buffer: []u8, |
| 54 | /// How many nanoseconds between writing updates to the terminal. | 59 | /// How many nanoseconds between writing updates to the terminal. |
| 55 | refresh_rate_ns: u64 = 50 * std.time.ns_per_ms, | 60 | refresh_rate_ns: u64 = 50 * std.time.ns_per_ms, |
| ... | @@ -64,66 +69,128 @@ pub const Options = struct { | ... | @@ -64,66 +69,128 @@ pub const Options = struct { |
| 64 | /// Represents one unit of progress. Each node can have children nodes, or | 69 | /// Represents one unit of progress. Each node can have children nodes, or |
| 65 | /// one can use integers with `update`. | 70 | /// one can use integers with `update`. |
| 66 | pub const Node = struct { | 71 | pub const Node = struct { |
| 67 | mutex: std.Thread.Mutex, | 72 | index: OptionalIndex, |
| 68 | /// Links to the parent and child nodes. | 73 | |
| 69 | parent_list_node: std.DoublyLinkedList(void).Node, | 74 | pub const max_name_len = 38; |
| 70 | /// Links to the prev and next sibling nodes. | | |
| 71 | sibling_list_node: std.DoublyLinkedList(void).Node, | | |
| 72 | | 75 | |
| 73 | name: []const u8, | 76 | const Storage = extern struct { |
| 74 | /// Must be handled atomically to be thread-safe. 0 means null. | 77 | /// Little endian. |
| 75 | unprotected_estimated_total_items: usize, | 78 | completed_count: u32, |
| 76 | /// Must be handled atomically to be thread-safe. | 79 | /// 0 means unknown. |
| 77 | unprotected_completed_items: usize, | 80 | /// Little endian. |
| | 81 | estimated_total_count: u32, |
| | 82 | name: [max_name_len]u8, |
| | 83 | }; |
| 78 | | 84 | |
| 79 | pub const ListNode = std.DoublyLinkedList(void); | 85 | const Parent = enum(u16) { |
| | 86 | /// Unallocated storage. |
| | 87 | unused = std.math.maxInt(u16) - 1, |
| | 88 | /// Indicates root node. |
| | 89 | none = std.math.maxInt(u16), |
| | 90 | /// Index into `node_storage`. |
| | 91 | _, |
| | 92 | |
| | 93 | fn unwrap(i: @This()) ?Index { |
| | 94 | return switch (i) { |
| | 95 | .unused, .none => return null, |
| | 96 | else => @enumFromInt(@intFromEnum(i)), |
| | 97 | }; |
| | 98 | } |
| | 99 | }; |
| | 100 | |
| | 101 | const OptionalIndex = enum(u16) { |
| | 102 | none = std.math.maxInt(u16), |
| | 103 | /// Index into `node_storage`. |
| | 104 | _, |
| | 105 | |
| | 106 | fn unwrap(i: @This()) ?Index { |
| | 107 | if (i == .none) return null; |
| | 108 | return @enumFromInt(@intFromEnum(i)); |
| | 109 | } |
| | 110 | |
| | 111 | fn toParent(i: @This()) Parent { |
| | 112 | assert(@intFromEnum(i) != @intFromEnum(Parent.unused)); |
| | 113 | return @enumFromInt(@intFromEnum(i)); |
| | 114 | } |
| | 115 | }; |
| | 116 | |
| | 117 | /// Index into `node_storage`. |
| | 118 | const Index = enum(u16) { |
| | 119 | _, |
| | 120 | |
| | 121 | fn toParent(i: @This()) Parent { |
| | 122 | assert(@intFromEnum(i) != @intFromEnum(Parent.unused)); |
| | 123 | assert(@intFromEnum(i) != @intFromEnum(Parent.none)); |
| | 124 | return @enumFromInt(@intFromEnum(i)); |
| | 125 | } |
| | 126 | |
| | 127 | fn toOptional(i: @This()) OptionalIndex { |
| | 128 | return @enumFromInt(@intFromEnum(i)); |
| | 129 | } |
| | 130 | }; |
| 80 | | 131 | |
| 81 | /// Create a new child progress node. Thread-safe. | 132 | /// Create a new child progress node. Thread-safe. |
| 82 | /// | 133 | /// |
| 83 | /// It is expected for the memory of the result to be stored in the | | |
| 84 | /// caller's stack and therefore is required to call `activate` immediately | | |
| 85 | /// on the result after initializing the memory location and `end` when done. | | |
| 86 | /// | | |
| 87 | /// Passing 0 for `estimated_total_items` means unknown. | 134 | /// Passing 0 for `estimated_total_items` means unknown. |
| 88 | pub fn start(self: *Node, name: []const u8, estimated_total_items: usize) Node { | 135 | pub fn start(node: Node, name: []const u8, estimated_total_items: usize) Node { |
| 89 | return .{ | 136 | const node_index = node.index.unwrap() orelse return .{ .index = .none }; |
| 90 | .mutex = .{}, | 137 | const parent = node_index.toParent(); |
| 91 | .parent_list_node = .{ | 138 | |
| 92 | .prev = &self.parent_list_node, | 139 | const freelist_head = &global_progress.node_freelist_first; |
| 93 | .next = null, | 140 | var opt_free_index = @atomicLoad(Node.OptionalIndex, freelist_head, .seq_cst); |
| 94 | .data = {}, | 141 | while (opt_free_index.unwrap()) |free_index| { |
| 95 | }, | 142 | const freelist_ptr = freelistByIndex(free_index); |
| 96 | .sibling_list_node = .{ .data = {} }, | 143 | opt_free_index = @cmpxchgWeak(Node.OptionalIndex, freelist_head, opt_free_index, freelist_ptr.*, .seq_cst, .seq_cst) orelse { |
| 97 | .name = name, | 144 | // We won the allocation race. |
| 98 | .unprotected_estimated_total_items = estimated_total_items, | 145 | return init(free_index, parent, name, estimated_total_items); |
| 99 | .unprotected_completed_items = 0, | 146 | }; |
| 100 | }; | 147 | } |
| 101 | } | 148 | |
| | 149 | const free_index = @atomicRmw(u32, &global_progress.node_end_index, .Add, 1, .monotonic); |
| | 150 | if (free_index >= global_progress.node_storage.len) { |
| | 151 | // Ran out of node storage memory. Progress for this node will not be tracked. |
| | 152 | _ = @atomicRmw(u32, &global_progress.node_end_index, .Sub, 1, .monotonic); |
| | 153 | return .{ .index = .none }; |
| | 154 | } |
| 102 | | 155 | |
| 103 | /// To be called exactly once after `start`. | 156 | return init(@enumFromInt(free_index), parent, name, estimated_total_items); |
| 104 | pub fn activate(n: *Node) void { | | |
| 105 | const p = n.parent().?; | | |
| 106 | p.mutex.lock(); | | |
| 107 | defer p.mutex.unlock(); | | |
| 108 | assert(p.parent_list_node.next == null); | | |
| 109 | p.parent_list_node.next = &n.parent_list_node; | | |
| 110 | } | 157 | } |
| 111 | | 158 | |
| 112 | /// This is the same as calling `start` and then `end` on the returned `Node`. Thread-safe. | 159 | /// This is the same as calling `start` and then `end` on the returned `Node`. Thread-safe. |
| 113 | pub fn completeOne(self: *Node) void { | 160 | pub fn completeOne(n: Node) void { |
| 114 | _ = @atomicRmw(usize, &self.unprotected_completed_items, .Add, 1, .monotonic); | 161 | const index = n.index.unwrap() orelse return; |
| | 162 | const storage = storageByIndex(index); |
| | 163 | _ = @atomicRmw(u32, &storage.completed_count, .Add, 1, .monotonic); |
| | 164 | } |
| | 165 | |
| | 166 | /// Thread-safe. |
| | 167 | pub fn setCompletedItems(n: Node, completed_items: usize) void { |
| | 168 | const index = n.index.unwrap() orelse return; |
| | 169 | const storage = storageByIndex(index); |
| | 170 | @atomicStore(u32, &storage.completed_count, std.math.lossyCast(u32, completed_items), .monotonic); |
| | 171 | } |
| | 172 | |
| | 173 | /// Thread-safe. 0 means unknown. |
| | 174 | pub fn setEstimatedTotalItems(n: Node, count: usize) void { |
| | 175 | const index = n.index.unwrap() orelse return; |
| | 176 | const storage = storageByIndex(index); |
| | 177 | @atomicStore(u32, &storage.estimated_total_count, std.math.lossyCast(u32, count), .monotonic); |
| 115 | } | 178 | } |
| 116 | | 179 | |
| 117 | /// Finish a started `Node`. Thread-safe. | 180 | /// Finish a started `Node`. Thread-safe. |
| 118 | pub fn end(child: *Node) void { | 181 | pub fn end(n: Node) void { |
| 119 | if (child.parent()) |p| { | 182 | const index = n.index.unwrap() orelse return; |
| 120 | // Make sure the other thread doesn't access this memory that is | 183 | const parent_ptr = parentByIndex(index); |
| 121 | // about to be released. | 184 | if (parent_ptr.unwrap()) |parent_index| { |
| 122 | child.mutex.lock(); | 185 | _ = @atomicRmw(u32, &storageByIndex(parent_index).completed_count, .Add, 1, .monotonic); |
| 123 | | 186 | @atomicStore(Node.Parent, parent_ptr, .unused, .seq_cst); |
| 124 | const other = if (child.sibling_list_node.next) |n| n else child.sibling_list_node.prev; | 187 | |
| 125 | _ = @cmpxchgStrong(std.DoublyLinkedList(void).Node, &p.parent_list_node.next, child, other, .seq_cst, .seq_cst); | 188 | const freelist_head = &global_progress.node_freelist_first; |
| 126 | p.completeOne(); | 189 | var first = @atomicLoad(Node.OptionalIndex, freelist_head, .seq_cst); |
| | 190 | while (true) { |
| | 191 | freelistByIndex(index).* = first; |
| | 192 | first = @cmpxchgWeak(Node.OptionalIndex, freelist_head, first, index.toOptional(), .seq_cst, .seq_cst) orelse break; |
| | 193 | } |
| 127 | } else { | 194 | } else { |
| 128 | @atomicStore(bool, &global_progress.done, true, .seq_cst); | 195 | @atomicStore(bool, &global_progress.done, true, .seq_cst); |
| 129 | global_progress.redraw_event.set(); | 196 | global_progress.redraw_event.set(); |
| ... | @@ -131,19 +198,35 @@ pub const Node = struct { | ... | @@ -131,19 +198,35 @@ pub const Node = struct { |
| 131 | } | 198 | } |
| 132 | } | 199 | } |
| 133 | | 200 | |
| 134 | /// Thread-safe. 0 means unknown. | 201 | fn storageByIndex(index: Node.Index) *Node.Storage { |
| 135 | pub fn setEstimatedTotalItems(self: *Node, count: usize) void { | 202 | return &global_progress.node_storage[@intFromEnum(index)]; |
| 136 | @atomicStore(usize, &self.unprotected_estimated_total_items, count, .monotonic); | | |
| 137 | } | 203 | } |
| 138 | | 204 | |
| 139 | /// Thread-safe. | 205 | fn parentByIndex(index: Node.Index) *Node.Parent { |
| 140 | pub fn setCompletedItems(self: *Node, completed_items: usize) void { | 206 | return &global_progress.node_parents[@intFromEnum(index)]; |
| 141 | @atomicStore(usize, &self.unprotected_completed_items, completed_items, .monotonic); | 207 | } |
| | 208 | |
| | 209 | fn freelistByIndex(index: Node.Index) *Node.OptionalIndex { |
| | 210 | return &global_progress.node_freelist[@intFromEnum(index)]; |
| 142 | } | 211 | } |
| 143 | | 212 | |
| 144 | fn parent(child: *Node) ?*Node { | 213 | fn init(free_index: Index, parent: Parent, name: []const u8, estimated_total_items: usize) Node { |
| 145 | const parent_node = child.parent_list_node.prev orelse return null; | 214 | assert(parent != .unused); |
| 146 | return @fieldParentPtr("parent_list_node", parent_node); | 215 | |
| | 216 | const storage = storageByIndex(free_index); |
| | 217 | storage.* = .{ |
| | 218 | .completed_count = 0, |
| | 219 | .estimated_total_count = std.math.lossyCast(u32, estimated_total_items), |
| | 220 | .name = [1]u8{0} ** max_name_len, |
| | 221 | }; |
| | 222 | const name_len = @min(max_name_len, name.len); |
| | 223 | @memcpy(storage.name[0..name_len], name[0..name_len]); |
| | 224 | |
| | 225 | const parent_ptr = parentByIndex(free_index); |
| | 226 | assert(parent_ptr.* == .unused); |
| | 227 | @atomicStore(Node.Parent, parent_ptr, parent, .release); |
| | 228 | |
| | 229 | return .{ .index = free_index.toOptional() }; |
| 147 | } | 230 | } |
| 148 | }; | 231 | }; |
| 149 | | 232 | |
| ... | @@ -151,25 +234,36 @@ var global_progress: Progress = .{ | ... | @@ -151,25 +234,36 @@ var global_progress: Progress = .{ |
| 151 | .terminal = null, | 234 | .terminal = null, |
| 152 | .is_windows_terminal = false, | 235 | .is_windows_terminal = false, |
| 153 | .supports_ansi_escape_codes = false, | 236 | .supports_ansi_escape_codes = false, |
| 154 | .root = undefined, | | |
| 155 | .update_thread = null, | 237 | .update_thread = null, |
| 156 | .redraw_event = .{}, | 238 | .redraw_event = .{}, |
| 157 | .initialized = false, | | |
| 158 | .refresh_rate_ns = undefined, | 239 | .refresh_rate_ns = undefined, |
| 159 | .initial_delay_ns = undefined, | 240 | .initial_delay_ns = undefined, |
| 160 | .rows = 0, | 241 | .rows = 0, |
| 161 | .cols = 0, | 242 | .cols = 0, |
| 162 | .draw_buffer = undefined, | 243 | .draw_buffer = undefined, |
| 163 | .done = false, | 244 | .done = false, |
| | 245 | |
| | 246 | // TODO: make these configurable and avoid including the globals in .data if unused |
| | 247 | .node_parents = &node_parents_buffer, |
| | 248 | .node_storage = &node_storage_buffer, |
| | 249 | .node_freelist = &node_freelist_buffer, |
| | 250 | .node_freelist_first = .none, |
| | 251 | .node_end_index = 0, |
| 164 | }; | 252 | }; |
| 165 | | 253 | |
| | 254 | const default_node_storage_buffer_len = 100; |
| | 255 | var node_parents_buffer: [default_node_storage_buffer_len]Node.Parent = undefined; |
| | 256 | var node_storage_buffer: [default_node_storage_buffer_len]Node.Storage = undefined; |
| | 257 | var node_freelist_buffer: [default_node_storage_buffer_len]Node.OptionalIndex = undefined; |
| | 258 | |
| 166 | /// Initializes a global Progress instance. | 259 | /// Initializes a global Progress instance. |
| 167 | /// | 260 | /// |
| 168 | /// Asserts there is only one global Progress instance. | 261 | /// Asserts there is only one global Progress instance. |
| 169 | /// | 262 | /// |
| 170 | /// Call `Node.end` when done. | 263 | /// Call `Node.end` when done. |
| 171 | pub fn start(options: Options) *Node { | 264 | pub fn start(options: Options) Node { |
| 172 | assert(!global_progress.initialized); | 265 | // Ensure there is only 1 global Progress object. |
| | 266 | assert(global_progress.node_end_index == 0); |
| 173 | const stderr = std.io.getStdErr(); | 267 | const stderr = std.io.getStdErr(); |
| 174 | if (stderr.supportsAnsiEscapeCodes()) { | 268 | if (stderr.supportsAnsiEscapeCodes()) { |
| 175 | global_progress.terminal = stderr; | 269 | global_progress.terminal = stderr; |
| ... | @@ -181,18 +275,12 @@ pub fn start(options: Options) *Node { | ... | @@ -181,18 +275,12 @@ pub fn start(options: Options) *Node { |
| 181 | // we are in a "dumb" terminal like in acme or writing to a file | 275 | // we are in a "dumb" terminal like in acme or writing to a file |
| 182 | global_progress.terminal = stderr; | 276 | global_progress.terminal = stderr; |
| 183 | } | 277 | } |
| 184 | global_progress.root = .{ | 278 | @memset(global_progress.node_parents, .unused); |
| 185 | .mutex = .{}, | 279 | const root_node = Node.init(@enumFromInt(0), .none, options.root_name, options.estimated_total_items); |
| 186 | .parent_list_node = .{ .data = {} }, | | |
| 187 | .sibling_list_node = .{ .data = {} }, | | |
| 188 | .name = options.root_name, | | |
| 189 | .unprotected_estimated_total_items = options.estimated_total_items, | | |
| 190 | .unprotected_completed_items = 0, | | |
| 191 | }; | | |
| 192 | global_progress.done = false; | 280 | global_progress.done = false; |
| 193 | global_progress.initialized = true; | 281 | global_progress.node_end_index = 1; |
| 194 | | 282 | |
| 195 | assert(options.draw_buffer.len >= 100); | 283 | assert(options.draw_buffer.len >= 200); |
| 196 | global_progress.draw_buffer = options.draw_buffer; | 284 | global_progress.draw_buffer = options.draw_buffer; |
| 197 | global_progress.refresh_rate_ns = options.refresh_rate_ns; | 285 | global_progress.refresh_rate_ns = options.refresh_rate_ns; |
| 198 | global_progress.initial_delay_ns = options.initial_delay_ns; | 286 | global_progress.initial_delay_ns = options.initial_delay_ns; |
| ... | @@ -204,7 +292,7 @@ pub fn start(options: Options) *Node { | ... | @@ -204,7 +292,7 @@ pub fn start(options: Options) *Node { |
| 204 | }; | 292 | }; |
| 205 | posix.sigaction(posix.SIG.WINCH, &act, null) catch { | 293 | posix.sigaction(posix.SIG.WINCH, &act, null) catch { |
| 206 | global_progress.terminal = null; | 294 | global_progress.terminal = null; |
| 207 | return &global_progress.root; | 295 | return root_node; |
| 208 | }; | 296 | }; |
| 209 | | 297 | |
| 210 | if (global_progress.terminal != null) { | 298 | if (global_progress.terminal != null) { |
| ... | @@ -215,7 +303,7 @@ pub fn start(options: Options) *Node { | ... | @@ -215,7 +303,7 @@ pub fn start(options: Options) *Node { |
| 215 | } | 303 | } |
| 216 | } | 304 | } |
| 217 | | 305 | |
| 218 | return &global_progress.root; | 306 | return root_node; |
| 219 | } | 307 | } |
| 220 | | 308 | |
| 221 | /// Returns whether a resize is needed to learn the terminal size. | 309 | /// Returns whether a resize is needed to learn the terminal size. |
| ... | @@ -263,11 +351,85 @@ const save = "\x1b7"; | ... | @@ -263,11 +351,85 @@ const save = "\x1b7"; |
| 263 | const restore = "\x1b8"; | 351 | const restore = "\x1b8"; |
| 264 | const finish_sync = "\x1b[?2026l"; | 352 | const finish_sync = "\x1b[?2026l"; |
| 265 | | 353 | |
| | 354 | const tree_tee = "\x1B\x28\x30\x74\x71\x1B\x28\x42 "; // ├─ |
| | 355 | const tree_line = "\x1B\x28\x30\x78\x1B\x28\x42 "; // │ |
| | 356 | const tree_langle = "\x1B\x28\x30\x6d\x71\x1B\x28\x42 "; // └─ |
| | 357 | |
| 266 | fn clearTerminal() void { | 358 | fn clearTerminal() void { |
| 267 | write(clear); | 359 | write(clear); |
| 268 | } | 360 | } |
| 269 | | 361 | |
| | 362 | const Children = struct { |
| | 363 | child: Node.OptionalIndex, |
| | 364 | sibling: Node.OptionalIndex, |
| | 365 | }; |
| | 366 | |
| 270 | fn computeRedraw() []u8 { | 367 | fn computeRedraw() []u8 { |
| | 368 | // TODO make this configurable |
| | 369 | var serialized_node_parents_buffer: [default_node_storage_buffer_len]Node.Parent = undefined; |
| | 370 | var serialized_node_storage_buffer: [default_node_storage_buffer_len]Node.Storage = undefined; |
| | 371 | var serialized_node_map_buffer: [default_node_storage_buffer_len]Node.Index = undefined; |
| | 372 | var serialized_len: usize = 0; |
| | 373 | |
| | 374 | // Iterate all of the nodes and construct a serializable copy of the state that can be examined |
| | 375 | // without atomics. |
| | 376 | const end_index = @atomicLoad(u32, &global_progress.node_end_index, .monotonic); |
| | 377 | const node_parents = global_progress.node_parents[0..end_index]; |
| | 378 | const node_storage = global_progress.node_storage[0..end_index]; |
| | 379 | for (node_parents, node_storage, 0..) |*parent_ptr, *storage_ptr, i| { |
| | 380 | var begin_parent = @atomicLoad(Node.Parent, parent_ptr, .seq_cst); |
| | 381 | while (begin_parent != .unused) { |
| | 382 | const dest_storage = &serialized_node_storage_buffer[serialized_len]; |
| | 383 | @memcpy(&dest_storage.name, &storage_ptr.name); |
| | 384 | dest_storage.completed_count = @atomicLoad(u32, &storage_ptr.completed_count, .monotonic); |
| | 385 | dest_storage.estimated_total_count = @atomicLoad(u32, &storage_ptr.estimated_total_count, .monotonic); |
| | 386 | |
| | 387 | const end_parent = @atomicLoad(Node.Parent, parent_ptr, .seq_cst); |
| | 388 | if (begin_parent == end_parent) { |
| | 389 | serialized_node_parents_buffer[serialized_len] = begin_parent; |
| | 390 | serialized_node_map_buffer[i] = @enumFromInt(serialized_len); |
| | 391 | serialized_len += 1; |
| | 392 | break; |
| | 393 | } |
| | 394 | |
| | 395 | begin_parent = end_parent; |
| | 396 | } |
| | 397 | } |
| | 398 | |
| | 399 | // Now we can analyze our copy of the graph without atomics, reconstructing |
| | 400 | // children lists which do not exist in the canonical data. These are |
| | 401 | // needed for tree traversal below. |
| | 402 | const serialized_node_parents = serialized_node_parents_buffer[0..serialized_len]; |
| | 403 | const serialized_node_storage = serialized_node_storage_buffer[0..serialized_len]; |
| | 404 | |
| | 405 | // Remap parents to point inside serialized arrays. |
| | 406 | for (serialized_node_parents) |*parent| { |
| | 407 | parent.* = switch (parent.*) { |
| | 408 | .unused => unreachable, |
| | 409 | .none => .none, |
| | 410 | _ => |p| serialized_node_map_buffer[@intFromEnum(p)].toParent(), |
| | 411 | }; |
| | 412 | } |
| | 413 | |
| | 414 | var children_buffer: [default_node_storage_buffer_len]Children = undefined; |
| | 415 | const children = children_buffer[0..serialized_len]; |
| | 416 | |
| | 417 | @memset(children, .{ .child = .none, .sibling = .none }); |
| | 418 | |
| | 419 | for (serialized_node_parents, 0..) |parent, child_index_usize| { |
| | 420 | const child_index: Node.Index = @enumFromInt(child_index_usize); |
| | 421 | assert(parent != .unused); |
| | 422 | const parent_index = parent.unwrap() orelse continue; |
| | 423 | const children_node = &children[@intFromEnum(parent_index)]; |
| | 424 | if (children_node.child.unwrap()) |existing_child_index| { |
| | 425 | const existing_child = &children[@intFromEnum(existing_child_index)]; |
| | 426 | existing_child.sibling = child_index.toOptional(); |
| | 427 | children[@intFromEnum(child_index)].sibling = existing_child.sibling; |
| | 428 | } else { |
| | 429 | children_node.child = child_index.toOptional(); |
| | 430 | } |
| | 431 | } |
| | 432 | |
| 271 | // The strategy is: keep the cursor at the beginning, and then with every redraw: | 433 | // The strategy is: keep the cursor at the beginning, and then with every redraw: |
| 272 | // erase, save, write, restore | 434 | // erase, save, write, restore |
| 273 | | 435 | |
| ... | @@ -280,32 +442,91 @@ fn computeRedraw() []u8 { | ... | @@ -280,32 +442,91 @@ fn computeRedraw() []u8 { |
| 280 | buf[0..prefix.len].* = prefix.*; | 442 | buf[0..prefix.len].* = prefix.*; |
| 281 | i = prefix.len; | 443 | i = prefix.len; |
| 282 | | 444 | |
| 283 | // Walk the tree and write the progress output to the buffer. | 445 | const root_node_index: Node.Index = @enumFromInt(0); |
| 284 | var node: *Node = &global_progress.root; | 446 | i = computeNode(buf, i, serialized_node_storage, serialized_node_parents, children, root_node_index); |
| 285 | while (true) { | | |
| 286 | const eti = @atomicLoad(usize, &node.unprotected_estimated_total_items, .monotonic); | | |
| 287 | const completed_items = @atomicLoad(usize, &node.unprotected_completed_items, .monotonic); | | |
| 288 | | 447 | |
| 289 | if (node.name.len != 0 or eti > 0) { | 448 | buf[i..][0..suffix.len].* = suffix.*; |
| 290 | if (node.name.len != 0) { | 449 | i += suffix.len; |
| 291 | i += (std.fmt.bufPrint(buf[i..], "{s}", .{node.name}) catch @panic("TODO")).len; | 450 | |
| 292 | } | 451 | return buf[0..i]; |
| 293 | if (eti > 0) { | 452 | } |
| 294 | i += (std.fmt.bufPrint(buf[i..], "[{d}/{d}] ", .{ completed_items, eti }) catch @panic("TODO")).len; | 453 | |
| 295 | } else if (completed_items != 0) { | 454 | fn computePrefix( |
| 296 | i += (std.fmt.bufPrint(buf[i..], "[{d}] ", .{completed_items}) catch @panic("TODO")).len; | 455 | buf: []u8, |
| 297 | } | 456 | start_i: usize, |
| | 457 | serialized_node_storage: []const Node.Storage, |
| | 458 | serialized_node_parents: []const Node.Parent, |
| | 459 | children: []const Children, |
| | 460 | node_index: Node.Index, |
| | 461 | ) usize { |
| | 462 | var i = start_i; |
| | 463 | const parent_index = serialized_node_parents[@intFromEnum(node_index)].unwrap() orelse return i; |
| | 464 | if (serialized_node_parents[@intFromEnum(parent_index)] == .none) return i; |
| | 465 | i = computePrefix(buf, i, serialized_node_storage, serialized_node_parents, children, parent_index); |
| | 466 | if (children[@intFromEnum(parent_index)].sibling == .none) { |
| | 467 | buf[i..][0..3].* = " ".*; |
| | 468 | i += 3; |
| | 469 | } else { |
| | 470 | buf[i..][0..tree_line.len].* = tree_line.*; |
| | 471 | i += tree_line.len; |
| | 472 | } |
| | 473 | return i; |
| | 474 | } |
| | 475 | |
| | 476 | fn computeNode( |
| | 477 | buf: []u8, |
| | 478 | start_i: usize, |
| | 479 | serialized_node_storage: []const Node.Storage, |
| | 480 | serialized_node_parents: []const Node.Parent, |
| | 481 | children: []const Children, |
| | 482 | node_index: Node.Index, |
| | 483 | ) usize { |
| | 484 | var i = start_i; |
| | 485 | i = computePrefix(buf, i, serialized_node_storage, serialized_node_parents, children, node_index); |
| | 486 | |
| | 487 | const storage = &serialized_node_storage[@intFromEnum(node_index)]; |
| | 488 | const estimated_total = storage.estimated_total_count; |
| | 489 | const completed_items = storage.completed_count; |
| | 490 | const name = if (std.mem.indexOfScalar(u8, &storage.name, 0)) |end| storage.name[0..end] else &storage.name; |
| | 491 | const parent = serialized_node_parents[@intFromEnum(node_index)]; |
| | 492 | |
| | 493 | if (parent != .none) { |
| | 494 | if (children[@intFromEnum(node_index)].sibling == .none) { |
| | 495 | buf[i..][0..tree_langle.len].* = tree_langle.*; |
| | 496 | i += tree_langle.len; |
| | 497 | } else { |
| | 498 | buf[i..][0..tree_tee.len].* = tree_tee.*; |
| | 499 | i += tree_tee.len; |
| 298 | } | 500 | } |
| | 501 | } |
| 299 | | 502 | |
| 300 | node = @atomicLoad(?*Node, &node.recently_updated_child, .acquire) orelse break; | 503 | if (name.len != 0 or estimated_total > 0) { |
| | 504 | if (estimated_total > 0) { |
| | 505 | i += (std.fmt.bufPrint(buf[i..], "[{d}/{d}] ", .{ completed_items, estimated_total }) catch &.{}).len; |
| | 506 | } else if (completed_items != 0) { |
| | 507 | i += (std.fmt.bufPrint(buf[i..], "[{d}] ", .{completed_items}) catch &.{}).len; |
| | 508 | } |
| | 509 | if (name.len != 0) { |
| | 510 | i += (std.fmt.bufPrint(buf[i..], "{s}", .{name}) catch &.{}).len; |
| | 511 | } |
| 301 | } | 512 | } |
| 302 | | 513 | |
| 303 | i = @min(global_progress.cols + prefix.len, i); | 514 | i = @min(global_progress.cols + start_i, i); |
| | 515 | buf[i] = '\n'; |
| | 516 | i += 1; |
| 304 | | 517 | |
| 305 | buf[i..][0..suffix.len].* = suffix.*; | 518 | if (children[@intFromEnum(node_index)].child.unwrap()) |child| { |
| 306 | i += suffix.len; | 519 | i = computeNode(buf, i, serialized_node_storage, serialized_node_parents, children, child); |
| | 520 | } |
| 307 | | 521 | |
| 308 | return buf[0..i]; | 522 | { |
| | 523 | var opt_sibling = children[@intFromEnum(node_index)].sibling; |
| | 524 | while (opt_sibling.unwrap()) |sibling| { |
| | 525 | i = computeNode(buf, i, serialized_node_storage, serialized_node_parents, children, sibling); |
| | 526 | } |
| | 527 | } |
| | 528 | |
| | 529 | return i; |
| 309 | } | 530 | } |
| 310 | | 531 | |
| 311 | fn write(buf: []const u8) void { | 532 | fn write(buf: []const u8) void { |