Skip to content

Commit

Permalink
Print a table of running commands in interactive terminals.
Browse files Browse the repository at this point in the history
Ninja will now print a table of up to 4 running commands below
the status line, when in interactive terminals (nothing in
changed for non-interactive ones).

The size of the table can be changed by setting
`NINJA_STATUS_MAX_COMMANDS=<count>` in the environment,
and using a value of 0, or a negative one, disables
the feature.

This also changes the default refresh update timeout to 100ms
for more dynamic updates, except if the feature is disabled
(in which case the default stays at 1s for time-sensitive
status updates).
  • Loading branch information
digit-google committed Oct 24, 2024
1 parent 214ceb0 commit 55d078e
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 14 deletions.
14 changes: 10 additions & 4 deletions doc/manual.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,17 @@ The default progress status is `"[%f/%t] "` (note the trailing space
to separate from the build rule). Another example of possible progress status
could be `"[%u/%r/%f] "`.
`NINJA_STATUS_MAX_COMMANDS`, the number of rows to display in the
commands table displayed during builds in interactive terminals.
The default value is 4. Using a negative or zero value disables
the feature.
`NINJA_STATUS_REFRESH_MILLIS`, the refresh timeout in milliseconds
for status updates in interactive terminals. The default value is 1000,
to allow time-sensitive formatters like `%w` to be updated during
long build runs (e.g. when one or more build commands run for a long
time).
for status updates in interactive terminals. The default value is 100,
if `NINJA_STATUS_MAX_COMMANDS` is positive, or 1000 otherwise.
This allows time-sensitive formatters like `%w` to be updated properly
during long build runs (e.g. when one or more build commands run for
a long time, without any other one completing).
Extra tools
~~~~~~~~~~~
Expand Down
4 changes: 4 additions & 0 deletions misc/output_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
default_env.pop('NINJA_STATUS', None)
default_env.pop('NINJA_STATUS_REFRESH_MILLIS', None)
default_env.pop('CLICOLOR_FORCE', None)

# Explicitly disable commands table feature for these tests.
default_env['NINJA_STATUS_MAX_COMMANDS'] = '0'

default_env['TERM'] = ''
NINJA_PATH = os.path.abspath('./ninja')

Expand Down
5 changes: 4 additions & 1 deletion src/build.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,10 @@ struct BuildConfig {
double max_load_average = -0.0f;
/// Number of milliseconds between status refreshes in interactive
/// terminals.
int status_refresh_millis = 1000;
int status_refresh_millis = 100;
/// Number of commands displayed in the table in interactive
/// terminals. Set to 0 or a negative value to disable the feature.
int status_max_commands = 4;
DepfileParserOptions depfile_parser_options;
};

Expand Down
9 changes: 9 additions & 0 deletions src/ninja.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1546,9 +1546,18 @@ NORETURN void real_main(int argc, char** argv) {
Options options = {};
options.input_file = "build.ninja";

const char* status_max_commands_env = getenv("NINJA_STATUS_MAX_COMMANDS");
if (status_max_commands_env) {
config.status_max_commands = atoi(status_max_commands_env);
}
const char* status_refresh_env = getenv("NINJA_STATUS_REFRESH_MILLIS");
if (status_refresh_env) {
config.status_refresh_millis = atoi(status_refresh_env);
} else if (config.status_max_commands <= 0) {
// Since the time-sensitive formatters only have a granularity
// of one second, change the default refresh timeout to 1000,
// to avoid reprinting the exact same status every 0.1s.
config.status_refresh_millis = 1000;
}

setvbuf(stdout, NULL, _IOLBF, BUFSIZ);
Expand Down
103 changes: 94 additions & 9 deletions src/status_printer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,41 @@

#include "build.h"
#include "debug_flags.h"
#include "status_table.h"

using namespace std;

Status* Status::factory(const BuildConfig& config) {
return new StatusPrinter(config);
}

StatusPrinter::~StatusPrinter() {}

// Derived StatusTable class that uses a LinePrinter to print to
// smart terminals.
struct StatusPrinter::StatusPrinterTable : public StatusTable {
StatusPrinterTable(StatusPrinter& status_printer,
const StatusTable::Config& config)
: StatusTable(config), status_printer_(status_printer) {}

virtual ~StatusPrinterTable() {}

std::string GetCommandDescription(
StatusTable::CommandPointer command) const override {
auto* edge = static_cast<const Edge*>(command);
std::string result = edge->GetBinding("description");
if (result.empty())
result = edge->GetBinding("command");
return result;
}

void PrintOnCurrentLine(const std::string& line) override {
status_printer_.printer_.Print(line, LinePrinter::ELIDE);
}

StatusPrinter& status_printer_;
};

StatusPrinter::StatusPrinter(const BuildConfig& config)
: config_(config), started_edges_(0), finished_edges_(0), total_edges_(0),
running_edges_(0), progress_status_format_(NULL),
Expand All @@ -51,6 +79,26 @@ StatusPrinter::StatusPrinter(const BuildConfig& config)
progress_status_format_ = getenv("NINJA_STATUS");
if (!progress_status_format_)
progress_status_format_ = "[%f/%t] ";

if (printer_.is_smart_terminal() && !config_.dry_run) {
StatusTable::Config table_config = {};

if (config_.status_max_commands > 0) {
table_config.max_commands =
static_cast<size_t>(config_.status_max_commands);
}

// Since elapsed times are displayed to the first decimal digit
// only, there is no point in using a value smaller than 0.1 seconds
// for the refresh timeout.
const long timeout_millis_min = 100;
long timeout_millis = config_.status_refresh_millis;
if (timeout_millis < timeout_millis_min)
timeout_millis = timeout_millis_min;
table_config.refresh_timeout_ms = static_cast<int64_t>(timeout_millis);

table_.reset(new StatusPrinterTable(*this, table_config));
}
}

void StatusPrinter::EdgeAddedToPlan(const Edge* edge) {
Expand Down Expand Up @@ -87,11 +135,22 @@ void StatusPrinter::BuildEdgeStarted(const Edge* edge,
++running_edges_;
time_millis_ = start_time_millis;

if (edge->use_console() || printer_.is_smart_terminal())
PrintStatus(edge, start_time_millis);
if (table_)
table_->CommandStarted(edge, start_time_millis);

if (edge->use_console())
if (edge->use_console()) {
// This command will print its output directly to stdout, so
// clear the commands to let Ninja update the status and
// lock the line printer.
if (table_)
table_->ClearTable();
PrintStatus(edge, start_time_millis);
printer_.SetConsoleLocked(true);
} else if (printer_.is_smart_terminal()) {
PrintStatus(edge, start_time_millis);
if (table_)
table_->UpdateTable(start_time_millis);
}
}

void StatusPrinter::RecalculateProgressPrediction() {
Expand Down Expand Up @@ -190,23 +249,38 @@ void StatusPrinter::BuildEdgeFinished(Edge* edge, int64_t start_time_millis,
} else
--eta_unpredictable_edges_remaining_;

if (table_)
table_->CommandEnded(edge);

--running_edges_;

if (edge->use_console())
printer_.SetConsoleLocked(false);

if (config_.verbosity == BuildConfig::QUIET)
return;

if (!edge->use_console())
if (!edge->use_console()) {
PrintStatus(edge, end_time_millis);
if (success && output.empty()) {
// Ninja doesn't need to print command output or error
// to the terminal, so update the table if needed, then
// exit.
if (table_)
table_->UpdateTable(end_time_millis);
return;
}
}

--running_edges_;
// Clear commands table, since Ninja is going to print something.
if (table_)
table_->ClearTable();

// Print the command that is spewing before printing its output.
if (!success) {
string outputs;
for (vector<Node*>::const_iterator o = edge->outputs_.begin();
o != edge->outputs_.end(); ++o)
outputs += (*o)->path() + " ";
std::string outputs;
for (const Node* out : edge->outputs_)
outputs += out->path() + " ";

if (printer_.supports_color()) {
printer_.PrintOnNewLine("\x1B[31m" "FAILED: " "\x1B[0m" + outputs + "\n");
Expand Down Expand Up @@ -252,9 +326,15 @@ void StatusPrinter::BuildStarted() {
started_edges_ = 0;
finished_edges_ = 0;
running_edges_ = 0;

if (table_)
table_->BuildStarted();
}

void StatusPrinter::BuildFinished() {
if (table_)
table_->BuildEnded();

printer_.SetConsoleLocked(false);
printer_.PrintOnNewLine("");
}
Expand Down Expand Up @@ -442,11 +522,16 @@ void StatusPrinter::RefreshStatus(int64_t cur_time_millis,
last_description_;
printer_.Print(to_print,
force_full_command ? LinePrinter::FULL : LinePrinter::ELIDE);

if (table_)
table_->SetStatus(to_print);
}

void StatusPrinter::Refresh(int64_t cur_time_millis) {
if (printer_.is_smart_terminal()) {
RefreshStatus(cur_time_millis, false);
if (table_)
table_->UpdateTable(cur_time_millis);
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/status_printer.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#pragma once

#include <cstdint>
#include <memory>
#include <queue>

#include "explanations.h"
Expand All @@ -24,6 +25,7 @@
/// human-readable strings to stdout
struct StatusPrinter : Status {
explicit StatusPrinter(const BuildConfig& config);
virtual ~StatusPrinter();

/// Callbacks for the Plan to notify us about adding/removing Edge's.
void EdgeAddedToPlan(const Edge* edge) override;
Expand Down Expand Up @@ -133,4 +135,8 @@ struct StatusPrinter : Status {
};

mutable SlidingRateInfo current_rate_;

// Optional commands table for interactive terminals.
struct StatusPrinterTable;
std::unique_ptr<StatusPrinterTable> table_;
};

0 comments on commit 55d078e

Please sign in to comment.