authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-05-22 20:42:12-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-05-27 20:56:48-07:00
loga3c9511ab9d56d4c06c612536a27b84d67ae415c
tree5a61cccf339d6c8b9c35c4f86fbb511aae9c4edc
parente1e4de2776901a0acb7a28454c0fe080c5c13a5e

rework std.Progress again

This time, we preallocate a fixed set of nodes and have the user-visible Node only be an index into them. This allows for lock-free management of the node storage. Only the parent indexes are stored, and the update thread makes a serialized copy of the state before trying to compute children lists. The update thread then walks the tree and outputs an entire tree of progress rather than only one line. There is a problem with clearing from the cursor to the end of the screen when the cursor is at the bottom of the terminal.

1 files changed, 317 insertions(+), 96 deletions(-)

lib/std/Progress.zig+317-96
......@@ -22,14 +22,10 @@ is_windows_terminal: bool,
2222/// Whether the terminal supports ANSI escape codes.
2323supports_ansi_escape_codes: bool,
2424
25root: Node,
26
2725update_thread: ?std.Thread,
2826
2927/// Atomically set by SIGWINCH as well as the root done() function.
3028redraw_event: std.Thread.ResetEvent,
31/// Ensure there is only 1 global Progress object.
32initialized: bool,
3329/// Indicates a request to shut down and reset global state.
3430/// Accessed atomically.
3531done: bool,
......@@ -43,13 +39,22 @@ cols: u16,
4339/// Accessed only by the update thread.
4440draw_buffer: []u8,
4541
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.
45node_parents: []Node.Parent,
46node_storage: []Node.Storage,
47node_freelist: []Node.OptionalIndex,
48node_freelist_first: Node.OptionalIndex,
49node_end_index: u32,
50
4651pub const Options = struct {
4752 /// User-provided buffer with static lifetime.
4853 ///
4954 /// Used to store the entire write buffer sent to the terminal. Progress output will be truncated if it
5055 /// cannot fit into this buffer which will look bad but not cause any malfunctions.
5156 ///
52 /// Must be at least 100 bytes.
57 /// Must be at least 200 bytes.
5358 draw_buffer: []u8,
5459 /// How many nanoseconds between writing updates to the terminal.
5560 refresh_rate_ns: u64 = 50 * std.time.ns_per_ms,
......@@ -64,66 +69,128 @@ pub const Options = struct {
6469/// Represents one unit of progress. Each node can have children nodes, or
6570/// one can use integers with `update`.
6671pub const Node = struct {
67 mutex: std.Thread.Mutex,
68 /// Links to the parent and child nodes.
69 parent_list_node: std.DoublyLinkedList(void).Node,
70 /// Links to the prev and next sibling nodes.
71 sibling_list_node: std.DoublyLinkedList(void).Node,
72 index: OptionalIndex,
73
74 pub const max_name_len = 38;
7275
73 name: []const u8,
74 /// Must be handled atomically to be thread-safe. 0 means null.
75 unprotected_estimated_total_items: usize,
76 /// Must be handled atomically to be thread-safe.
77 unprotected_completed_items: usize,
76 const Storage = extern struct {
77 /// Little endian.
78 completed_count: u32,
79 /// 0 means unknown.
80 /// Little endian.
81 estimated_total_count: u32,
82 name: [max_name_len]u8,
83 };
7884
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 };
80131
81132 /// Create a new child progress node. Thread-safe.
82133 ///
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 ///
87134 /// Passing 0 for `estimated_total_items` means unknown.
88 pub fn start(self: *Node, name: []const u8, estimated_total_items: usize) Node {
89 return .{
90 .mutex = .{},
91 .parent_list_node = .{
92 .prev = &self.parent_list_node,
93 .next = null,
94 .data = {},
95 },
96 .sibling_list_node = .{ .data = {} },
97 .name = name,
98 .unprotected_estimated_total_items = estimated_total_items,
99 .unprotected_completed_items = 0,
100 };
101 }
135 pub fn start(node: Node, name: []const u8, estimated_total_items: usize) Node {
136 const node_index = node.index.unwrap() orelse return .{ .index = .none };
137 const parent = node_index.toParent();
138
139 const freelist_head = &global_progress.node_freelist_first;
140 var opt_free_index = @atomicLoad(Node.OptionalIndex, freelist_head, .seq_cst);
141 while (opt_free_index.unwrap()) |free_index| {
142 const freelist_ptr = freelistByIndex(free_index);
143 opt_free_index = @cmpxchgWeak(Node.OptionalIndex, freelist_head, opt_free_index, freelist_ptr.*, .seq_cst, .seq_cst) orelse {
144 // We won the allocation race.
145 return init(free_index, parent, name, estimated_total_items);
146 };
147 }
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 }
102155
103 /// To be called exactly once after `start`.
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;
156 return init(@enumFromInt(free_index), parent, name, estimated_total_items);
110157 }
111158
112159 /// This is the same as calling `start` and then `end` on the returned `Node`. Thread-safe.
113 pub fn completeOne(self: *Node) void {
114 _ = @atomicRmw(usize, &self.unprotected_completed_items, .Add, 1, .monotonic);
160 pub fn completeOne(n: Node) void {
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);
115178 }
116179
117180 /// Finish a started `Node`. Thread-safe.
118 pub fn end(child: *Node) void {
119 if (child.parent()) |p| {
120 // Make sure the other thread doesn't access this memory that is
121 // about to be released.
122 child.mutex.lock();
123
124 const other = if (child.sibling_list_node.next) |n| n else child.sibling_list_node.prev;
125 _ = @cmpxchgStrong(std.DoublyLinkedList(void).Node, &p.parent_list_node.next, child, other, .seq_cst, .seq_cst);
126 p.completeOne();
181 pub fn end(n: Node) void {
182 const index = n.index.unwrap() orelse return;
183 const parent_ptr = parentByIndex(index);
184 if (parent_ptr.unwrap()) |parent_index| {
185 _ = @atomicRmw(u32, &storageByIndex(parent_index).completed_count, .Add, 1, .monotonic);
186 @atomicStore(Node.Parent, parent_ptr, .unused, .seq_cst);
187
188 const freelist_head = &global_progress.node_freelist_first;
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 }
127194 } else {
128195 @atomicStore(bool, &global_progress.done, true, .seq_cst);
129196 global_progress.redraw_event.set();
......@@ -131,19 +198,35 @@ pub const Node = struct {
131198 }
132199 }
133200
134 /// Thread-safe. 0 means unknown.
135 pub fn setEstimatedTotalItems(self: *Node, count: usize) void {
136 @atomicStore(usize, &self.unprotected_estimated_total_items, count, .monotonic);
201 fn storageByIndex(index: Node.Index) *Node.Storage {
202 return &global_progress.node_storage[@intFromEnum(index)];
137203 }
138204
139 /// Thread-safe.
140 pub fn setCompletedItems(self: *Node, completed_items: usize) void {
141 @atomicStore(usize, &self.unprotected_completed_items, completed_items, .monotonic);
205 fn parentByIndex(index: Node.Index) *Node.Parent {
206 return &global_progress.node_parents[@intFromEnum(index)];
207 }
208
209 fn freelistByIndex(index: Node.Index) *Node.OptionalIndex {
210 return &global_progress.node_freelist[@intFromEnum(index)];
142211 }
143212
144 fn parent(child: *Node) ?*Node {
145 const parent_node = child.parent_list_node.prev orelse return null;
146 return @fieldParentPtr("parent_list_node", parent_node);
213 fn init(free_index: Index, parent: Parent, name: []const u8, estimated_total_items: usize) Node {
214 assert(parent != .unused);
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() };
147230 }
148231};
149232
......@@ -151,25 +234,36 @@ var global_progress: Progress = .{
151234 .terminal = null,
152235 .is_windows_terminal = false,
153236 .supports_ansi_escape_codes = false,
154 .root = undefined,
155237 .update_thread = null,
156238 .redraw_event = .{},
157 .initialized = false,
158239 .refresh_rate_ns = undefined,
159240 .initial_delay_ns = undefined,
160241 .rows = 0,
161242 .cols = 0,
162243 .draw_buffer = undefined,
163244 .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,
164252};
165253
254const default_node_storage_buffer_len = 100;
255var node_parents_buffer: [default_node_storage_buffer_len]Node.Parent = undefined;
256var node_storage_buffer: [default_node_storage_buffer_len]Node.Storage = undefined;
257var node_freelist_buffer: [default_node_storage_buffer_len]Node.OptionalIndex = undefined;
258
166259/// Initializes a global Progress instance.
167260///
168261/// Asserts there is only one global Progress instance.
169262///
170263/// Call `Node.end` when done.
171pub fn start(options: Options) *Node {
172 assert(!global_progress.initialized);
264pub fn start(options: Options) Node {
265 // Ensure there is only 1 global Progress object.
266 assert(global_progress.node_end_index == 0);
173267 const stderr = std.io.getStdErr();
174268 if (stderr.supportsAnsiEscapeCodes()) {
175269 global_progress.terminal = stderr;
......@@ -181,18 +275,12 @@ pub fn start(options: Options) *Node {
181275 // we are in a "dumb" terminal like in acme or writing to a file
182276 global_progress.terminal = stderr;
183277 }
184 global_progress.root = .{
185 .mutex = .{},
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 };
278 @memset(global_progress.node_parents, .unused);
279 const root_node = Node.init(@enumFromInt(0), .none, options.root_name, options.estimated_total_items);
192280 global_progress.done = false;
193 global_progress.initialized = true;
281 global_progress.node_end_index = 1;
194282
195 assert(options.draw_buffer.len >= 100);
283 assert(options.draw_buffer.len >= 200);
196284 global_progress.draw_buffer = options.draw_buffer;
197285 global_progress.refresh_rate_ns = options.refresh_rate_ns;
198286 global_progress.initial_delay_ns = options.initial_delay_ns;
......@@ -204,7 +292,7 @@ pub fn start(options: Options) *Node {
204292 };
205293 posix.sigaction(posix.SIG.WINCH, &act, null) catch {
206294 global_progress.terminal = null;
207 return &global_progress.root;
295 return root_node;
208296 };
209297
210298 if (global_progress.terminal != null) {
......@@ -215,7 +303,7 @@ pub fn start(options: Options) *Node {
215303 }
216304 }
217305
218 return &global_progress.root;
306 return root_node;
219307}
220308
221309/// Returns whether a resize is needed to learn the terminal size.
......@@ -263,11 +351,85 @@ const save = "\x1b7";
263351const restore = "\x1b8";
264352const finish_sync = "\x1b[?2026l";
265353
354const tree_tee = "\x1B\x28\x30\x74\x71\x1B\x28\x42 "; // ├─
355const tree_line = "\x1B\x28\x30\x78\x1B\x28\x42 "; // │
356const tree_langle = "\x1B\x28\x30\x6d\x71\x1B\x28\x42 "; // └─
357
266358fn clearTerminal() void {
267359 write(clear);
268360}
269361
362const Children = struct {
363 child: Node.OptionalIndex,
364 sibling: Node.OptionalIndex,
365};
366
270367fn 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
271433 // The strategy is: keep the cursor at the beginning, and then with every redraw:
272434 // erase, save, write, restore
273435
......@@ -280,32 +442,91 @@ fn computeRedraw() []u8 {
280442 buf[0..prefix.len].* = prefix.*;
281443 i = prefix.len;
282444
283 // Walk the tree and write the progress output to the buffer.
284 var node: *Node = &global_progress.root;
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);
445 const root_node_index: Node.Index = @enumFromInt(0);
446 i = computeNode(buf, i, serialized_node_storage, serialized_node_parents, children, root_node_index);
288447
289 if (node.name.len != 0 or eti > 0) {
290 if (node.name.len != 0) {
291 i += (std.fmt.bufPrint(buf[i..], "{s}", .{node.name}) catch @panic("TODO")).len;
292 }
293 if (eti > 0) {
294 i += (std.fmt.bufPrint(buf[i..], "[{d}/{d}] ", .{ completed_items, eti }) catch @panic("TODO")).len;
295 } else if (completed_items != 0) {
296 i += (std.fmt.bufPrint(buf[i..], "[{d}] ", .{completed_items}) catch @panic("TODO")).len;
297 }
448 buf[i..][0..suffix.len].* = suffix.*;
449 i += suffix.len;
450
451 return buf[0..i];
452}
453
454fn computePrefix(
455 buf: []u8,
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
476fn 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;
298500 }
501 }
299502
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 }
301512 }
302513
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;
304517
305 buf[i..][0..suffix.len].* = suffix.*;
306 i += suffix.len;
518 if (children[@intFromEnum(node_index)].child.unwrap()) |child| {
519 i = computeNode(buf, i, serialized_node_storage, serialized_node_parents, children, child);
520 }
307521
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;
309530}
310531
311532fn write(buf: []const u8) void {