authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-10-18 18:53:01-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-10-18 18:53:44-07:00
log1952dd6437a73e3de211b649924a55fcb6e030be
tree803b7a98b7e689bf55436f36430147f1b486db71
parent14c173b2009806e3f408bb8a56f11501bda52820

Revert recent std.Progress implementation changes

I have noticed this causing my terminal to stop accepting input sometimes. The previous implementation with all of its flaws was better in the sense that it never caused this to happen. This commit has multiple reverts in it: Revert "Merge pull request #13148 from r00ster91/progressfollowup" This reverts commit cb257d59f97ea5655bf453d8e7f07bbfb0a88e58, reversing changes made to f5f28e0d2c49d5c62914edf0bff8f1941eef721f. Revert "`std.Progress`: fix inaccurate line truncation and use optimal max terminal width (#12079)" This reverts commit cd3d8f3a4ee22a41098b1daf2a36d7fbb342d0fa.

1 files changed, 48 insertions(+), 234 deletions(-)

lib/std/Progress.zig+48-234
...@@ -1,30 +1,23 @@...@@ -1,30 +1,23 @@
1//! This is a non-allocating, non-fallible, and thread-safe API for printing1//! This API non-allocating, non-fallible, and thread-safe.
2//! progress indicators to the terminal.
3//! The tradeoff is that users of this API must provide the storage2//! The tradeoff is that users of this API must provide the storage
4//! for each `Progress.Node`.3//! for each `Progress.Node`.
5//!4//!
6//! This library purposefully keeps its output simple and is ASCII-compatible.
7//!
8//! Initialize the struct directly, overriding these fields as desired:5//! Initialize the struct directly, overriding these fields as desired:
9//! * `refresh_rate_ms`6//! * `refresh_rate_ms`
10//! * `initial_delay_ms`7//! * `initial_delay_ms`
11//! * `dont_print_on_dumb`
12//! * `max_width`
138
14const std = @import("std");9const std = @import("std");
15const builtin = @import("builtin");10const builtin = @import("builtin");
16const windows = std.os.windows;11const windows = std.os.windows;
17const testing = std.testing;12const testing = std.testing;
18const assert = std.debug.assert;13const assert = std.debug.assert;
19const os = std.os;
20const time = std.time;
21const Progress = @This();14const Progress = @This();
2215
23/// `null` if the current node (and its children) should16/// `null` if the current node (and its children) should
24/// not print on update()17/// not print on update()
25terminal: ?std.fs.File = undefined,18terminal: ?std.fs.File = undefined,
2619
27/// Is this a Windows API terminal (note: this is not the same as being run on Windows20/// Is this a windows API terminal (note: this is not the same as being run on windows
28/// because other terminals exist like MSYS/git-bash)21/// because other terminals exist like MSYS/git-bash)
29is_windows_terminal: bool = false,22is_windows_terminal: bool = false,
3023
...@@ -42,31 +35,21 @@ root: Node = undefined,...@@ -42,31 +35,21 @@ root: Node = undefined,
4235
43/// Keeps track of how much time has passed since the beginning.36/// Keeps track of how much time has passed since the beginning.
44/// Used to compare with `initial_delay_ms` and `refresh_rate_ms`.37/// Used to compare with `initial_delay_ms` and `refresh_rate_ms`.
45timer: ?time.Timer = null,38timer: ?std.time.Timer = null,
4639
47/// When the previous refresh was written to the terminal.40/// When the previous refresh was written to the terminal.
48/// Used to compare with `refresh_rate_ms`.41/// Used to compare with `refresh_rate_ms`.
49prev_refresh_timestamp: u64 = undefined,42prev_refresh_timestamp: u64 = undefined,
5043
51/// This is the maximum number of bytes that can be written to the terminal each refresh.44/// This buffer represents the maximum number of bytes written to the terminal
52/// Anything larger than this is truncated.45/// with each refresh.
53// we can bump this up if we need to46output_buffer: [100]u8 = undefined,
54output_buffer: [256]u8 = undefined,
55output_buffer_slice: []u8 = undefined,
56
57/// This is the maximum number of bytes written to the terminal with each refresh.
58///
59/// It is recommended to leave this as `null` so that `start` can automatically decide an
60/// optimal width for the terminal.
61///
62/// Note that this will be clamped to at least 4 and output will appear malformed if it is < 4.
63max_width: ?usize = null,
6447
65/// How many nanoseconds between writing updates to the terminal.48/// How many nanoseconds between writing updates to the terminal.
66refresh_rate_ns: u64 = 50 * time.ns_per_ms,49refresh_rate_ns: u64 = 50 * std.time.ns_per_ms,
6750
68/// How many nanoseconds to keep the output hidden.51/// How many nanoseconds to keep the output hidden
69initial_delay_ns: u64 = 500 * time.ns_per_ms,52initial_delay_ns: u64 = 500 * std.time.ns_per_ms,
7053
71done: bool = true,54done: bool = true,
7255
...@@ -79,14 +62,11 @@ update_mutex: std.Thread.Mutex = .{},...@@ -79,14 +62,11 @@ update_mutex: std.Thread.Mutex = .{},
79/// we can move the cursor back later.62/// we can move the cursor back later.
80columns_written: usize = undefined,63columns_written: usize = undefined,
8164
82const truncation_suffix = "... ";
83
84/// Represents one unit of progress. Each node can have children nodes, or65/// Represents one unit of progress. Each node can have children nodes, or
85/// one can use integers with `update`.66/// one can use integers with `update`.
86pub const Node = struct {67pub const Node = struct {
87 context: *Progress,68 context: *Progress,
88 parent: ?*Node,69 parent: ?*Node,
89 /// The name that will be displayed for this node.
90 name: []const u8,70 name: []const u8,
91 /// Must be handled atomically to be thread-safe.71 /// Must be handled atomically to be thread-safe.
92 recently_updated_child: ?*Node = null,72 recently_updated_child: ?*Node = null,
...@@ -159,13 +139,9 @@ pub const Node = struct {...@@ -159,13 +139,9 @@ pub const Node = struct {
159139
160/// Create a new progress node.140/// Create a new progress node.
161/// Call `Node.end` when done.141/// Call `Node.end` when done.
142/// TODO solve https://github.com/ziglang/zig/issues/2765 and then change this
143/// API to return Progress rather than accept it as a parameter.
162/// `estimated_total_items` value of 0 means unknown.144/// `estimated_total_items` value of 0 means unknown.
163///
164/// Note that as soon as work is started and progress output is printed,
165/// `std.Progress` expects you to lean back and wait and not resize the terminal.
166/// Resizing the terminal during progress output may result in malformed output.
167// TODO: solve https://github.com/ziglang/zig/issues/2765 and then change this
168// API to return Progress rather than accept it as a parameter.
169pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *Node {145pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *Node {
170 const stderr = std.io.getStdErr();146 const stderr = std.io.getStdErr();
171 self.terminal = null;147 self.terminal = null;
...@@ -179,7 +155,6 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N...@@ -179,7 +155,6 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N
179 // we are in a "dumb" terminal like in acme or writing to a file155 // we are in a "dumb" terminal like in acme or writing to a file
180 self.terminal = stderr;156 self.terminal = stderr;
181 }157 }
182 self.calculateMaxWidth();
183 self.root = Node{158 self.root = Node{
184 .context = self,159 .context = self,
185 .parent = null,160 .parent = null,
...@@ -189,83 +164,11 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N...@@ -189,83 +164,11 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N
189 };164 };
190 self.columns_written = 0;165 self.columns_written = 0;
191 self.prev_refresh_timestamp = 0;166 self.prev_refresh_timestamp = 0;
192 self.timer = time.Timer.start() catch null;167 self.timer = std.time.Timer.start() catch null;
193 self.done = false;168 self.done = false;
194 return &self.root;169 return &self.root;
195}170}
196171
197fn calculateMaxWidth(self: *Progress) void {
198 if (self.max_width == null) {
199 if (self.terminal) |terminal| {
200 // choose an optimal width and account for progress output that could have been printed
201 // before us by another `std.Progress` instance
202 const terminal_width = self.getTerminalWidth(terminal.handle) catch 100;
203 const chars_already_printed = self.getTerminalCursorColumn(terminal) catch 0;
204 self.max_width = terminal_width - chars_already_printed;
205 } else {
206 self.max_width = 100;
207 }
208 }
209 self.max_width = std.math.clamp(
210 self.max_width.?,
211 truncation_suffix.len, // make sure we can at least truncate
212 self.output_buffer.len - 1,
213 );
214}
215
216fn getTerminalWidth(self: Progress, file_handle: os.fd_t) !u16 {
217 if (builtin.os.tag == .linux) {
218 // TODO: figure out how to get this working on FreeBSD, macOS etc. too.
219 // they too should have capabilities to figure out the cursor column.
220 var winsize: os.linux.winsize = undefined;
221 switch (os.errno(os.linux.ioctl(file_handle, os.linux.T.IOCGWINSZ, @ptrToInt(&winsize)))) {
222 .SUCCESS => return winsize.ws_col,
223 else => return error.Unexpected,
224 }
225 } else if (builtin.os.tag == .windows) {
226 std.debug.assert(self.is_windows_terminal);
227 var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
228 if (windows.kernel32.GetConsoleScreenBufferInfo(file_handle, &info) != windows.TRUE)
229 return error.Unexpected;
230 return @intCast(u16, info.dwSize.X);
231 } else {
232 return error.Unsupported;
233 }
234}
235
236fn getTerminalCursorColumn(self: Progress, file: std.fs.File) !u16 {
237 // TODO: figure out how to get this working on FreeBSD, macOS etc. too.
238 // they too should have termios or capabilities to figure out the terminal width.
239 if (builtin.os.tag == .linux and self.supports_ansi_escape_codes) {
240 // First, disable echo and enable non-canonical mode
241 // (so that no enter press required for us to read the output of the escape sequence below)
242 const original_termios = try os.tcgetattr(file.handle);
243 var new_termios = original_termios;
244 new_termios.lflag &= ~(os.linux.ECHO | os.linux.ICANON);
245 try os.tcsetattr(file.handle, .NOW, new_termios);
246 defer os.tcsetattr(file.handle, .NOW, original_termios) catch {
247 // Sorry for ruining your terminal
248 };
249
250 try file.writeAll("\x1b[6n");
251 var buf: ["\x1b[65536;65536R".len]u8 = undefined;
252 const output = try file.reader().readUntilDelimiter(&buf, 'R');
253 var splitter = std.mem.split(u8, output, ";");
254 _ = splitter.next().?; // skip first half
255 const column_half = splitter.next() orelse return error.UnexpectedEnd;
256 const column = try std.fmt.parseUnsigned(u16, column_half, 10);
257 return column - 1; // it's one-based
258 } else if (builtin.os.tag == .windows) {
259 std.debug.assert(self.is_windows_terminal);
260 var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
261 if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE)
262 return error.Unexpected;
263 return @intCast(u16, info.dwCursorPosition.X);
264 } else {
265 return error.Unsupported;
266 }
267}
268
269/// Updates the terminal if enough time has passed since last update. Thread-safe.172/// Updates the terminal if enough time has passed since last update. Thread-safe.
270pub fn maybeRefresh(self: *Progress) void {173pub fn maybeRefresh(self: *Progress) void {
271 if (self.timer) |*timer| {174 if (self.timer) |*timer| {
...@@ -295,16 +198,14 @@ fn refreshWithHeldLock(self: *Progress) void {...@@ -295,16 +198,14 @@ fn refreshWithHeldLock(self: *Progress) void {
295198
296 const file = self.terminal orelse return;199 const file = self.terminal orelse return;
297200
298 // prepare for printing unprintable characters
299 self.output_buffer_slice = &self.output_buffer;
300
301 var end: usize = 0;201 var end: usize = 0;
302 if (self.columns_written > 0) {202 if (self.columns_written > 0) {
303 // restore the cursor position by moving the cursor203 // restore the cursor position by moving the cursor
304 // `columns_written` cells to the left, then clear the rest of the line204 // `columns_written` cells to the left, then clear the rest of the
205 // line
305 if (self.supports_ansi_escape_codes) {206 if (self.supports_ansi_escape_codes) {
306 end += (std.fmt.bufPrint(self.output_buffer_slice[end..], "\x1b[{d}D", .{self.columns_written}) catch unreachable).len;207 end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[{d}D", .{self.columns_written}) catch unreachable).len;
307 end += (std.fmt.bufPrint(self.output_buffer_slice[end..], "\x1b[0K", .{}) catch unreachable).len;208 end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[0K", .{}) catch unreachable).len;
308 } else if (builtin.os.tag == .windows) winapi: {209 } else if (builtin.os.tag == .windows) winapi: {
309 std.debug.assert(self.is_windows_terminal);210 std.debug.assert(self.is_windows_terminal);
310211
...@@ -346,53 +247,47 @@ fn refreshWithHeldLock(self: *Progress) void {...@@ -346,53 +247,47 @@ fn refreshWithHeldLock(self: *Progress) void {
346 unreachable;247 unreachable;
347 } else {248 } else {
348 // we are in a "dumb" terminal like in acme or writing to a file249 // we are in a "dumb" terminal like in acme or writing to a file
349 self.output_buffer_slice[end] = '\n';250 self.output_buffer[end] = '\n';
350 end += 1;251 end += 1;
351 }252 }
352253
353 self.columns_written = 0;254 self.columns_written = 0;
354 }255 }
355256
356 // from here on we will write printable characters. we also make sure the unprintable characters
357 // we possibly wrote previously don't affect whether we truncate the line in `bufWrite`.
358 const unprintables = end;
359 end = 0;
360 self.output_buffer_slice = self.output_buffer[unprintables..@min(self.output_buffer.len, unprintables + self.max_width.?)];
361
362 if (!self.done) {257 if (!self.done) {
363 var need_ellipsis = false;258 var need_ellipse = false;
364 var maybe_node: ?*Node = &self.root;259 var maybe_node: ?*Node = &self.root;
365 while (maybe_node) |node| {260 while (maybe_node) |node| {
366 if (need_ellipsis) {261 if (need_ellipse) {
367 self.bufWrite(&end, "... ", .{});262 self.bufWrite(&end, "... ", .{});
368 }263 }
369 need_ellipsis = false;264 need_ellipse = false;
370 const estimated_total_items = @atomicLoad(usize, &node.unprotected_estimated_total_items, .Monotonic);265 const eti = @atomicLoad(usize, &node.unprotected_estimated_total_items, .Monotonic);
371 const completed_items = @atomicLoad(usize, &node.unprotected_completed_items, .Monotonic);266 const completed_items = @atomicLoad(usize, &node.unprotected_completed_items, .Monotonic);
372 const current_item = completed_items + 1;267 const current_item = completed_items + 1;
373 if (node.name.len != 0 or estimated_total_items > 0) {268 if (node.name.len != 0 or eti > 0) {
374 if (node.name.len != 0) {269 if (node.name.len != 0) {
375 self.bufWrite(&end, "{s}", .{node.name});270 self.bufWrite(&end, "{s}", .{node.name});
376 need_ellipsis = true;271 need_ellipse = true;
377 }272 }
378 if (estimated_total_items > 0) {273 if (eti > 0) {
379 if (need_ellipsis) self.bufWrite(&end, " ", .{});274 if (need_ellipse) self.bufWrite(&end, " ", .{});
380 self.bufWrite(&end, "[{d}/{d}] ", .{ current_item, estimated_total_items });275 self.bufWrite(&end, "[{d}/{d}] ", .{ current_item, eti });
381 need_ellipsis = false;276 need_ellipse = false;
382 } else if (completed_items != 0) {277 } else if (completed_items != 0) {
383 if (need_ellipsis) self.bufWrite(&end, " ", .{});278 if (need_ellipse) self.bufWrite(&end, " ", .{});
384 self.bufWrite(&end, "[{d}] ", .{current_item});279 self.bufWrite(&end, "[{d}] ", .{current_item});
385 need_ellipsis = false;280 need_ellipse = false;
386 }281 }
387 }282 }
388 maybe_node = @atomicLoad(?*Node, &node.recently_updated_child, .Acquire);283 maybe_node = @atomicLoad(?*Node, &node.recently_updated_child, .Acquire);
389 }284 }
390 if (need_ellipsis) {285 if (need_ellipse) {
391 self.bufWrite(&end, "... ", .{});286 self.bufWrite(&end, "... ", .{});
392 }287 }
393 }288 }
394289
395 _ = file.write(self.output_buffer[0 .. end + unprintables]) catch {290 _ = file.write(self.output_buffer[0..end]) catch {
396 // Stop trying to write to this file once it errors.291 // Stop trying to write to this file once it errors.
397 self.terminal = null;292 self.terminal = null;
398 };293 };
...@@ -415,113 +310,32 @@ pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void {...@@ -415,113 +310,32 @@ pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void {
415}310}
416311
417fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void {312fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void {
418 if (std.fmt.bufPrint(self.output_buffer_slice[end.*..], format, args)) |written| {313 if (std.fmt.bufPrint(self.output_buffer[end.*..], format, args)) |written| {
419 const amt = written.len;314 const amt = written.len;
420 end.* += amt;315 end.* += amt;
421 self.columns_written += amt;316 self.columns_written += amt;
422 } else |err| switch (err) {317 } else |err| switch (err) {
423 error.NoSpaceLeft => {318 error.NoSpaceLeft => {
424 // truncate the line with a suffix.319 self.columns_written += self.output_buffer.len - end.*;
425 // for example if we have "hello world" (len=11) and 10 is the limit,320 end.* = self.output_buffer.len;
426 // it would become "hello w... "321 const suffix = "... ";
427 self.columns_written += self.output_buffer_slice.len - end.*;322 std.mem.copy(u8, self.output_buffer[self.output_buffer.len - suffix.len ..], suffix);
428 end.* = self.output_buffer_slice.len;
429 std.mem.copy(
430 u8,
431 self.output_buffer_slice[self.output_buffer_slice.len - truncation_suffix.len ..],
432 truncation_suffix,
433 );
434 },323 },
435 }324 }
436}325}
437326
438// By default these tests are disabled because they use time.sleep()
439// and are therefore slow. They also prints bogus progress data to stderr.
440const skip_tests = true;
441
442test "behavior on buffer overflow" {
443 if (skip_tests)
444 return error.SkipZigTest;
445
446 // uncomment this to move the cursor
447 //std.debug.print("{s}", .{"A" ** 300});
448
449 var progress = Progress{};
450
451 const long_string = "A" ** 300;
452 var node = progress.start(long_string, 0);
453
454 const speed_factor = time.ns_per_s / 4;
455
456 time.sleep(speed_factor);
457 node.activate();
458 time.sleep(speed_factor);
459 node.end();
460}
461
462test "multiple tasks with long names" {
463 if (skip_tests)
464 return error.SkipZigTest;
465
466 var progress = Progress{};
467
468 const tasks = [_][]const u8{
469 "A" ** 99,
470 "A" ** 100,
471 "A" ** 101,
472 "A" ** 102,
473 "A" ** 103,
474 };
475
476 const speed_factor = time.ns_per_s / 6;
477
478 for (tasks) |task| {
479 var node = progress.start(task, 3);
480 time.sleep(speed_factor);
481 node.activate();
482
483 time.sleep(speed_factor);
484 node.completeOne();
485 time.sleep(speed_factor);
486 node.completeOne();
487 time.sleep(speed_factor);
488 node.completeOne();
489
490 node.end();
491 }
492}
493
494test "very short max width" {
495 if (skip_tests)
496 return error.SkipZigTest;
497
498 var progress = Progress{ .max_width = 4 };
499
500 const task = "A" ** 300;
501
502 const speed_factor = time.ns_per_s / 2;
503
504 var node = progress.start(task, 3);
505 time.sleep(speed_factor);
506 node.activate();
507
508 time.sleep(speed_factor);
509 node.completeOne();
510 time.sleep(speed_factor);
511 node.completeOne();
512
513 node.end();
514}
515
516test "basic functionality" {327test "basic functionality" {
517 if (skip_tests)328 var disable = true;
329 if (disable) {
330 // This test is disabled because it uses time.sleep() and is therefore slow. It also
331 // prints bogus progress data to stderr.
518 return error.SkipZigTest;332 return error.SkipZigTest;
519333 }
520 var progress = Progress{};334 var progress = Progress{};
521 const root_node = progress.start("", 100);335 const root_node = progress.start("", 100);
522 defer root_node.end();336 defer root_node.end();
523337
524 const speed_factor = time.ns_per_ms;338 const speed_factor = std.time.ns_per_ms;
525339
526 const sub_task_names = [_][]const u8{340 const sub_task_names = [_][]const u8{
527 "reticulating splines",341 "reticulating splines",
...@@ -538,24 +352,24 @@ test "basic functionality" {...@@ -538,24 +352,24 @@ test "basic functionality" {
538 next_sub_task = (next_sub_task + 1) % sub_task_names.len;352 next_sub_task = (next_sub_task + 1) % sub_task_names.len;
539353
540 node.completeOne();354 node.completeOne();
541 time.sleep(5 * speed_factor);355 std.time.sleep(5 * speed_factor);
542 node.completeOne();356 node.completeOne();
543 node.completeOne();357 node.completeOne();
544 time.sleep(5 * speed_factor);358 std.time.sleep(5 * speed_factor);
545 node.completeOne();359 node.completeOne();
546 node.completeOne();360 node.completeOne();
547 time.sleep(5 * speed_factor);361 std.time.sleep(5 * speed_factor);
548362
549 node.end();363 node.end();
550364
551 time.sleep(5 * speed_factor);365 std.time.sleep(5 * speed_factor);
552 }366 }
553 {367 {
554 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);368 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);
555 node.activate();369 node.activate();
556 time.sleep(10 * speed_factor);370 std.time.sleep(10 * speed_factor);
557 progress.refresh();371 progress.refresh();
558 time.sleep(10 * speed_factor);372 std.time.sleep(10 * speed_factor);
559 node.end();373 node.end();
560 }374 }
561}375}