diff --git a/Cargo.lock b/Cargo.lock index 29b93e19..cda4cb77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3475,7 +3475,7 @@ dependencies = [ [[package]] name = "jjuicy-cli" -version = "1.0.1" +version = "1.0.2" dependencies = [ "anyhow", "assert_matches", diff --git a/src/worker/gui_util.rs b/src/worker/gui_util.rs index 2d3099c0..6575e3bd 100644 --- a/src/worker/gui_util.rs +++ b/src/worker/gui_util.rs @@ -447,24 +447,26 @@ impl WorkspaceSession<'_> { return Ok(HashMap::new()); } - // for each bookmark, BFS from its commit toward ancestors, stopping at log commits. - // those boundary log commits are the nearest visible ancestors (fork points). - // cost: O(depth_to_log) per bookmark instead of O(repo_size) per bookmark. + const MAX_BFS_VISITS: usize = 100_000; let mut fork_map: HashMap> = HashMap::new(); - for (commit_id, labels) in &commits_to_labels { + let mut total_visits: usize = 0; + 'outer: for (commit_id, labels) in &commits_to_labels { let mut queue = VecDeque::new(); queue.push_back(commit_id.clone()); let mut visited: HashSet = HashSet::new(); visited.insert(commit_id.clone()); while let Some(current) = queue.pop_front() { + total_visits += 1; + if total_visits > MAX_BFS_VISITS { + break 'outer; + } let commit = self.get_commit(¤t)?; for parent_id in commit.parent_ids() { if !visited.insert(parent_id.clone()) { continue; } if log_contains(parent_id)? { - // nearest visible ancestor — record without walking further let entry = fork_map.entry(parent_id.clone()).or_default(); for label in labels { if !entry.contains(label) { diff --git a/src/worker/tests/mod.rs b/src/worker/tests/mod.rs index 285070d4..fb10298c 100644 --- a/src/worker/tests/mod.rs +++ b/src/worker/tests/mod.rs @@ -1,3 +1,4 @@ +mod perf; mod queries; mod session; diff --git a/src/worker/tests/perf.rs b/src/worker/tests/perf.rs new file mode 100644 index 00000000..8cf2c7f2 --- /dev/null +++ b/src/worker/tests/perf.rs @@ -0,0 +1,116 @@ +use std::path::PathBuf; +use std::sync::mpsc; +use std::time::{Duration, Instant}; + +use anyhow::Result; + +use crate::worker::{WorkerSession, queries}; + +const TIMEOUT_SECS: u64 = 30; + +struct RevsetBench { + name: &'static str, + revset: &'static str, +} + +const REVSETS: &[RevsetBench] = &[ + RevsetBench { + name: "all()", + revset: "all()", + }, + RevsetBench { + name: "::@", + revset: "::@", + }, + RevsetBench { + name: "default", + revset: r#"present(@) | ancestors(immutable_heads().., 2) | trunk()"#, + }, + RevsetBench { + name: "mine", + revset: "trunk() | (trunk()..(bookmarks() & mine())) | parents(bookmarks() & mine()) | @", + }, +]; + +/// Run with: BENCH_REPO=/path/to/repo cargo test query_bench -- --ignored --nocapture +#[tokio::test] +#[ignore] +async fn query_bench() -> Result<()> { + let repo_path = match std::env::var("BENCH_REPO") { + Ok(p) => PathBuf::from(p), + Err(_) => { + eprintln!("SKIP: set BENCH_REPO=/path/to/jj/repo to run this benchmark"); + return Ok(()); + } + }; + + if !repo_path.join(".jj").exists() { + eprintln!("SKIP: no .jj directory found at {}", repo_path.display()); + return Ok(()); + } + + eprintln!("repo: {}", repo_path.display()); + + let mut session = WorkerSession::default(); + + let t0 = Instant::now(); + let mut ws = session.load_workspace(&repo_path).await?; + eprintln!("load_workspace: {}ms", t0.elapsed().as_millis()); + + let t1 = Instant::now(); + ws.import_and_snapshot(false, true).await?; + eprintln!("import_and_snapshot: {}ms", t1.elapsed().as_millis()); + + eprintln!(); + eprintln!( + "{:<12} {:>10} {:>10} {:>10} {:>10} {:<6}", + "revset", "eval", "forks", "page", "total", "rows" + ); + eprintln!("{}", "-".repeat(70)); + + for bench in REVSETS { + let (_cancel, rx) = mpsc::channel::<()>(); + let bench_name = bench.name; + + std::thread::spawn(move || { + if let Err(mpsc::RecvTimeoutError::Timeout) = + rx.recv_timeout(Duration::from_secs(TIMEOUT_SECS)) + { + eprintln!("{bench_name:<12} TIMEOUT (>{TIMEOUT_SECS}s)"); + std::process::exit(1); + } + }); + + let t_total = Instant::now(); + + let t_eval = Instant::now(); + let revset_result = ws.evaluate_revset_str(bench.revset); + let eval_ms = t_eval.elapsed().as_millis(); + + let revset = match revset_result { + Ok(r) => r, + Err(e) => { + eprintln!("{:<12} ERROR: {e}", bench.name); + continue; + } + }; + + let t_forks = Instant::now(); + let hidden_forks = ws.compute_hidden_forks(bench.revset).unwrap_or_default(); + let forks_ms = t_forks.elapsed().as_millis(); + + let t_page = Instant::now(); + let state = queries::QueryState::new(50); + let mut qs = queries::QuerySession::new(&ws, &*revset, state, hidden_forks); + let page = qs.get_page()?; + let page_ms = t_page.elapsed().as_millis(); + + let total_ms = t_total.elapsed().as_millis(); + eprintln!( + "{:<12} {:>9}ms {:>9}ms {:>9}ms {:>9}ms {:<6}", + bench.name, eval_ms, forks_ms, page_ms, total_ms, page.rows.len() + ); + } + + Ok(()) +}