Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

std.Progress: keep the cursor at the beginning #20139

Merged
merged 1 commit into from
May 31, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 83 additions & 85 deletions lib/std/Progress.zig
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,13 @@ redraw_event: std.Thread.ResetEvent,
/// Indicates a request to shut down and reset global state.
/// Accessed atomically.
done: bool,
need_clear: bool,

refresh_rate_ns: u64,
initial_delay_ns: u64,

rows: u16,
cols: u16,
/// Tracks the number of newlines that have been actually written to the terminal.
written_newline_count: u16,
/// Tracks the number of newlines that will be written to the terminal if the
/// draw buffer is sent.
accumulated_newline_count: u16,

/// Accessed only by the update thread.
draw_buffer: []u8,
Expand Down Expand Up @@ -312,10 +308,9 @@ var global_progress: Progress = .{
.initial_delay_ns = undefined,
.rows = 0,
.cols = 0,
.written_newline_count = 0,
.accumulated_newline_count = 0,
.draw_buffer = undefined,
.done = false,
.need_clear = false,

.node_parents = &node_parents_buffer,
.node_storage = &node_storage_buffer,
Expand Down Expand Up @@ -446,10 +441,11 @@ fn updateThreadRun() void {
if (@atomicLoad(bool, &global_progress.done, .seq_cst)) return;
maybeUpdateSize(resize_flag);

const buffer = computeRedraw(&serialized_buffer);
const buffer, _ = computeRedraw(&serialized_buffer);
if (stderr_mutex.tryLock()) {
defer stderr_mutex.unlock();
write(buffer) catch return;
global_progress.need_clear = true;
}
}

Expand All @@ -464,10 +460,11 @@ fn updateThreadRun() void {

maybeUpdateSize(resize_flag);

const buffer = computeRedraw(&serialized_buffer);
const buffer, _ = computeRedraw(&serialized_buffer);
if (stderr_mutex.tryLock()) {
defer stderr_mutex.unlock();
write(buffer) catch return;
global_progress.need_clear = true;
}
}
}
Expand All @@ -488,11 +485,13 @@ fn windowsApiUpdateThreadRun() void {
if (@atomicLoad(bool, &global_progress.done, .seq_cst)) return;
maybeUpdateSize(resize_flag);

const buffer = computeRedraw(&serialized_buffer);
const buffer, const nl_n = computeRedraw(&serialized_buffer);
if (stderr_mutex.tryLock()) {
defer stderr_mutex.unlock();
windowsApiWriteMarker();
write(buffer) catch return;
global_progress.need_clear = true;
windowsApiMoveToMarker(nl_n) catch return;
}
}

Expand All @@ -507,12 +506,14 @@ fn windowsApiUpdateThreadRun() void {

maybeUpdateSize(resize_flag);

const buffer = computeRedraw(&serialized_buffer);
const buffer, const nl_n = computeRedraw(&serialized_buffer);
if (stderr_mutex.tryLock()) {
defer stderr_mutex.unlock();
clearWrittenWindowsApi() catch return;
windowsApiWriteMarker();
write(buffer) catch return;
global_progress.need_clear = true;
windowsApiMoveToMarker(nl_n) catch return;
}
}
}
Expand Down Expand Up @@ -645,40 +646,16 @@ fn appendTreeSymbol(symbol: TreeSymbol, buf: []u8, start_i: usize) usize {
}

fn clearWrittenWithEscapeCodes() anyerror!void {
if (global_progress.written_newline_count == 0) return;
if (!global_progress.need_clear) return;

var i: usize = 0;
const buf = global_progress.draw_buffer;

buf[i..][0..start_sync.len].* = start_sync.*;
i += start_sync.len;

i = computeClear(buf, i);

buf[i..][0..finish_sync.len].* = finish_sync.*;
i += finish_sync.len;

global_progress.accumulated_newline_count = 0;
try write(buf[0..i]);
}

fn computeClear(buf: []u8, start_i: usize) usize {
var i = start_i;

const prev_nl_n = global_progress.written_newline_count;
if (prev_nl_n > 0) {
buf[i] = '\r';
i += 1;
for (0..prev_nl_n) |_| {
buf[i..][0..up_one_line.len].* = up_one_line.*;
i += up_one_line.len;
}
}

buf[i..][0..clear.len].* = clear.*;
i += clear.len;

return i;
global_progress.need_clear = false;
try write(buf[0..i]);
}

/// U+25BA or ►
Expand All @@ -704,38 +681,44 @@ fn clearWrittenWindowsApi() error{Unexpected}!void {
// but it must be a valid attribute and it actually needs to apply to the first
// character in order to be readable via ReadConsoleOutputAttribute. It doesn't seem
// like any of the available attributes are invisible/benign.
const prev_nl_n = global_progress.written_newline_count;
if (prev_nl_n > 0) {
const handle = global_progress.terminal.handle;
const screen_area = @as(windows.DWORD, global_progress.cols) * global_progress.rows;
if (!global_progress.need_clear) return;
const handle = global_progress.terminal.handle;
const screen_area = @as(windows.DWORD, global_progress.cols) * global_progress.rows;

var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
if (windows.kernel32.GetConsoleScreenBufferInfo(handle, &console_info) == 0) {
return error.Unexpected;
}
const cursor_pos = console_info.dwCursorPosition;
const expected_y = cursor_pos.Y - @as(i16, @intCast(prev_nl_n));
var start_pos = windows.COORD{ .X = 0, .Y = expected_y };
while (start_pos.Y >= 0) {
var wchar: [1]u16 = undefined;
var num_console_chars_read: windows.DWORD = undefined;
if (windows.kernel32.ReadConsoleOutputCharacterW(handle, &wchar, wchar.len, start_pos, &num_console_chars_read) == 0) {
return error.Unexpected;
}
var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
if (windows.kernel32.GetConsoleScreenBufferInfo(handle, &console_info) == 0) {
return error.Unexpected;
}
var num_chars_written: windows.DWORD = undefined;
if (windows.kernel32.FillConsoleOutputCharacterW(handle, ' ', screen_area, console_info.dwCursorPosition, &num_chars_written) == 0) {
return error.Unexpected;
}
}

if (wchar[0] == windows_api_start_marker) break;
start_pos.Y -= 1;
} else {
// If we couldn't find the marker, then just assume that no lines wrapped
start_pos = .{ .X = 0, .Y = expected_y };
}
var num_chars_written: windows.DWORD = undefined;
if (windows.kernel32.FillConsoleOutputCharacterW(handle, ' ', screen_area, start_pos, &num_chars_written) == 0) {
return error.Unexpected;
}
if (windows.kernel32.SetConsoleCursorPosition(handle, start_pos) == 0) {
fn windowsApiMoveToMarker(nl_n: usize) error{Unexpected}!void {
const handle = global_progress.terminal.handle;
var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
if (windows.kernel32.GetConsoleScreenBufferInfo(handle, &console_info) == 0) {
return error.Unexpected;
}
const cursor_pos = console_info.dwCursorPosition;
const expected_y = cursor_pos.Y - @as(i16, @intCast(nl_n));
var start_pos: windows.COORD = .{ .X = 0, .Y = expected_y };
while (start_pos.Y >= 0) {
var wchar: [1]u16 = undefined;
var num_console_chars_read: windows.DWORD = undefined;
if (windows.kernel32.ReadConsoleOutputCharacterW(handle, &wchar, wchar.len, start_pos, &num_console_chars_read) == 0) {
return error.Unexpected;
}

if (wchar[0] == windows_api_start_marker) break;
start_pos.Y -= 1;
} else {
// If we couldn't find the marker, then just assume that no lines wrapped
start_pos = .{ .X = 0, .Y = expected_y };
}
if (windows.kernel32.SetConsoleCursorPosition(handle, start_pos) == 0) {
return error.Unexpected;
}
}

Expand Down Expand Up @@ -1052,7 +1035,7 @@ fn useSavedIpcData(
return start_serialized_len + storage.len;
}

fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 {
fn computeRedraw(serialized_buffer: *Serialized.Buffer) struct { []u8, usize } {
const serialized = serialize(serialized_buffer);

// Now we can analyze our copy of the graph without atomics, reconstructing
Expand All @@ -1078,8 +1061,10 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 {
}
}

// The strategy is: keep the cursor at the end, and then with every redraw:
// move cursor to beginning of line, move cursor up N lines, erase to end of screen, write
// The strategy is, with every redraw:
// erase to end of screen, write, move cursor to beginning of line, move cursor up N lines
// This keeps the cursor at the beginning so that unlocked stderr writes
// don't get eaten by the clear.

var i: usize = 0;
const buf = global_progress.draw_buffer;
Expand All @@ -1091,20 +1076,31 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 {

switch (global_progress.terminal_mode) {
.off => unreachable,
.ansi_escape_codes => i = computeClear(buf, i),
.ansi_escape_codes => {
buf[i..][0..clear.len].* = clear.*;
i += clear.len;
},
.windows_api => if (!is_windows) unreachable,
}

global_progress.accumulated_newline_count = 0;
const root_node_index: Node.Index = @enumFromInt(0);
i = computeNode(buf, i, serialized, children, root_node_index);
i, const nl_n = computeNode(buf, i, 0, serialized, children, root_node_index);

if (global_progress.terminal_mode == .ansi_escape_codes) {
if (nl_n > 0) {
buf[i] = '\r';
i += 1;
for (0..nl_n) |_| {
buf[i..][0..up_one_line.len].* = up_one_line.*;
i += up_one_line.len;
}
}

buf[i..][0..finish_sync.len].* = finish_sync.*;
i += finish_sync.len;
}

return buf[0..i];
return .{ buf[0..i], nl_n };
}

fn computePrefix(
Expand Down Expand Up @@ -1138,20 +1134,23 @@ fn computePrefix(
}

const line_upper_bound_len = @max(TreeSymbol.tee.maxByteLen(), TreeSymbol.langle.maxByteLen()) +
"[4294967296/4294967296] ".len + Node.max_name_len + finish_sync.len;
"[4294967296/4294967296] ".len + Node.max_name_len + (1 + up_one_line.len) + finish_sync.len;

fn computeNode(
buf: []u8,
start_i: usize,
start_nl_n: usize,
serialized: Serialized,
children: []const Children,
node_index: Node.Index,
) usize {
) struct { usize, usize } {
var i = start_i;
var nl_n = start_nl_n;

i = computePrefix(buf, i, serialized, children, node_index);

if (i + line_upper_bound_len > buf.len)
return start_i;
return .{ start_i, start_nl_n };

const storage = &serialized.storage[@intFromEnum(node_index)];
const estimated_total = storage.estimated_total_count;
Expand Down Expand Up @@ -1186,34 +1185,33 @@ fn computeNode(
i = @min(global_progress.cols + start_i, i);
buf[i] = '\n';
i += 1;
global_progress.accumulated_newline_count += 1;
nl_n += 1;
}

if (global_progress.withinRowLimit()) {
if (global_progress.withinRowLimit(nl_n)) {
if (children[@intFromEnum(node_index)].child.unwrap()) |child| {
i = computeNode(buf, i, serialized, children, child);
i, nl_n = computeNode(buf, i, nl_n, serialized, children, child);
}
}

if (global_progress.withinRowLimit()) {
if (global_progress.withinRowLimit(nl_n)) {
if (children[@intFromEnum(node_index)].sibling.unwrap()) |sibling| {
i = computeNode(buf, i, serialized, children, sibling);
i, nl_n = computeNode(buf, i, nl_n, serialized, children, sibling);
}
}

return i;
return .{ i, nl_n };
}

fn withinRowLimit(p: *Progress) bool {
fn withinRowLimit(p: *Progress, nl_n: usize) bool {
// The +2 here is so that the PS1 is not scrolled off the top of the terminal.
// one because we keep the cursor on the next line
// one more to account for the PS1
return p.accumulated_newline_count + 2 < p.rows;
return nl_n + 2 < p.rows;
}

fn write(buf: []const u8) anyerror!void {
try global_progress.terminal.writeAll(buf);
global_progress.written_newline_count = global_progress.accumulated_newline_count;
}

var remaining_write_trash_bytes: usize = 0;
Expand Down