-
Notifications
You must be signed in to change notification settings - Fork 17
Fix dependency tracking and show shortest path dependency cycle #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
643d068
cf62715
4435955
fddd5e4
1aa10f9
20daf8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -330,7 +330,7 @@ pub fn incremental_build( | |
pb.finish(); | ||
} | ||
|
||
log::error!("Could not parse source files: {}", &err); | ||
println!("Could not parse source files: {}", &err); | ||
return Err(IncrementalBuildError::SourceFileParseError); | ||
} | ||
} | ||
|
@@ -405,7 +405,7 @@ pub fn incremental_build( | |
pb.finish(); | ||
if !compile_errors.is_empty() { | ||
if helpers::contains_ascii_characters(&compile_warnings) { | ||
log::error!("{}", &compile_warnings); | ||
println!("{}", &compile_warnings); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same |
||
} | ||
if show_progress { | ||
println!( | ||
|
@@ -417,7 +417,7 @@ pub fn incremental_build( | |
default_timing.unwrap_or(compile_duration).as_secs_f64() | ||
); | ||
} | ||
log::error!("{}", &compile_errors); | ||
println!("{}", &compile_errors); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same |
||
// mark the original files as dirty again, because we didn't complete a full build | ||
for (module_name, module) in build_state.modules.iter_mut() { | ||
if tracked_dirty_modules.contains(module_name) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,71 +1,153 @@ | ||
use super::super::build_types::*; | ||
use crate::helpers; | ||
use ahash::AHashSet; | ||
use std::collections::{HashMap, HashSet, VecDeque}; | ||
|
||
pub fn find(modules: &Vec<(&String, &Module)>) -> Vec<String> { | ||
let mut visited: AHashSet<String> = AHashSet::new(); | ||
let mut stack: Vec<String> = vec![]; | ||
find_shortest_cycle(modules) | ||
} | ||
|
||
// we want to sort the module names so that we always return the same | ||
// dependency cycle (there can be more than one) | ||
let mut module_names = modules | ||
.iter() | ||
.map(|(name, _)| name.to_string()) | ||
.collect::<Vec<String>>(); | ||
fn find_shortest_cycle(modules: &Vec<(&String, &Module)>) -> Vec<String> { | ||
let mut shortest_cycle: Vec<String> = Vec::new(); | ||
|
||
// Build a graph representation for easier traversal | ||
|
||
module_names.sort(); | ||
for module_name in module_names { | ||
if find_dependency_cycle_helper(&module_name, modules, &mut visited, &mut stack) { | ||
return stack; | ||
let mut graph: HashMap<&String, &AHashSet<String>> = HashMap::new(); | ||
let mut in_degrees: HashMap<&String, usize> = HashMap::new(); | ||
|
||
let empty = AHashSet::new(); | ||
// First pass: collect all nodes and initialize in-degrees | ||
for (name, _) in modules { | ||
graph.insert(name, &empty); | ||
in_degrees.insert(name, 0); | ||
} | ||
|
||
// Second pass: build the graph and count in-degrees | ||
for (name, module) in modules { | ||
// Update in-degrees | ||
for dep in module.deps.iter() { | ||
if let Some(count) = in_degrees.get_mut(dep) { | ||
*count += 1; | ||
} | ||
} | ||
visited.clear(); | ||
stack.clear(); | ||
|
||
// Update the graph | ||
*graph.get_mut(*name).unwrap() = &module.deps; | ||
} | ||
stack | ||
} | ||
// Remove all nodes in the graph that have no incoming edges | ||
graph.retain(|_, deps| !deps.is_empty()); | ||
|
||
fn find_dependency_cycle_helper( | ||
module_name: &String, | ||
modules: &Vec<(&String, &Module)>, | ||
visited: &mut AHashSet<String>, | ||
stack: &mut Vec<String>, | ||
) -> bool { | ||
if let Some(module) = modules | ||
.iter() | ||
.find(|(name, _)| *name == module_name) | ||
.map(|(_, module)| module) | ||
{ | ||
visited.insert(module_name.to_string()); | ||
// if the module is a mlmap (namespace), we don't want to show this in the path | ||
// because the namespace is not a module the user created, so only add source files | ||
// to the stack | ||
if let SourceType::SourceFile(_) = module.source_type { | ||
stack.push(module_name.to_string()) | ||
// OPTIMIZATION 1: Start with nodes that are more likely to be in cycles | ||
// Sort nodes by their connectivity (in-degree + out-degree) | ||
let mut start_nodes: Vec<&String> = graph.keys().cloned().collect(); | ||
start_nodes.sort_by(|a, b| { | ||
let a_connectivity = in_degrees.get(a).unwrap_or(&0) + graph.get(a).map_or(0, |v| v.len()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If |
||
let b_connectivity = in_degrees.get(b).unwrap_or(&0) + graph.get(b).map_or(0, |v| v.len()); | ||
b_connectivity.cmp(&a_connectivity) // Sort in descending order | ||
}); | ||
|
||
// OPTIMIZATION 2: Keep track of the current shortest cycle length for early termination | ||
let mut current_shortest_length = usize::MAX; | ||
|
||
// OPTIMIZATION 3: Cache nodes that have been checked and don't have cycles | ||
let mut no_cycle_cache: HashSet<String> = HashSet::new(); | ||
|
||
// Try BFS from each node to find the shortest cycle | ||
for start_node in start_nodes { | ||
// Skip nodes that we know don't have cycles | ||
if no_cycle_cache.contains(start_node) { | ||
continue; | ||
} | ||
for dep in &module.deps { | ||
if !visited.contains(dep) { | ||
if find_dependency_cycle_helper(dep, modules, visited, stack) { | ||
return true; | ||
|
||
// Skip nodes with no incoming edges | ||
if in_degrees.get(&start_node).map_or(true, |&d| d == 0) { | ||
no_cycle_cache.insert(start_node.clone()); | ||
continue; | ||
} | ||
|
||
if let Some(cycle) = find_cycle_bfs(&start_node, &graph, current_shortest_length) { | ||
if shortest_cycle.is_empty() || cycle.len() < shortest_cycle.len() { | ||
shortest_cycle = cycle.clone(); | ||
current_shortest_length = cycle.len(); | ||
|
||
// OPTIMIZATION 4: If we find a very short cycle (length <= 3), we can stop early | ||
// as it's unlikely to find a shorter one | ||
if cycle.len() <= 3 { | ||
break; | ||
} | ||
} else if stack.contains(dep) { | ||
stack.push(dep.to_string()); | ||
return true; | ||
} | ||
} else { | ||
// Cache this node as not having a cycle | ||
no_cycle_cache.insert(start_node.to_string()); | ||
} | ||
// because we only pushed source files to the stack, we also only need to | ||
// pop these from the stack if we don't find a dependency cycle | ||
if let SourceType::SourceFile(_) = module.source_type { | ||
let _ = stack.pop(); | ||
} | ||
|
||
shortest_cycle | ||
} | ||
|
||
fn find_cycle_bfs( | ||
start: &String, | ||
graph: &HashMap<&String, &AHashSet<String>>, | ||
max_length: usize, | ||
) -> Option<Vec<String>> { | ||
// Use a BFS to find the shortest cycle | ||
let mut queue = VecDeque::new(); | ||
// Store node -> (distance, parent) | ||
let mut visited: HashMap<String, (usize, Option<String>)> = HashMap::new(); | ||
|
||
// Initialize with start node | ||
visited.insert(start.clone(), (0, None)); | ||
queue.push_back(start.clone()); | ||
|
||
while let Some(current) = queue.pop_front() { | ||
let (dist, _) = *visited.get(¤t).unwrap(); | ||
|
||
// OPTIMIZATION: Early termination if we've gone too far | ||
// If we're already at max_length, we won't find a shorter cycle from here | ||
if dist >= max_length - 1 { | ||
continue; | ||
} | ||
|
||
// Check all neighbors | ||
if let Some(neighbors) = graph.get(¤t) { | ||
for neighbor in neighbors.iter() { | ||
// If we found the start node again, we have a cycle | ||
if neighbor == start { | ||
// Reconstruct the cycle | ||
let mut path = Vec::new(); | ||
path.push(start.clone()); | ||
|
||
// Backtrack from current to start using parent pointers | ||
let mut curr = current.clone(); | ||
while curr != *start { | ||
path.push(curr.clone()); | ||
curr = visited.get(&curr).unwrap().1.clone().unwrap(); | ||
} | ||
|
||
return Some(path); | ||
} | ||
|
||
// If not visited, add to queue | ||
if !visited.contains_key(neighbor) { | ||
visited.insert(neighbor.clone(), (dist + 1, Some(current.clone()))); | ||
queue.push_back(neighbor.clone()); | ||
} | ||
} | ||
} | ||
return false; | ||
} | ||
false | ||
|
||
None | ||
} | ||
|
||
pub fn format(cycle: &[String]) -> String { | ||
let mut cycle = cycle.to_vec(); | ||
cycle.reverse(); | ||
// add the first module to the end of the cycle | ||
cycle.push(cycle[0].clone()); | ||
|
||
cycle | ||
.iter() | ||
.map(|s| helpers::format_namespaced_module_name(s)) | ||
.collect::<Vec<String>>() | ||
.join(" -> ") | ||
.join("\n → ") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -282,7 +282,7 @@ pub fn start( | |
) | ||
.await | ||
{ | ||
log::error!("{:?}", e) | ||
println!("{:?}", e) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps
log::warn!
orlog::info!
here instead?println!
cannot be supressed by log levelsThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The log::error etc are prepended with
ERROR:
it's a bit ugly if it's expected output of the application