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.
This commit is contained in:
2025-09-30 00:24:33 -04:00
parent 1aec779351
commit d06ba8957a
3 changed files with 331 additions and 12 deletions
+85
View File
@@ -0,0 +1,85 @@
#ifndef DEPENDENCY_GRAPH_H
#define DEPENDENCY_GRAPH_H
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <memory>
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<AsynchronousContinuationChainLink> Node;
// Each node maps to a set of nodes it depends on
typedef std::unordered_map<Node, std::unordered_set<Node>> 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<std::vector<Node>> 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<Node>& visited,
std::unordered_set<Node>& recursionStack,
std::vector<Node>& path,
std::vector<std::vector<Node>>& cycles)
const;
private:
AdjacencyList adjacencyList;
};
} // namespace smo
#endif // DEPENDENCY_GRAPH_H
+11
View File
@@ -13,6 +13,7 @@ namespace smo {
// Forward declarations // Forward declarations
class Qutex; class Qutex;
class AsynchronousContinuationChainLink; class AsynchronousContinuationChainLink;
class DependencyGraph;
/** /**
* @brief QutexAcquisitionHistoryTracker - Tracks acquisition history for * @brief QutexAcquisitionHistoryTracker - Tracks acquisition history for
@@ -125,6 +126,16 @@ public:
bool completelyTraceContinuationHistoryForGridlockOn( bool completelyTraceContinuationHistoryForGridlockOn(
Qutex &firstFailedQutex); 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<DependencyGraph>&& generateGraph(
bool dontAcquireLock = false);
// Disable copy constructor and assignment operator // Disable copy constructor and assignment operator
QutexAcquisitionHistoryTracker( QutexAcquisitionHistoryTracker(
const QutexAcquisitionHistoryTracker&) = delete; const QutexAcquisitionHistoryTracker&) = delete;
+235 -12
View File
@@ -1,13 +1,177 @@
#include "qutexAcquisitionHistoryTracker.h" #include "qutexAcquisitionHistoryTracker.h"
#include "serializedAsynchronousContinuation.h" #include "serializedAsynchronousContinuation.h"
#include "qutex.h" #include "qutex.h"
#include "dependencyGraph.h"
#include <memory> #include <memory>
#include <forward_list> #include <forward_list>
#include <functional> #include <functional>
#include <iostream> #include <iostream>
#include <algorithm>
namespace smo { 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<std::vector<DependencyGraph::Node>>
DependencyGraph::findCycles() const
{
std::unordered_set<Node> visited;
std::unordered_set<Node> recursionStack;
std::vector<std::vector<Node>> cycles;
std::vector<Node> 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<Node> visited;
std::unordered_set<Node> recursionStack;
std::vector<std::vector<Node>> cycles;
std::vector<Node> 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<Node>& visited,
std::unordered_set<Node>& recursionStack,
std::vector<Node>& path,
std::vector<std::vector<Node>>& 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<Node> 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<DependencyGraph>&&
QutexAcquisitionHistoryTracker::generateGraph(bool dontAcquireLock)
{
auto graph = std::make_unique<DependencyGraph>();
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: /** EXPLANATION - GRIDLOCK DETECTION ALGORITHM:
* This file implements gridlock detection algorithms that use a central * This file implements gridlock detection algorithms that use a central
* acquisition history to track all lockvokers suspected of being gridlocked. * acquisition history to track all lockvokers suspected of being gridlocked.
@@ -105,7 +269,8 @@ bool QutexAcquisitionHistoryTracker
acquisitionHistoryLock.acquire(); acquisitionHistoryLock.acquire();
// Iterate through all entries in the acquisition history // 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& continuation = entry.first;
const auto& historyEntry = entry.second; const auto& historyEntry = entry.second;
@@ -151,20 +316,78 @@ bool QutexAcquisitionHistoryTracker
(void)firstFailedQutex; (void)firstFailedQutex;
/** ALGORITHMICALLY COMPLETE VERSION: /** ALGORITHMICALLY COMPLETE VERSION:
* This function is intended to implement the algorithmically complete * This function implements the algorithmically complete version of gridlock
* version of gridlock detection that performs full circularity detection. * detection that performs full circularity detection. It builds a dependency
* This would involve building a dependency graph from the acquisition * graph from the acquisition history and uses DFS with cycle detection to
* history and using graph traversal algorithms (such as DFS with cycle * identify true circular dependencies.
* detection) to identify true circular dependencies.
* *
* See the file-local comment above for the complete algorithm * See the file-local comment above for the complete algorithm explanation.
* explanation.
*/ */
// acquisitionHistoryLock.acquire(); acquisitionHistoryLock.acquire();
// TODO: Implement full circularity detection algorithm
// acquisitionHistoryLock.release(); // Helper function to print continuation dependency info
return false; 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 } // namespace smo