Skip to content

Commit d80c054

Browse files
committed
Implement jobserver pool in Ninja.
This allows Ninja to implement a jobserver-style pool of job slots, to better coordinate parallel jobs between spawned processes which compete for CPU cores/threads. With this feature, there is no need for being invoked from GNU Make or a script like misc/jobserver_pool.py. NOTE: This implementation is basic and doesn't support broken protocol clients that release more tokens than they acquired. If your build includes these, expect severe build performance degradation. To enable this use --jobserver or --jobserver=MODE on the command-line, where MODE is one of the following values: 0 Do not enable the feature (the default) 1 Enable the feature, using best mode for the current system. pipe Implement the pool with an anonymous pipe (Posix only). fifo Implement the pool with a FIFO file (Posix only). sem Implement the pool with a Win32 semaphore (Windows only). NOTE: The `fifo` mode is only implemented since GNU Make 4.4 and many older clients may not support it. Alternatively, set the NINJA_JOBSERVER environment variable to one of these values to activate it without a command-line option. Note that if MAKEFLAGS is set in the environment, Ninja assumes that it is already running in the context of another jobserver and will not try to create its own pool.
1 parent 71483dc commit d80c054

File tree

5 files changed

+201
-25
lines changed

5 files changed

+201
-25
lines changed

.github/workflows/linux.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
run: |
2828
./ninja_test
2929
../../misc/output_test.py
30+
../../misc/jobserver_test.py
3031
- name: Build release ninja
3132
run: ninja -f build-Release.ninja
3233
working-directory: build
@@ -35,6 +36,7 @@ jobs:
3536
run: |
3637
./ninja_test
3738
../../misc/output_test.py
39+
../../misc/jobserver_test.py
3840
3941
build:
4042
runs-on: [ubuntu-latest]
@@ -170,6 +172,7 @@ jobs:
170172
./ninja all
171173
python3 misc/ninja_syntax_test.py
172174
./misc/output_test.py
175+
./misc/jobserver_test.py
173176
174177
build-aarch64:
175178
name: Build Linux ARM64

doc/manual.asciidoc

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -190,31 +190,49 @@ you don't need to pass `-j`.)
190190
GNU Jobserver support
191191
~~~~~~~~~~~~~~~~~~~~~
192192
193-
Since version 1.13., Ninja builds can follow the
193+
Since version 1.13., Ninja builds support the
194194
https://https://www.gnu.org/software/make/manual/html_node/Job-Slots.html[GNU Make jobserver]
195-
client protocol (on Posix systems). This is useful when Ninja
196-
is invoked as part of a larger build system controlled by a top-level
197-
GNU Make instance, as it allows better coordination between
198-
concurrent build tasks.
195+
protocol (on Posix systems). If supports both client and
196+
server modes.
199197
200-
This feature is automatically enabled under the following
201-
conditions:
198+
Client mode is useful when Ninja is invoked as part of a larger
199+
build system controlled by a top-level GNU Make instance, as it
200+
allows better coordination between concurrent build tasks.
201+
202+
Server mode is useful when Ninja is the top-level build tool that
203+
invokes sub-builds recursively in a similar setup.
204+
205+
To enable server mode, use `--jobserver` or `--jobserver=MODE`
206+
on the command line, or set `NINJA_JOBSERVER=MODE` in your
207+
environment, where `MODE` can be one of the following values:
208+
209+
`0`: Do not enable the feature (the default)
210+
`1`: Enable the feature, using the best mode for the current system.
211+
`pipe`: Enable the feature, implemented with an anonymous pipe (Posix only).
212+
`fifo`: Enable the feature, implemented with a FIFO file path (Posix only).
213+
`sem`: Enable the feature, implemented with a Win32 semaphore (Windows only).
214+
215+
Note that `--jobserver` is equivalent to `--jobserver=1`.
216+
217+
Otherwise, the client feature is automatically enabled for builds
218+
(not tools) under the following conditions:
202219
203220
- Dry-run (i.e. `-n` or `--dry-run`) is not enabled.
204221
205-
- Neither `-j1` (no parallelism) or `-j0` (infinite parallelism)
206-
are specified on the Ninja command line.
222+
- `-j1` (no parallelism) is not used on the command line.
223+
Note that `-j0` means "infinite" parallelism and does not
224+
disable client mode.
207225
208226
- The `MAKEFLAGS` environment variable is defined and
209227
describes a valid jobserver mode using `--jobserver-auth` or
210228
even `--jobserver-fds`.
211229
212-
In this case, Ninja will use the jobserver pool of job slots
230+
In this case, Ninja will use the shared pool of job slots
213231
to control parallelism, instead of its default implementation
214232
of `-j<count>`.
215233
216-
Note that load-average limitations (i.e. when using `-l<count>`)
217-
are still being enforced in this mode.
234+
Note that other parallelism limitations, (such as `-l<count>`) are *still*
235+
being enforced in this mode however.
218236
219237
Environment variables
220238
~~~~~~~~~~~~~~~~~~~~~
@@ -244,9 +262,8 @@ The default progress status is `"[%f/%t] "` (note the trailing space
244262
to separate from the build rule). Another example of possible progress status
245263
could be `"[%u/%r/%f] "`.
246264
247-
If `MAKEFLAGS` is defined in the environment, if may alter how
248-
Ninja dispatches parallel build commands. See the GNU Jobserver support
249-
section for details.
265+
`NINJA_JOBSERVER` and `MAKEFLAGS` may impact how Ninja dispatches
266+
parallel jobs, as described in the "GNU Jobserver support" section.
250267
251268
Extra tools
252269
~~~~~~~~~~~

misc/jobserver_test.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,68 @@ def test_client_passes_MAKEFLAGS(self):
241241
prefix_args=[sys.executable, "-S", _JOBSERVER_POOL_SCRIPT, "--check"]
242242
)
243243

244+
def _run_pool_test(self, mode: str) -> None:
245+
task_count = 10
246+
build_plan = generate_build_plan(task_count)
247+
extra_env = {"NINJA_JOBSERVER": mode}
248+
with BuildDir(build_plan) as b:
249+
# First, run the full 10 tasks with with 10 tokens, this should allow all
250+
# tasks to run in parallel.
251+
b.ninja_run([f"-j{task_count}", "all"], extra_env=extra_env)
252+
max_overlaps = compute_max_overlapped_spans(b.path, task_count)
253+
self.assertEqual(max_overlaps, 10)
254+
255+
# Second, use 4 tokens only, and verify that this was enforced by Ninja.
256+
b.ninja_clean()
257+
b.ninja_run(["-j4", "all"], extra_env=extra_env)
258+
max_overlaps = compute_max_overlapped_spans(b.path, task_count)
259+
self.assertEqual(max_overlaps, 4)
260+
261+
# Finally, verify that --token-count=1 serializes all tasks.
262+
b.ninja_clean()
263+
b.ninja_run(["-j1", "all"], extra_env=extra_env)
264+
max_overlaps = compute_max_overlapped_spans(b.path, task_count)
265+
self.assertEqual(max_overlaps, 1)
266+
267+
def test_jobserver_pool_with_default_mode(self):
268+
self._run_pool_test("1")
269+
270+
def test_server_passes_MAKEFLAGS(self):
271+
self._test_MAKEFLAGS_value(ninja_args=["--jobserver"])
272+
273+
def _verify_NINJA_JOBSERVER_value(
274+
self, expected_value, ninja_args=[], env_vars={}, msg=None
275+
):
276+
build_plan = r"""
277+
rule print
278+
command = echo NINJA_JOBSERVER="[$$NINJA_JOBSERVER]"
279+
280+
build all: print
281+
"""
282+
env = dict(os.environ)
283+
env.update(env_vars)
284+
285+
with BuildDir(build_plan) as b:
286+
extra_env = {"NINJA_JOBSERVER": "1"}
287+
ret = b.ninja_spawn(["--quiet"] + ninja_args + ["all"], extra_env=extra_env)
288+
self.assertEqual(ret.returncode, 0)
289+
self.assertEqual(
290+
ret.stdout.strip(), f"NINJA_JOBSERVER=[{expected_value}]", msg=msg
291+
)
292+
293+
def test_server_unsets_NINJA_JOBSERVER(self):
294+
env_jobserver_1 = {"NINJA_JOBSERVER": "1"}
295+
self._verify_NINJA_JOBSERVER_value("", env_vars=env_jobserver_1)
296+
self._verify_NINJA_JOBSERVER_value("", ninja_args=["--jobserver"])
297+
298+
@unittest.skipIf(_PLATFORM_IS_WINDOWS, "These test methods do not work on Windows")
299+
def test_jobserver_pool_with_posix_pipe(self):
300+
self._run_pool_test("pipe")
301+
302+
@unittest.skipIf(_PLATFORM_IS_WINDOWS, "These test methods do not work on Windows")
303+
def test_jobserver_pool_with_posix_fifo(self):
304+
self._run_pool_test("fifo")
305+
244306

245307
if __name__ == "__main__":
246308
unittest.main()

src/build.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ struct BuildConfig {
184184
/// means that we do not have any limit.
185185
double max_load_average;
186186
DepfileParserOptions depfile_parser_options;
187+
Jobserver::Config::Mode jobserver_mode = Jobserver::Config::kModeNone;
187188
};
188189

189190
/// Builder wraps the build process: starting commands, updating status.

src/ninja.cc

Lines changed: 103 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,8 +1372,8 @@ int NinjaMain::RunBuild(int argc, char** argv, Status* status) {
13721372
Builder builder(&state_, config_, &build_log_, &deps_log_, &disk_interface_,
13731373
status, start_time_millis_);
13741374

1375-
// Detect jobserver context and inject Jobserver::Client into the builder
1376-
// if needed.
1375+
// If MAKEFLAGS is set, only setup a Jobserver client if needed.
1376+
// (this means that an empty MAKEFLAGS value disables the feature).
13771377
std::unique_ptr<Jobserver::Client> jobserver_client;
13781378

13791379
// Determine whether to use a Jobserver client in this build.
@@ -1502,15 +1502,16 @@ int ReadFlags(int* argc, char*** argv,
15021502
Options* options, BuildConfig* config) {
15031503
DeferGuessParallelism deferGuessParallelism(config);
15041504

1505-
enum { OPT_VERSION = 1, OPT_QUIET = 2 };
1506-
const option kLongOptions[] = {
1507-
{ "help", no_argument, NULL, 'h' },
1508-
{ "version", no_argument, NULL, OPT_VERSION },
1509-
{ "verbose", no_argument, NULL, 'v' },
1510-
{ "quiet", no_argument, NULL, OPT_QUIET },
1511-
{ NULL, 0, NULL, 0 }
1512-
};
1505+
enum { OPT_VERSION = 1, OPT_QUIET = 2, OPT_JOBSERVER = 3 };
1506+
const option kLongOptions[] = { { "help", no_argument, NULL, 'h' },
1507+
{ "version", no_argument, NULL, OPT_VERSION },
1508+
{ "verbose", no_argument, NULL, 'v' },
1509+
{ "quiet", no_argument, NULL, OPT_QUIET },
1510+
{ "jobserver", optional_argument, NULL,
1511+
OPT_JOBSERVER },
1512+
{ NULL, 0, NULL, 0 } };
15131513

1514+
const char* jobserver_mode = nullptr;
15141515
int opt;
15151516
while (!options->tool &&
15161517
(opt = getopt_long(*argc, *argv, "d:f:j:k:l:nt:vw:C:h", kLongOptions,
@@ -1579,6 +1580,9 @@ int ReadFlags(int* argc, char*** argv,
15791580
case OPT_VERSION:
15801581
printf("%s\n", kNinjaVersion);
15811582
return 0;
1583+
case OPT_JOBSERVER:
1584+
jobserver_mode = optarg ? optarg : "1";
1585+
break;
15821586
case 'h':
15831587
default:
15841588
deferGuessParallelism.Refresh();
@@ -1589,6 +1593,29 @@ int ReadFlags(int* argc, char*** argv,
15891593
*argv += optind;
15901594
*argc -= optind;
15911595

1596+
// If an explicit --jobserver has not been used, lookup the NINJA_JOBSERVER
1597+
// environment variable. Ignore it if parallelism was set explicitly on the
1598+
// command line though (and warn about it).
1599+
if (jobserver_mode == nullptr) {
1600+
jobserver_mode = getenv("NINJA_JOBSERVER");
1601+
if (jobserver_mode && !deferGuessParallelism.needGuess) {
1602+
if (!config->dry_run && config->verbosity > BuildConfig::QUIET)
1603+
Warning(
1604+
"Explicit parallelism (-j), ignoring NINJA_JOBSERVER environment "
1605+
"variable.");
1606+
jobserver_mode = nullptr;
1607+
}
1608+
}
1609+
if (jobserver_mode) {
1610+
auto ret = Jobserver::Config::ModeFromString(jobserver_mode);
1611+
config->jobserver_mode = ret.second;
1612+
if (!ret.first && !config->dry_run &&
1613+
config->verbosity > BuildConfig::QUIET) {
1614+
Warning("Invalid jobserver mode '%s': Must be one of: %s", jobserver_mode,
1615+
Jobserver::Config::GetValidModesListAsString(", ").c_str());
1616+
}
1617+
}
1618+
15921619
return -1;
15931620
}
15941621

@@ -1628,6 +1655,72 @@ NORETURN void real_main(int argc, char** argv) {
16281655
exit((ninja.*options.tool->func)(&options, argc, argv));
16291656
}
16301657

1658+
// Determine whether to setup a Jobserver pool. This depends on
1659+
// --jobserver or --jobserver=MODE being passed on the command-line,
1660+
// or NINJA_JOBSERVER=MODE being set in the environment.
1661+
//
1662+
// This must be ignored if a tool is being used, or no/infinite
1663+
// parallelism is being asked.
1664+
//
1665+
// At the moment, this overrides any MAKEFLAGS definition in
1666+
// the environment.
1667+
std::unique_ptr<Jobserver::Pool> jobserver_pool;
1668+
1669+
do {
1670+
if (options.tool) // Do not setup pool when a tool is used.
1671+
break;
1672+
1673+
if (config.parallelism == 1 || config.parallelism == INT_MAX) {
1674+
// No-parallelism (-j1) or infinite parallelism (-j0) was specified.
1675+
break;
1676+
}
1677+
1678+
if (config.jobserver_mode == Jobserver::Config::kModeNone) {
1679+
// --jobserver was not used, and NINJA_JOBSERVER is not set.
1680+
break;
1681+
}
1682+
1683+
if (config.verbosity >= BuildConfig::VERBOSE)
1684+
status->Info("Creating jobserver pool for %d parallel jobs",
1685+
config.parallelism);
1686+
1687+
std::string err;
1688+
jobserver_pool = Jobserver::Pool::Create(
1689+
static_cast<size_t>(config.parallelism), config.jobserver_mode, &err);
1690+
if (!jobserver_pool.get()) {
1691+
if (config.verbosity > BuildConfig::QUIET)
1692+
status->Warning("Jobserver pool creation failed: %s", err.c_str());
1693+
break;
1694+
}
1695+
1696+
std::string makeflags = jobserver_pool->GetEnvMakeFlagsValue();
1697+
1698+
// Set or override the MAKEFLAGS environment variable in
1699+
// the current process. This ensures it is passed to sub-commands
1700+
// as well.
1701+
#ifdef _WIN32
1702+
// TODO(digit): Verify that this works correctly on Win32.
1703+
// this code assumes that _putenv(), unlike Posix putenv()
1704+
// does create a copy of the input string, and that the
1705+
// resulting environment is passed to processes launched
1706+
// with CreateProcess (the documentation only mentions
1707+
// _spawn() and _exec()).
1708+
std::string env = "MAKEFLAGS=" + makeflags;
1709+
_putenv(env.c_str());
1710+
#else // !_WIN32
1711+
setenv("MAKEFLAGS", makeflags.c_str(), 1);
1712+
#endif // !_WIN32
1713+
1714+
} while (0);
1715+
1716+
// Unset NINJA_JOBSERVER unconditionally in subprocesses
1717+
// to avoid multiple sub-pools to be started by mistake.
1718+
#ifdef _WIN32
1719+
_putenv("NINJA_JOBSERVER=");
1720+
#else // !_WIN32
1721+
unsetenv("NINJA_JOBSERVER");
1722+
#endif // !_WIN32
1723+
16311724
// Limit number of rebuilds, to prevent infinite loops.
16321725
const int kCycleLimit = 100;
16331726
for (int cycle = 1; cycle <= kCycleLimit; ++cycle) {

0 commit comments

Comments
 (0)