| // 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 <stdint.h> |
| #include <string.h> |
| |
| #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) { |
| commands_max_queue_.reserve(config_.max_commands); |
| older_commands_.reserve(config_.max_commands); |
| } |
| |
| 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] { |
| Refresh(); |
| 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_.emplace( |
| std::piecewise_construct, std::forward_as_tuple(command), |
| std::forward_as_tuple(async_loop_.NowMs() - start_build_time_ms_, |
| GetCommandDescription(command))); |
| } |
| |
| 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(); |
| } |
| |
| 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 = async_loop_.NowMs(); |
| 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) { |
| size_t max_commands = config_.max_commands; |
| if (!max_commands) |
| return; |
| |
| // The total elapsed time from the start of the build in milliseconds. |
| int64_t build_time_ms = cur_time_millis - start_build_time_ms_; |
| |
| // Find the |max_commands| older running edges, by using a fixed-size |
| // max-priority-queue. Then sort the result from oldest to newest |
| // commands. |
| |
| // Fill queue with empty items to simplify loop below. |
| assert(commands_max_queue_.empty()); |
| commands_max_queue_.resize(max_commands); |
| for (const auto& pair : pending_commands_) { |
| int64_t start_time_ms = pair.second.start_time_ms; |
| // If this command is newer than the current top, ignore it |
| // otherwise replace the top with it in the queue. |
| if (start_time_ms < commands_max_queue_.top().start_time_ms()) { |
| commands_max_queue_.pop(); |
| commands_max_queue_.emplace(pair); |
| } |
| } |
| |
| // Compute the older commands in _decreasing_ starting time, |
| // then parse it in reverse order for printing (to avoid an |
| // std::reverse() call). |
| older_commands_.clear(); |
| while (!commands_max_queue_.empty()) { |
| const auto& info = commands_max_queue_.top(); |
| // Ignore entries with |command == nullptr|, which happen |
| // when there are less than |config_.max_commands| items in |
| //|pending_commands_|. |
| if (info.command() != nullptr) |
| older_commands_.push_back(info); |
| commands_max_queue_.pop(); |
| } |
| |
| std::string pending_line; |
| |
| for (auto info_reverse_it = older_commands_.end(); |
| info_reverse_it != older_commands_.begin();) { |
| const CommandInfo& info = *(--info_reverse_it); |
| // Format the elapsed time in a human friendly format. |
| char elapsed_buffer[16]; |
| int64_t elapsed_ms = build_time_ms - info.start_time_ms(); |
| 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. |
| StringPiece description = info.description(); |
| |
| // 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.begin(), description.size()); |
| |
| PrintOnNextLine(pending_line); |
| } |
| |
| // Clear previous lines that are not needed anymore. |
| size_t count = older_commands_.size(); |
| 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; |
| } |