Add benchmark for project search

Piotr Osiewicz created

Change summary

Cargo.lock                            |  17 +++
Cargo.toml                            |   1 
crates/project_benchmarks/Cargo.toml  |  21 ++++
crates/project_benchmarks/LICENSE-GPL |   1 
crates/project_benchmarks/src/main.rs | 136 +++++++++++++++++++++++++++++
5 files changed, 176 insertions(+)

Detailed changes

Cargo.lock 🔗

@@ -12876,6 +12876,23 @@ dependencies = [
  "zlog",
 ]
 
+[[package]]
+name = "project_benchmarks"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "client",
+ "futures 0.3.31",
+ "gpui",
+ "http_client",
+ "language",
+ "node_runtime",
+ "project",
+ "settings",
+ "watch",
+]
+
 [[package]]
 name = "project_panel"
 version = "0.1.0"

Cargo.toml 🔗

@@ -126,6 +126,7 @@ members = [
     "crates/picker",
     "crates/prettier",
     "crates/project",
+    "crates/project_benchmarks",
     "crates/project_panel",
     "crates/project_symbols",
     "crates/prompt_store",

crates/project_benchmarks/Cargo.toml 🔗

@@ -0,0 +1,21 @@
+[package]
+name = "project_benchmarks"
+version = "0.1.0"
+publish.workspace = true
+edition.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+clap.workspace = true
+client.workspace = true
+futures.workspace = true
+gpui = { workspace = true, features = ["windows-manifest"] }
+http_client = { workspace = true, features = ["test-support"]}
+language.workspace = true
+node_runtime.workspace = true
+project.workspace = true
+settings.workspace = true
+watch.workspace = true
+
+[lints]
+workspace = true

crates/project_benchmarks/src/main.rs 🔗

@@ -0,0 +1,136 @@
+use std::sync::Arc;
+
+use clap::Parser;
+use client::{Client, UserStore};
+use gpui::{AppContext as _, Application};
+use http_client::FakeHttpClient;
+use language::LanguageRegistry;
+use node_runtime::NodeRuntime;
+use project::{
+    Project, RealFs,
+    search::{SearchQuery, SearchResult},
+};
+
+#[derive(Parser)]
+struct Args {
+    /// List of worktrees to run the search against.
+    worktrees: Vec<String>,
+    #[clap(short)]
+    query: String,
+    /// Treat query as a regex.
+    #[clap(short, long)]
+    regex: bool,
+    /// Matches have to be standalone words.
+    #[clap(long)]
+    whole_word: bool,
+    /// Make matching case-sensitive.
+    #[clap(long, default_value_t = true)]
+    case_sensitive: bool,
+    /// Include gitignored files in the search.
+    #[clap(long)]
+    include_ignored: bool,
+}
+
+fn main() -> Result<(), anyhow::Error> {
+    let args = Args::parse();
+    let query = if args.regex {
+        SearchQuery::regex(
+            args.query,
+            args.whole_word,
+            args.case_sensitive,
+            args.include_ignored,
+            false,
+            Default::default(),
+            Default::default(),
+            false,
+            None,
+        )
+    } else {
+        SearchQuery::text(
+            args.query,
+            args.whole_word,
+            args.case_sensitive,
+            args.include_ignored,
+            Default::default(),
+            Default::default(),
+            false,
+            None,
+        )
+    }?;
+    Application::headless().run(|cx| {
+        settings::init(cx);
+        client::init_settings(cx);
+        language::init(cx);
+        Project::init_settings(cx);
+        let client = Client::production(cx);
+        let http_client = FakeHttpClient::with_200_response();
+        let (_, rx) = watch::channel(None);
+        let node = NodeRuntime::new(http_client, None, rx);
+        let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
+        let registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
+        let fs = Arc::new(RealFs::new(None, cx.background_executor().clone()));
+        let project = Project::local(
+            client,
+            node,
+            user_store,
+            registry,
+            fs,
+            Some(Default::default()),
+            cx,
+        );
+
+        project.clone().update(cx, move |_, cx| {
+            cx.spawn(async move |_, cx| {
+                println!("Loading worktrees");
+                let worktrees = project.update(cx, |this, cx| {
+                    args.worktrees
+                        .into_iter()
+                        .map(|worktree| this.find_or_create_worktree(worktree, true, cx))
+                        .collect::<Vec<_>>()
+                })?;
+
+                let worktrees = futures::future::join_all(worktrees)
+                    .await
+                    .into_iter()
+                    .collect::<Result<Vec<_>, anyhow::Error>>()?;
+
+                for (worktree, _) in &worktrees {
+                    worktree
+                        .update(cx, |this, _| this.as_local().unwrap().scan_complete())?
+                        .await;
+                }
+                println!("Worktrees loaded");
+
+                println!("Starting a project search");
+                let timer = std::time::Instant::now();
+                let mut first_match = None;
+                let matches = project
+                    .update(cx, |this, cx| this.search(query, cx))
+                    .unwrap();
+                let mut matched_files = 0;
+                let mut matched_chunks = 0;
+                while let Ok(match_result) = matches.recv().await {
+                    if first_match.is_none() {
+                        let time = timer.elapsed();
+                        first_match = Some(time);
+                        println!("First match found after {time:?}");
+                    }
+                    if let SearchResult::Buffer { ranges, .. } = match_result {
+                        matched_files += 1;
+                        matched_chunks += ranges.len();
+                    }
+                }
+                let elapsed = timer.elapsed();
+                println!(
+                    "Finished project search after {elapsed:?}. Matched {matched_files} files and {matched_chunks} excerpts"
+                );
+                drop(project);
+                cx.update(|cx| cx.quit())?;
+
+                anyhow::Ok(())
+            })
+            .detach();
+        });
+    });
+    Ok(())
+}