authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-05-31 11:54:44-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-05-31 19:00:14-04:00
loge09963d8544c19812db20e520f09f6e5d9a57d64
tree8c6281bf5f0b9b04a6dbfb2c368d2a6497015745
parent4918c2ce2d739ba5e1b207a2ef32afcc04bfc435

std.Progress: keep the cursor at the beginning

This changes the terminal display to keep the cursor at the top left of the progress display, so that unlocked stderr writes, perhaps by child processes, don't get eaten by the clear.

1 files changed, 83 insertions(+), 85 deletions(-)

lib/std/Progress.zig+83-85
...@@ -23,17 +23,13 @@ redraw_event: std.Thread.ResetEvent,...@@ -23,17 +23,13 @@ redraw_event: std.Thread.ResetEvent,
23/// Indicates a request to shut down and reset global state.23/// Indicates a request to shut down and reset global state.
24/// Accessed atomically.24/// Accessed atomically.
25done: bool,25done: bool,
26need_clear: bool,
2627
27refresh_rate_ns: u64,28refresh_rate_ns: u64,
28initial_delay_ns: u64,29initial_delay_ns: u64,
2930
30rows: u16,31rows: u16,
31cols: u16,32cols: u16,
32/// Tracks the number of newlines that have been actually written to the terminal.
33written_newline_count: u16,
34/// Tracks the number of newlines that will be written to the terminal if the
35/// draw buffer is sent.
36accumulated_newline_count: u16,
3733
38/// Accessed only by the update thread.34/// Accessed only by the update thread.
39draw_buffer: []u8,35draw_buffer: []u8,
...@@ -312,10 +308,9 @@ var global_progress: Progress = .{...@@ -312,10 +308,9 @@ var global_progress: Progress = .{
312 .initial_delay_ns = undefined,308 .initial_delay_ns = undefined,
313 .rows = 0,309 .rows = 0,
314 .cols = 0,310 .cols = 0,
315 .written_newline_count = 0,
316 .accumulated_newline_count = 0,
317 .draw_buffer = undefined,311 .draw_buffer = undefined,
318 .done = false,312 .done = false,
313 .need_clear = false,
319314
320 .node_parents = &node_parents_buffer,315 .node_parents = &node_parents_buffer,
321 .node_storage = &node_storage_buffer,316 .node_storage = &node_storage_buffer,
...@@ -446,10 +441,11 @@ fn updateThreadRun() void {...@@ -446,10 +441,11 @@ fn updateThreadRun() void {
446 if (@atomicLoad(bool, &global_progress.done, .seq_cst)) return;441 if (@atomicLoad(bool, &global_progress.done, .seq_cst)) return;
447 maybeUpdateSize(resize_flag);442 maybeUpdateSize(resize_flag);
448443
449 const buffer = computeRedraw(&serialized_buffer);444 const buffer, _ = computeRedraw(&serialized_buffer);
450 if (stderr_mutex.tryLock()) {445 if (stderr_mutex.tryLock()) {
451 defer stderr_mutex.unlock();446 defer stderr_mutex.unlock();
452 write(buffer) catch return;447 write(buffer) catch return;
448 global_progress.need_clear = true;
453 }449 }
454 }450 }
455451
...@@ -464,10 +460,11 @@ fn updateThreadRun() void {...@@ -464,10 +460,11 @@ fn updateThreadRun() void {
464460
465 maybeUpdateSize(resize_flag);461 maybeUpdateSize(resize_flag);
466462
467 const buffer = computeRedraw(&serialized_buffer);463 const buffer, _ = computeRedraw(&serialized_buffer);
468 if (stderr_mutex.tryLock()) {464 if (stderr_mutex.tryLock()) {
469 defer stderr_mutex.unlock();465 defer stderr_mutex.unlock();
470 write(buffer) catch return;466 write(buffer) catch return;
467 global_progress.need_clear = true;
471 }468 }
472 }469 }
473}470}
...@@ -488,11 +485,13 @@ fn windowsApiUpdateThreadRun() void {...@@ -488,11 +485,13 @@ fn windowsApiUpdateThreadRun() void {
488 if (@atomicLoad(bool, &global_progress.done, .seq_cst)) return;485 if (@atomicLoad(bool, &global_progress.done, .seq_cst)) return;
489 maybeUpdateSize(resize_flag);486 maybeUpdateSize(resize_flag);
490487
491 const buffer = computeRedraw(&serialized_buffer);488 const buffer, const nl_n = computeRedraw(&serialized_buffer);
492 if (stderr_mutex.tryLock()) {489 if (stderr_mutex.tryLock()) {
493 defer stderr_mutex.unlock();490 defer stderr_mutex.unlock();
494 windowsApiWriteMarker();491 windowsApiWriteMarker();
495 write(buffer) catch return;492 write(buffer) catch return;
493 global_progress.need_clear = true;
494 windowsApiMoveToMarker(nl_n) catch return;
496 }495 }
497 }496 }
498497
...@@ -507,12 +506,14 @@ fn windowsApiUpdateThreadRun() void {...@@ -507,12 +506,14 @@ fn windowsApiUpdateThreadRun() void {
507506
508 maybeUpdateSize(resize_flag);507 maybeUpdateSize(resize_flag);
509508
510 const buffer = computeRedraw(&serialized_buffer);509 const buffer, const nl_n = computeRedraw(&serialized_buffer);
511 if (stderr_mutex.tryLock()) {510 if (stderr_mutex.tryLock()) {
512 defer stderr_mutex.unlock();511 defer stderr_mutex.unlock();
513 clearWrittenWindowsApi() catch return;512 clearWrittenWindowsApi() catch return;
514 windowsApiWriteMarker();513 windowsApiWriteMarker();
515 write(buffer) catch return;514 write(buffer) catch return;
515 global_progress.need_clear = true;
516 windowsApiMoveToMarker(nl_n) catch return;
516 }517 }
517 }518 }
518}519}
...@@ -645,40 +646,16 @@ fn appendTreeSymbol(symbol: TreeSymbol, buf: []u8, start_i: usize) usize {...@@ -645,40 +646,16 @@ fn appendTreeSymbol(symbol: TreeSymbol, buf: []u8, start_i: usize) usize {
645}646}
646647
647fn clearWrittenWithEscapeCodes() anyerror!void {648fn clearWrittenWithEscapeCodes() anyerror!void {
648 if (global_progress.written_newline_count == 0) return;649 if (!global_progress.need_clear) return;
649650
650 var i: usize = 0;651 var i: usize = 0;
651 const buf = global_progress.draw_buffer;652 const buf = global_progress.draw_buffer;
652653
653 buf[i..][0..start_sync.len].* = start_sync.*;
654 i += start_sync.len;
655
656 i = computeClear(buf, i);
657
658 buf[i..][0..finish_sync.len].* = finish_sync.*;
659 i += finish_sync.len;
660
661 global_progress.accumulated_newline_count = 0;
662 try write(buf[0..i]);
663}
664
665fn computeClear(buf: []u8, start_i: usize) usize {
666 var i = start_i;
667
668 const prev_nl_n = global_progress.written_newline_count;
669 if (prev_nl_n > 0) {
670 buf[i] = '\r';
671 i += 1;
672 for (0..prev_nl_n) |_| {
673 buf[i..][0..up_one_line.len].* = up_one_line.*;
674 i += up_one_line.len;
675 }
676 }
677
678 buf[i..][0..clear.len].* = clear.*;654 buf[i..][0..clear.len].* = clear.*;
679 i += clear.len;655 i += clear.len;
680656
681 return i;657 global_progress.need_clear = false;
658 try write(buf[0..i]);
682}659}
683660
684/// U+25BA or ►661/// U+25BA or ►
...@@ -704,38 +681,44 @@ fn clearWrittenWindowsApi() error{Unexpected}!void {...@@ -704,38 +681,44 @@ fn clearWrittenWindowsApi() error{Unexpected}!void {
704 // but it must be a valid attribute and it actually needs to apply to the first681 // but it must be a valid attribute and it actually needs to apply to the first
705 // character in order to be readable via ReadConsoleOutputAttribute. It doesn't seem682 // character in order to be readable via ReadConsoleOutputAttribute. It doesn't seem
706 // like any of the available attributes are invisible/benign.683 // like any of the available attributes are invisible/benign.
707 const prev_nl_n = global_progress.written_newline_count;684 if (!global_progress.need_clear) return;
708 if (prev_nl_n > 0) {685 const handle = global_progress.terminal.handle;
709 const handle = global_progress.terminal.handle;686 const screen_area = @as(windows.DWORD, global_progress.cols) * global_progress.rows;
710 const screen_area = @as(windows.DWORD, global_progress.cols) * global_progress.rows;
711687
712 var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;688 var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
713 if (windows.kernel32.GetConsoleScreenBufferInfo(handle, &console_info) == 0) {689 if (windows.kernel32.GetConsoleScreenBufferInfo(handle, &console_info) == 0) {
714 return error.Unexpected;690 return error.Unexpected;
715 }691 }
716 const cursor_pos = console_info.dwCursorPosition;692 var num_chars_written: windows.DWORD = undefined;
717 const expected_y = cursor_pos.Y - @as(i16, @intCast(prev_nl_n));693 if (windows.kernel32.FillConsoleOutputCharacterW(handle, ' ', screen_area, console_info.dwCursorPosition, &num_chars_written) == 0) {
718 var start_pos = windows.COORD{ .X = 0, .Y = expected_y };694 return error.Unexpected;
719 while (start_pos.Y >= 0) {695 }
720 var wchar: [1]u16 = undefined;696}
721 var num_console_chars_read: windows.DWORD = undefined;
722 if (windows.kernel32.ReadConsoleOutputCharacterW(handle, &wchar, wchar.len, start_pos, &num_console_chars_read) == 0) {
723 return error.Unexpected;
724 }
725697
726 if (wchar[0] == windows_api_start_marker) break;698fn windowsApiMoveToMarker(nl_n: usize) error{Unexpected}!void {
727 start_pos.Y -= 1;699 const handle = global_progress.terminal.handle;
728 } else {700 var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
729 // If we couldn't find the marker, then just assume that no lines wrapped701 if (windows.kernel32.GetConsoleScreenBufferInfo(handle, &console_info) == 0) {
730 start_pos = .{ .X = 0, .Y = expected_y };702 return error.Unexpected;
731 }703 }
732 var num_chars_written: windows.DWORD = undefined;704 const cursor_pos = console_info.dwCursorPosition;
733 if (windows.kernel32.FillConsoleOutputCharacterW(handle, ' ', screen_area, start_pos, &num_chars_written) == 0) {705 const expected_y = cursor_pos.Y - @as(i16, @intCast(nl_n));
734 return error.Unexpected;706 var start_pos: windows.COORD = .{ .X = 0, .Y = expected_y };
735 }707 while (start_pos.Y >= 0) {
736 if (windows.kernel32.SetConsoleCursorPosition(handle, start_pos) == 0) {708 var wchar: [1]u16 = undefined;
709 var num_console_chars_read: windows.DWORD = undefined;
710 if (windows.kernel32.ReadConsoleOutputCharacterW(handle, &wchar, wchar.len, start_pos, &num_console_chars_read) == 0) {
737 return error.Unexpected;711 return error.Unexpected;
738 }712 }
713
714 if (wchar[0] == windows_api_start_marker) break;
715 start_pos.Y -= 1;
716 } else {
717 // If we couldn't find the marker, then just assume that no lines wrapped
718 start_pos = .{ .X = 0, .Y = expected_y };
719 }
720 if (windows.kernel32.SetConsoleCursorPosition(handle, start_pos) == 0) {
721 return error.Unexpected;
739 }722 }
740}723}
741724
...@@ -1052,7 +1035,7 @@ fn useSavedIpcData(...@@ -1052,7 +1035,7 @@ fn useSavedIpcData(
1052 return start_serialized_len + storage.len;1035 return start_serialized_len + storage.len;
1053}1036}
10541037
1055fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 {1038fn computeRedraw(serialized_buffer: *Serialized.Buffer) struct { []u8, usize } {
1056 const serialized = serialize(serialized_buffer);1039 const serialized = serialize(serialized_buffer);
10571040
1058 // Now we can analyze our copy of the graph without atomics, reconstructing1041 // Now we can analyze our copy of the graph without atomics, reconstructing
...@@ -1078,8 +1061,10 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 {...@@ -1078,8 +1061,10 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 {
1078 }1061 }
1079 }1062 }
10801063
1081 // The strategy is: keep the cursor at the end, and then with every redraw:1064 // The strategy is, with every redraw:
1082 // move cursor to beginning of line, move cursor up N lines, erase to end of screen, write1065 // erase to end of screen, write, move cursor to beginning of line, move cursor up N lines
1066 // This keeps the cursor at the beginning so that unlocked stderr writes
1067 // don't get eaten by the clear.
10831068
1084 var i: usize = 0;1069 var i: usize = 0;
1085 const buf = global_progress.draw_buffer;1070 const buf = global_progress.draw_buffer;
...@@ -1091,20 +1076,31 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 {...@@ -1091,20 +1076,31 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 {
10911076
1092 switch (global_progress.terminal_mode) {1077 switch (global_progress.terminal_mode) {
1093 .off => unreachable,1078 .off => unreachable,
1094 .ansi_escape_codes => i = computeClear(buf, i),1079 .ansi_escape_codes => {
1080 buf[i..][0..clear.len].* = clear.*;
1081 i += clear.len;
1082 },
1095 .windows_api => if (!is_windows) unreachable,1083 .windows_api => if (!is_windows) unreachable,
1096 }1084 }
10971085
1098 global_progress.accumulated_newline_count = 0;
1099 const root_node_index: Node.Index = @enumFromInt(0);1086 const root_node_index: Node.Index = @enumFromInt(0);
1100 i = computeNode(buf, i, serialized, children, root_node_index);1087 i, const nl_n = computeNode(buf, i, 0, serialized, children, root_node_index);
11011088
1102 if (global_progress.terminal_mode == .ansi_escape_codes) {1089 if (global_progress.terminal_mode == .ansi_escape_codes) {
1090 if (nl_n > 0) {
1091 buf[i] = '\r';
1092 i += 1;
1093 for (0..nl_n) |_| {
1094 buf[i..][0..up_one_line.len].* = up_one_line.*;
1095 i += up_one_line.len;
1096 }
1097 }
1098
1103 buf[i..][0..finish_sync.len].* = finish_sync.*;1099 buf[i..][0..finish_sync.len].* = finish_sync.*;
1104 i += finish_sync.len;1100 i += finish_sync.len;
1105 }1101 }
11061102
1107 return buf[0..i];1103 return .{ buf[0..i], nl_n };
1108}1104}
11091105
1110fn computePrefix(1106fn computePrefix(
...@@ -1138,20 +1134,23 @@ fn computePrefix(...@@ -1138,20 +1134,23 @@ fn computePrefix(
1138}1134}
11391135
1140const line_upper_bound_len = @max(TreeSymbol.tee.maxByteLen(), TreeSymbol.langle.maxByteLen()) +1136const line_upper_bound_len = @max(TreeSymbol.tee.maxByteLen(), TreeSymbol.langle.maxByteLen()) +
1141 "[4294967296/4294967296] ".len + Node.max_name_len + finish_sync.len;1137 "[4294967296/4294967296] ".len + Node.max_name_len + (1 + up_one_line.len) + finish_sync.len;
11421138
1143fn computeNode(1139fn computeNode(
1144 buf: []u8,1140 buf: []u8,
1145 start_i: usize,1141 start_i: usize,
1142 start_nl_n: usize,
1146 serialized: Serialized,1143 serialized: Serialized,
1147 children: []const Children,1144 children: []const Children,
1148 node_index: Node.Index,1145 node_index: Node.Index,
1149) usize {1146) struct { usize, usize } {
1150 var i = start_i;1147 var i = start_i;
1148 var nl_n = start_nl_n;
1149
1151 i = computePrefix(buf, i, serialized, children, node_index);1150 i = computePrefix(buf, i, serialized, children, node_index);
11521151
1153 if (i + line_upper_bound_len > buf.len)1152 if (i + line_upper_bound_len > buf.len)
1154 return start_i;1153 return .{ start_i, start_nl_n };
11551154
1156 const storage = &serialized.storage[@intFromEnum(node_index)];1155 const storage = &serialized.storage[@intFromEnum(node_index)];
1157 const estimated_total = storage.estimated_total_count;1156 const estimated_total = storage.estimated_total_count;
...@@ -1186,34 +1185,33 @@ fn computeNode(...@@ -1186,34 +1185,33 @@ fn computeNode(
1186 i = @min(global_progress.cols + start_i, i);1185 i = @min(global_progress.cols + start_i, i);
1187 buf[i] = '\n';1186 buf[i] = '\n';
1188 i += 1;1187 i += 1;
1189 global_progress.accumulated_newline_count += 1;1188 nl_n += 1;
1190 }1189 }
11911190
1192 if (global_progress.withinRowLimit()) {1191 if (global_progress.withinRowLimit(nl_n)) {
1193 if (children[@intFromEnum(node_index)].child.unwrap()) |child| {1192 if (children[@intFromEnum(node_index)].child.unwrap()) |child| {
1194 i = computeNode(buf, i, serialized, children, child);1193 i, nl_n = computeNode(buf, i, nl_n, serialized, children, child);
1195 }1194 }
1196 }1195 }
11971196
1198 if (global_progress.withinRowLimit()) {1197 if (global_progress.withinRowLimit(nl_n)) {
1199 if (children[@intFromEnum(node_index)].sibling.unwrap()) |sibling| {1198 if (children[@intFromEnum(node_index)].sibling.unwrap()) |sibling| {
1200 i = computeNode(buf, i, serialized, children, sibling);1199 i, nl_n = computeNode(buf, i, nl_n, serialized, children, sibling);
1201 }1200 }
1202 }1201 }
12031202
1204 return i;1203 return .{ i, nl_n };
1205}1204}
12061205
1207fn withinRowLimit(p: *Progress) bool {1206fn withinRowLimit(p: *Progress, nl_n: usize) bool {
1208 // The +2 here is so that the PS1 is not scrolled off the top of the terminal.1207 // The +2 here is so that the PS1 is not scrolled off the top of the terminal.
1209 // one because we keep the cursor on the next line1208 // one because we keep the cursor on the next line
1210 // one more to account for the PS11209 // one more to account for the PS1
1211 return p.accumulated_newline_count + 2 < p.rows;1210 return nl_n + 2 < p.rows;
1212}1211}
12131212
1214fn write(buf: []const u8) anyerror!void {1213fn write(buf: []const u8) anyerror!void {
1215 try global_progress.terminal.writeAll(buf);1214 try global_progress.terminal.writeAll(buf);
1216 global_progress.written_newline_count = global_progress.accumulated_newline_count;
1217}1215}
12181216
1219var remaining_write_trash_bytes: usize = 0;1217var remaining_write_trash_bytes: usize = 0;