From 71382d1b722fd2388262b6ddc9c7c4e63924ec8c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 18:10:29 +0000 Subject: [PATCH] perf(flow): defer PathBuf allocations during graph traversal Refactors `ensure_node` in `graph.rs` and `tarjan_dfs` in `invalidation.rs` to eliminate redundant O(E) heap allocations for path lookups. Map queries now use borrowed `&Path` references, deferring `to_path_buf()` calls until strictly needed for insertion. Co-authored-by: bashandbone <89049923+bashandbone@users.noreply.github.com> --- .jules/bolt.md | 4 ++++ crates/flow/src/incremental/graph.rs | 7 ++++--- crates/flow/src/incremental/invalidation.rs | 17 +++++++++-------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index fb3e8f1..ad764af 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -2,3 +2,7 @@ ## 2026-04-08 - [Performance: Defer Allocation during Traversal] **Learning:** During DAG traversals, creating owned variants of identifiers (like `file.to_path_buf()`) *before* checking `visited` HashSets results in heap allocations (O(E)) for every edge instead of every visited node (O(V)). By moving the `&PathBuf` allocation strictly *after* all HashSet `contains` checks using the borrowed reference (`&Path`), we drastically reduce memory churn. **Action:** Always check `HashSet::contains` with a borrowed reference *before* creating the owned version required by `HashSet::insert`, especially in performance-critical graph traversal paths. + +## 2024-05-24 - Defer PathBuf allocations during graph traversal +**Learning:** Repeated `.to_path_buf()` calls in tight loops for map lookups (e.g. `entry()` or `get()`) create redundant `O(E)` memory allocations, becoming a critical bottleneck. +**Action:** Use borrowed `&Path` references for `RapidMap` lookups (`contains_key`, `get`, `get_mut`) and defer `PathBuf` heap allocation until strictly needed for initial insertion. diff --git a/crates/flow/src/incremental/graph.rs b/crates/flow/src/incremental/graph.rs index f0b07a9..9f0a55a 100644 --- a/crates/flow/src/incremental/graph.rs +++ b/crates/flow/src/incremental/graph.rs @@ -400,9 +400,10 @@ impl DependencyGraph { /// Ensures a node exists in the graph for the given file path. /// Creates a default fingerprint entry if the node does not exist. fn ensure_node(&mut self, file: &Path) { - self.nodes - .entry(file.to_path_buf()) - .or_insert_with(|| AnalysisDefFingerprint::new(b"")); + if !self.nodes.contains_key(file) { + self.nodes + .insert(file.to_path_buf(), AnalysisDefFingerprint::new(b"")); + } } /// DFS visit for topological sort with cycle detection. diff --git a/crates/flow/src/incremental/invalidation.rs b/crates/flow/src/incremental/invalidation.rs index da9ac9c..821d6f9 100644 --- a/crates/flow/src/incremental/invalidation.rs +++ b/crates/flow/src/incremental/invalidation.rs @@ -342,11 +342,12 @@ impl InvalidationDetector { fn tarjan_dfs(&self, v: &Path, state: &mut TarjanState, sccs: &mut Vec>) { // Initialize node let index = state.index_counter; - state.indices.insert(v.to_path_buf(), index); - state.lowlinks.insert(v.to_path_buf(), index); + let v_buf = v.to_path_buf(); + state.indices.insert(v_buf.clone(), index); + state.lowlinks.insert(v_buf.clone(), index); state.index_counter += 1; - state.stack.push(v.to_path_buf()); - state.on_stack.insert(v.to_path_buf()); + state.stack.push(v_buf.clone()); + state.on_stack.insert(v_buf); // Visit all successors (dependencies) let dependencies = self.graph.get_dependencies(v); @@ -358,19 +359,19 @@ impl InvalidationDetector { // Update lowlink let w_lowlink = *state.lowlinks.get(dep).unwrap(); - let v_lowlink = state.lowlinks.get_mut(&v.to_path_buf()).unwrap(); + let v_lowlink = state.lowlinks.get_mut(v).unwrap(); *v_lowlink = (*v_lowlink).min(w_lowlink); } else if state.on_stack.contains(dep) { // Successor is on stack (part of current SCC) let w_index = *state.indices.get(dep).unwrap(); - let v_lowlink = state.lowlinks.get_mut(&v.to_path_buf()).unwrap(); + let v_lowlink = state.lowlinks.get_mut(v).unwrap(); *v_lowlink = (*v_lowlink).min(w_index); } } // If v is a root node, pop the stack to create an SCC - let v_index = *state.indices.get(&v.to_path_buf()).unwrap(); - let v_lowlink = *state.lowlinks.get(&v.to_path_buf()).unwrap(); + let v_index = *state.indices.get(v).unwrap(); + let v_lowlink = *state.lowlinks.get(v).unwrap(); if v_lowlink == v_index { let mut scc = Vec::new();