blob: 8c31d0b5970cf07c2be1d78d82383b58dd4262c5 [file] [log] [blame]
// Copyright 2024 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "status_table.h"
#include <algorithm>
#include "assert.h"
#ifdef _WIN32
#undef min // make std::min() work on Windows.
#endif
StatusTable::StatusTable(const Config& config, AsyncLoop& async_loop)
: config_(config), async_loop_(async_loop) {}
StatusTable::~StatusTable() = default;
void StatusTable::BuildStarted() {
start_build_time_ms_ = async_loop_.NowMs();
if (config_.refresh_timeout_ms > 0) {
if (!timer_) {
timer_ = AsyncTimer::CreateWithDuration(
config_.refresh_timeout_ms, async_loop_, [this] {
int64_t cur_time_ms = async_loop_.NowMs() - start_build_time_ms_;
Refresh(cur_time_ms);
timer_.SetDurationMs(config_.refresh_timeout_ms);
});
}
EnableTimer();
}
}
void StatusTable::BuildEnded() {
if (timer_) {
timer_.Close();
}
start_build_time_ms_ = 0;
last_update_time_ms_ = -1;
ClearTable();
}
void StatusTable::CommandStarted(CommandPointer command) {
pending_commands_[command] = async_loop_.NowMs() - start_build_time_ms_;
}
void StatusTable::CommandEnded(CommandPointer command) {
auto it = pending_commands_.find(command);
assert(it != pending_commands_.end());
pending_commands_.erase(it);
}
void StatusTable::SetStatus(const std::string& status) {
last_status_ = status;
}
void StatusTable::UpdateTable() {
Refresh(async_loop_.NowMs());
}
void StatusTable::EnableTimer() {
if (timer_)
timer_.SetDurationMs(config_.refresh_timeout_ms);
}
void StatusTable::DisableTimer() {
if (timer_)
timer_.Cancel();
}
void StatusTable::Refresh(int64_t current_time_ms) {
if (last_update_time_ms_ >= 0) {
int64_t since_last_ms = current_time_ms - last_update_time_ms_;
if (since_last_ms < config_.refresh_timeout_ms) {
// No need to update more than necessary when tasks complete
// really really fast.
return;
}
}
last_update_time_ms_ = current_time_ms;
PrintPending(current_time_ms);
}
void StatusTable::PrintPending(int64_t cur_time_millis) {
if (!config_.max_commands)
return;
// Find the N-th older running edges, where N is max_height_.
// Reuse the sorted_pending_edges_ vector between calls.
auto& sorted_commands = sorted_pending_commands_;
sorted_commands.assign(pending_commands_.begin(), pending_commands_.end());
auto less = [](const CommandInfo& a, const CommandInfo& b) -> bool {
return a.second < b.second;
};
size_t count = std::min(sorted_commands.size(), config_.max_commands);
std::partial_sort(sorted_commands.begin(), sorted_commands.begin() + count,
sorted_commands.end(), less);
std::string pending_line;
for (size_t n = 0; n < count; ++n) {
CommandInfo& pair = sorted_commands[n];
// Format the elapsed time in a human friendly format.
char elapsed_buffer[16];
int64_t elapsed_ms = cur_time_millis - pair.second;
if (elapsed_ms < 0) {
snprintf(elapsed_buffer, sizeof(elapsed_buffer), "??????");
} else {
if (elapsed_ms < 60000) {
snprintf(elapsed_buffer, sizeof(elapsed_buffer), "%d.%ds",
static_cast<int>((elapsed_ms / 1000)),
static_cast<int>((elapsed_ms % 1000) / 100));
} else {
snprintf(elapsed_buffer, sizeof(elapsed_buffer), "%dm%ds",
static_cast<int>((elapsed_ms / 60000)),
static_cast<int>((elapsed_ms % 60000) / 1000));
}
}
// Get edge description or command.
std::string description = GetCommandDescription(pair.first);
// Format '<elapsed> | <description>' where <elapsed> is
// right-justified.
size_t justification_width = 6;
size_t elapsed_width = strlen(elapsed_buffer);
size_t justified_elapsed_width =
std::min(justification_width, elapsed_width);
size_t needed_capacity = justified_elapsed_width + 3 + description.size();
if (needed_capacity > pending_line.capacity())
pending_line.reserve(needed_capacity);
if (elapsed_width < justification_width) {
pending_line.assign(justification_width - elapsed_width, ' ');
} else {
pending_line.clear();
}
pending_line.append(elapsed_buffer, elapsed_width);
pending_line.append(" | ", 3);
pending_line.append(description);
PrintOnNextLine(pending_line);
}
// Clear previous lines that are not needed anymore.
size_t next_height = count;
for (; count < last_command_count_; ++count) {
ClearNextLine();
}
if (count > 0) {
// Move up to the top status line. Then print the status
// again to reposition the cursor to the right position.
// Note that using ASCII sequences to save/restore the
// cursor position does not work reliably in all terminals
// (and terminal emulators like mosh or asciinema).
MoveUp(count);
PrintOnCurrentLine(last_status_);
}
Flush();
last_command_count_ = next_height;
}
std::string StatusTable::GetCommandDescription(
StatusTable::CommandPointer command) const {
return "command $" + std::to_string(reinterpret_cast<uintptr_t>(command));
}
void StatusTable::PrintOnCurrentLine(const std::string& line) {
printf("%s\x1B[0K", line.c_str());
}
void StatusTable::PrintOnNextLine(const std::string& line) {
printf("\n");
PrintOnCurrentLine(line);
}
void StatusTable::ClearNextLine() {
printf("\x1B[1B\x1B[2K");
}
void StatusTable::MoveUp(size_t lines_count) {
printf("\x1B[%dA", static_cast<int>(lines_count));
}
void StatusTable::Flush() {
fflush(stdout);
}
void StatusTable::ClearTable() {
if (last_command_count_ == 0)
return;
// repeat "go down 1 line; erase whole line" |last_height_| times.
for (size_t n = 0; n < last_command_count_; ++n)
ClearNextLine();
// move up |last_height_| lines.
MoveUp(last_command_count_);
Flush();
last_command_count_ = 0;
}