From d06ba8957ae90a05771204d1421dc8ad3d177dfc Mon Sep 17 00:00:00 2001 From: Hayodea Hekol Date: Tue, 30 Sep 2025 00:24:33 -0400 Subject: [PATCH] QtxAcqHistTracker: Implement complete algo; add depend graph The dependency graph class enables us to perform analysis on the qutex acquisition history data. By generating the graph and detecting cycles in it, we can find true gridlocks. We use this graph analysis code to implement the algorithmically complete version of the gridlock detector. --- include/dependencyGraph.h | 85 +++++++ include/qutexAcquisitionHistoryTracker.h | 11 + smocore/qutexAcquisitionHistoryTracker.cpp | 247 ++++++++++++++++++++- 3 files changed, 331 insertions(+), 12 deletions(-) create mode 100644 include/dependencyGraph.h diff --git a/include/dependencyGraph.h b/include/dependencyGraph.h new file mode 100644 index 0000000..8433604 --- /dev/null +++ b/include/dependencyGraph.h @@ -0,0 +1,85 @@ +#ifndef DEPENDENCY_GRAPH_H +#define DEPENDENCY_GRAPH_H + +#include +#include +#include +#include + +namespace smo { + +// Forward declarations +class AsynchronousContinuationChainLink; + +/** + * @brief DependencyGraph - Represents a directed graph for lock dependency analysis + * + * This graph represents dependencies between continuations (lockvokers) where + * an edge from A to B means that continuation A wants a lock that is held by + * continuation B. This is used to detect circular dependencies (gridlocks). + */ +class DependencyGraph +{ +public: + typedef std::shared_ptr Node; + // Each node maps to a set of nodes it depends on + typedef std::unordered_map> AdjacencyList; + +public: + void addNode(const Node& node); + + /** + * @brief Add a directed edge from source to target + * @param source The continuation that wants a lock + * @param target The continuation that holds the wanted lock + */ + void addEdge(const Node& source, const Node& target); + + /** + * @brief Find all cycles in the graph using DFS + * @return Vector of cycles, where each cycle is a vector of nodes + */ + std::vector> findCycles() const; + + /** + * @brief Check if there are any cycles in the graph + * @return true if cycles exist, false otherwise + */ + bool hasCycles() const; + + /** + * @brief Get the number of nodes in the graph + * @return Number of nodes + */ + size_t getNodeCount() const; + + /** + * @brief Get the adjacency list for debugging + * @return Reference to the adjacency list + */ + const AdjacencyList& getAdjacencyList() const { return adjacencyList; } + +private: + /** + * @brief DFS helper for cycle detection + * @param node Current node being visited + * @param visited Set of nodes that have been fully processed + * @param recursionStack Set of nodes currently in the recursion stack + * @param path Current path being explored + * @param cycles Vector to store found cycles + */ + void dfsCycleDetection( + const Node& node, + std::unordered_set& visited, + std::unordered_set& recursionStack, + std::vector& path, + std::vector>& cycles) + const; + +private: + AdjacencyList adjacencyList; +}; + +} // namespace smo + +#endif // DEPENDENCY_GRAPH_H diff --git a/include/qutexAcquisitionHistoryTracker.h b/include/qutexAcquisitionHistoryTracker.h index 80246cc..8c52652 100644 --- a/include/qutexAcquisitionHistoryTracker.h +++ b/include/qutexAcquisitionHistoryTracker.h @@ -13,6 +13,7 @@ namespace smo { // Forward declarations class Qutex; class AsynchronousContinuationChainLink; +class DependencyGraph; /** * @brief QutexAcquisitionHistoryTracker - Tracks acquisition history for @@ -125,6 +126,16 @@ public: bool completelyTraceContinuationHistoryForGridlockOn( Qutex &firstFailedQutex); + /** + * @brief Generates a dependency graph among known continuations, based on + * the currently known acquisition history. There may well be a cyclical + * dependency which hasn't been reported to the history tracker yet. + * @param dontAcquireLock If true, skips acquiring the internal spinlock + * (assumes caller already holds it) + */ + [[nodiscard]] std::unique_ptr&& generateGraph( + bool dontAcquireLock = false); + // Disable copy constructor and assignment operator QutexAcquisitionHistoryTracker( const QutexAcquisitionHistoryTracker&) = delete; diff --git a/smocore/qutexAcquisitionHistoryTracker.cpp b/smocore/qutexAcquisitionHistoryTracker.cpp index beacefb..89f2990 100644 --- a/smocore/qutexAcquisitionHistoryTracker.cpp +++ b/smocore/qutexAcquisitionHistoryTracker.cpp @@ -1,13 +1,177 @@ #include "qutexAcquisitionHistoryTracker.h" #include "serializedAsynchronousContinuation.h" #include "qutex.h" +#include "dependencyGraph.h" #include #include #include #include +#include namespace smo { +void DependencyGraph::addNode(const Node& node) +{ + adjacencyList[node]; // Creates empty set if node doesn't exist +} + +void DependencyGraph::addEdge(const Node& source, const Node& target) +{ + addNode(source); + addNode(target); + adjacencyList[source].insert(target); +} + +std::vector> +DependencyGraph::findCycles() const +{ + std::unordered_set visited; + std::unordered_set recursionStack; + std::vector> cycles; + std::vector path; + + for (const auto& entry : adjacencyList) + { + const Node& node = entry.first; + if (visited.find(node) == visited.end()) { + dfsCycleDetection(node, visited, recursionStack, path, cycles); + } + } + + return cycles; +} + +bool DependencyGraph::hasCycles() const +{ + std::unordered_set visited; + std::unordered_set recursionStack; + std::vector> cycles; + std::vector path; + + for (const auto& entry : adjacencyList) + { + const Node& node = entry.first; + if (visited.find(node) == visited.end()) + { + dfsCycleDetection(node, visited, recursionStack, path, cycles); + if (!cycles.empty()) + { return true; } + } + } + + return false; +} + +size_t DependencyGraph::getNodeCount() const +{ + return adjacencyList.size(); +} + +void DependencyGraph::dfsCycleDetection( + const Node& node, + std::unordered_set& visited, + std::unordered_set& recursionStack, + std::vector& path, + std::vector>& cycles + ) + const +{ + // Mark current node as visited and add to recursion stack + visited.insert(node); + recursionStack.insert(node); + path.push_back(node); + + // Check all adjacent nodes + auto it = adjacencyList.find(node); + if (it != adjacencyList.end()) + { + for (const auto& adjacent : it->second) + { + // If adjacent node is in recursion stack, we found a cycle + if (recursionStack.find(adjacent) != recursionStack.end()) + { + // Find the start of the cycle in the current path + auto cycleStart = std::find(path.begin(), path.end(), adjacent); + if (cycleStart != path.end()) + { + std::vector cycle(cycleStart, path.end()); + cycle.push_back(adjacent); // Complete the cycle + cycles.push_back(cycle); + } + } + // If adjacent node hasn't been visited, recurse + else if (visited.find(adjacent) == visited.end()) + { + dfsCycleDetection( + adjacent, visited, recursionStack, path, cycles); + } + } + } + + // Remove from recursion stack and path when backtracking + recursionStack.erase(node); + path.pop_back(); +} + +// QutexAcquisitionHistoryTracker implementation +std::unique_ptr&& +QutexAcquisitionHistoryTracker::generateGraph(bool dontAcquireLock) +{ + auto graph = std::make_unique(); + + if (!dontAcquireLock) { + acquisitionHistoryLock.acquire(); + } + + // First pass: Add all continuations as nodes + for (const auto& entry : acquisitionHistory) + { + const auto& continuation = entry.first; + graph->addNode(continuation); + } + + // Second pass: Add edges based on lock dependencies + for (const auto& entry : acquisitionHistory) + { + const auto& continuation = entry.first; + const auto& historyEntry = entry.second; + const auto& wantedLock = historyEntry.first; + const auto& heldLocks = historyEntry.second; + + if (!heldLocks) { continue; } + + // Check if any other continuation holds the lock this continuation wants + for (const auto& otherEntry : acquisitionHistory) + { + const auto& otherContinuation = otherEntry.first; + const auto& otherHistoryEntry = otherEntry.second; + const auto& otherHeldLocks = otherHistoryEntry.second; + + // Skip self-comparison + if (continuation == otherContinuation) { continue; } + if (!otherHeldLocks) { continue; } + + // Check if other continuation holds the wanted lock + for (const auto& otherHeldLock : *otherHeldLocks) + { + if (&otherHeldLock.get() == &wantedLock.get()) + { + // Add edge: continuation -> otherContinuation + // (continuation wants a lock held by otherContinuation) + graph->addEdge(continuation, otherContinuation); + break; + } + } + } + } + + if (!dontAcquireLock) { + acquisitionHistoryLock.release(); + } + + return std::move(graph); +} + /** EXPLANATION - GRIDLOCK DETECTION ALGORITHM: * This file implements gridlock detection algorithms that use a central * acquisition history to track all lockvokers suspected of being gridlocked. @@ -105,7 +269,8 @@ bool QutexAcquisitionHistoryTracker acquisitionHistoryLock.acquire(); // Iterate through all entries in the acquisition history - for (const auto& entry : acquisitionHistory) { + for (const auto& entry : acquisitionHistory) + { const auto& continuation = entry.first; const auto& historyEntry = entry.second; @@ -151,20 +316,78 @@ bool QutexAcquisitionHistoryTracker (void)firstFailedQutex; /** ALGORITHMICALLY COMPLETE VERSION: - * This function is intended to implement the algorithmically complete - * version of gridlock detection that performs full circularity detection. - * This would involve building a dependency graph from the acquisition - * history and using graph traversal algorithms (such as DFS with cycle - * detection) to identify true circular dependencies. + * This function implements the algorithmically complete version of gridlock + * detection that performs full circularity detection. It builds a dependency + * graph from the acquisition history and uses DFS with cycle detection to + * identify true circular dependencies. * - * See the file-local comment above for the complete algorithm - * explanation. + * See the file-local comment above for the complete algorithm explanation. */ - // acquisitionHistoryLock.acquire(); - // TODO: Implement full circularity detection algorithm - // acquisitionHistoryLock.release(); - return false; + acquisitionHistoryLock.acquire(); + + // Helper function to print continuation dependency info + auto printContinuationDependency = [&]( + const auto& fromContinuation, const auto& toContinuation + ) + { + auto it = acquisitionHistory.find(fromContinuation); + if (it != acquisitionHistory.end()) + { + const auto& wantedLock = it->second.first; + std::cerr << " Continuation @" << fromContinuation.get() + << " wants lock[\"" << wantedLock.get().name << "\"], " + << "held by continuation @" << toContinuation.get() + << std::endl; + } + else + { + std::cerr << " Continuation @" << fromContinuation.get() + << " -> continuation @" << toContinuation.get() + << std::endl; + } + }; + + // Pass true to dontAcquireLock since we already hold it + auto graph = generateGraph(true); + + // Early return if no graph or no cycles + if (!graph || !graph->hasCycles()) + { + acquisitionHistoryLock.release(); + return false; + } + + auto cycles = graph->findCycles(); + + std::cerr << __func__ << ": CIRCULAR DEPENDENCIES DETECTED: Found " + << cycles.size() << " cycle(s) in lock dependency graph:" << std::endl; + + for (size_t i = 0; i < cycles.size(); ++i) + { + const auto& cycle = cycles[i]; + std::cerr << " Cycle " << (i + 1) << ":\n"; + + for (size_t j = 0; j < cycle.size() - 1; ++j) + { + const auto& currentContinuation = cycle[j]; + const auto& nextContinuation = cycle[j + 1]; + printContinuationDependency(currentContinuation, nextContinuation); + } + + if (cycle.empty()) + { continue; } + + /* Handle the last edge (back to start of cycle) + */ + const auto& lastContinuation = cycle[cycle.size() - 1]; + const auto& firstContinuation = cycle[0]; + printContinuationDependency(lastContinuation, firstContinuation); + } + + acquisitionHistoryLock.release(); + + return true; } } // namespace smo