Skip to content

Commit

Permalink
Add jobserver support to sccache
Browse files Browse the repository at this point in the history
This commit alters the main sccache server to operate and orchestrate its own
GNU make style jobserver. This is primarily intended for interoperation with
rustc itself.

The Rust compiler currently has a multithreaded mode where it will execute code
generation and optimization on the LLVM side of things in parallel. This
parallelism, however, can overload a machine quickly if not properly accounted
for (e.g. if 10 rustcs all spawn 10 threads...). The usage of a GNU make style
jobserver is intended to arbitrate and rate limit all these rustc instances to
ensure that one build's maximal parallelism never exceeds a particular amount.

Currently for Rust Cargo is the primary driver for setting up a jobserver. Cargo
will create this and manage this per compilation, ensuring that any one `cargo
build` invocation never exceeds a maximal parallelism. When sccache enters the
picture, however, the story gets slightly more odd.

The jobserver implementation on Unix relies on inheritance of file descriptors
in spawned processes. With sccache, however, there's no inheritance as the
actual rustc invocation is spawned by the server, not the client. In this case
the env vars used to configure the jobsever are usually incorrect.

To handle this problem this commit bakes a jobserver directly into sccache
itself. The jobserver then overrides whatever jobserver the client has
configured in its own env vars to ensure correct operation. The settings of each
jobserver may be misconfigured (there's no way to configure sccache's jobserver
right now), but hopefully that's not too much of a problem for the forseeable
future.

The implementation here was to provide a thin wrapper around the `jobserver`
crate with a futures-based interface. This interface was then hooked into the
mock command infrastructure to automatically acquire a jobserver token when
spawning a process and automatically drop the token when the process exits.
Additionally, all spawned processes will now automatically receive a configured
jobserver.

cc rust-lang/rust#42867, the original motivation for this commit
  • Loading branch information
alexcrichton authored and luser committed Jan 18, 2018
1 parent bda1bc2 commit adaa4ef
Show file tree
Hide file tree
Showing 12 changed files with 243 additions and 103 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Expand Up @@ -28,12 +28,14 @@ futures = "0.1.11"
futures-cpupool = "0.1"
hyper = { version = "0.11", optional = true }
hyper-tls = { version = "0.1", optional = true }
jobserver = "0.1"
jsonwebtoken = { version = "4.0", optional = true }
libc = "0.2.10"
local-encoding = "0.2.0"
log = "0.3.6"
lru-disk-cache = { path = "lru-disk-cache", version = "0.1.0" }
native-tls = "0.1"
num_cpus = "1.0"
number_prefix = "0.2.5"
openssl = { version = "0.9", optional = true }
redis = { version = "0.8.0", optional = true }
Expand Down
10 changes: 10 additions & 0 deletions README.md
Expand Up @@ -22,6 +22,7 @@ Table of Contents (ToC)
* [Usage](#usage)
* [Storage Options](#storage-options)
* [Debugging](#debugging)
* [Interaction with GNU `make` jobserver](#interaction-with-gnu-make-jobserver)
* [Known Caveats](#known-caveats)

---
Expand Down Expand Up @@ -94,6 +95,15 @@ You can set the `SCCACHE_ERROR_LOG` environment variable to a path to cause the

---

Interaction with GNU `make` jobserver
-------------------------------------

Sccache provides support for a [GNU make jobserver](https://www.gnu.org/software/make/manual/html_node/Job-Slots.html). When the server is started from a process that provides a jobserver, sccache will use that jobserver and provide it to any processes it spawns. (If you are running sccache from a GNU make recipe, you will need to prefix the command with `+` to get this behavior.) If the sccache server is started without a jobserver present it will create its own with the number of slots equal to the number of available CPU cores.

This is most useful when using sccache for Rust compilation, as rustc supports using a jobserver for parallel codegen, so this ensures that rustc will not overwhelm the system with codegen tasks. Cargo implements its own jobserver ([see the information on `NUM_JOBS` in the cargo documentation](https://doc.rust-lang.org/stable/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts)) for rustc to use, so using sccache for Rust compilation in cargo via `RUSTC_WRAPPER` should do the right thing automatically.

---

Known caveats
-------------

Expand Down
4 changes: 3 additions & 1 deletion src/commands.rs
Expand Up @@ -18,6 +18,7 @@ use client::{
ServerConnection,
};
use cmdline::{Command, StatsFormat};
use jobserver::Client;
use log::LogLevel::Trace;
use mock_command::{
CommandCreatorSync,
Expand Down Expand Up @@ -601,9 +602,10 @@ pub fn run_command(cmd: Command) -> Result<i32> {
}
Command::Compile { exe, cmdline, cwd, env_vars } => {
trace!("Command::Compile {{ {:?}, {:?}, {:?} }}", exe, cmdline, cwd);
let jobserver = unsafe { Client::new() };
let conn = connect_or_start_server(get_port())?;
let mut core = Core::new()?;
let res = do_compile(ProcessCommandCreator::new(&core.handle()),
let res = do_compile(ProcessCommandCreator::new(&core.handle(), &jobserver),
&mut core,
conn,
exe.as_ref(),
Expand Down
44 changes: 16 additions & 28 deletions src/compiler/compiler.rs
Expand Up @@ -470,14 +470,13 @@ fn detect_compiler<T>(creator: &T, executable: &Path, pool: &CpuPool)
};
let is_rustc = if filename.to_string_lossy().to_lowercase() == "rustc" {
// Sanity check that it's really rustc.
let executable = executable.to_path_buf();
let child = creator.clone().new_command_sync(&executable)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.args(&["--version"])
.spawn().chain_err(|| {
format!("failed to execute {:?}", executable)
});
let output = child.into_future().and_then(move |child| {
.spawn();
let output = child.and_then(move |child| {
child.wait_with_output()
.chain_err(|| "failed to read child output")
});
Expand Down Expand Up @@ -530,10 +529,7 @@ gcc
let output = write.and_then(move |(tempdir, src)| {
cmd.arg("-E").arg(src);
trace!("compiler {:?}", cmd);
let child = cmd.spawn().chain_err(|| {
format!("failed to execute {:?}", cmd)
});
child.into_future().and_then(|child| {
cmd.spawn().and_then(|child| {
child.wait_with_output().chain_err(|| "failed to read child output")
}).map(|e| {
drop(tempdir);
Expand Down Expand Up @@ -724,11 +720,9 @@ mod test {
let o = obj.clone();
next_command_calls(&creator, move |_| {
// Pretend to compile something.
match File::create(&o)
.and_then(|mut f| f.write_all(b"file contents")) {
Ok(_) => Ok(MockChild::new(exit_status(0), COMPILER_STDOUT, COMPILER_STDERR)),
Err(e) => Err(e),
}
let mut f = File::create(&o)?;
f.write_all(b"file contents")?;
Ok(MockChild::new(exit_status(0), COMPILER_STDOUT, COMPILER_STDERR))
});
let cwd = f.tempdir.path();
let arguments = ovec!["-c", "foo.c", "-o", "foo.o"];
Expand Down Expand Up @@ -805,11 +799,9 @@ mod test {
let o = obj.clone();
next_command_calls(&creator, move |_| {
// Pretend to compile something.
match File::create(&o)
.and_then(|mut f| f.write_all(b"file contents")) {
Ok(_) => Ok(MockChild::new(exit_status(0), COMPILER_STDOUT, COMPILER_STDERR)),
Err(e) => Err(e),
}
let mut f = File::create(&o)?;
f.write_all(b"file contents")?;
Ok(MockChild::new(exit_status(0), COMPILER_STDOUT, COMPILER_STDERR))
});
let cwd = f.tempdir.path();
let arguments = ovec!["-c", "foo.c", "-o", "foo.o"];
Expand Down Expand Up @@ -887,11 +879,9 @@ mod test {
let o = obj.clone();
next_command_calls(&creator, move |_| {
// Pretend to compile something.
match File::create(&o)
.and_then(|mut f| f.write_all(b"file contents")) {
Ok(_) => Ok(MockChild::new(exit_status(0), COMPILER_STDOUT, COMPILER_STDERR)),
Err(e) => Err(e),
}
let mut f = File::create(&o)?;
f.write_all(b"file contents")?;
Ok(MockChild::new(exit_status(0), COMPILER_STDOUT, COMPILER_STDERR))
});
let cwd = f.tempdir.path();
let arguments = ovec!["-c", "foo.c", "-o", "foo.o"];
Expand Down Expand Up @@ -954,11 +944,9 @@ mod test {
let o = obj.clone();
next_command_calls(&creator, move |_| {
// Pretend to compile something.
match File::create(&o)
.and_then(|mut f| f.write_all(b"file contents")) {
Ok(_) => Ok(MockChild::new(exit_status(0), COMPILER_STDOUT, COMPILER_STDERR)),
Err(e) => Err(e),
}
let mut f = File::create(&o)?;
f.write_all(b"file contents")?;
Ok(MockChild::new(exit_status(0), COMPILER_STDOUT, COMPILER_STDERR))
});
}
let cwd = f.tempdir.path();
Expand Down
71 changes: 71 additions & 0 deletions src/jobserver.rs
@@ -0,0 +1,71 @@
extern crate jobserver;

use std::io;
use std::process::Command;
use std::sync::Arc;

use futures::prelude::*;
use futures::sync::mpsc;
use futures::sync::oneshot;
use num_cpus;

use errors::*;

pub use self::jobserver::Acquired;

#[derive(Clone)]
pub struct Client {
helper: Arc<jobserver::HelperThread>,
inner: jobserver::Client,
tx: mpsc::UnboundedSender<oneshot::Sender<io::Result<Acquired>>>
}

impl Client {
// unsafe because `from_env` is unsafe (can use the wrong fds)
pub unsafe fn new() -> Client {
match jobserver::Client::from_env() {
Some(c) => Client::_new(c),
None => Client::new_num(num_cpus::get()),
}
}

pub fn new_num(num: usize) -> Client {
let inner = jobserver::Client::new(num)
.expect("failed to create jobserver");
Client::_new(inner)
}

fn _new(inner: jobserver::Client) -> Client {
let (tx, rx) = mpsc::unbounded::<oneshot::Sender<_>>();
let mut rx = rx.wait();
let helper = inner.clone().into_helper_thread(move |token| {
if let Some(Ok(sender)) = rx.next() {
drop(sender.send(token));
}
}).expect("failed to spawn helper thread");

Client {
inner: inner,
helper: Arc::new(helper),
tx: tx,
}
}

/// Configures this jobserver to be inherited by the specified command
pub fn configure(&self, cmd: &mut Command) {
self.inner.configure(cmd)
}

/// Returns a future that represents an acquired jobserver token.
///
/// This should be invoked before any "work" is spawend (for whatever the
/// defnition of "work" is) to ensure that the system is properly
/// rate-limiting itself.
pub fn acquire(&self) -> SFuture<Acquired> {
let (tx, rx) = oneshot::channel();
self.helper.request_token();
self.tx.unbounded_send(tx).unwrap();
Box::new(rx.chain_err(|| "jobserver helper panicked")
.and_then(|t| t.chain_err(|| "failed to acquire jobserver token")))
}
}
2 changes: 2 additions & 0 deletions src/main.rs
Expand Up @@ -52,6 +52,7 @@ extern crate libc;
#[cfg(windows)]
extern crate mio_named_pipes;
extern crate native_tls;
extern crate num_cpus;
extern crate number_prefix;
#[cfg(feature = "openssl")]
extern crate openssl;
Expand Down Expand Up @@ -93,6 +94,7 @@ mod client;
mod cmdline;
mod commands;
mod compiler;
mod jobserver;
mod mock_command;
mod protocol;
mod server;
Expand Down

0 comments on commit adaa4ef

Please sign in to comment.