Speedup no-op builds by caching rustc invocations
authorAleksey Kladov <aleksey.kladov@gmail.com>
Sat, 14 Apr 2018 10:39:59 +0000 (13:39 +0300)
committerAleksey Kladov <aleksey.kladov@gmail.com>
Sat, 14 Apr 2018 10:39:59 +0000 (13:39 +0300)
src/bin/cli.rs
src/cargo/core/compiler/context/target_info.rs
src/cargo/core/compiler/mod.rs
src/cargo/ops/cargo_clean.rs
src/cargo/ops/cargo_compile.rs
src/cargo/util/config.rs
src/cargo/util/paths.rs
src/cargo/util/rustc.rs
tests/testsuite/cargotest/mod.rs
tests/testsuite/main.rs
tests/testsuite/rustc_info_cache.rs [new file with mode: 0644]

index 768012a44454825d1bd416c1329c5883149790e3..0161a0f6bf9ca1454fe64b4c3a89fb8c0df31dc3 100644 (file)
@@ -47,7 +47,7 @@ Run with 'cargo -Z [FLAG] [SUBCOMMAND]'"
     }
 
     if let Some(ref code) = args.value_of("explain") {
-        let mut procss = config.new_rustc()?.process();
+        let mut procss = config.rustc(None)?.process();
         procss.arg("--explain").arg(code).exec()?;
         return Ok(());
     }
index 78a2220ca53be0e7b396a8a4569e86af077427af..ff691ff2a306e7cb51279f02e4a35b423928f7d8 100644 (file)
@@ -78,20 +78,19 @@ impl TargetInfo {
         with_cfg.arg("--print=cfg");
 
         let mut has_cfg_and_sysroot = true;
-        let output = with_cfg
-            .exec_with_output()
+        let (output, error) = build_config
+            .rustc
+            .cached_output(&with_cfg)
             .or_else(|_| {
                 has_cfg_and_sysroot = false;
-                process.exec_with_output()
+                build_config.rustc.cached_output(&process)
             })
             .chain_err(|| "failed to run `rustc` to learn about target-specific information")?;
 
-        let error = str::from_utf8(&output.stderr).unwrap();
-        let output = str::from_utf8(&output.stdout).unwrap();
         let mut lines = output.lines();
         let mut map = HashMap::new();
         for crate_type in KNOWN_CRATE_TYPES {
-            let out = parse_crate_type(crate_type, error, &mut lines)?;
+            let out = parse_crate_type(crate_type, &error, &mut lines)?;
             map.insert(crate_type.to_string(), out);
         }
 
index 7f9c503b960b9b5cab443e84723f26d9c49b4112..c414cafd8b42b60d6eb9b910905e4d9da443b7eb 100644 (file)
@@ -80,6 +80,7 @@ impl BuildConfig {
         config: &Config,
         jobs: Option<u32>,
         requested_target: &Option<String>,
+        rustc_info_cache: Option<PathBuf>,
     ) -> CargoResult<BuildConfig> {
         if let &Some(ref s) = requested_target {
             if s.trim().is_empty() {
@@ -117,7 +118,7 @@ impl BuildConfig {
             None => None,
         };
         let jobs = jobs.or(cfg_jobs).unwrap_or(::num_cpus::get() as u32);
-        let rustc = config.new_rustc()?;
+        let rustc = config.rustc(rustc_info_cache)?;
         let host_config = TargetConfig::new(config, &rustc.host)?;
         let target_config = match target.as_ref() {
             Some(triple) => TargetConfig::new(config, triple)?,
index 43a89ea64230ef0b8d355482e45c6181b5104343..a8c388752dd736704a66757eb1108b72b9ff5235 100644 (file)
@@ -84,7 +84,7 @@ pub fn clean(ws: &Workspace, opts: &CleanOptions) -> CargoResult<()> {
         }
     }
 
-    let mut build_config = BuildConfig::new(config, Some(1), &opts.target)?;
+    let mut build_config = BuildConfig::new(config, Some(1), &opts.target, None)?;
     build_config.release = opts.release;
     let mut cx = Context::new(ws, &resolve, &packages, opts.config, build_config, profiles)?;
     cx.prepare_units(None, &units)?;
index e734c4cb8010756fedba77826c603b5af1695165..485f2b46af0f0d4dd49bbf1c1839a0957527fed1 100644 (file)
@@ -260,7 +260,8 @@ pub fn compile_ws<'a>(
         bail!("jobs must be at least 1")
     }
 
-    let mut build_config = BuildConfig::new(config, jobs, &target)?;
+    let rustc_info_cache = ws.target_dir().join("rustc_info.json").into_path_unlocked();
+    let mut build_config = BuildConfig::new(config, jobs, &target, Some(rustc_info_cache))?;
     build_config.release = release;
     build_config.test = mode == CompileMode::Test || mode == CompileMode::Bench;
     build_config.json_messages = message_format == MessageFormat::Json;
index a95c56c7e0ace339cf17f46c9cd681784dffd836..4596f07c143f770b9233ec69e27f26cb28fcfd6d 100644 (file)
@@ -158,10 +158,11 @@ impl Config {
     }
 
     /// Get the path to the `rustc` executable
-    pub fn new_rustc(&self) -> CargoResult<Rustc> {
+    pub fn rustc(&self, cache_location: Option<PathBuf>) -> CargoResult<Rustc> {
         Rustc::new(
             self.get_tool("rustc")?,
             self.maybe_get_tool("rustc_wrapper")?,
+            cache_location,
         )
     }
 
@@ -191,25 +192,7 @@ impl Config {
                         .map(PathBuf::from)
                         .next()
                         .ok_or(format_err!("no argv[0]"))?;
-                    if argv0.components().count() == 1 {
-                        probe_path(argv0)
-                    } else {
-                        Ok(argv0.canonicalize()?)
-                    }
-                }
-
-                fn probe_path(argv0: PathBuf) -> CargoResult<PathBuf> {
-                    let paths = env::var_os("PATH").ok_or(format_err!("no PATH"))?;
-                    for path in env::split_paths(&paths) {
-                        let candidate = PathBuf::from(path).join(&argv0);
-                        if candidate.is_file() {
-                            // PATH may have a component like "." in it, so we still need to
-                            // canonicalize.
-                            return Ok(candidate.canonicalize()?);
-                        }
-                    }
-
-                    bail!("no cargo executable candidate found in PATH")
+                    paths::resolve_executable(&argv0)
                 }
 
                 let exe = from_current_exe()
index b08a589d4b521316570579a04155559cf54dbda3..eeee00ddde95f77f6cc3f9d6b455bedd5703c6e5 100644 (file)
@@ -85,6 +85,24 @@ pub fn without_prefix<'a>(long_path: &'a Path, prefix: &'a Path) -> Option<&'a P
     }
 }
 
+pub fn resolve_executable(exec: &Path) -> CargoResult<PathBuf> {
+    if exec.components().count() == 1 {
+        let paths = env::var_os("PATH").ok_or(format_err!("no PATH"))?;
+        for path in env::split_paths(&paths) {
+            let candidate = PathBuf::from(path).join(&exec);
+            if candidate.is_file() {
+                // PATH may have a component like "." in it, so we still need to
+                // canonicalize.
+                return Ok(candidate.canonicalize()?);
+            }
+        }
+
+        bail!("no executable for `{}` found in PATH", exec.display())
+    } else {
+        Ok(exec.canonicalize()?)
+    }
+}
+
 pub fn read(path: &Path) -> CargoResult<String> {
     match String::from_utf8(read_bytes(path)?) {
         Ok(s) => Ok(s),
index d9e8fe657dd869aa5bef7d16dccbcdf47ff80ae9..b9ec492c20ae159bd016aedcea9479844dadb774 100644 (file)
@@ -1,6 +1,15 @@
-use std::path::PathBuf;
+#![allow(deprecated)] // for SipHasher
+
+use std::path::{Path, PathBuf};
+use std::hash::{Hash, Hasher, SipHasher};
+use std::collections::hash_map::{Entry, HashMap};
+use std::sync::Mutex;
+use std::env;
+
+use serde_json;
 
 use util::{self, internal, profile, CargoResult, ProcessBuilder};
+use util::paths;
 
 /// Information on the `rustc` executable
 #[derive(Debug)]
@@ -14,6 +23,7 @@ pub struct Rustc {
     pub verbose_version: String,
     /// The host triple (arch-platform-OS), this comes from verbose_version.
     pub host: String,
+    cache: Mutex<Cache>,
 }
 
 impl Rustc {
@@ -22,16 +32,18 @@ impl Rustc {
     ///
     /// If successful this function returns a description of the compiler along
     /// with a list of its capabilities.
-    pub fn new(path: PathBuf, wrapper: Option<PathBuf>) -> CargoResult<Rustc> {
+    pub fn new(
+        path: PathBuf,
+        wrapper: Option<PathBuf>,
+        cache_location: Option<PathBuf>,
+    ) -> CargoResult<Rustc> {
         let _p = profile::start("Rustc::new");
 
+        let mut cache = Cache::load(&path, cache_location);
+
         let mut cmd = util::process(&path);
         cmd.arg("-vV");
-
-        let output = cmd.exec_with_output()?;
-
-        let verbose_version = String::from_utf8(output.stdout)
-            .map_err(|_| internal("rustc -v didn't return utf8 output"))?;
+        let verbose_version = cache.cached_output(&cmd)?.0;
 
         let host = {
             let triple = verbose_version
@@ -47,6 +59,7 @@ impl Rustc {
             wrapper,
             verbose_version,
             host,
+            cache: Mutex::new(cache),
         })
     }
 
@@ -62,4 +75,157 @@ impl Rustc {
             util::process(&self.path)
         }
     }
+
+    pub fn cached_output(&self, cmd: &ProcessBuilder) -> CargoResult<(String, String)> {
+        self.cache.lock().unwrap().cached_output(cmd)
+    }
+}
+
+/// It is a well known that `rustc` is not the fastest compiler in the world.
+/// What is less known is that even `rustc --version --verbose` takes about a
+/// hundred milliseconds! Because we need compiler version info even for no-op
+/// builds, we cache it here, based on compiler's mtime and rustup's current
+/// toolchain.
+#[derive(Debug)]
+struct Cache {
+    cache_location: Option<PathBuf>,
+    dirty: bool,
+    data: CacheData,
+}
+
+#[derive(Serialize, Deserialize, Debug, Default)]
+struct CacheData {
+    rustc_fingerprint: u64,
+    outputs: HashMap<u64, (String, String)>,
+}
+
+impl Cache {
+    fn load(rustc: &Path, cache_location: Option<PathBuf>) -> Cache {
+        match (cache_location, rustc_fingerprint(rustc)) {
+            (Some(cache_location), Ok(rustc_fingerprint)) => {
+                let empty = CacheData {
+                    rustc_fingerprint,
+                    outputs: HashMap::new(),
+                };
+                let mut dirty = true;
+                let data = match read(&cache_location) {
+                    Ok(data) => {
+                        if data.rustc_fingerprint == rustc_fingerprint {
+                            info!("reusing existing rustc info cache");
+                            dirty = false;
+                            data
+                        } else {
+                            info!("different compiler, creating new rustc info cache");
+                            empty
+                        }
+                    }
+                    Err(e) => {
+                        info!("failed to read rustc info cache: {}", e);
+                        empty
+                    }
+                };
+                return Cache {
+                    cache_location: Some(cache_location),
+                    dirty,
+                    data,
+                };
+
+                fn read(path: &Path) -> CargoResult<CacheData> {
+                    let json = paths::read(path)?;
+                    Ok(serde_json::from_str(&json)?)
+                }
+            }
+            (_, fingerprint) => {
+                if let Err(e) = fingerprint {
+                    warn!("failed to calculate rustc fingerprint: {}", e);
+                }
+                info!("rustc info cache disabled");
+                Cache {
+                    cache_location: None,
+                    dirty: false,
+                    data: CacheData::default(),
+                }
+            }
+        }
+    }
+
+    fn cached_output(&mut self, cmd: &ProcessBuilder) -> CargoResult<(String, String)> {
+        let calculate = || {
+            let output = cmd.exec_with_output()?;
+            let stdout = String::from_utf8(output.stdout)
+                .map_err(|_| internal("rustc didn't return utf8 output"))?;
+            let stderr = String::from_utf8(output.stderr)
+                .map_err(|_| internal("rustc didn't return utf8 output"))?;
+            Ok((stdout, stderr))
+        };
+        if self.cache_location.is_none() {
+            info!("rustc info uncached");
+            return calculate();
+        }
+
+        let key = process_fingerprint(cmd);
+        match self.data.outputs.entry(key) {
+            Entry::Occupied(entry) => {
+                info!("rustc info cache hit");
+                Ok(entry.get().clone())
+            }
+            Entry::Vacant(entry) => {
+                info!("rustc info cache miss");
+                let output = calculate()?;
+                entry.insert(output.clone());
+                self.dirty = true;
+                Ok(output)
+            }
+        }
+    }
+}
+
+impl Drop for Cache {
+    fn drop(&mut self) {
+        match (&self.cache_location, self.dirty) {
+            (&Some(ref path), true) => {
+                let json = serde_json::to_string(&self.data).unwrap();
+                match paths::write(path, json.as_bytes()) {
+                    Ok(()) => info!("updated rustc info cache"),
+                    Err(e) => warn!("failed to update rustc info cache: {}", e),
+                }
+            }
+            _ => (),
+        }
+    }
+}
+
+fn rustc_fingerprint(path: &Path) -> CargoResult<u64> {
+    let mut hasher = SipHasher::new_with_keys(0, 0);
+
+    let path = paths::resolve_executable(path)?;
+    path.hash(&mut hasher);
+
+    paths::mtime(&path)?.hash(&mut hasher);
+
+    match (env::var("RUSTUP_HOME"), env::var("RUSTUP_TOOLCHAIN")) {
+        (Ok(rustup_home), Ok(rustup_toolchain)) => {
+            debug!("adding rustup info to rustc fingerprint");
+            rustup_toolchain.hash(&mut hasher);
+            rustup_home.hash(&mut hasher);
+            let rustup_rustc = Path::new(&rustup_home)
+                .join("toolchains")
+                .join(rustup_toolchain)
+                .join("bin")
+                .join("rustc");
+            paths::mtime(&rustup_rustc)?.hash(&mut hasher);
+        }
+        _ => (),
+    }
+
+    Ok(hasher.finish())
+}
+
+fn process_fingerprint(cmd: &ProcessBuilder) -> u64 {
+    let mut hasher = SipHasher::new_with_keys(0, 0);
+    cmd.get_args().hash(&mut hasher);
+    let mut env = cmd.get_envs().iter().collect::<Vec<_>>();
+    env.sort();
+    env.hash(&mut hasher);
+    hasher.finish()
 }
index 73d5c370bd7cdb58ee8e688177a9049fbad5fac3..23efab70360cceb804bc078f9a2cf9f366bdd3b8 100644 (file)
@@ -10,7 +10,7 @@ pub mod support;
 
 pub mod install;
 
-thread_local!(pub static RUSTC: Rustc = Rustc::new(PathBuf::from("rustc"), None).unwrap());
+thread_local!(pub static RUSTC: Rustc = Rustc::new(PathBuf::from("rustc"), None, None).unwrap());
 
 pub fn rustc_host() -> String {
     RUSTC.with(|r| r.host.clone())
index da6ef5ea04a5ea4493a803c365fc143d2534fb93..e1d3181cd082837fc044f8006e8d8037aa10da5e 100644 (file)
@@ -78,6 +78,7 @@ mod required_features;
 mod resolve;
 mod run;
 mod rustc;
+mod rustc_info_cache;
 mod rustdocflags;
 mod rustdoc;
 mod rustflags;
diff --git a/tests/testsuite/rustc_info_cache.rs b/tests/testsuite/rustc_info_cache.rs
new file mode 100644 (file)
index 0000000..7d3bcb5
--- /dev/null
@@ -0,0 +1,120 @@
+use cargotest::support::{execs, project};
+use cargotest::support::paths::CargoPathExt;
+use hamcrest::assert_that;
+use std::env;
+
+#[test]
+fn rustc_info_cache() {
+    let p = project("foo")
+        .file(
+            "Cargo.toml",
+            r#"
+            [project]
+            name = "foo"
+            version = "0.0.1"
+            authors = []
+        "#,
+        )
+        .file("src/main.rs", r#"fn main() { println!("hello"); }"#)
+        .build();
+
+    let miss = "[..] rustc info cache miss[..]";
+    let hit = "[..] rustc info cache hit[..]";
+
+    assert_that(
+        p.cargo("build").env("RUST_LOG", "cargo::util::rustc=info"),
+        execs()
+            .with_status(0)
+            .with_stderr_contains("[..]failed to read rustc info cache[..]")
+            .with_stderr_contains(miss)
+            .with_stderr_does_not_contain(hit),
+    );
+
+    assert_that(
+        p.cargo("build").env("RUST_LOG", "cargo::util::rustc=info"),
+        execs()
+            .with_status(0)
+            .with_stderr_contains("[..]reusing existing rustc info cache[..]")
+            .with_stderr_contains(hit)
+            .with_stderr_does_not_contain(miss),
+    );
+
+    let other_rustc = {
+        let p = project("compiler")
+            .file(
+                "Cargo.toml",
+                r#"
+            [package]
+            name = "compiler"
+            version = "0.1.0"
+        "#,
+            )
+            .file(
+                "src/main.rs",
+                r#"
+            use std::process::Command;
+            use std::env;
+
+            fn main() {
+                let mut cmd = Command::new("rustc");
+                for arg in env::args_os().skip(1) {
+                    cmd.arg(arg);
+                }
+                std::process::exit(cmd.status().unwrap().code().unwrap());
+            }
+        "#,
+            )
+            .build();
+        assert_that(p.cargo("build"), execs().with_status(0));
+
+        p.root()
+            .join("target/debug/compiler")
+            .with_extension(env::consts::EXE_EXTENSION)
+    };
+
+    assert_that(
+        p.cargo("build")
+            .env("RUST_LOG", "cargo::util::rustc=info")
+            .env("RUSTC", other_rustc.display().to_string()),
+        execs()
+            .with_status(0)
+            .with_stderr_contains("[..]different compiler, creating new rustc info cache[..]")
+            .with_stderr_contains(miss)
+            .with_stderr_does_not_contain(hit),
+    );
+
+    assert_that(
+        p.cargo("build")
+            .env("RUST_LOG", "cargo::util::rustc=info")
+            .env("RUSTC", other_rustc.display().to_string()),
+        execs()
+            .with_status(0)
+            .with_stderr_contains("[..]reusing existing rustc info cache[..]")
+            .with_stderr_contains(hit)
+            .with_stderr_does_not_contain(miss),
+    );
+
+    other_rustc.move_into_the_future();
+
+    assert_that(
+        p.cargo("build")
+            .env("RUST_LOG", "cargo::util::rustc=info")
+            .env("RUSTC", other_rustc.display().to_string()),
+        execs()
+            .with_status(0)
+            .with_stderr_contains("[..]different compiler, creating new rustc info cache[..]")
+            .with_stderr_contains(miss)
+            .with_stderr_does_not_contain(hit),
+    );
+
+    assert_that(
+        p.cargo("build")
+            .env("RUST_LOG", "cargo::util::rustc=info")
+            .env("RUSTC", other_rustc.display().to_string()),
+        execs()
+            .with_status(0)
+            .with_stderr_contains("[..]reusing existing rustc info cache[..]")
+            .with_stderr_contains(hit)
+            .with_stderr_does_not_contain(miss),
+    );
+}