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//! This is a non-allocating, non-fallible, and thread-safe API for printing
2//! progress indicators to the terminal.
1//! This API non-allocating, non-fallible, and thread-safe.
32//! The tradeoff is that users of this API must provide the storage
43//! for each `Progress.Node`.
54//!
6//! This library purposefully keeps its output simple and is ASCII-compatible.
7//!
85//! Initialize the struct directly, overriding these fields as desired:
96//! * `refresh_rate_ms`
107//! * `initial_delay_ms`
11//! * `dont_print_on_dumb`
12//! * `max_width`
138
149const std = @import("std");
1510const builtin = @import("builtin");
1611const windows = std.os.windows;
1712const testing = std.testing;
1813const assert = std.debug.assert;
19const os = std.os;
20const time = std.time;
2114const Progress = @This();
2215
2316/// `null` if the current node (and its children) should
2417/// not print on update()
2518terminal: ?std.fs.File = undefined,
2619
27/// Is this a Windows API terminal (note: this is not the same as being run on Windows
20/// Is this a windows API terminal (note: this is not the same as being run on windows
2821/// because other terminals exist like MSYS/git-bash)
2922is_windows_terminal: bool = false,
3023
......@@ -42,31 +35,21 @@ root: Node = undefined,
4235
4336/// Keeps track of how much time has passed since the beginning.
4437/// Used to compare with `initial_delay_ms` and `refresh_rate_ms`.
45timer: ?time.Timer = null,
38timer: ?std.time.Timer = null,
4639
4740/// When the previous refresh was written to the terminal.
4841/// Used to compare with `refresh_rate_ms`.
4942prev_refresh_timestamp: u64 = undefined,
5043
51/// This is the maximum number of bytes that can be written to the terminal each refresh.
52/// Anything larger than this is truncated.
53// we can bump this up if we need to
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,
44/// This buffer represents the maximum number of bytes written to the terminal
45/// with each refresh.
46output_buffer: [100]u8 = undefined,
6447
6548/// 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.
69initial_delay_ns: u64 = 500 * time.ns_per_ms,
51/// How many nanoseconds to keep the output hidden
52initial_delay_ns: u64 = 500 * std.time.ns_per_ms,
7053
7154done: bool = true,
7255
......@@ -79,14 +62,11 @@ update_mutex: std.Thread.Mutex = .{},
7962/// we can move the cursor back later.
8063columns_written: usize = undefined,
8164
82const truncation_suffix = "... ";
83
8465/// Represents one unit of progress. Each node can have children nodes, or
8566/// one can use integers with `update`.
8667pub const Node = struct {
8768 context: *Progress,
8869 parent: ?*Node,
89 /// The name that will be displayed for this node.
9070 name: []const u8,
9171 /// Must be handled atomically to be thread-safe.
9272 recently_updated_child: ?*Node = null,
......@@ -159,13 +139,9 @@ pub const Node = struct {
159139
160140/// Create a new progress node.
161141/// 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.
162144/// `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.
169145pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *Node {
170146 const stderr = std.io.getStdErr();
171147 self.terminal = null;
......@@ -179,7 +155,6 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N
179155 // we are in a "dumb" terminal like in acme or writing to a file
180156 self.terminal = stderr;
181157 }
182 self.calculateMaxWidth();
183158 self.root = Node{
184159 .context = self,
185160 .parent = null,
......@@ -189,83 +164,11 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N
189164 };
190165 self.columns_written = 0;
191166 self.prev_refresh_timestamp = 0;
192 self.timer = time.Timer.start() catch null;
167 self.timer = std.time.Timer.start() catch null;
193168 self.done = false;
194169 return &self.root;
195170}
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
269172/// Updates the terminal if enough time has passed since last update. Thread-safe.
270173pub fn maybeRefresh(self: *Progress) void {
271174 if (self.timer) |*timer| {
......@@ -295,16 +198,14 @@ fn refreshWithHeldLock(self: *Progress) void {
295198
296199 const file = self.terminal orelse return;
297200
298 // prepare for printing unprintable characters
299 self.output_buffer_slice = &self.output_buffer;
300
301201 var end: usize = 0;
302202 if (self.columns_written > 0) {
303203 // restore the cursor position by moving the cursor
304 // `columns_written` cells to the left, then clear the rest of the line
204 // `columns_written` cells to the left, then clear the rest of the
205 // line
305206 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;
307 end += (std.fmt.bufPrint(self.output_buffer_slice[end..], "\x1b[0K", .{}) catch unreachable).len;
207 end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[{d}D", .{self.columns_written}) catch unreachable).len;
208 end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[0K", .{}) catch unreachable).len;
308209 } else if (builtin.os.tag == .windows) winapi: {
309210 std.debug.assert(self.is_windows_terminal);
310211
......@@ -346,53 +247,47 @@ fn refreshWithHeldLock(self: *Progress) void {
346247 unreachable;
347248 } else {
348249 // 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';
350251 end += 1;
351252 }
352253
353254 self.columns_written = 0;
354255 }
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
362257 if (!self.done) {
363 var need_ellipsis = false;
258 var need_ellipse = false;
364259 var maybe_node: ?*Node = &self.root;
365260 while (maybe_node) |node| {
366 if (need_ellipsis) {
261 if (need_ellipse) {
367262 self.bufWrite(&end, "... ", .{});
368263 }
369 need_ellipsis = false;
370 const estimated_total_items = @atomicLoad(usize, &node.unprotected_estimated_total_items, .Monotonic);
264 need_ellipse = false;
265 const eti = @atomicLoad(usize, &node.unprotected_estimated_total_items, .Monotonic);
371266 const completed_items = @atomicLoad(usize, &node.unprotected_completed_items, .Monotonic);
372267 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) {
374269 if (node.name.len != 0) {
375270 self.bufWrite(&end, "{s}", .{node.name});
376 need_ellipsis = true;
271 need_ellipse = true;
377272 }
378 if (estimated_total_items > 0) {
379 if (need_ellipsis) self.bufWrite(&end, " ", .{});
380 self.bufWrite(&end, "[{d}/{d}] ", .{ current_item, estimated_total_items });
381 need_ellipsis = false;
273 if (eti > 0) {
274 if (need_ellipse) self.bufWrite(&end, " ", .{});
275 self.bufWrite(&end, "[{d}/{d}] ", .{ current_item, eti });
276 need_ellipse = false;
382277 } else if (completed_items != 0) {
383 if (need_ellipsis) self.bufWrite(&end, " ", .{});
278 if (need_ellipse) self.bufWrite(&end, " ", .{});
384279 self.bufWrite(&end, "[{d}] ", .{current_item});
385 need_ellipsis = false;
280 need_ellipse = false;
386281 }
387282 }
388283 maybe_node = @atomicLoad(?*Node, &node.recently_updated_child, .Acquire);
389284 }
390 if (need_ellipsis) {
285 if (need_ellipse) {
391286 self.bufWrite(&end, "... ", .{});
392287 }
393288 }
394289
395 _ = file.write(self.output_buffer[0 .. end + unprintables]) catch {
290 _ = file.write(self.output_buffer[0..end]) catch {
396291 // Stop trying to write to this file once it errors.
397292 self.terminal = null;
398293 };
......@@ -415,113 +310,32 @@ pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void {
415310}
416311
417312fn 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| {
419314 const amt = written.len;
420315 end.* += amt;
421316 self.columns_written += amt;
422317 } else |err| switch (err) {
423318 error.NoSpaceLeft => {
424 // truncate the line with a suffix.
425 // for example if we have "hello world" (len=11) and 10 is the limit,
426 // it would become "hello w... "
427 self.columns_written += self.output_buffer_slice.len - end.*;
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 );
319 self.columns_written += self.output_buffer.len - end.*;
320 end.* = self.output_buffer.len;
321 const suffix = "... ";
322 std.mem.copy(u8, self.output_buffer[self.output_buffer.len - suffix.len ..], suffix);
434323 },
435324 }
436325}
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
516327test "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.
518332 return error.SkipZigTest;
519
333 }
520334 var progress = Progress{};
521335 const root_node = progress.start("", 100);
522336 defer root_node.end();
523337
524 const speed_factor = time.ns_per_ms;
338 const speed_factor = std.time.ns_per_ms;
525339
526340 const sub_task_names = [_][]const u8{
527341 "reticulating splines",
......@@ -538,24 +352,24 @@ test "basic functionality" {
538352 next_sub_task = (next_sub_task + 1) % sub_task_names.len;
539353
540354 node.completeOne();
541 time.sleep(5 * speed_factor);
355 std.time.sleep(5 * speed_factor);
542356 node.completeOne();
543357 node.completeOne();
544 time.sleep(5 * speed_factor);
358 std.time.sleep(5 * speed_factor);
545359 node.completeOne();
546360 node.completeOne();
547 time.sleep(5 * speed_factor);
361 std.time.sleep(5 * speed_factor);
548362
549363 node.end();
550364
551 time.sleep(5 * speed_factor);
365 std.time.sleep(5 * speed_factor);
552366 }
553367 {
554368 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);
555369 node.activate();
556 time.sleep(10 * speed_factor);
370 std.time.sleep(10 * speed_factor);
557371 progress.refresh();
558 time.sleep(10 * speed_factor);
372 std.time.sleep(10 * speed_factor);
559373 node.end();
560374 }
561375}