diff --git a/crates/ra_batch/src/lib.rs b/crates/ra_batch/src/lib.rs
index fa244e86c38..43d3fb7e3af 100644
--- a/crates/ra_batch/src/lib.rs
+++ b/crates/ra_batch/src/lib.rs
@@ -8,7 +8,7 @@ use ra_db::{
     CrateGraph, FileId, SourceRootId,
 };
 use ra_ide_api::{AnalysisHost, AnalysisChange};
-use ra_project_model::ProjectWorkspace;
+use ra_project_model::{ProjectWorkspace, ProjectRoot};
 use ra_vfs::{Vfs, VfsChange};
 use vfs_filter::IncludeRustFiles;
 
@@ -21,13 +21,11 @@ fn vfs_root_to_id(r: ra_vfs::VfsRoot) -> SourceRootId {
     SourceRootId(r.0)
 }
 
-pub fn load_cargo(root: &Path) -> Result<(AnalysisHost, Vec<SourceRootId>)> {
+pub fn load_cargo(root: &Path) -> Result<(AnalysisHost, FxHashMap<SourceRootId, ProjectRoot>)> {
     let root = std::env::current_dir()?.join(root);
     let ws = ProjectWorkspace::discover(root.as_ref())?;
-    let mut roots = Vec::new();
-    roots.push(IncludeRustFiles::member(root.clone()));
-    roots.extend(IncludeRustFiles::from_roots(ws.to_roots()));
-    let (mut vfs, roots) = Vfs::new(roots);
+    let project_roots = ws.to_roots();
+    let (mut vfs, roots) = Vfs::new(IncludeRustFiles::from_roots(project_roots.clone()).collect());
     let crate_graph = ws.to_crate_graph(&mut |path: &Path| {
         let vfs_file = vfs.load(path);
         log::debug!("vfs file {:?} -> {:?}", path, vfs_file);
@@ -35,17 +33,27 @@ pub fn load_cargo(root: &Path) -> Result<(AnalysisHost, Vec<SourceRootId>)> {
     });
     log::debug!("crate graph: {:?}", crate_graph);
 
-    let local_roots = roots
-        .into_iter()
-        .filter(|r| vfs.root2path(*r).starts_with(&root))
-        .map(vfs_root_to_id)
-        .collect();
-
-    let host = load(root.as_path(), crate_graph, &mut vfs);
-    Ok((host, local_roots))
+    let source_roots = roots
+        .iter()
+        .map(|&vfs_root| {
+            let source_root_id = vfs_root_to_id(vfs_root);
+            let project_root = project_roots
+                .iter()
+                .find(|it| it.path() == &vfs.root2path(vfs_root))
+                .unwrap()
+                .clone();
+            (source_root_id, project_root)
+        })
+        .collect::<FxHashMap<_, _>>();
+    let host = load(&source_roots, crate_graph, &mut vfs);
+    Ok((host, source_roots))
 }
 
-pub fn load(project_root: &Path, crate_graph: CrateGraph, vfs: &mut Vfs) -> AnalysisHost {
+pub fn load(
+    source_roots: &FxHashMap<SourceRootId, ProjectRoot>,
+    crate_graph: CrateGraph,
+    vfs: &mut Vfs,
+) -> AnalysisHost {
     let lru_cap = std::env::var("RA_LRU_CAP").ok().and_then(|it| it.parse::<usize>().ok());
     let mut host = AnalysisHost::new(lru_cap);
     let mut analysis_change = AnalysisChange::new();
@@ -60,8 +68,8 @@ pub fn load(project_root: &Path, crate_graph: CrateGraph, vfs: &mut Vfs) -> Anal
         for change in vfs.commit_changes() {
             match change {
                 VfsChange::AddRoot { root, files } => {
-                    let is_local = vfs.root2path(root).starts_with(&project_root);
                     let source_root_id = vfs_root_to_id(root);
+                    let is_local = source_roots[&source_root_id].is_member();
                     log::debug!(
                         "loaded source root {:?} with path {:?}",
                         source_root_id,
@@ -106,7 +114,7 @@ mod tests {
         let path = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().parent().unwrap();
         let (host, roots) = load_cargo(path).unwrap();
         let mut n_crates = 0;
-        for root in roots {
+        for (root, _) in roots {
             for _krate in Crate::source_root_crates(host.raw_database(), root) {
                 n_crates += 1;
             }
diff --git a/crates/ra_batch/src/vfs_filter.rs b/crates/ra_batch/src/vfs_filter.rs
index dd20c12031f..8552ac999ca 100644
--- a/crates/ra_batch/src/vfs_filter.rs
+++ b/crates/ra_batch/src/vfs_filter.rs
@@ -1,54 +1,54 @@
-use std::path::PathBuf;
-use ra_project_model::ProjectRoot;
-use ra_vfs::{RootEntry, Filter, RelativePath};
-
-/// `IncludeRustFiles` is used to convert
-/// from `ProjectRoot` to `RootEntry` for VFS
-pub struct IncludeRustFiles {
-    root: ProjectRoot,
-}
-
-impl IncludeRustFiles {
-    pub fn from_roots<R>(roots: R) -> impl Iterator<Item = RootEntry>
-    where
-        R: IntoIterator<Item = ProjectRoot>,
-    {
-        roots.into_iter().map(IncludeRustFiles::from_root)
-    }
-
-    pub fn from_root(root: ProjectRoot) -> RootEntry {
-        IncludeRustFiles::from(root).into()
-    }
-
-    #[allow(unused)]
-    pub fn external(path: PathBuf) -> RootEntry {
-        IncludeRustFiles::from_root(ProjectRoot::new(path, false))
-    }
-
-    pub fn member(path: PathBuf) -> RootEntry {
-        IncludeRustFiles::from_root(ProjectRoot::new(path, true))
-    }
-}
-
-impl Filter for IncludeRustFiles {
-    fn include_dir(&self, dir_path: &RelativePath) -> bool {
-        self.root.include_dir(dir_path)
-    }
-
-    fn include_file(&self, file_path: &RelativePath) -> bool {
-        self.root.include_file(file_path)
-    }
-}
-
-impl std::convert::From<ProjectRoot> for IncludeRustFiles {
-    fn from(v: ProjectRoot) -> IncludeRustFiles {
-        IncludeRustFiles { root: v }
-    }
-}
-
-impl std::convert::From<IncludeRustFiles> for RootEntry {
-    fn from(v: IncludeRustFiles) -> RootEntry {
-        let path = v.root.path().clone();
-        RootEntry::new(path, Box::new(v))
-    }
-}
+use std::path::PathBuf;
+use ra_project_model::ProjectRoot;
+use ra_vfs::{RootEntry, Filter, RelativePath};
+
+/// `IncludeRustFiles` is used to convert
+/// from `ProjectRoot` to `RootEntry` for VFS
+pub struct IncludeRustFiles {
+    root: ProjectRoot,
+}
+
+impl IncludeRustFiles {
+    pub fn from_roots<R>(roots: R) -> impl Iterator<Item = RootEntry>
+    where
+        R: IntoIterator<Item = ProjectRoot>,
+    {
+        roots.into_iter().map(IncludeRustFiles::from_root)
+    }
+
+    pub fn from_root(root: ProjectRoot) -> RootEntry {
+        IncludeRustFiles::from(root).into()
+    }
+
+    #[allow(unused)]
+    pub fn external(path: PathBuf) -> RootEntry {
+        IncludeRustFiles::from_root(ProjectRoot::new(path, false))
+    }
+
+    pub fn member(path: PathBuf) -> RootEntry {
+        IncludeRustFiles::from_root(ProjectRoot::new(path, true))
+    }
+}
+
+impl Filter for IncludeRustFiles {
+    fn include_dir(&self, dir_path: &RelativePath) -> bool {
+        self.root.include_dir(dir_path)
+    }
+
+    fn include_file(&self, file_path: &RelativePath) -> bool {
+        self.root.include_file(file_path)
+    }
+}
+
+impl From<ProjectRoot> for IncludeRustFiles {
+    fn from(v: ProjectRoot) -> IncludeRustFiles {
+        IncludeRustFiles { root: v }
+    }
+}
+
+impl From<IncludeRustFiles> for RootEntry {
+    fn from(v: IncludeRustFiles) -> RootEntry {
+        let path = v.root.path().clone();
+        RootEntry::new(path, Box::new(v))
+    }
+}
diff --git a/crates/ra_cli/src/analysis_bench.rs b/crates/ra_cli/src/analysis_bench.rs
new file mode 100644
index 00000000000..33d47283877
--- /dev/null
+++ b/crates/ra_cli/src/analysis_bench.rs
@@ -0,0 +1,92 @@
+use std::{
+    path::{PathBuf, Path},
+    time::Instant,
+};
+
+use ra_db::{SourceDatabase, salsa::Database};
+use ra_ide_api::{AnalysisHost, Analysis, LineCol, FilePosition};
+
+use crate::Result;
+
+pub(crate) enum Op {
+    Highlight { path: PathBuf },
+    Complete { path: PathBuf, line: u32, column: u32 },
+}
+
+pub(crate) fn run(verbose: bool, path: &Path, op: Op) -> Result<()> {
+    let start = Instant::now();
+    eprint!("loading: ");
+    let (host, roots) = ra_batch::load_cargo(path)?;
+    let db = host.raw_database();
+    eprintln!("{:?}\n", start.elapsed());
+
+    let file_id = {
+        let path = match &op {
+            Op::Highlight { path } => path,
+            Op::Complete { path, .. } => path,
+        };
+        let path = std::env::current_dir()?.join(path).canonicalize()?;
+        roots
+            .iter()
+            .find_map(|(source_root_id, project_root)| {
+                if project_root.is_member() {
+                    for (rel_path, file_id) in &db.source_root(*source_root_id).files {
+                        let abs_path = rel_path.to_path(project_root.path());
+                        if abs_path == path {
+                            return Some(*file_id);
+                        }
+                    }
+                }
+                None
+            })
+            .ok_or_else(|| format!("Can't find {:?}", path))?
+    };
+
+    match op {
+        Op::Highlight { .. } => {
+            let res = do_work(&host, |analysis| {
+                analysis.diagnostics(file_id).unwrap();
+                analysis.highlight_as_html(file_id, false).unwrap()
+            });
+            if verbose {
+                println!("\n{}", res);
+            }
+        }
+        Op::Complete { line, column, .. } => {
+            let offset = host
+                .analysis()
+                .file_line_index(file_id)
+                .offset(LineCol { line, col_utf16: column });
+            let file_postion = FilePosition { file_id, offset };
+
+            let res = do_work(&host, |analysis| analysis.completions(file_postion));
+            if verbose {
+                println!("\n{:#?}", res);
+            }
+        }
+    }
+    Ok(())
+}
+
+fn do_work<F: Fn(&Analysis) -> T, T>(host: &AnalysisHost, work: F) -> T {
+    {
+        let start = Instant::now();
+        eprint!("from scratch:   ");
+        work(&host.analysis());
+        eprintln!("{:?}", start.elapsed());
+    }
+    {
+        let start = Instant::now();
+        eprint!("no change:      ");
+        work(&host.analysis());
+        eprintln!("{:?}", start.elapsed());
+    }
+    {
+        let start = Instant::now();
+        eprint!("trivial change: ");
+        host.raw_database().salsa_runtime().next_revision();
+        let res = work(&host.analysis());
+        eprintln!("{:?}", start.elapsed());
+        res
+    }
+}
diff --git a/crates/ra_cli/src/analysis_stats.rs b/crates/ra_cli/src/analysis_stats.rs
index d76c37d847a..ed98fc7f655 100644
--- a/crates/ra_cli/src/analysis_stats.rs
+++ b/crates/ra_cli/src/analysis_stats.rs
@@ -1,4 +1,4 @@
-use std::{collections::HashSet, time::Instant, fmt::Write};
+use std::{collections::HashSet, time::Instant, fmt::Write, path::Path};
 
 use ra_db::SourceDatabase;
 use ra_hir::{Crate, ModuleDef, Ty, ImplItem, HasSource};
@@ -6,20 +6,23 @@ use ra_syntax::AstNode;
 
 use crate::Result;
 
-pub fn run(verbose: bool, path: &str, only: Option<&str>) -> Result<()> {
+pub fn run(verbose: bool, path: &Path, only: Option<&str>) -> Result<()> {
     let db_load_time = Instant::now();
-    let (host, roots) = ra_batch::load_cargo(path.as_ref())?;
+    let (host, roots) = ra_batch::load_cargo(path)?;
     let db = host.raw_database();
     println!("Database loaded, {} roots, {:?}", roots.len(), db_load_time.elapsed());
     let analysis_time = Instant::now();
     let mut num_crates = 0;
     let mut visited_modules = HashSet::new();
     let mut visit_queue = Vec::new();
-    for root in roots {
-        for krate in Crate::source_root_crates(db, root) {
-            num_crates += 1;
-            let module = krate.root_module(db).expect("crate in source root without root module");
-            visit_queue.push(module);
+    for (source_root_id, project_root) in roots {
+        if project_root.is_member() {
+            for krate in Crate::source_root_crates(db, source_root_id) {
+                num_crates += 1;
+                let module =
+                    krate.root_module(db).expect("crate in source root without root module");
+                visit_queue.push(module);
+            }
         }
     }
     println!("Crates in this dir: {}", num_crates);
diff --git a/crates/ra_cli/src/main.rs b/crates/ra_cli/src/main.rs
index 1db98aec130..5adf8b09676 100644
--- a/crates/ra_cli/src/main.rs
+++ b/crates/ra_cli/src/main.rs
@@ -1,4 +1,5 @@
 mod analysis_stats;
+mod analysis_bench;
 
 use std::{io::Read, error::Error};
 
@@ -26,6 +27,27 @@ fn main() -> Result<()> {
                 .arg(Arg::with_name("only").short("o").takes_value(true))
                 .arg(Arg::with_name("path")),
         )
+        .subcommand(
+            SubCommand::with_name("analysis-bench")
+                .arg(Arg::with_name("verbose").short("v").long("verbose"))
+                .arg(
+                    Arg::with_name("highlight")
+                        .long("highlight")
+                        .takes_value(true)
+                        .conflicts_with("complete")
+                        .value_name("PATH")
+                        .help("highlight this file"),
+                )
+                .arg(
+                    Arg::with_name("complete")
+                        .long("complete")
+                        .takes_value(true)
+                        .conflicts_with("highlight")
+                        .value_name("PATH:LINE:COLUMN")
+                        .help("compute completions at this location"),
+                )
+                .arg(Arg::with_name("path").value_name("PATH").help("project to analyze")),
+        )
         .get_matches();
     match matches.subcommand() {
         ("parse", Some(matches)) => {
@@ -51,7 +73,25 @@ fn main() -> Result<()> {
             let verbose = matches.is_present("verbose");
             let path = matches.value_of("path").unwrap_or("");
             let only = matches.value_of("only");
-            analysis_stats::run(verbose, path, only)?;
+            analysis_stats::run(verbose, path.as_ref(), only)?;
+        }
+        ("analysis-bench", Some(matches)) => {
+            let verbose = matches.is_present("verbose");
+            let path = matches.value_of("path").unwrap_or("");
+            let op = if let Some(path) = matches.value_of("highlight") {
+                analysis_bench::Op::Highlight { path: path.into() }
+            } else if let Some(path_line_col) = matches.value_of("complete") {
+                let (path_line, column) = rsplit_at_char(path_line_col, ':')?;
+                let (path, line) = rsplit_at_char(path_line, ':')?;
+                analysis_bench::Op::Complete {
+                    path: path.into(),
+                    line: line.parse()?,
+                    column: column.parse()?,
+                }
+            } else {
+                panic!("either --highlight or --complete must be set")
+            };
+            analysis_bench::run(verbose, path.as_ref(), op)?;
         }
         _ => unreachable!(),
     }
@@ -68,3 +108,8 @@ fn read_stdin() -> Result<String> {
     std::io::stdin().read_to_string(&mut buff)?;
     Ok(buff)
 }
+
+fn rsplit_at_char(s: &str, c: char) -> Result<(&str, &str)> {
+    let idx = s.rfind(":").ok_or_else(|| format!("no `{}` in {}", c, s))?;
+    Ok((&s[..idx], &s[idx + 1..]))
+}
diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs
index a68c5e2a588..e61d5627e00 100644
--- a/crates/ra_ide_api/src/lib.rs
+++ b/crates/ra_ide_api/src/lib.rs
@@ -276,7 +276,7 @@ impl AnalysisHost {
     pub fn collect_garbage(&mut self) {
         self.db.collect_garbage();
     }
-    pub fn raw_database(&self) -> &impl hir::db::HirDatabase {
+    pub fn raw_database(&self) -> &(impl hir::db::HirDatabase + salsa::Database) {
         &self.db
     }
 }
diff --git a/crates/ra_ide_api/src/line_index.rs b/crates/ra_ide_api/src/line_index.rs
index 087dfafed82..a53cf9ee006 100644
--- a/crates/ra_ide_api/src/line_index.rs
+++ b/crates/ra_ide_api/src/line_index.rs
@@ -10,7 +10,9 @@ pub struct LineIndex {
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 pub struct LineCol {
+    /// Zero-based
     pub line: u32,
+    /// Zero-based
     pub col_utf16: u32,
 }
 
diff --git a/crates/ra_project_model/src/lib.rs b/crates/ra_project_model/src/lib.rs
index a3af153f1aa..42156bea6fe 100644
--- a/crates/ra_project_model/src/lib.rs
+++ b/crates/ra_project_model/src/lib.rs
@@ -37,6 +37,7 @@ pub enum ProjectWorkspace {
 /// `ProjectRoot` describes a workspace root folder.
 /// Which may be an external dependency, or a member of
 /// the current workspace.
+#[derive(Clone)]
 pub struct ProjectRoot {
     /// Path to the root folder
     path: PathBuf,
diff --git a/docs/dev/README.md b/docs/dev/README.md
index d34ff96c826..3dc37e86eba 100644
--- a/docs/dev/README.md
+++ b/docs/dev/README.md
@@ -147,3 +147,16 @@ RA_PROFILE=*@3>10        // dump everything, up to depth 3, if it takes more tha
 ```
 
 In particular, I have `export RA_PROFILE='*>10' in my shell profile.
+
+To measure time for from-scratch analysis, use something like this:
+
+```
+$ cargo run --release -p ra_cli -- analysis-stats ../chalk/
+```
+
+For measuring time of incremental analysis, use either of these:
+
+```
+$ cargo run --release -p ra_cli -- analysis-bench ../chalk/ --highlight ../chalk/chalk-engine/src/logic.rs
+$ cargo run --release -p ra_cli -- analysis-bench ../chalk/ --complete ../chalk/chalk-engine/src/logic.rs:94:0
+```