blob: 707908145f1a6678cea9c5d4c2bdda84a637e0d5 [file]
// Copyright 2017 The Abseil Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// GraphCycles provides incremental cycle detection on a dynamic
// graph using the following algorithm:
//
// A dynamic topological sort algorithm for directed acyclic graphs
// David J. Pearce, Paul H. J. Kelly
// Journal of Experimental Algorithmics (JEA) JEA Homepage archive
// Volume 11, 2006, Article No. 1.7
//
// Brief summary of the algorithm:
//
// (1) Maintain a rank for each node that is consistent
// with the topological sort of the graph. I.e., path from x to y
// implies rank[x] < rank[y].
// (2) When a new edge (x->y) is inserted, do nothing if rank[x] < rank[y].
// (3) Otherwise: adjust ranks in the neighborhood of x and y.
//
// Unlike in the paper, ranks are spaced apart, which often lets step (3) move
// just x or just y (see "Rank allocation" below).
#include "absl/base/internal/low_level_alloc.h" // IWYU pragma: keep
// This file is a no-op if the required LowLevelAlloc support is missing.
#ifndef ABSL_LOW_LEVEL_ALLOC_MISSING
#include <algorithm>
#include <array>
#include <cinttypes>
#include <cstddef>
#include <cstdint>
#include <limits>
#include "absl/algorithm/container.h"
#include "absl/base/attributes.h"
#include "absl/base/config.h"
#include "absl/base/internal/hide_ptr.h"
#include "absl/base/internal/raw_logging.h"
#include "absl/base/internal/scheduling_mode.h"
#include "absl/base/internal/spinlock.h"
#include "absl/base/macros.h"
#include "absl/synchronization/internal/graphcycles.h"
// Do not use STL. This module does not use standard memory allocation.
namespace absl {
ABSL_NAMESPACE_BEGIN
namespace synchronization_internal {
namespace {
// Avoid LowLevelAlloc's default arena since it calls malloc hooks in
// which people are doing things like acquiring Mutexes.
ABSL_CONST_INIT static absl::base_internal::SpinLock arena_mu(
base_internal::SCHEDULE_KERNEL_ONLY);
ABSL_CONST_INIT static base_internal::LowLevelAlloc::Arena* arena;
static void InitArenaIfNecessary() {
base_internal::SpinLockHolder l(arena_mu);
if (arena == nullptr) {
arena = base_internal::LowLevelAlloc::NewArena(0);
}
}
// Number of inlined elements in Vec. Hash table implementation
// relies on this being a power of two.
static const uint32_t kInline = 8;
// A simple LowLevelAlloc based resizable vector with inlined storage
// for a few elements. T must be a plain type since constructor
// and destructor are not run on elements of type T managed by Vec.
template <typename T>
class Vec {
public:
Vec() { Init(); }
~Vec() { Discard(); }
void clear() {
Discard();
Init();
}
bool empty() const { return size_ == 0; }
uint32_t size() const { return size_; }
T* begin() { return ptr_; }
T* end() { return ptr_ + size_; }
const T& operator[](uint32_t i) const { return ptr_[i]; }
T& operator[](uint32_t i) { return ptr_[i]; }
const T& back() const { return ptr_[size_ - 1]; }
void pop_back() { size_--; }
void push_back(const T& v) {
if (size_ == capacity_) Grow(size_ + 1);
ptr_[size_] = v;
size_++;
}
void resize(uint32_t n) {
if (n > capacity_) Grow(n);
size_ = n;
}
void fill(const T& val) {
for (uint32_t i = 0; i < size(); i++) {
ptr_[i] = val;
}
}
// Guarantees src is empty at end.
// Provided for the hash table resizing code below.
void MoveFrom(Vec<T>* src) {
if (src->ptr_ == src->space_) {
// Need to actually copy
resize(src->size_);
std::copy_n(src->ptr_, src->size_, ptr_);
src->size_ = 0;
} else {
Discard();
ptr_ = src->ptr_;
size_ = src->size_;
capacity_ = src->capacity_;
src->Init();
}
}
private:
T* ptr_;
T space_[kInline];
uint32_t size_;
uint32_t capacity_;
void Init() {
ptr_ = space_;
size_ = 0;
capacity_ = kInline;
}
void Discard() {
if (ptr_ != space_) base_internal::LowLevelAlloc::Free(ptr_);
}
void Grow(uint32_t n) {
while (capacity_ < n) {
capacity_ *= 2;
}
size_t request = static_cast<size_t>(capacity_) * sizeof(T);
T* copy = static_cast<T*>(
base_internal::LowLevelAlloc::AllocWithArena(request, arena));
std::copy_n(ptr_, size_, copy);
Discard();
ptr_ = copy;
}
Vec(const Vec&) = delete;
Vec& operator=(const Vec&) = delete;
};
// A hash set of non-negative int32_t that uses Vec for its underlying storage.
class NodeSet {
public:
NodeSet() { Init(); }
void clear() { Init(); }
bool contains(int32_t v) const { return table_[FindIndex(v)] == v; }
bool insert(int32_t v) {
uint32_t i = FindIndex(v);
if (table_[i] == v) {
return false;
}
if (table_[i] == kEmpty) {
// Only inserting over an empty cell increases the number of occupied
// slots.
occupied_++;
}
table_[i] = v;
// Double when 75% full.
if (occupied_ >= table_.size() - table_.size() / 4) Grow();
return true;
}
void erase(int32_t v) {
uint32_t i = FindIndex(v);
if (table_[i] == v) {
table_[i] = kDel;
}
}
// Iteration: is done via HASH_FOR_EACH
// Example:
// HASH_FOR_EACH(elem, node->out) { ... }
#define HASH_FOR_EACH(elem, eset) \
for (int32_t elem, _cursor = 0; (eset).Next(&_cursor, &elem);)
bool Next(int32_t* cursor, int32_t* elem) {
while (static_cast<uint32_t>(*cursor) < table_.size()) {
int32_t v = table_[static_cast<uint32_t>(*cursor)];
(*cursor)++;
if (v >= 0) {
*elem = v;
return true;
}
}
return false;
}
private:
enum : int32_t { kEmpty = -1, kDel = -2 };
Vec<int32_t> table_;
uint32_t occupied_; // Count of non-empty slots (includes deleted slots)
static uint32_t Hash(int32_t a) { return static_cast<uint32_t>(a) * 41; }
// Return index for storing v. May return an empty index or deleted index
uint32_t FindIndex(int32_t v) const {
// Search starting at hash index.
const uint32_t mask = table_.size() - 1;
uint32_t i = Hash(v) & mask;
uint32_t deleted_index = 0; // index of first deleted element we see
bool seen_deleted_element = false;
while (true) {
int32_t e = table_[i];
if (v == e) {
return i;
} else if (e == kEmpty) {
// Return any previously encountered deleted slot.
return seen_deleted_element ? deleted_index : i;
} else if (e == kDel && !seen_deleted_element) {
// Keep searching since v might be present later.
deleted_index = i;
seen_deleted_element = true;
}
i = (i + 1) & mask; // Linear probing; quadratic is slightly slower.
}
}
void Init() {
table_.clear();
table_.resize(kInline);
table_.fill(kEmpty);
occupied_ = 0;
}
void Grow() {
Vec<int32_t> copy;
copy.MoveFrom(&table_);
occupied_ = 0;
table_.resize(copy.size() * 2);
table_.fill(kEmpty);
for (const auto& e : copy) {
if (e >= 0) insert(e);
}
}
NodeSet(const NodeSet&) = delete;
NodeSet& operator=(const NodeSet&) = delete;
};
// We encode a node index and a node version in GraphId. The version
// number is incremented when the GraphId is freed which automatically
// invalidates all copies of the GraphId.
inline GraphId MakeId(int32_t index, uint32_t version) {
GraphId g;
g.handle =
(static_cast<uint64_t>(version) << 32) | static_cast<uint32_t>(index);
return g;
}
inline int32_t NodeIndex(GraphId id) { return static_cast<int32_t>(id.handle); }
inline uint32_t NodeVersion(GraphId id) {
return static_cast<uint32_t>(id.handle >> 32);
}
struct Node {
int64_t rank; // Unique rank (see "Rank allocation")
int32_t prev_slot; // Slot with the next lower rank, or -1 if none
int32_t next_slot; // Slot with the next higher rank, or -1 if none
uint32_t version; // Current version number
int32_t next_hash; // Next entry in hash table
bool visited; // Temporary marker used by depth-first-search
uintptr_t masked_ptr; // User-supplied pointer
NodeSet in; // List of immediate predecessor nodes in graph
NodeSet out; // List of immediate successor nodes in graph
int priority; // Priority of recorded stack trace.
int nstack; // Depth of recorded stack trace.
void* stack[40]; // stack[0,nstack-1] holds stack trace for node.
};
// Hash table for pointer to node index lookups.
class PointerMap {
public:
explicit PointerMap(const Vec<Node*>* nodes) : nodes_(nodes) {
table_.fill(-1);
}
int32_t Find(void* ptr) {
auto masked = base_internal::HidePtr(ptr);
for (int32_t i = table_[Hash(ptr)]; i != -1;) {
Node* n = (*nodes_)[static_cast<uint32_t>(i)];
if (n->masked_ptr == masked) return i;
i = n->next_hash;
}
return -1;
}
void Add(void* ptr, int32_t i) {
int32_t* head = &table_[Hash(ptr)];
(*nodes_)[static_cast<uint32_t>(i)]->next_hash = *head;
*head = i;
}
int32_t Remove(void* ptr) {
// Advance through linked list while keeping track of the
// predecessor slot that points to the current entry.
auto masked = base_internal::HidePtr(ptr);
for (int32_t* slot = &table_[Hash(ptr)]; *slot != -1;) {
int32_t index = *slot;
Node* n = (*nodes_)[static_cast<uint32_t>(index)];
if (n->masked_ptr == masked) {
*slot = n->next_hash; // Remove n from linked list
n->next_hash = -1;
return index;
}
slot = &n->next_hash;
}
return -1;
}
private:
// Number of buckets in hash table for pointer lookups.
static constexpr uint32_t kHashTableSize = 262139; // should be prime
const Vec<Node*>* nodes_;
std::array<int32_t, kHashTableSize> table_;
static uint32_t Hash(void* ptr) {
return reinterpret_cast<uintptr_t>(ptr) % kHashTableSize;
}
};
} // namespace
// Rank allocation
//
// Ranks are unique int64_t values. All slots, including free slots, are kept in
// a doubly linked list sorted by rank (Node::prev_slot and Node::next_slot,
// from Rep::lowest_slot to Rep::highest_slot). Free slots keep their rank and
// their place in the list, which are reused along with the slot. New slots get
// ranks kRankStride above the highest rank.
//
// When a new edge x->y violates the rank order, InsertEdge first tries to move
// just x below y, or just y above x (see TryMoveXBelowY()): either to the
// midpoint between two ranks that are adjacent in the list, or kRankStride
// beyond the lowest or highest rank. No other slot can have such a rank, so
// ranks stay unique.
//
// kRankStride leaves room for about 32 moves, one after another, into the gap
// between two adjacent new slots. When a gap has no room left, Respace() first
// spreads out the ranks around it. Ranks beyond the lowest and highest ones
// stay within [kMinRank, kMaxRank], which leaves room for about 2^30 moves at
// each end before all slots are respaced.
static constexpr int64_t kRankStride = int64_t{1} << 32;
static constexpr int64_t kMinRank = -(int64_t{1} << 62);
static constexpr int64_t kMaxRank = int64_t{1} << 62;
// Respace() spreads out ranks until they can be at least this far apart.
static constexpr uint64_t kMinRespacedGap = uint64_t{1} << 16;
// Maximum number of in-neighbors of x (or out-neighbors of y) that
// TryMoveXBelowY() (or TryMoveYAboveX()) will examine.
static constexpr int kMaxSingleNodeDegree = 16;
struct GraphCycles::Rep {
Vec<Node*> nodes_;
Vec<int32_t> free_nodes_; // Indices for unused entries in nodes_
PointerMap ptrmap_;
int32_t lowest_slot = -1; // Slot with the lowest rank, or -1 if none
int32_t highest_slot = -1; // Slot with the highest rank, or -1 if none
int64_t rank_stride = kRankStride; // See TestOnlySetRankStride()
// Temporary state.
Vec<int32_t> deltaf_; // Results of forward DFS
Vec<int32_t> deltab_; // Results of backward DFS
Vec<int32_t> list_; // All nodes to reprocess
Vec<int32_t> stack_; // Emulates recursion stack for depth-first searches
// Used by Reorder(): the slots to reorder, sorted by their current ranks, and
// the rank, prev_slot and next_slot of each.
Vec<int32_t> sorted_slots;
Vec<int64_t> sorted_ranks;
Vec<int32_t> old_prev_slots;
Vec<int32_t> old_next_slots;
Rep() : ptrmap_(&nodes_) {}
};
static Node* FindNode(GraphCycles::Rep* rep, GraphId id) {
Node* n = rep->nodes_[static_cast<uint32_t>(NodeIndex(id))];
return (n->version == NodeVersion(id)) ? n : nullptr;
}
static Node* NodeAt(GraphCycles::Rep* r, int32_t slot) {
return r->nodes_[static_cast<uint32_t>(slot)];
}
// Makes next_slot follow prev_slot in the rank list. prev_slot == -1 makes
// next_slot the lowest slot, and next_slot == -1 makes prev_slot the highest.
static void Link(GraphCycles::Rep* r, int32_t prev_slot, int32_t next_slot) {
if (prev_slot == -1) {
r->lowest_slot = next_slot;
} else {
NodeAt(r, prev_slot)->next_slot = next_slot;
}
if (next_slot == -1) {
r->highest_slot = prev_slot;
} else {
NodeAt(r, next_slot)->prev_slot = prev_slot;
}
}
// Removes slot from the rank list.
static void Unlink(GraphCycles::Rep* r, int32_t slot) {
Link(r, NodeAt(r, slot)->prev_slot, NodeAt(r, slot)->next_slot);
}
// Returns upper - lower, for lower <= upper, without overflow.
static uint64_t RankDistance(int64_t lower, int64_t upper) {
return static_cast<uint64_t>(upper) - static_cast<uint64_t>(lower);
}
// Gives the run of adjacent slots from first_slot to last_slot in the rank list
// evenly spaced ranks, keeping their order. First grows the run on both sides
// until its slots can be at least kMinRespacedGap apart between the ranks just
// outside it (kMinRank and kMaxRank beyond the ends of the list), or until it
// covers the whole list. The whole list is spaced across
// [kMinRank / 2, kMaxRank / 2], which leaves room beyond both of its ends.
static void Respace(GraphCycles::Rep* r, int32_t first_slot,
int32_t last_slot) {
uint64_t count = 1;
for (int32_t slot = first_slot; slot != last_slot;
slot = NodeAt(r, slot)->next_slot) {
count++;
}
int64_t lower;
int64_t upper;
while (true) {
const int32_t before_slot = NodeAt(r, first_slot)->prev_slot;
const int32_t after_slot = NodeAt(r, last_slot)->next_slot;
if (before_slot == -1 && after_slot == -1) {
lower = kMinRank / 2;
upper = kMaxRank / 2;
break;
}
lower = before_slot == -1 ? kMinRank : NodeAt(r, before_slot)->rank;
upper = after_slot == -1 ? kMaxRank : NodeAt(r, after_slot)->rank;
if (RankDistance(lower, upper) / (count + 1) >= kMinRespacedGap) break;
// Double the size of the run, as far as the ends of the list allow.
for (const uint64_t target = 2 * count; count < target;) {
const int32_t prev_slot = NodeAt(r, first_slot)->prev_slot;
const int32_t next_slot = NodeAt(r, last_slot)->next_slot;
if (prev_slot == -1 && next_slot == -1) break;
if (prev_slot != -1) {
first_slot = prev_slot;
count++;
}
if (next_slot != -1) {
last_slot = next_slot;
count++;
}
}
}
const uint64_t step = RankDistance(lower, upper) / (count + 1);
int64_t rank = lower;
for (int32_t slot = first_slot;; slot = NodeAt(r, slot)->next_slot) {
rank += static_cast<int64_t>(step);
NodeAt(r, slot)->rank = rank;
if (slot == last_slot) return;
}
}
// Links slot, which is not in the rank list, between prev_slot and next_slot,
// which are adjacent in it (-1 beyond its ends), with a rank between theirs:
// rank_stride (kRankStride by default) beyond prev_slot or next_slot at the
// ends of the list, and halfway between them otherwise. If there is no room
// for such a rank, respaces ranks first.
static void LinkBetween(GraphCycles::Rep* r, int32_t slot, int32_t prev_slot,
int32_t next_slot) {
int64_t rank;
if (prev_slot == -1 && next_slot == -1) {
rank = 0;
} else if (prev_slot == -1) {
if (NodeAt(r, next_slot)->rank < kMinRank + r->rank_stride) {
Respace(r, r->lowest_slot, r->highest_slot);
}
rank = NodeAt(r, next_slot)->rank - r->rank_stride;
} else if (next_slot == -1) {
if (NodeAt(r, prev_slot)->rank > kMaxRank - r->rank_stride) {
Respace(r, r->lowest_slot, r->highest_slot);
}
rank = NodeAt(r, prev_slot)->rank + r->rank_stride;
} else {
const Node* prev_node = NodeAt(r, prev_slot);
const Node* next_node = NodeAt(r, next_slot);
if (RankDistance(prev_node->rank, next_node->rank) < 2) {
Respace(r, prev_slot, next_slot);
}
const uint64_t gap = RankDistance(prev_node->rank, next_node->rank);
rank = prev_node->rank + static_cast<int64_t>(gap / 2);
}
NodeAt(r, slot)->rank = rank;
Link(r, prev_slot, slot);
Link(r, slot, next_slot);
}
void GraphCycles::TestOnlyAddNodes(uint32_t n) {
uint32_t old_size = rep_->nodes_.size();
rep_->nodes_.resize(n);
for (auto i = old_size; i < n; ++i) {
rep_->nodes_[i] = nullptr;
}
}
void GraphCycles::TestOnlySetRankStride(int64_t stride) {
// After Respace() of the whole list, there is room for a stride of 2^61
// beyond each end.
ABSL_RAW_CHECK(stride >= 1 && stride <= kMaxRank / 2, "Bad rank stride");
rep_->rank_stride = stride;
}
GraphCycles::GraphCycles() {
InitArenaIfNecessary();
rep_ = new (base_internal::LowLevelAlloc::AllocWithArena(sizeof(Rep), arena))
Rep;
}
GraphCycles::~GraphCycles() {
for (auto* node : rep_->nodes_) {
if (node == nullptr) {
continue;
}
node->Node::~Node();
base_internal::LowLevelAlloc::Free(node);
}
rep_->Rep::~Rep();
base_internal::LowLevelAlloc::Free(rep_);
}
bool GraphCycles::CheckInvariants() const {
Rep* r = rep_;
for (uint32_t x = 0; x < r->nodes_.size(); x++) {
Node* nx = r->nodes_[x];
void* ptr = base_internal::UnhidePtr<void>(nx->masked_ptr);
if (ptr != nullptr && static_cast<uint32_t>(r->ptrmap_.Find(ptr)) != x) {
ABSL_RAW_LOG(FATAL, "Did not find live node in hash table %" PRIu32 " %p",
x, ptr);
}
if (nx->visited) {
ABSL_RAW_LOG(FATAL, "Did not clear visited marker on node %" PRIu32, x);
}
HASH_FOR_EACH(y, nx->out) {
Node* ny = r->nodes_[static_cast<uint32_t>(y)];
if (nx->rank >= ny->rank) {
ABSL_RAW_LOG(FATAL,
"Edge %" PRIu32 " ->%" PRId32
" has bad rank assignment %" PRId64 "->%" PRId64,
x, y, nx->rank, ny->rank);
}
}
}
// The rank list holds each slot once, in increasing rank order, so ranks are
// unique.
const uint32_t num_slots = r->nodes_.size();
uint32_t num_listed = 0;
int32_t prev_slot = -1;
for (int32_t slot = r->lowest_slot; slot != -1;
slot = NodeAt(r, slot)->next_slot) {
if (NodeAt(r, slot)->prev_slot != prev_slot) {
ABSL_RAW_LOG(FATAL,
"Slot %" PRId32 " has prev_slot %" PRId32
" instead of %" PRId32,
slot, NodeAt(r, slot)->prev_slot, prev_slot);
}
if (prev_slot != -1 &&
NodeAt(r, prev_slot)->rank >= NodeAt(r, slot)->rank) {
ABSL_RAW_LOG(
FATAL, "Rank of slot %" PRId32 " is not above rank of slot %" PRId32,
slot, prev_slot);
}
if (++num_listed > num_slots) {
ABSL_RAW_LOG(FATAL, "Rank list has more entries than there are slots");
}
prev_slot = slot;
}
if (prev_slot != r->highest_slot || num_listed != num_slots) {
ABSL_RAW_LOG(FATAL, "Rank list has %" PRIu32 " of %" PRIu32 " slots",
num_listed, num_slots);
}
return true;
}
GraphId GraphCycles::GetId(void* ptr) {
int32_t i = rep_->ptrmap_.Find(ptr);
if (i != -1) {
return MakeId(i, rep_->nodes_[static_cast<uint32_t>(i)]->version);
} else if (rep_->free_nodes_.empty()) {
const int32_t slot = static_cast<int32_t>(rep_->nodes_.size());
Node* n =
new (base_internal::LowLevelAlloc::AllocWithArena(sizeof(Node), arena))
Node;
n->version = 1; // Avoid 0 since it is used by InvalidGraphId()
n->visited = false;
n->masked_ptr = base_internal::HidePtr(ptr);
n->nstack = 0;
n->priority = 0;
rep_->nodes_.push_back(n);
LinkBetween(rep_, slot, rep_->highest_slot, /*next_slot=*/-1);
rep_->ptrmap_.Add(ptr, slot);
return MakeId(slot, n->version);
} else {
// The slot keeps its rank (see "Rank allocation").
int32_t slot = rep_->free_nodes_.back();
rep_->free_nodes_.pop_back();
Node* n = rep_->nodes_[static_cast<uint32_t>(slot)];
n->masked_ptr = base_internal::HidePtr(ptr);
n->nstack = 0;
n->priority = 0;
rep_->ptrmap_.Add(ptr, slot);
return MakeId(slot, n->version);
}
}
void GraphCycles::RemoveNode(void* ptr) {
int32_t i = rep_->ptrmap_.Remove(ptr);
if (i == -1) {
return;
}
Node* x = rep_->nodes_[static_cast<uint32_t>(i)];
HASH_FOR_EACH(y, x->out) {
rep_->nodes_[static_cast<uint32_t>(y)]->in.erase(i);
}
HASH_FOR_EACH(y, x->in) {
rep_->nodes_[static_cast<uint32_t>(y)]->out.erase(i);
}
x->in.clear();
x->out.clear();
x->masked_ptr = base_internal::HidePtr<void>(nullptr);
if (x->version == std::numeric_limits<uint32_t>::max()) {
// Cannot use x any more
} else {
x->version++; // Invalidates all copies of node.
rep_->free_nodes_.push_back(i);
}
}
void* GraphCycles::Ptr(GraphId id) {
Node* n = FindNode(rep_, id);
return n == nullptr ? nullptr : base_internal::UnhidePtr<void>(n->masked_ptr);
}
bool GraphCycles::HasNode(GraphId node) {
return FindNode(rep_, node) != nullptr;
}
bool GraphCycles::HasEdge(GraphId x, GraphId y) const {
Node* xn = FindNode(rep_, x);
return xn && FindNode(rep_, y) && xn->out.contains(NodeIndex(y));
}
void GraphCycles::RemoveEdge(GraphId x, GraphId y) {
Node* xn = FindNode(rep_, x);
Node* yn = FindNode(rep_, y);
if (xn && yn) {
xn->out.erase(NodeIndex(y));
yn->in.erase(NodeIndex(x));
// No need to update the rank assignment since a previous valid
// rank assignment remains valid after an edge deletion.
}
}
static bool ForwardDFS(GraphCycles::Rep* r, int32_t n, int64_t upper_bound);
static void BackwardDFS(GraphCycles::Rep* r, int32_t n, int64_t lower_bound);
static void Reorder(GraphCycles::Rep* r);
static void Sort(const Vec<Node*>&, Vec<int32_t>* delta);
static void MoveToList(GraphCycles::Rep* r, Vec<int32_t>* src,
Vec<int32_t>* dst);
// TryMoveXBelowY() and TryMoveYAboveX() are called by InsertEdge(x, y) when
// rank(x) > rank(y). They try to restore the rank order by moving only x below
// y, or only y above x, which is much cheaper than the Pearce-Kelly search and
// reorder.
//
// x can move below y if all of x's in-neighbors rank below y. This also shows
// that the new edge does not close a cycle: every node reachable from y ranks
// at or above y, so a path from y to x would have to enter x from such a node.
// Similarly, y can move above x if all of y's out-neighbors rank above x.
//
// Each returns false, changing nothing, if the move is not possible or if
// there are more than kMaxSingleNodeDegree neighbors to check.
static bool TryMoveXBelowY(GraphCycles::Rep* r, int32_t x, int32_t y) {
Node* nx = NodeAt(r, x);
Node* ny = NodeAt(r, y);
int degree = 0;
HASH_FOR_EACH(u, nx->in) {
if (++degree > kMaxSingleNodeDegree) return false;
if (NodeAt(r, u)->rank >= ny->rank) return false;
}
Unlink(r, x);
if (degree == 0) {
// Move x below all other nodes.
LinkBetween(r, x, /*prev_slot=*/-1, r->lowest_slot);
} else {
// Move x right below y. Its in-neighbors rank below y, so they rank no
// higher than the node right below y.
LinkBetween(r, x, ny->prev_slot, y);
}
return true;
}
static bool TryMoveYAboveX(GraphCycles::Rep* r, int32_t x, int32_t y) {
Node* nx = NodeAt(r, x);
Node* ny = NodeAt(r, y);
int degree = 0;
HASH_FOR_EACH(w, ny->out) {
if (++degree > kMaxSingleNodeDegree) return false;
if (NodeAt(r, w)->rank <= nx->rank) return false;
}
Unlink(r, y);
if (degree == 0) {
// Move y above all other nodes.
LinkBetween(r, y, r->highest_slot, /*next_slot=*/-1);
} else {
// Move y right above x. Its out-neighbors rank above x, so they rank no
// lower than the node right above x.
LinkBetween(r, y, x, nx->next_slot);
}
return true;
}
bool GraphCycles::InsertEdge(GraphId idx, GraphId idy) {
Rep* r = rep_;
const int32_t x = NodeIndex(idx);
const int32_t y = NodeIndex(idy);
Node* nx = FindNode(r, idx);
Node* ny = FindNode(r, idy);
if (nx == nullptr || ny == nullptr) return true; // Expired ids
if (nx == ny) return false; // Self edge
if (!nx->out.insert(y)) {
// Edge already exists.
return true;
}
ny->in.insert(x);
if (nx->rank <= ny->rank) {
// New edge is consistent with existing rank assignment.
return true;
}
// Try moving just x or just y.
if (TryMoveXBelowY(r, x, y) || TryMoveYAboveX(r, x, y)) return true;
// Current rank assignments are incompatible with the new edge. Recompute.
// We only need to consider nodes that fall in the range [ny->rank,nx->rank].
if (!ForwardDFS(r, y, nx->rank)) {
// Found a cycle. Undo the insertion and tell caller.
nx->out.erase(y);
ny->in.erase(x);
// Since we do not call Reorder() on this path, clear any visited
// markers left by ForwardDFS.
for (const auto& d : r->deltaf_) {
r->nodes_[static_cast<uint32_t>(d)]->visited = false;
}
return false;
}
BackwardDFS(r, x, ny->rank);
Reorder(r);
return true;
}
static bool ForwardDFS(GraphCycles::Rep* r, int32_t n, int64_t upper_bound) {
// Avoid recursion since stack space might be limited.
// We instead keep a stack of nodes to visit.
r->deltaf_.clear();
r->stack_.clear();
r->stack_.push_back(n);
while (!r->stack_.empty()) {
n = r->stack_.back();
r->stack_.pop_back();
Node* nn = r->nodes_[static_cast<uint32_t>(n)];
if (nn->visited) continue;
nn->visited = true;
r->deltaf_.push_back(n);
HASH_FOR_EACH(w, nn->out) {
Node* nw = r->nodes_[static_cast<uint32_t>(w)];
if (nw->rank == upper_bound) {
return false; // Cycle
}
if (!nw->visited && nw->rank < upper_bound) {
r->stack_.push_back(w);
}
}
}
return true;
}
static void BackwardDFS(GraphCycles::Rep* r, int32_t n, int64_t lower_bound) {
r->deltab_.clear();
r->stack_.clear();
r->stack_.push_back(n);
while (!r->stack_.empty()) {
n = r->stack_.back();
r->stack_.pop_back();
Node* nn = r->nodes_[static_cast<uint32_t>(n)];
if (nn->visited) continue;
nn->visited = true;
r->deltab_.push_back(n);
HASH_FOR_EACH(w, nn->in) {
Node* nw = r->nodes_[static_cast<uint32_t>(w)];
if (!nw->visited && lower_bound < nw->rank) {
r->stack_.push_back(w);
}
}
}
}
static void Reorder(GraphCycles::Rep* r) {
Sort(r->nodes_, &r->deltab_);
Sort(r->nodes_, &r->deltaf_);
// The slots to reorder, sorted by their current ranks. In their new order
// (list_), they take over the same ranks and places in the rank list.
const uint32_t count = r->deltab_.size() + r->deltaf_.size();
r->sorted_slots.resize(count);
std::merge(r->deltab_.begin(), r->deltab_.end(), r->deltaf_.begin(),
r->deltaf_.end(), r->sorted_slots.begin(),
[r](int32_t a, int32_t b) {
return NodeAt(r, a)->rank < NodeAt(r, b)->rank;
});
// Adds contents of delta lists to list_ (backwards deltas first).
r->list_.clear();
MoveToList(r, &r->deltab_, &r->list_);
MoveToList(r, &r->deltaf_, &r->list_);
// Record the rank, prev_slot and next_slot of each place before changing
// them.
r->sorted_ranks.resize(count);
r->old_prev_slots.resize(count);
r->old_next_slots.resize(count);
for (uint32_t i = 0; i < count; i++) {
const Node* p = NodeAt(r, r->sorted_slots[i]);
r->sorted_ranks[i] = p->rank;
r->old_prev_slots[i] = p->prev_slot;
r->old_next_slots[i] = p->next_slot;
}
// Assign the ranks and places in order to the collected list. A prev_slot
// (or next_slot) that is also being reordered must be the previous (or next)
// place in sorted_slots, so link to the slot that now takes that place
// instead.
for (uint32_t i = 0; i < count; i++) {
const int32_t slot = r->list_[i];
NodeAt(r, slot)->rank = r->sorted_ranks[i];
if (i > 0 && r->old_prev_slots[i] == r->sorted_slots[i - 1]) {
Link(r, r->list_[i - 1], slot);
} else {
Link(r, r->old_prev_slots[i], slot);
}
if (i + 1 == count || r->old_next_slots[i] != r->sorted_slots[i + 1]) {
Link(r, slot, r->old_next_slots[i]);
}
}
}
static void Sort(const Vec<Node*>& nodes, Vec<int32_t>* delta) {
struct ByRank {
const Vec<Node*>* nodes;
bool operator()(int32_t a, int32_t b) const {
return (*nodes)[static_cast<uint32_t>(a)]->rank <
(*nodes)[static_cast<uint32_t>(b)]->rank;
}
};
ByRank cmp;
cmp.nodes = &nodes;
absl::c_sort(*delta, cmp);
}
static void MoveToList(GraphCycles::Rep* r, Vec<int32_t>* src,
Vec<int32_t>* dst) {
for (int32_t w : *src) {
// Prepare for future DFS calls
r->nodes_[static_cast<uint32_t>(w)]->visited = false;
dst->push_back(w);
}
}
int GraphCycles::FindPath(GraphId idx, GraphId idy, int max_path_len,
GraphId path[]) const {
Rep* r = rep_;
if (FindNode(r, idx) == nullptr || FindNode(r, idy) == nullptr) return 0;
const int32_t x = NodeIndex(idx);
const int32_t y = NodeIndex(idy);
// Forward depth first search starting at x until we hit y.
// As we descend into a node, we push it onto the path.
// As we leave a node, we remove it from the path.
int path_len = 0;
NodeSet seen;
r->stack_.clear();
r->stack_.push_back(x);
while (!r->stack_.empty()) {
int32_t n = r->stack_.back();
r->stack_.pop_back();
if (n < 0) {
// Marker to indicate that we are leaving a node
path_len--;
continue;
}
if (path_len < max_path_len) {
path[path_len] =
MakeId(n, rep_->nodes_[static_cast<uint32_t>(n)]->version);
}
path_len++;
r->stack_.push_back(-1); // Will remove tentative path entry
if (n == y) {
return path_len;
}
HASH_FOR_EACH(w, r->nodes_[static_cast<uint32_t>(n)]->out) {
if (seen.insert(w)) {
r->stack_.push_back(w);
}
}
}
return 0;
}
bool GraphCycles::IsReachable(GraphId x, GraphId y) const {
return FindPath(x, y, 0, nullptr) > 0;
}
void GraphCycles::UpdateStackTrace(GraphId id, int priority,
int (*get_stack_trace)(void** stack, int)) {
Node* n = FindNode(rep_, id);
if (n == nullptr || n->priority >= priority) {
return;
}
n->nstack =
(*get_stack_trace)(n->stack, static_cast<int>(std::size(n->stack)));
n->priority = priority;
}
int GraphCycles::GetStackTrace(GraphId id, void*** ptr) {
Node* n = FindNode(rep_, id);
if (n == nullptr) {
*ptr = nullptr;
return 0;
} else {
*ptr = n->stack;
return n->nstack;
}
}
} // namespace synchronization_internal
ABSL_NAMESPACE_END
} // namespace absl
#endif // ABSL_LOW_LEVEL_ALLOC_MISSING