Skip to content

Commit

Permalink
RealCommandRunner: allow periodic status updates during builds.
Browse files Browse the repository at this point in the history
Use the new Subprocess::DoWork(int64_t) method to wait for at
most one second before updating the status in the Ninja terminal.

NOTE: A new output_test.py is added to check this feature, but
      since it is time-dependent, it tends to fail on Github CI
      so is disabled by default. It is possible to run it manually
      touch on a lightly-loaded machine.
  • Loading branch information
digit-google committed Oct 23, 2024
1 parent f89fac6 commit 2c7cd3a
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 14 deletions.
9 changes: 8 additions & 1 deletion doc/manual.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ you don't need to pass `-j`.)
Environment variables
~~~~~~~~~~~~~~~~~~~~~
Ninja supports one environment variable to control its behavior:
Ninja supports a few environment variables to control its behavior:
`NINJA_STATUS`, the progress status printed before the rule being run.
Several placeholders are available:
Expand All @@ -215,6 +216,12 @@ 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_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).
Extra tools
~~~~~~~~~~~
Expand Down
36 changes: 36 additions & 0 deletions misc/output_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

default_env = dict(os.environ)
default_env.pop('NINJA_STATUS', None)
default_env.pop('NINJA_STATUS_REFRESH_MILLIS', None)
default_env.pop('CLICOLOR_FORCE', None)
default_env['TERM'] = ''
NINJA_PATH = os.path.abspath('./ninja')
Expand Down Expand Up @@ -285,6 +286,41 @@ def test_ninja_status_quiet(self) -> None:
output = run(Output.BUILD_SIMPLE_ECHO, flags='--quiet')
self.assertEqual(output, 'do thing\n')

@unittest.skip("Time-based test fails on Github CI")
def test_ninja_status_periodic_update(self) -> None:
b = BuildDir('''\
rule sleep_then_print
command = sleep 2 && echo done
description = sleep2s
build all: sleep_then_print
''')
with b:
env = default_env.copy()
env["NINJA_STATUS"] = "[%w] "
self.assertListEqual(
b.run('all', raw_output=True, env=env).replace("\r\n", "<CRLF>").split("\r"),
[
"",
"[00:00] sleep2s\x1b[K",
"[00:01] sleep2s\x1b[K",
"[00:02] sleep2s\x1b[K",
"[00:02] sleep2s\x1b[K<CRLF>done<CRLF>",
])

env["NINJA_STATUS_REFRESH_MILLIS"] = "500"
self.assertListEqual(
b.run('all', raw_output=True, env=env).replace("\r\n", "<CRLF>").split("\r"),
[
"",
"[00:00] sleep2s\x1b[K",
"[00:00] sleep2s\x1b[K",
"[00:01] sleep2s\x1b[K",
"[00:01] sleep2s\x1b[K",
"[00:02] sleep2s\x1b[K",
"[00:02] sleep2s\x1b[K<CRLF>done<CRLF>",
])

def test_entering_directory_on_stdout(self) -> None:
output = run(Output.BUILD_SIMPLE_ECHO, flags='-C$PWD', pipe=True)
self.assertEqual(output.splitlines()[0][:25], "ninja: Entering directory")
Expand Down
26 changes: 20 additions & 6 deletions src/build.cc
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ bool DryRunCommandRunner::WaitForCommand(Result* result) {
return true;
}

// A callable value used to refresh the current Ninja status.
using StatusRefresher = std::function<void(void)>;

} // namespace

Plan::Plan(Builder* builder)
Expand Down Expand Up @@ -592,14 +595,17 @@ void Plan::Dump() const {
}

struct RealCommandRunner : public CommandRunner {
explicit RealCommandRunner(const BuildConfig& config) : config_(config) {}
explicit RealCommandRunner(const BuildConfig& config,
StatusRefresher&& refresh_status)
: config_(config), refresh_status_(std::move(refresh_status)) {}
size_t CanRunMore() const override;
bool StartCommand(Edge* edge) override;
bool WaitForCommand(Result* result) override;
vector<Edge*> GetActiveEdges() override;
void Abort() override;

const BuildConfig& config_;
StatusRefresher refresh_status_;
SubprocessSet subprocs_;
map<const Subprocess*, Edge*> subproc_to_edge_;
};
Expand Down Expand Up @@ -651,8 +657,13 @@ bool RealCommandRunner::StartCommand(Edge* edge) {
bool RealCommandRunner::WaitForCommand(Result* result) {
Subprocess* subproc;
while ((subproc = subprocs_.NextFinished()) == NULL) {
bool interrupted = subprocs_.DoWork();
if (interrupted)
SubprocessSet::WorkResult ret =
subprocs_.DoWork(config_.status_refresh_millis);
if (ret == SubprocessSet::WorkResult::TIMEOUT) {
refresh_status_();
continue;
}
if (ret == SubprocessSet::WorkResult::INTERRUPTION)
return false;
}

Expand Down Expand Up @@ -772,10 +783,13 @@ bool Builder::Build(string* err) {

// Set up the command runner if we haven't done so already.
if (!command_runner_.get()) {
if (config_.dry_run)
if (config_.dry_run) {
command_runner_.reset(new DryRunCommandRunner);
else
command_runner_.reset(new RealCommandRunner(config_));
} else {
command_runner_.reset(new RealCommandRunner(config_, [this]() {
status_->Refresh(GetTimeMillis() - start_time_millis_);
}));
}
}

// We are about to start the build process.
Expand Down
16 changes: 9 additions & 7 deletions src/build.h
Original file line number Diff line number Diff line change
Expand Up @@ -166,22 +166,24 @@ struct CommandRunner {

/// Options (e.g. verbosity, parallelism) passed to a build.
struct BuildConfig {
BuildConfig() : verbosity(NORMAL), dry_run(false), parallelism(1),
failures_allowed(1), max_load_average(-0.0f) {}
BuildConfig() = default;

enum Verbosity {
QUIET, // No output -- used when testing.
NO_STATUS_UPDATE, // just regular output but suppress status update
NORMAL, // regular output and status update
VERBOSE
};
Verbosity verbosity;
bool dry_run;
int parallelism;
int failures_allowed;
Verbosity verbosity = NORMAL;
bool dry_run = false;
int parallelism = 1;
int failures_allowed = 1;
/// The maximum load average we must not exceed. A negative value
/// means that we do not have any limit.
double max_load_average;
double max_load_average = -0.0f;
/// Number of milliseconds between status refreshes in interactive
/// terminals.
int status_refresh_millis = 1000;
DepfileParserOptions depfile_parser_options;
};

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

const char* status_refresh_env = getenv("NINJA_STATUS_REFRESH_MILLIS");
if (status_refresh_env) {
config.status_refresh_millis = atoi(status_refresh_env);
}

setvbuf(stdout, NULL, _IOLBF, BUFSIZ);
const char* ninja_command = argv[0];

Expand Down

0 comments on commit 2c7cd3a

Please sign in to comment.