-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
refactor(libstore): add BGL-based dependency graph for path analysis #14392
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
Open
lovesegfault
wants to merge
1
commit into
NixOS:master
Choose a base branch
from
lovesegfault:bgl-depgraph-init
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| #include "nix/store/dependency-graph-impl.hh" | ||
|
|
||
| #include <gtest/gtest.h> | ||
|
|
||
| namespace nix { | ||
|
|
||
| TEST(DependencyGraph, BasicAddEdge) | ||
| { | ||
| FilePathGraph depGraph; | ||
| depGraph.addEdge("a", "b"); | ||
| depGraph.addEdge("b", "c"); | ||
|
|
||
| EXPECT_TRUE(depGraph.hasNode("a")); | ||
| EXPECT_TRUE(depGraph.hasNode("b")); | ||
| EXPECT_TRUE(depGraph.hasNode("c")); | ||
| EXPECT_FALSE(depGraph.hasNode("d")); | ||
|
|
||
| // Verify edges using high-level API | ||
| auto successors = depGraph.getSuccessors("a"); | ||
| EXPECT_EQ(successors.size(), 1); | ||
| EXPECT_EQ(successors[0], "b"); | ||
| } | ||
|
|
||
| TEST(DependencyGraph, DfsTraversalOrder) | ||
| { | ||
| // Build a graph: A->B->D, A->C->D | ||
| // Successors should be visited in distance order (B and C before recursing) | ||
| FilePathGraph depGraph; | ||
| depGraph.addEdge("a", "b"); | ||
| depGraph.addEdge("a", "c"); | ||
| depGraph.addEdge("b", "d"); | ||
| depGraph.addEdge("c", "d"); | ||
|
|
||
| std::vector<std::string> visitedNodes; | ||
| std::vector<std::pair<std::string, std::string>> visitedEdges; | ||
|
|
||
| depGraph.dfsFromTarget( | ||
| "a", | ||
| "d", | ||
| [&](const std::string & node, size_t depth) { | ||
| visitedNodes.push_back(node); | ||
| return true; | ||
| }, | ||
| [&](const std::string & from, const std::string & to, bool isLast, size_t depth) { | ||
| visitedEdges.emplace_back(from, to); | ||
| }, | ||
| [](const std::string &) { return false; }); | ||
|
|
||
| EXPECT_EQ(visitedNodes[0], "a"); | ||
| // B and C both at distance 1, could be in either order | ||
| EXPECT_TRUE( | ||
| (visitedNodes[1] == "b" && visitedNodes[2] == "d") || (visitedNodes[1] == "c" && visitedNodes[2] == "d")); | ||
| } | ||
|
|
||
| TEST(DependencyGraph, GetSuccessors) | ||
| { | ||
| FilePathGraph depGraph; | ||
| depGraph.addEdge("a", "b"); | ||
| depGraph.addEdge("a", "c"); | ||
|
|
||
| auto successors = depGraph.getSuccessors("a"); | ||
| EXPECT_EQ(successors.size(), 2); | ||
| EXPECT_TRUE(std::ranges::contains(successors, "b")); | ||
| EXPECT_TRUE(std::ranges::contains(successors, "c")); | ||
| } | ||
|
|
||
| TEST(DependencyGraph, GetAllNodes) | ||
| { | ||
| FilePathGraph depGraph; | ||
| depGraph.addEdge("foo", "bar"); | ||
| depGraph.addEdge("bar", "baz"); | ||
|
|
||
| auto nodes = depGraph.getAllNodes(); | ||
| EXPECT_EQ(nodes.size(), 3); | ||
| EXPECT_TRUE(std::ranges::contains(nodes, "foo")); | ||
| EXPECT_TRUE(std::ranges::contains(nodes, "bar")); | ||
| EXPECT_TRUE(std::ranges::contains(nodes, "baz")); | ||
| } | ||
|
|
||
| TEST(DependencyGraph, ThrowsOnMissingNode) | ||
| { | ||
| FilePathGraph depGraph; | ||
| depGraph.addEdge("a", "b"); | ||
|
|
||
| EXPECT_THROW((void) depGraph.getSuccessors("nonexistent"), nix::Error); | ||
| } | ||
|
|
||
| TEST(DependencyGraph, EmptyGraph) | ||
| { | ||
| FilePathGraph depGraph; | ||
|
|
||
| EXPECT_FALSE(depGraph.hasNode("anything")); | ||
| EXPECT_EQ(depGraph.numVertices(), 0); | ||
| EXPECT_EQ(depGraph.getAllNodes().size(), 0); | ||
| } | ||
|
|
||
| } // namespace nix |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| #include "nix/store/dependency-graph.hh" | ||
| #include "nix/store/dependency-graph-impl.hh" | ||
|
|
||
| #include <string> | ||
|
|
||
| namespace nix { | ||
|
|
||
| // Explicit instantiations for common types | ||
| template class DependencyGraph<StorePath>; | ||
| template class DependencyGraph<std::string>; | ||
| template class DependencyGraph<StorePath, FileListEdgeProperty>; | ||
|
|
||
| } // namespace nix |
232 changes: 232 additions & 0 deletions
232
src/libstore/include/nix/store/dependency-graph-impl.hh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,232 @@ | ||
| #pragma once | ||
| /** | ||
| * @file | ||
| * | ||
| * Template implementations (as opposed to mere declarations). | ||
| * | ||
| * This file is an example of the "impl.hh" pattern. See the | ||
| * contributing guide. | ||
| * | ||
| * One only needs to include this when instantiating DependencyGraph | ||
| * with custom NodeId or EdgeProperty types beyond the pre-instantiated | ||
| * common types (StorePath, std::string). | ||
| */ | ||
|
|
||
| #include "nix/store/dependency-graph.hh" | ||
| #include "nix/store/store-api.hh" | ||
| #include "nix/util/error.hh" | ||
|
|
||
| #include <boost/graph/graph_traits.hpp> | ||
| #include <boost/graph/reverse_graph.hpp> | ||
| #include <boost/graph/properties.hpp> | ||
|
|
||
| #include <algorithm> | ||
| #include <ranges> | ||
|
|
||
| namespace nix { | ||
|
|
||
| template<GraphNodeId NodeId, typename EdgeProperty> | ||
| DependencyGraph<NodeId, EdgeProperty>::DependencyGraph(Store & store, const StorePathSet & closure) | ||
| requires std::same_as<NodeId, StorePath> | ||
| { | ||
| for (auto & path : closure) { | ||
| for (auto & ref : store.queryPathInfo(path)->references) { | ||
| addEdge(path, ref); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| template<GraphNodeId NodeId, typename EdgeProperty> | ||
| typename DependencyGraph<NodeId, EdgeProperty>::vertex_descriptor | ||
| DependencyGraph<NodeId, EdgeProperty>::addOrGetVertex(const NodeId & id) | ||
| { | ||
| auto it = nodeToVertex.find(id); | ||
| if (it != nodeToVertex.end()) { | ||
| return it->second; | ||
| } | ||
|
|
||
| auto v = boost::add_vertex(VertexProperty{std::make_optional(id)}, graph); | ||
| nodeToVertex[id] = v; | ||
| return v; | ||
| } | ||
|
|
||
| template<GraphNodeId NodeId, typename EdgeProperty> | ||
| void DependencyGraph<NodeId, EdgeProperty>::addEdge(const NodeId & from, const NodeId & to) | ||
| { | ||
| auto vFrom = addOrGetVertex(from); | ||
| auto vTo = addOrGetVertex(to); | ||
|
|
||
| // Check for existing edge to prevent duplicates (idempotent) | ||
| auto [existingEdge, found] = boost::edge(vFrom, vTo, graph); | ||
| if (!found) { | ||
| boost::add_edge(vFrom, vTo, graph); | ||
| } | ||
| // If edge exists, this is a no-op (idempotent) | ||
| } | ||
|
|
||
| template<GraphNodeId NodeId, typename EdgeProperty> | ||
| void DependencyGraph<NodeId, EdgeProperty>::addEdge(const NodeId & from, const NodeId & to, const EdgeProperty & prop) | ||
| requires(!std::same_as<EdgeProperty, boost::no_property>) | ||
| { | ||
| auto vFrom = addOrGetVertex(from); | ||
| auto vTo = addOrGetVertex(to); | ||
|
|
||
| auto [existingEdge, found] = boost::edge(vFrom, vTo, graph); | ||
| if (found) { | ||
| // Merge properties for existing edge | ||
| if constexpr (std::same_as<EdgeProperty, FileListEdgeProperty>) { | ||
| // Set handles deduplication automatically | ||
| auto & edgeFiles = graph[existingEdge].files; | ||
| edgeFiles.insert(prop.files.begin(), prop.files.end()); | ||
| } else { | ||
| // For other property types, overwrite with new value | ||
| graph[existingEdge] = prop; | ||
| } | ||
| } else { | ||
| // New edge | ||
| boost::add_edge(vFrom, vTo, prop, graph); | ||
| } | ||
| } | ||
|
|
||
| template<GraphNodeId NodeId, typename EdgeProperty> | ||
| std::optional<typename DependencyGraph<NodeId, EdgeProperty>::vertex_descriptor> | ||
| DependencyGraph<NodeId, EdgeProperty>::getVertex(const NodeId & id) const | ||
| { | ||
| auto it = nodeToVertex.find(id); | ||
| if (it == nodeToVertex.end()) { | ||
| return std::nullopt; | ||
| } | ||
| return it->second; | ||
| } | ||
|
|
||
| template<GraphNodeId NodeId, typename EdgeProperty> | ||
| const NodeId & DependencyGraph<NodeId, EdgeProperty>::getNodeId(vertex_descriptor v) const | ||
| { | ||
| return *graph[v].id; | ||
| } | ||
|
|
||
| template<GraphNodeId NodeId, typename EdgeProperty> | ||
| bool DependencyGraph<NodeId, EdgeProperty>::hasNode(const NodeId & id) const | ||
| { | ||
| return nodeToVertex.contains(id); | ||
| } | ||
|
|
||
| template<GraphNodeId NodeId, typename EdgeProperty> | ||
| typename DependencyGraph<NodeId, EdgeProperty>::vertex_descriptor | ||
| DependencyGraph<NodeId, EdgeProperty>::getVertexOrThrow(const NodeId & id) const | ||
| { | ||
| auto opt = getVertex(id); | ||
| if (!opt.has_value()) { | ||
| // Note: NodeId is not included as it may not be formattable in all instantiations | ||
| throw Error("node not found in graph"); | ||
| } | ||
| return *opt; | ||
| } | ||
|
|
||
| template<GraphNodeId NodeId, typename EdgeProperty> | ||
| template<typename NodeVisitor, typename EdgeVisitor, typename StopPredicate> | ||
| void DependencyGraph<NodeId, EdgeProperty>::dfsFromTarget( | ||
| const NodeId & start, | ||
| const NodeId & target, | ||
| NodeVisitor && visitNode, | ||
| EdgeVisitor && visitEdge, | ||
| StopPredicate && shouldStop) const | ||
| { | ||
| // Compute distances locally for this traversal | ||
| auto targetVertex = getVertexOrThrow(target); | ||
| size_t n = boost::num_vertices(graph); | ||
|
|
||
| std::vector<size_t> distances(n, std::numeric_limits<size_t>::max()); | ||
| distances[targetVertex] = 0; | ||
|
|
||
| // Use reverse_graph to follow incoming edges | ||
| auto reversedGraph = boost::make_reverse_graph(graph); | ||
|
|
||
| // Create uniform weight map (all edges have weight 1) | ||
| auto weightMap = | ||
| boost::make_constant_property<typename boost::graph_traits<decltype(reversedGraph)>::edge_descriptor>(1); | ||
|
|
||
| // Run Dijkstra on reversed graph with uniform weights | ||
| boost::dijkstra_shortest_paths( | ||
| reversedGraph, | ||
| targetVertex, | ||
| boost::weight_map(weightMap).distance_map( | ||
| boost::make_iterator_property_map(distances.begin(), boost::get(boost::vertex_index, reversedGraph)))); | ||
|
|
||
| // DFS with distance-based ordering | ||
| std::function<bool(const NodeId &, size_t)> dfs = [&](const NodeId & node, size_t depth) -> bool { | ||
| // Visit node - if returns false, skip this subtree | ||
| if (!visitNode(node, depth)) { | ||
| return false; | ||
| } | ||
|
|
||
| // Check if we should stop the entire traversal | ||
| if (shouldStop(node)) { | ||
| return true; // Signal to stop | ||
| } | ||
|
|
||
| // Get and sort successors by distance | ||
| auto successors = getSuccessors(node); | ||
| auto sortedSuccessors = successors | std::views::transform([&](const auto & ref) -> std::pair<size_t, NodeId> { | ||
| auto v = getVertexOrThrow(ref); | ||
| return {distances[v], ref}; // Use local distances | ||
| }) | ||
| | std::views::filter([](const auto & p) { | ||
| // Filter unreachable nodes | ||
| return p.first != std::numeric_limits<size_t>::max(); | ||
| }) | ||
| | std::ranges::to<std::vector>(); | ||
|
|
||
| std::ranges::sort(sortedSuccessors); | ||
|
|
||
| // Visit each edge and recurse | ||
| for (size_t i = 0; i < sortedSuccessors.size(); ++i) { | ||
| const auto & [dist, successor] = sortedSuccessors[i]; | ||
| bool isLast = (i == sortedSuccessors.size() - 1); | ||
|
|
||
| visitEdge(node, successor, isLast, depth); | ||
|
|
||
| if (dfs(successor, depth + 1)) { | ||
| return true; // Propagate stop signal | ||
| } | ||
| } | ||
|
|
||
| return false; // Continue traversal | ||
| }; | ||
|
|
||
| dfs(start, 0); | ||
| } | ||
|
|
||
| template<GraphNodeId NodeId, typename EdgeProperty> | ||
| std::vector<NodeId> DependencyGraph<NodeId, EdgeProperty>::getSuccessors(const NodeId & node) const | ||
| { | ||
| auto v = getVertexOrThrow(node); | ||
| auto [adjBegin, adjEnd] = boost::adjacent_vertices(v, graph); | ||
|
|
||
| return std::ranges::subrange(adjBegin, adjEnd) | std::views::transform([&](auto v) { return getNodeId(v); }) | ||
| | std::ranges::to<std::vector>(); | ||
| } | ||
|
|
||
| template<GraphNodeId NodeId, typename EdgeProperty> | ||
| std::optional<EdgeProperty> | ||
| DependencyGraph<NodeId, EdgeProperty>::getEdgeProperty(const NodeId & from, const NodeId & to) const | ||
| requires(!std::same_as<EdgeProperty, boost::no_property>) | ||
| { | ||
| auto vFrom = getVertexOrThrow(from); | ||
| auto vTo = getVertexOrThrow(to); | ||
|
|
||
| auto [edge, found] = boost::edge(vFrom, vTo, graph); | ||
| if (!found) { | ||
| return std::nullopt; | ||
| } | ||
|
|
||
| return graph[edge]; | ||
| } | ||
|
|
||
| template<GraphNodeId NodeId, typename EdgeProperty> | ||
| std::vector<NodeId> DependencyGraph<NodeId, EdgeProperty>::getAllNodes() const | ||
| { | ||
| return nodeToVertex | std::views::keys | std::ranges::to<std::vector>(); | ||
| } | ||
|
|
||
| } // namespace nix | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.