Skip to content
This repository was archived by the owner on Dec 29, 2022. It is now read-only.

Commit 00e4f29

Browse files
committed
Auto merge of #1536 - Xanewok:ipc-everything, r=Xanewok
Implement support for out-of-process compilation This is quite a lengthy patch, but the gist of it is as follows: - `rls-ipc` crate is introduced which acts as the IPC interface along with a server/client implementation - `rls-rustc` is enhanced with optional support for the IPC - RLS can optionally support it via setting `RLS_OUT_OF_PROCESS` env var (like `rls-rustc` it needs to be compiled `ipc` feature) The IPC is async JSON-RPC running on Tokio using `parity-tokio-ipc` (UDS on unices and named pipes on Windows) - Tokio because I wanted to more or less easily come up with a PoC - RPC because I needed a request->response model for VFS IPC function calls - uds/pipes because it's somewhat cross-platform and we don't have to worry about `rustc` potentially polluting stdio (maybe just capturing the output in `run_compiler` would be enough?) However, the implementation is far from efficient - it currently starts a thread per requested compilation, which in turn starts a single-threaded async runtime to drive the IPC server for a given compilation. I imagine we could either just initiate the runtime globally and spawn the servers on it and drive them to completion on each compilation to reduce the thread spawn/coordination overhead. While this gets rid of the global environment lock on each (previously) in-process crate compilation, what still needs to be addressed is the [sequential compilation](https://github.com/rust-lang/rls/blob/35eba227650eee482bedac7d691a69a8487b2135/rls/src/build/plan.rs#L122-L124) of cached build plan for this implementation to truly benefit from the unlocked parallelization potential. I did some rough test runs (~5) and on a warm cache had the following results: - integration test suite (release) 3.6 +- 0.2s (in-process) vs 3.8 +- 0.3s (out-of-process) - rustfmt master whitespace change (release) 6.4 +- 0.2s (in-process) vs 6.6 +- 0.3s (out-of-process) which at least initially confirms that the performance overhead is somewhat negligible if we can really parallelize the work and leverage process isolation for increased stability. cc #1307 (I'll squash the commits in the final merge, 30+ commits is a tad too much 😅 ) If possible I'd like to get more eyes on the patch to see if it's a good approach and what might be directly improved: - @matklad for potentially shared rustc-with-patched-filesystem - @alexheretic for the RLS/implementation itself - @alexcrichton @nrc do you have thoughts on if we can share the parallel graph compilation logic with Cargo somehow? For now we just rolled our own linear queue here because we didn't need much more but maybe it might be worthwhile to extract the pure execution bits somehow?
2 parents 4fdafdf + 3bcfa0f commit 00e4f29

19 files changed

+1042
-71
lines changed

Cargo.lock

Lines changed: 194 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,17 @@ rls-data = "0.19"
2727
rls-rustc = { version = "0.6.0", path = "rls-rustc" }
2828
rls-span = "0.5"
2929
rls-vfs = "0.8"
30+
rls-ipc = { version = "0.1.0", path = "rls-ipc", optional = true }
3031

3132
cargo = { git = "https://github.com/rust-lang/cargo", rev = "1f74bdf4494f4d51dbe3a6af5474e39c8d194ad6" }
3233
cargo_metadata = "0.8"
3334
clippy_lints = { git = "https://github.com/rust-lang/rust-clippy", rev = "72da1015d6d918fe1b29170acbf486d30e0c2695", optional = true }
3435
env_logger = "0.6"
3536
failure = "0.1.1"
37+
futures = { version = "0.1", optional = true }
3638
home = "0.5"
3739
itertools = "0.8"
38-
jsonrpc-core = "12"
40+
jsonrpc-core = "13"
3941
lsp-types = { version = "0.60", features = ["proposed"] }
4042
lazy_static = "1"
4143
log = "0.4"
@@ -50,6 +52,7 @@ serde = "1.0"
5052
serde_json = "1.0"
5153
serde_derive = "1.0"
5254
serde_ignored = "0.1"
55+
tokio = { version = "0.1", optional = true }
5356
url = "2"
5457
walkdir = "2"
5558
regex = "1"
@@ -76,4 +79,6 @@ tokio-timer = "0.2"
7679
rustc_tools_util = "0.2"
7780

7881
[features]
79-
clippy = ["clippy_lints"]
82+
clippy = ["clippy_lints", "rls-rustc/clippy"]
83+
ipc = ["tokio", "futures", "rls-rustc/ipc", "rls-ipc/server"]
84+
default = []

rls-ipc/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/target/
2+
**/*.rs.bk
3+
Cargo.lock

rls-ipc/Cargo.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "rls-ipc"
3+
version = "0.1.0"
4+
authors = ["Igor Matuszewski <[email protected]>"]
5+
edition = "2018"
6+
description = "Inter-process communication (IPC) layer between RLS and rustc"
7+
license = "Apache-2.0/MIT"
8+
repository = "https://github.com/rust-lang/rls"
9+
categories = ["development-tools"]
10+
11+
[dependencies]
12+
jsonrpc-core = "13"
13+
jsonrpc-core-client = "13"
14+
jsonrpc-derive = "13"
15+
jsonrpc-ipc-server = { version = "13", optional = true }
16+
rls-data = "0.19"
17+
serde = { version = "1.0", features = ["derive"] }
18+
19+
[features]
20+
client = ["jsonrpc-core-client/ipc"]
21+
server = ["jsonrpc-ipc-server"]

rls-ipc/src/client.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//! Allows to connect to an IPC server.
2+
3+
use crate::rpc::callbacks::gen_client::Client as CallbacksClient;
4+
use crate::rpc::file_loader::gen_client::Client as FileLoaderClient;
5+
6+
pub use jsonrpc_core_client::transports::ipc::connect;
7+
pub use jsonrpc_core_client::{RpcChannel, RpcError};
8+
9+
/// Joint IPC client.
10+
#[derive(Clone)]
11+
pub struct Client {
12+
/// File loader interface
13+
pub file_loader: FileLoaderClient,
14+
/// Callbacks interface
15+
pub callbacks: CallbacksClient,
16+
}
17+
18+
impl From<RpcChannel> for Client {
19+
fn from(channel: RpcChannel) -> Self {
20+
Client {
21+
file_loader: FileLoaderClient::from(channel.clone()),
22+
callbacks: CallbacksClient::from(channel),
23+
}
24+
}
25+
}

rls-ipc/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//! Inter-process communication (IPC) layer between RLS and rustc.
2+
3+
#![deny(missing_docs)]
4+
5+
#[cfg(feature = "client")]
6+
pub mod client;
7+
pub mod rpc;
8+
#[cfg(feature = "server")]
9+
pub mod server;

rls-ipc/src/rpc.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//! Available remote procedure call (RPC) interfaces.
2+
3+
use std::collections::{HashMap, HashSet};
4+
use std::path::PathBuf;
5+
6+
use jsonrpc_derive::rpc;
7+
use serde::{Deserialize, Serialize};
8+
9+
pub use jsonrpc_core::{Error, Result};
10+
11+
// Separated because #[rpc] macro generated a `gen_client` mod and so two
12+
// interfaces cannot be derived in the same scope due to a generated name clash
13+
/// RPC interface for an overriden file loader to be used inside `rustc`.
14+
pub mod file_loader {
15+
use super::*;
16+
// Expanded via #[rpc]
17+
pub use gen_client::Client;
18+
pub use rpc_impl_Rpc::gen_server::Rpc as Server;
19+
20+
#[rpc]
21+
/// RPC interface for an overriden file loader to be used inside `rustc`.
22+
pub trait Rpc {
23+
/// Query the existence of a file.
24+
#[rpc(name = "file_exists")]
25+
fn file_exists(&self, path: PathBuf) -> Result<bool>;
26+
27+
/// Returns an absolute path to a file, if possible.
28+
#[rpc(name = "abs_path")]
29+
fn abs_path(&self, path: PathBuf) -> Result<Option<PathBuf>>;
30+
31+
/// Read the contents of an UTF-8 file into memory.
32+
#[rpc(name = "read_file")]
33+
fn read_file(&self, path: PathBuf) -> Result<String>;
34+
}
35+
}
36+
37+
// Separated because #[rpc] macro generated a `gen_client` mod and so two
38+
// interfaces cannot be derived in the same scope due to a generated name clash
39+
/// RPC interface to feed back data from `rustc` instances.
40+
pub mod callbacks {
41+
use super::*;
42+
// Expanded via #[rpc]
43+
pub use gen_client::Client;
44+
pub use rpc_impl_Rpc::gen_server::Rpc as Server;
45+
46+
#[rpc]
47+
/// RPC interface to feed back data from `rustc` instances.
48+
pub trait Rpc {
49+
/// Hands back computed analysis data for the compiled crate
50+
#[rpc(name = "complete_analysis")]
51+
fn complete_analysis(&self, analysis: rls_data::Analysis) -> Result<()>;
52+
53+
/// Hands back computed input files for the compiled crate
54+
#[rpc(name = "input_files")]
55+
fn input_files(&self, input_files: HashMap<PathBuf, HashSet<Crate>>) -> Result<()>;
56+
}
57+
}
58+
59+
/// Build system-agnostic, basic compilation unit
60+
#[derive(PartialEq, Eq, Hash, Debug, Clone, Deserialize, Serialize)]
61+
pub struct Crate {
62+
/// Crate name
63+
pub name: String,
64+
/// Optional path to a crate root
65+
pub src_path: Option<PathBuf>,
66+
/// Edition in which a given crate is compiled
67+
pub edition: Edition,
68+
/// From rustc; mainly used to group other properties used to disambiguate a
69+
/// given compilation unit.
70+
pub disambiguator: (u64, u64),
71+
}
72+
73+
/// Rust edition
74+
#[derive(PartialEq, Eq, Hash, Debug, PartialOrd, Ord, Copy, Clone, Deserialize, Serialize)]
75+
pub enum Edition {
76+
/// Rust 2015
77+
Edition2015,
78+
/// Rust 2018
79+
Edition2018,
80+
}

rls-ipc/src/server.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
//! Includes facility functions to start an IPC server.
2+
3+
pub use jsonrpc_ipc_server::{CloseHandle, ServerBuilder};

rls-rustc/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,18 @@ repository = "https://github.com/rust-lang/rls"
99
categories = ["development-tools"]
1010

1111
[dependencies]
12+
env_logger = "0.6"
13+
log = "0.4"
14+
failure = "0.1"
15+
rand = "0.6"
16+
clippy_lints = { git = "https://github.com/rust-lang/rust-clippy", rev = "72da1015d6d918fe1b29170acbf486d30e0c2695", optional = true }
17+
tokio = { version = "0.1", optional = true }
18+
futures = { version = "0.1", optional = true }
19+
serde = { version = "1", features = ["derive"], optional = true }
20+
rls-data = { version = "0.19", optional = true }
21+
rls-ipc = { path = "../rls-ipc", optional = true }
22+
23+
[features]
24+
clippy = ["clippy_lints"]
25+
ipc = ["tokio", "futures", "serde", "rls-data", "rls-ipc/client"]
26+
default = []

rls-rustc/src/bin/rustc.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
fn main() {
2-
rls_rustc::run();
1+
fn main() -> Result<(), ()> {
2+
rls_rustc::run()
33
}

rls-rustc/src/clippy.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//! Copied from rls/src/config.rs
2+
3+
use std::str::FromStr;
4+
5+
#[derive(Debug, Clone, Copy, PartialEq)]
6+
pub enum ClippyPreference {
7+
/// Disable clippy.
8+
Off,
9+
/// Enable clippy, but `allow` clippy lints (i.e., require `warn` override).
10+
OptIn,
11+
/// Enable clippy.
12+
On,
13+
}
14+
15+
pub fn preference() -> Option<ClippyPreference> {
16+
std::env::var("RLS_CLIPPY_PREFERENCE").ok().and_then(|pref| FromStr::from_str(&pref).ok())
17+
}
18+
19+
/// Permissive deserialization for `ClippyPreference`
20+
/// "opt-in", "Optin" -> `ClippyPreference::OptIn`
21+
impl FromStr for ClippyPreference {
22+
type Err = ();
23+
fn from_str(s: &str) -> Result<Self, Self::Err> {
24+
match s.to_lowercase().as_str() {
25+
"off" => Ok(ClippyPreference::Off),
26+
"optin" | "opt-in" => Ok(ClippyPreference::OptIn),
27+
"on" => Ok(ClippyPreference::On),
28+
_ => Err(()),
29+
}
30+
}
31+
}
32+
33+
pub fn adjust_args(args: Vec<String>, preference: ClippyPreference) -> Vec<String> {
34+
if preference != ClippyPreference::Off {
35+
// Allow feature gating in the same way as `cargo clippy`
36+
let mut clippy_args = vec!["--cfg".to_owned(), r#"feature="cargo-clippy""#.to_owned()];
37+
38+
if preference == ClippyPreference::OptIn {
39+
// `OptIn`: Require explicit `#![warn(clippy::all)]` annotation in each workspace crate
40+
clippy_args.push("-A".to_owned());
41+
clippy_args.push("clippy::all".to_owned());
42+
}
43+
44+
args.iter().map(ToOwned::to_owned).chain(clippy_args).collect()
45+
} else {
46+
args.to_owned()
47+
}
48+
}
49+
50+
#[cfg(feature = "clippy")]
51+
pub fn after_parse_callback(compiler: &rustc_interface::interface::Compiler) {
52+
use rustc_plugin::registry::Registry;
53+
54+
let sess = compiler.session();
55+
let mut registry = Registry::new(
56+
sess,
57+
compiler
58+
.parse()
59+
.expect(
60+
"at this compilation stage \
61+
the crate must be parsed",
62+
)
63+
.peek()
64+
.span,
65+
);
66+
registry.args_hidden = Some(Vec::new());
67+
68+
let conf = clippy_lints::read_conf(&registry);
69+
clippy_lints::register_plugins(&mut registry, &conf);
70+
71+
let Registry {
72+
early_lint_passes, late_lint_passes, lint_groups, llvm_passes, attributes, ..
73+
} = registry;
74+
let mut ls = sess.lint_store.borrow_mut();
75+
for pass in early_lint_passes {
76+
ls.register_early_pass(Some(sess), true, false, pass);
77+
}
78+
for pass in late_lint_passes {
79+
ls.register_late_pass(Some(sess), true, false, false, pass);
80+
}
81+
82+
for (name, (to, deprecated_name)) in lint_groups {
83+
ls.register_group(Some(sess), true, name, deprecated_name, to);
84+
}
85+
clippy_lints::register_pre_expansion_lints(sess, &mut ls, &conf);
86+
clippy_lints::register_renamed(&mut ls);
87+
88+
sess.plugin_llvm_passes.borrow_mut().extend(llvm_passes);
89+
sess.plugin_attributes.borrow_mut().extend(attributes);
90+
}

rls-rustc/src/ipc.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use std::collections::{HashMap, HashSet};
2+
use std::io;
3+
use std::path::{Path, PathBuf};
4+
5+
use failure::Fail;
6+
use futures::Future;
7+
8+
use rls_ipc::client::{Client as JointClient, RpcChannel, RpcError};
9+
use rls_ipc::rpc::callbacks::Client as CallbacksClient;
10+
use rls_ipc::rpc::file_loader::Client as FileLoaderClient;
11+
12+
pub use rls_ipc::client::connect;
13+
14+
#[derive(Clone)]
15+
pub struct Client(JointClient);
16+
17+
impl From<RpcChannel> for Client {
18+
fn from(channel: RpcChannel) -> Self {
19+
Client(channel.into())
20+
}
21+
}
22+
23+
#[derive(Clone)]
24+
pub struct IpcFileLoader(FileLoaderClient);
25+
26+
impl IpcFileLoader {
27+
pub fn into_boxed(self) -> Option<Box<dyn syntax::source_map::FileLoader + Send + Sync>> {
28+
Some(Box::new(self))
29+
}
30+
}
31+
32+
impl syntax::source_map::FileLoader for IpcFileLoader {
33+
fn file_exists(&self, path: &Path) -> bool {
34+
self.0.file_exists(path.to_owned()).wait().unwrap()
35+
}
36+
37+
fn abs_path(&self, path: &Path) -> Option<PathBuf> {
38+
self.0.abs_path(path.to_owned()).wait().ok()?
39+
}
40+
41+
fn read_file(&self, path: &Path) -> io::Result<String> {
42+
self.0
43+
.read_file(path.to_owned())
44+
.wait()
45+
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.compat()))
46+
}
47+
}
48+
49+
#[derive(Clone)]
50+
pub struct IpcCallbacks(CallbacksClient);
51+
52+
impl IpcCallbacks {
53+
pub fn complete_analysis(
54+
&self,
55+
analysis: rls_data::Analysis,
56+
) -> impl Future<Item = (), Error = RpcError> {
57+
self.0.complete_analysis(analysis)
58+
}
59+
60+
pub fn input_files(
61+
&self,
62+
input_files: HashMap<PathBuf, HashSet<rls_ipc::rpc::Crate>>,
63+
) -> impl Future<Item = (), Error = RpcError> {
64+
self.0.input_files(input_files)
65+
}
66+
}
67+
68+
impl Client {
69+
pub fn split(self) -> (IpcFileLoader, IpcCallbacks) {
70+
let JointClient { file_loader, callbacks } = self.0;
71+
(IpcFileLoader(file_loader), IpcCallbacks(callbacks))
72+
}
73+
}

0 commit comments

Comments
 (0)