Skip to content

Commit b09f853

Browse files
committed
RealCommandRunner: allow periodic status updates during builds.
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 though on a lightly-loaded machine.
1 parent c186c14 commit b09f853

File tree

6 files changed

+85
-18
lines changed

6 files changed

+85
-18
lines changed

doc/manual.asciidoc

+8-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ you don't need to pass `-j`.)
190190
Environment variables
191191
~~~~~~~~~~~~~~~~~~~~~
192192
193-
Ninja supports one environment variable to control its behavior:
193+
Ninja supports a few environment variables to control its behavior:
194+
194195
`NINJA_STATUS`, the progress status printed before the rule being run.
195196
196197
Several placeholders are available:
@@ -215,6 +216,12 @@ The default progress status is `"[%f/%t] "` (note the trailing space
215216
to separate from the build rule). Another example of possible progress status
216217
could be `"[%u/%r/%f] "`.
217218
219+
`NINJA_STATUS_REFRESH_MILLIS`, the refresh timeout in milliseconds
220+
for status updates in interactive terminals. The default value is 1000,
221+
to allow time-sensitive formatters like `%w` to be updated during
222+
long build runs (e.g. when one or more build commands run for a long
223+
time).
224+
218225
Extra tools
219226
~~~~~~~~~~~
220227

misc/output_test.py

+36
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
default_env = dict(os.environ)
1818
default_env.pop('NINJA_STATUS', None)
19+
default_env.pop('NINJA_STATUS_REFRESH_MILLIS', None)
1920
default_env.pop('CLICOLOR_FORCE', None)
2021
default_env['TERM'] = ''
2122
NINJA_PATH = os.path.abspath('./ninja')
@@ -310,6 +311,41 @@ def test_ninja_status_quiet(self) -> None:
310311
output = run(Output.BUILD_SIMPLE_ECHO, flags='--quiet')
311312
self.assertEqual(output, 'do thing\n')
312313

314+
@unittest.skip("Time-based test fails on Github CI")
315+
def test_ninja_status_periodic_update(self) -> None:
316+
b = BuildDir('''\
317+
rule sleep_then_print
318+
command = sleep 2 && echo done
319+
description = sleep2s
320+
321+
build all: sleep_then_print
322+
''')
323+
with b:
324+
env = default_env.copy()
325+
env["NINJA_STATUS"] = "[%w] "
326+
self.assertListEqual(
327+
b.run('all', raw_output=True, env=env).replace("\r\n", "<CRLF>").split("\r"),
328+
[
329+
"",
330+
"[00:00] sleep2s\x1b[K",
331+
"[00:01] sleep2s\x1b[K",
332+
"[00:02] sleep2s\x1b[K",
333+
"[00:02] sleep2s\x1b[K<CRLF>done<CRLF>",
334+
])
335+
336+
env["NINJA_STATUS_REFRESH_MILLIS"] = "500"
337+
self.assertListEqual(
338+
b.run('all', raw_output=True, env=env).replace("\r\n", "<CRLF>").split("\r"),
339+
[
340+
"",
341+
"[00:00] sleep2s\x1b[K",
342+
"[00:00] sleep2s\x1b[K",
343+
"[00:01] sleep2s\x1b[K",
344+
"[00:01] sleep2s\x1b[K",
345+
"[00:02] sleep2s\x1b[K",
346+
"[00:02] sleep2s\x1b[K<CRLF>done<CRLF>",
347+
])
348+
313349
def test_entering_directory_on_stdout(self) -> None:
314350
output = run(Output.BUILD_SIMPLE_ECHO, flags='-C$PWD', pipe=True)
315351
self.assertEqual(output.splitlines()[0][:25], "ninja: Entering directory")

src/build.cc

+6-3
Original file line numberDiff line numberDiff line change
@@ -696,10 +696,13 @@ bool Builder::Build(string* err) {
696696

697697
// Set up the command runner if we haven't done so already.
698698
if (!command_runner_.get()) {
699-
if (config_.dry_run)
699+
if (config_.dry_run) {
700700
command_runner_.reset(new DryRunCommandRunner);
701-
else
702-
command_runner_.reset(CommandRunner::factory(config_));
701+
} else {
702+
command_runner_.reset(CommandRunner::factory(config_, [this]() {
703+
status_->Refresh(GetTimeMillis() - start_time_millis_);
704+
}));
705+
}
703706
}
704707

705708
// We are about to start the build process.

src/build.h

+15-8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#define NINJA_BUILD_H_
1717

1818
#include <cstdio>
19+
#include <functional>
1920
#include <map>
2021
#include <memory>
2122
#include <string>
@@ -151,6 +152,9 @@ struct CommandRunner {
151152
virtual size_t CanRunMore() const = 0;
152153
virtual bool StartCommand(Edge* edge) = 0;
153154

155+
// A callable value used to refresh the current Ninja status.
156+
using StatusRefresher = std::function<void(void)>;
157+
154158
/// The result of waiting for a command.
155159
struct Result {
156160
Result() : edge(NULL) {}
@@ -166,27 +170,30 @@ struct CommandRunner {
166170
virtual void Abort() {}
167171

168172
/// Creates the RealCommandRunner
169-
static CommandRunner* factory(const BuildConfig& config);
173+
static CommandRunner* factory(const BuildConfig& config,
174+
StatusRefresher&& refresh_status);
170175
};
171176

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

177181
enum Verbosity {
178182
QUIET, // No output -- used when testing.
179183
NO_STATUS_UPDATE, // just regular output but suppress status update
180184
NORMAL, // regular output and status update
181185
VERBOSE
182186
};
183-
Verbosity verbosity;
184-
bool dry_run;
185-
int parallelism;
186-
int failures_allowed;
187+
Verbosity verbosity = NORMAL;
188+
bool dry_run = false;
189+
int parallelism = 1;
190+
int failures_allowed = 1;
187191
/// The maximum load average we must not exceed. A negative value
188192
/// means that we do not have any limit.
189-
double max_load_average;
193+
double max_load_average = -0.0f;
194+
/// Number of milliseconds between status refreshes in interactive
195+
/// terminals.
196+
int status_refresh_millis = 1000;
190197
DepfileParserOptions depfile_parser_options;
191198
};
192199

src/ninja.cc

+5
Original file line numberDiff line numberDiff line change
@@ -1729,6 +1729,11 @@ NORETURN void real_main(int argc, char** argv) {
17291729
Options options = {};
17301730
options.input_file = "build.ninja";
17311731

1732+
const char* status_refresh_env = getenv("NINJA_STATUS_REFRESH_MILLIS");
1733+
if (status_refresh_env) {
1734+
config.status_refresh_millis = atoi(status_refresh_env);
1735+
}
1736+
17321737
setvbuf(stdout, NULL, _IOLBF, BUFSIZ);
17331738
const char* ninja_command = argv[0];
17341739

src/real_command_runner.cc

+15-6
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616
#include "subprocess.h"
1717

1818
struct RealCommandRunner : public CommandRunner {
19-
explicit RealCommandRunner(const BuildConfig& config) : config_(config) {}
19+
explicit RealCommandRunner(const BuildConfig& config,
20+
StatusRefresher&& refresh_status)
21+
: config_(config), refresh_status_(std::move(refresh_status)) {}
2022
size_t CanRunMore() const override;
2123
bool StartCommand(Edge* edge) override;
2224
bool WaitForCommand(Result* result) override;
2325
std::vector<Edge*> GetActiveEdges() override;
2426
void Abort() override;
2527

2628
const BuildConfig& config_;
29+
StatusRefresher refresh_status_;
2730
SubprocessSet subprocs_;
2831
std::map<const Subprocess*, Edge*> subproc_to_edge_;
2932
};
@@ -75,9 +78,14 @@ bool RealCommandRunner::StartCommand(Edge* edge) {
7578

7679
bool RealCommandRunner::WaitForCommand(Result* result) {
7780
Subprocess* subproc;
78-
while ((subproc = subprocs_.NextFinished()) == NULL) {
79-
bool interrupted = subprocs_.DoWork();
80-
if (interrupted)
81+
while ((subproc = subprocs_.NextFinished()) == nullptr) {
82+
SubprocessSet::WorkResult ret =
83+
subprocs_.DoWork(config_.status_refresh_millis);
84+
if (ret == SubprocessSet::WorkResult::TIMEOUT) {
85+
refresh_status_();
86+
continue;
87+
}
88+
if (ret == SubprocessSet::WorkResult::INTERRUPTION)
8189
return false;
8290
}
8391

@@ -93,6 +101,7 @@ bool RealCommandRunner::WaitForCommand(Result* result) {
93101
return true;
94102
}
95103

96-
CommandRunner* CommandRunner::factory(const BuildConfig& config) {
97-
return new RealCommandRunner(config);
104+
CommandRunner* CommandRunner::factory(const BuildConfig& config,
105+
StatusRefresher&& refresh_status) {
106+
return new RealCommandRunner(config, std::move(refresh_status));
98107
}

0 commit comments

Comments
 (0)