Repository: rust-analyzer/rust-analyzer
Pull request #4119 (2020-04-24 02:26:40)

Cache proc-macro dlls
This PR try to fix a deadlock in proc-macro srv by not unloading dlls.

Currently we load and unload dlls for each request, however rustc TLS is leaky , such that if we do it a lot of times, all TLS index will be consumed and it will be deadlocked inside panic (it is because panic itself is using TLS too).


bjorn3: Does this handle proc macros that change?
edwin0cheng: Oh, no. It does not handle that case. And you reminded me that in Windwos if a dll is loaded in a process, its file content is locked too. 

So thats mean that this approach may be wrong. Um.. any idea? 
edwin0cheng: One hacky trick we might use :
1. Copy the dll to a temp dir.
2. Load it into process
3. For the following requests, if the path of req is loaded, check whether it is changed. If so, goto 1.
lnicola: Maybe we could restart the host if the proc macro changes?
bjorn3: On Windoes that will still need a copy of the DLL to avoid locking it. Otherwise rustc can't write a new version.
matklad: >Does this handle proc macros that change?

I think, strictly speaking, it shouldn't. Specifically, we should only change proc-macros with an explicit `set` call to the salsa database. So, UI wise, we should detect when proc-macros go stale, show "you might need to reload proc macros" status, and then re-run `cargo check` when the user asks us to. 

However, the problem with locking on windows seems real. It would be pretty horrible to block `cargo` from running simply because `rust-analyzer` holds a macro. And we must hold onto the old version of macro, otherwise we get bad non-reproducible behavior in salsa. Copying macros to `./target/rust-analyzer` seems the only reasonable solution here (unless there's an API to load a DLL from a byte blob, rather than from a file on disk). 

I wonder if we should "just" fix rustc to use wasm? 
edwin0cheng: > Copying macros to ./target/rust-analyzer

May I know why not `std::env::temp_dir` ?
edwin0cheng: I implemented the copy to temp-dir stategy (if we really want to copy to `./target/rust-analyzer`, I could change it later), and add a timestamp for comparing the caching. 

Patch:
diff --git a/crates/ra_proc_macro_srv/src/cli.rs b/crates/ra_proc_macro_srv/src/cli.rs
index 7282e5b9b7..1437794c9e 100644
--- a/crates/ra_proc_macro_srv/src/cli.rs
+++ b/crates/ra_proc_macro_srv/src/cli.rs
@@ -1,15 +1,17 @@
 //! Driver for proc macro server
 
-use crate::{expand_task, list_macros};
+use crate::ProcMacroSrv;
 use ra_proc_macro::msg::{self, Message};
 use std::io;
 
 pub fn run() -> io::Result<()> {
+    let mut srv = ProcMacroSrv::default();
+
     while let Some(req) = read_request()? {
         let res = match req {
-            msg::Request::ListMacro(task) => Ok(msg::Response::ListMacro(list_macros(&task))),
+            msg::Request::ListMacro(task) => srv.list_macros(&task).map(msg::Response::ListMacro),
             msg::Request::ExpansionMacro(task) => {
-                expand_task(&task).map(msg::Response::ExpansionMacro)
+                srv.expand(&task).map(msg::Response::ExpansionMacro)
             }
         };
 
diff --git a/crates/ra_proc_macro_srv/src/dylib.rs b/crates/ra_proc_macro_srv/src/dylib.rs
index d202eb0fde..aa84e951cd 100644
--- a/crates/ra_proc_macro_srv/src/dylib.rs
+++ b/crates/ra_proc_macro_srv/src/dylib.rs
@@ -2,13 +2,12 @@
 
 use crate::{proc_macro::bridge, rustc_server::TokenStream};
 use std::fs::File;
-use std::path::Path;
+use std::path::{Path, PathBuf};
 
 use goblin::{mach::Mach, Object};
 use libloading::Library;
 use memmap::Mmap;
 use ra_proc_macro::ProcMacroKind;
-
 use std::io;
 
 const NEW_REGISTRAR_SYMBOL: &str = "_rustc_proc_macro_decls_";
@@ -109,23 +108,21 @@ impl ProcMacroLibraryLibloading {
     }
 }
 
-type ProcMacroLibraryImpl = ProcMacroLibraryLibloading;
-
 pub struct Expander {
-    libs: Vec<ProcMacroLibraryImpl>,
+    inner: ProcMacroLibraryLibloading,
 }
 
 impl Expander {
-    pub fn new(lib: &Path) -> Result<Expander, String> {
+    pub fn new(lib: &Path) -> io::Result<Expander> {
         // Some libraries for dynamic loading require canonicalized path even when it is
         // already absolute
-        let lib = lib
-            .canonicalize()
-            .unwrap_or_else(|err| panic!("Cannot canonicalize {}: {:?}", lib.display(), err));
+        let lib = lib.canonicalize()?;
+
+        let lib = ensure_file_with_lock_free_access(&lib)?;
 
-        let library = ProcMacroLibraryImpl::open(&lib).map_err(|e| e.to_string())?;
+        let library = ProcMacroLibraryLibloading::open(&lib)?;
 
-        Ok(Expander { libs: vec![library] })
+        Ok(Expander { inner: library })
     }
 
     pub fn expand(
@@ -141,38 +138,36 @@ impl Expander {
                 TokenStream::with_subtree(attr.clone())
             });
 
-        for lib in &self.libs {
-            for proc_macro in &lib.exported_macros {
-                match proc_macro {
-                    bridge::client::ProcMacro::CustomDerive { trait_name, client, .. }
-                        if *trait_name == macro_name =>
-                    {
-                        let res = client.run(
-                            &crate::proc_macro::bridge::server::SameThread,
-                            crate::rustc_server::Rustc::default(),
-                            parsed_body,
-                        );
-                        return res.map(|it| it.subtree);
-                    }
-                    bridge::client::ProcMacro::Bang { name, client } if *name == macro_name => {
-                        let res = client.run(
-                            &crate::proc_macro::bridge::server::SameThread,
-                            crate::rustc_server::Rustc::default(),
-                            parsed_body,
-                        );
-                        return res.map(|it| it.subtree);
-                    }
-                    bridge::client::ProcMacro::Attr { name, client } if *name == macro_name => {
-                        let res = client.run(
-                            &crate::proc_macro::bridge::server::SameThread,
-                            crate::rustc_server::Rustc::default(),
-                            parsed_attributes,
-                            parsed_body,
-                        );
-                        return res.map(|it| it.subtree);
-                    }
-                    _ => continue,
+        for proc_macro in &self.inner.exported_macros {
+            match proc_macro {
+                bridge::client::ProcMacro::CustomDerive { trait_name, client, .. }
+                    if *trait_name == macro_name =>
+                {
+                    let res = client.run(
+                        &crate::proc_macro::bridge::server::SameThread,
+                        crate::rustc_server::Rustc::default(),
+                        parsed_body,
+                    );
+                    return res.map(|it| it.subtree);
+                }
+                bridge::client::ProcMacro::Bang { name, client } if *name == macro_name => {
+                    let res = client.run(
+                        &crate::proc_macro::bridge::server::SameThread,
+                        crate::rustc_server::Rustc::default(),
+                        parsed_body,
+                    );
+                    return res.map(|it| it.subtree);
+                }
+                bridge::client::ProcMacro::Attr { name, client } if *name == macro_name => {
+                    let res = client.run(
+                        &crate::proc_macro::bridge::server::SameThread,
+                        crate::rustc_server::Rustc::default(),
+                        parsed_attributes,
+                        parsed_body,
+                    );
+                    return res.map(|it| it.subtree);
                 }
+                _ => continue,
             }
         }
 
@@ -180,9 +175,9 @@ impl Expander {
     }
 
     pub fn list_macros(&self) -> Vec<(String, ProcMacroKind)> {
-        self.libs
+        self.inner
+            .exported_macros
             .iter()
-            .flat_map(|it| &it.exported_macros)
             .map(|proc_macro| match proc_macro {
                 bridge::client::ProcMacro::CustomDerive { trait_name, .. } => {
                     (trait_name.to_string(), ProcMacroKind::CustomDerive)
@@ -197,3 +192,33 @@ impl Expander {
             .collect()
     }
 }
+
+/// Copy the dylib to temp directory to prevent locking in Windows
+#[cfg(windows)]
+fn ensure_file_with_lock_free_access(path: &Path) -> io::Result<PathBuf> {
+    use std::{ffi::OsString, time::SystemTime};
+
+    let mut to = std::env::temp_dir();
+
+    let file_name = path.file_name().ok_or_else(|| {
+        io::Error::new(
+            io::ErrorKind::InvalidInput,
+            format!("File path is invalid: {}", path.display()),
+        )
+    })?;
+
+    // generate a time deps unique number
+    let t = SystemTime::now().duration_since(std::time::UNIX_EPOCH).expect("Time went backwards");
+
+    let mut unique_name = OsString::from(t.as_millis().to_string());
+    unique_name.push(file_name);
+
+    to.push(unique_name);
+    std::fs::copy(path, &to).unwrap();
+    Ok(to)
+}
+
+#[cfg(unix)]
+fn ensure_file_with_lock_free_access(path: &Path) -> io::Result<PathBuf> {
+    Ok(path.to_path_buf())
+}
diff --git a/crates/ra_proc_macro_srv/src/lib.rs b/crates/ra_proc_macro_srv/src/lib.rs
index 3aca859db3..922bb84bbf 100644
--- a/crates/ra_proc_macro_srv/src/lib.rs
+++ b/crates/ra_proc_macro_srv/src/lib.rs
@@ -21,28 +21,46 @@ mod dylib;
 
 use proc_macro::bridge::client::TokenStream;
 use ra_proc_macro::{ExpansionResult, ExpansionTask, ListMacrosResult, ListMacrosTask};
-use std::path::Path;
+use std::{
+    collections::{hash_map::Entry, HashMap},
+    fs,
+    path::{Path, PathBuf},
+    time::SystemTime,
+};
 
-pub(crate) fn expand_task(task: &ExpansionTask) -> Result<ExpansionResult, String> {
-    let expander = create_expander(&task.lib);
+#[derive(Default)]
+pub(crate) struct ProcMacroSrv {
+    expanders: HashMap<(PathBuf, SystemTime), dylib::Expander>,
+}
 
-    match expander.expand(&task.macro_name, &task.macro_body, task.attributes.as_ref()) {
-        Ok(expansion) => Ok(ExpansionResult { expansion }),
-        Err(msg) => {
-            Err(format!("Cannot perform expansion for {}: error {:?}", &task.macro_name, msg))
+impl ProcMacroSrv {
+    pub fn expand(&mut self, task: &ExpansionTask) -> Result<ExpansionResult, String> {
+        let expander = self.expander(&task.lib)?;
+        match expander.expand(&task.macro_name, &task.macro_body, task.attributes.as_ref()) {
+            Ok(expansion) => Ok(ExpansionResult { expansion }),
+            Err(msg) => {
+                Err(format!("Cannot perform expansion for {}: error {:?}", &task.macro_name, msg))
+            }
         }
     }
-}
 
-pub(crate) fn list_macros(task: &ListMacrosTask) -> ListMacrosResult {
-    let expander = create_expander(&task.lib);
+    pub fn list_macros(&mut self, task: &ListMacrosTask) -> Result<ListMacrosResult, String> {
+        let expander = self.expander(&task.lib)?;
+        Ok(ListMacrosResult { macros: expander.list_macros() })
+    }
 
-    ListMacrosResult { macros: expander.list_macros() }
-}
+    fn expander(&mut self, path: &Path) -> Result<&dylib::Expander, String> {
+        let time = fs::metadata(path).and_then(|it| it.modified()).map_err(|err| {
+            format!("Failed to get file metadata for {}: {:?}", path.display(), err)
+        })?;
 
-fn create_expander(lib: &Path) -> dylib::Expander {
-    dylib::Expander::new(lib)
-        .unwrap_or_else(|err| panic!("Cannot create expander for {}: {:?}", lib.display(), err))
+        Ok(match self.expanders.entry((path.to_path_buf(), time)) {
+            Entry::Vacant(v) => v.insert(dylib::Expander::new(path).map_err(|err| {
+                format!("Cannot create expander for {}: {:?}", path.display(), err)
+            })?),
+            Entry::Occupied(e) => e.into_mut(),
+        })
+    }
 }
 
 pub mod cli;


Test patch:
diff --git a/crates/ra_proc_macro_srv/src/tests/utils.rs b/crates/ra_proc_macro_srv/src/tests/utils.rs
index 2139ec7a4d..646a427c56 100644
--- a/crates/ra_proc_macro_srv/src/tests/utils.rs
+++ b/crates/ra_proc_macro_srv/src/tests/utils.rs
@@ -1,7 +1,7 @@
 //! utils used in proc-macro tests
 
 use crate::dylib;
-use crate::list_macros;
+use crate::ProcMacroSrv;
 pub use difference::Changeset as __Changeset;
 use ra_proc_macro::ListMacrosTask;
 use std::str::FromStr;
@@ -59,7 +59,7 @@ pub fn assert_expand(
 pub fn list(crate_name: &str, version: &str) -> Vec<String> {
     let path = fixtures::dylib_path(crate_name, version);
     let task = ListMacrosTask { lib: path };
-
-    let res = list_macros(&task);
+    let mut srv = ProcMacroSrv::default();
+    let res = srv.list_macros(&task).unwrap();
     res.macros.into_iter().map(|(name, kind)| format!("{} [{:?}]", name, kind)).collect()
 }