blob: 70548e400e74a062193823a2c5a68de9e9d28117 [file] [log] [blame]
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
* vim: set ts=8 sts=4 et sw=4 tw=99:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef js_UbiNodeDominatorTree_h
#define js_UbiNodeDominatorTree_h
#include "mozilla/DebugOnly.h"
#include "mozilla/Maybe.h"
#include "mozilla/Move.h"
#include "mozilla/UniquePtr.h"
#include "jsalloc.h"
#include "js/UbiNode.h"
#include "js/UbiNodePostOrder.h"
#include "js/Utility.h"
#include "js/Vector.h"
namespace JS {
namespace ubi {
/**
* In a directed graph with a root node `R`, a node `A` is said to "dominate" a
* node `B` iff every path from `R` to `B` contains `A`. A node `A` is said to
* be the "immediate dominator" of a node `B` iff it dominates `B`, is not `B`
* itself, and does not dominate any other nodes which also dominate `B` in
* turn.
*
* If we take every node from a graph `G` and create a new graph `T` with edges
* to each node from its immediate dominator, then `T` is a tree (each node has
* only one immediate dominator, or none if it is the root). This tree is called
* a "dominator tree".
*
* This class represents a dominator tree constructed from a `JS::ubi::Node`
* heap graph. The domination relationship and dominator trees are useful tools
* for analyzing heap graphs because they tell you:
*
* - Exactly what could be reclaimed by the GC if some node `A` became
* unreachable: those nodes which are dominated by `A`,
*
* - The "retained size" of a node in the heap graph, in contrast to its
* "shallow size". The "shallow size" is the space taken by a node itself,
* not counting anything it references. The "retained size" of a node is its
* shallow size plus the size of all the things that would be collected if
* the original node wasn't (directly or indirectly) referencing them. In
* other words, the retained size is the shallow size of a node plus the
* shallow sizes of every other node it dominates. For example, the root
* node in a binary tree might have a small shallow size that does not take
* up much space itself, but it dominates the rest of the binary tree and
* its retained size is therefore significant (assuming no external
* references into the tree).
*
* The simple, engineered algorithm presented in "A Simple, Fast Dominance
* Algorithm" by Cooper el al[0] is used to find dominators and construct the
* dominator tree. This algorithm runs in O(n^2) time, but is faster in practice
* than alternative algorithms with better theoretical running times, such as
* Lengauer-Tarjan which runs in O(e * log(n)). The big caveat to that statement
* is that Cooper et al found it is faster in practice *on control flow graphs*
* and I'm not convinced that this property also holds on *heap* graphs. That
* said, the implementation of this algorithm is *much* simpler than
* Lengauer-Tarjan and has been found to be fast enough at least for the time
* being.
*
* [0]: http://www.cs.rice.edu/~keith/EMBED/dom.pdf
*/
class JS_PUBLIC_API(DominatorTree)
{
private:
// Types.
using NodeSet = js::HashSet<Node, js::DefaultHasher<Node>, js::SystemAllocPolicy>;
using NodeSetPtr = mozilla::UniquePtr<NodeSet, JS::DeletePolicy<NodeSet>>;
using PredecessorSets = js::HashMap<Node, NodeSetPtr, js::DefaultHasher<Node>,
js::SystemAllocPolicy>;
using NodeToIndexMap = js::HashMap<Node, uint32_t, js::DefaultHasher<Node>,
js::SystemAllocPolicy>;
class DominatedSets;
public:
class DominatedSetRange;
/**
* A pointer to an immediately dominated node.
*
* Don't use this type directly; it is no safer than regular pointers. This
* is only for use indirectly with range-based for loops and
* `DominatedSetRange`.
*
* @see JS::ubi::DominatorTree::getDominatedSet
*/
class DominatedNodePtr
{
friend class DominatedSetRange;
const mozilla::Vector<Node>& postOrder;
const uint32_t* ptr;
DominatedNodePtr(const mozilla::Vector<Node>& postOrder, const uint32_t* ptr)
: postOrder(postOrder)
, ptr(ptr)
{ }
public:
bool operator!=(const DominatedNodePtr& rhs) const { return ptr != rhs.ptr; }
void operator++() { ptr++; }
const Node& operator*() const { return postOrder[*ptr]; }
};
/**
* A range of immediately dominated `JS::ubi::Node`s for use with
* range-based for loops.
*
* @see JS::ubi::DominatorTree::getDominatedSet
*/
class DominatedSetRange
{
friend class DominatedSets;
const mozilla::Vector<Node>& postOrder;
const uint32_t* beginPtr;
const uint32_t* endPtr;
DominatedSetRange(mozilla::Vector<Node>& postOrder, const uint32_t* begin, const uint32_t* end)
: postOrder(postOrder)
, beginPtr(begin)
, endPtr(end)
{
MOZ_ASSERT(begin <= end);
}
public:
DominatedNodePtr begin() const {
MOZ_ASSERT(beginPtr <= endPtr);
return DominatedNodePtr(postOrder, beginPtr);
}
DominatedNodePtr end() const {
return DominatedNodePtr(postOrder, endPtr);
}
size_t length() const {
MOZ_ASSERT(beginPtr <= endPtr);
return endPtr - beginPtr;
}
/**
* Safely skip ahead `n` dominators in the range, in O(1) time.
*
* Example usage:
*
* mozilla::Maybe<DominatedSetRange> range = myDominatorTree.getDominatedSet(myNode);
* if (range.isNothing()) {
* // Handle unknown nodes however you see fit...
* return false;
* }
*
* // Don't care about the first ten, for whatever reason.
* range->skip(10);
* for (const JS::ubi::Node& dominatedNode : *range) {
* // ...
* }
*/
void skip(size_t n) {
beginPtr += n;
if (beginPtr > endPtr)
beginPtr = endPtr;
}
};
private:
/**
* The set of all dominated sets in a dominator tree.
*
* Internally stores the sets in a contiguous array, with a side table of
* indices into that contiguous array to denote the start index of each
* individual set.
*/
class DominatedSets
{
mozilla::Vector<uint32_t> dominated;
mozilla::Vector<uint32_t> indices;
DominatedSets(mozilla::Vector<uint32_t>&& dominated, mozilla::Vector<uint32_t>&& indices)
: dominated(mozilla::Move(dominated))
, indices(mozilla::Move(indices))
{ }
public:
// DominatedSets is not copy-able.
DominatedSets(const DominatedSets& rhs) = delete;
DominatedSets& operator=(const DominatedSets& rhs) = delete;
// DominatedSets is move-able.
DominatedSets(DominatedSets&& rhs)
: dominated(mozilla::Move(rhs.dominated))
, indices(mozilla::Move(rhs.indices))
{
MOZ_ASSERT(this != &rhs, "self-move not allowed");
}
DominatedSets& operator=(DominatedSets&& rhs) {
this->~DominatedSets();
new (this) DominatedSets(mozilla::Move(rhs));
return *this;
}
/**
* Create the DominatedSets given the mapping of a node index to its
* immediate dominator. Returns `Some` on success, `Nothing` on OOM
* failure.
*/
static mozilla::Maybe<DominatedSets> Create(const mozilla::Vector<uint32_t>& doms) {
auto length = doms.length();
MOZ_ASSERT(length < UINT32_MAX);
// Create a vector `dominated` holding a flattened set of buckets of
// immediately dominated children nodes, with a lookup table
// `indices` mapping from each node to the beginning of its bucket.
//
// This has three phases:
//
// 1. Iterate over the full set of nodes and count up the size of
// each bucket. These bucket sizes are temporarily stored in the
// `indices` vector.
//
// 2. Convert the `indices` vector to store the cumulative sum of
// the sizes of all buckets before each index, resulting in a
// mapping from node index to one past the end of that node's
// bucket.
//
// 3. Iterate over the full set of nodes again, filling in bucket
// entries from the end of the bucket's range to its
// beginning. This decrements each index as a bucket entry is
// filled in. After having filled in all of a bucket's entries,
// the index points to the start of the bucket.
mozilla::Vector<uint32_t> dominated;
mozilla::Vector<uint32_t> indices;
if (!dominated.growBy(length) || !indices.growBy(length))
return mozilla::Nothing();
// 1
memset(indices.begin(), 0, length * sizeof(uint32_t));
for (uint32_t i = 0; i < length; i++)
indices[doms[i]]++;
// 2
uint32_t sumOfSizes = 0;
for (uint32_t i = 0; i < length; i++) {
sumOfSizes += indices[i];
MOZ_ASSERT(sumOfSizes <= length);
indices[i] = sumOfSizes;
}
// 3
for (uint32_t i = 0; i < length; i++) {
auto idxOfDom = doms[i];
indices[idxOfDom]--;
dominated[indices[idxOfDom]] = i;
}
#ifdef DEBUG
// Assert that our buckets are non-overlapping and don't run off the
// end of the vector.
uint32_t lastIndex = 0;
for (uint32_t i = 0; i < length; i++) {
MOZ_ASSERT(indices[i] >= lastIndex);
MOZ_ASSERT(indices[i] < length);
lastIndex = indices[i];
}
#endif
return mozilla::Some(DominatedSets(mozilla::Move(dominated), mozilla::Move(indices)));
}
/**
* Get the set of nodes immediately dominated by the node at
* `postOrder[nodeIndex]`.
*/
DominatedSetRange dominatedSet(mozilla::Vector<Node>& postOrder, uint32_t nodeIndex) const {
MOZ_ASSERT(postOrder.length() == indices.length());
MOZ_ASSERT(nodeIndex < indices.length());
auto end = nodeIndex == indices.length() - 1
? dominated.end()
: &dominated[indices[nodeIndex + 1]];
return DominatedSetRange(postOrder, &dominated[indices[nodeIndex]], end);
}
};
private:
// Data members.
mozilla::Vector<Node> postOrder;
NodeToIndexMap nodeToPostOrderIndex;
mozilla::Vector<uint32_t> doms;
DominatedSets dominatedSets;
mozilla::Maybe<mozilla::Vector<JS::ubi::Node::Size>> retainedSizes;
private:
// We use `UNDEFINED` as a sentinel value in the `doms` vector to signal
// that we haven't found any dominators for the node at the corresponding
// index in `postOrder` yet.
static const uint32_t UNDEFINED = UINT32_MAX;
DominatorTree(mozilla::Vector<Node>&& postOrder, NodeToIndexMap&& nodeToPostOrderIndex,
mozilla::Vector<uint32_t>&& doms, DominatedSets&& dominatedSets)
: postOrder(mozilla::Move(postOrder))
, nodeToPostOrderIndex(mozilla::Move(nodeToPostOrderIndex))
, doms(mozilla::Move(doms))
, dominatedSets(mozilla::Move(dominatedSets))
, retainedSizes(mozilla::Nothing())
{ }
static uint32_t intersect(mozilla::Vector<uint32_t>& doms, uint32_t finger1, uint32_t finger2) {
while (finger1 != finger2) {
if (finger1 < finger2)
finger1 = doms[finger1];
else if (finger2 < finger1)
finger2 = doms[finger2];
}
return finger1;
}
// Do the post order traversal of the heap graph and populate our
// predecessor sets.
static bool doTraversal(JSRuntime* rt, AutoCheckCannotGC& noGC, const Node& root,
mozilla::Vector<Node>& postOrder, PredecessorSets& predecessorSets) {
uint32_t nodeCount = 0;
auto onNode = [&](const Node& node) {
nodeCount++;
if (MOZ_UNLIKELY(nodeCount == UINT32_MAX))
return false;
return postOrder.append(node);
};
auto onEdge = [&](const Node& origin, const Edge& edge) {
auto p = predecessorSets.lookupForAdd(edge.referent);
if (!p) {
mozilla::UniquePtr<NodeSet, DeletePolicy<NodeSet>> set(js_new<NodeSet>());
if (!set ||
!set->init() ||
!predecessorSets.add(p, edge.referent, mozilla::Move(set)))
{
return false;
}
}
MOZ_ASSERT(p && p->value());
return p->value()->put(origin);
};
PostOrder traversal(rt, noGC);
return traversal.init() &&
traversal.addStart(root) &&
traversal.traverse(onNode, onEdge);
}
// Populates the given `map` with an entry for each node to its index in
// `postOrder`.
static bool mapNodesToTheirIndices(mozilla::Vector<Node>& postOrder, NodeToIndexMap& map) {
MOZ_ASSERT(!map.initialized());
MOZ_ASSERT(postOrder.length() < UINT32_MAX);
uint32_t length = postOrder.length();
if (!map.init(length))
return false;
for (uint32_t i = 0; i < length; i++)
map.putNewInfallible(postOrder[i], i);
return true;
}
// Convert the Node -> NodeSet predecessorSets to a index -> Vector<index>
// form.
static bool convertPredecessorSetsToVectors(
const Node& root,
mozilla::Vector<Node>& postOrder,
PredecessorSets& predecessorSets,
NodeToIndexMap& nodeToPostOrderIndex,
mozilla::Vector<mozilla::Vector<uint32_t>>& predecessorVectors)
{
MOZ_ASSERT(postOrder.length() < UINT32_MAX);
uint32_t length = postOrder.length();
MOZ_ASSERT(predecessorVectors.length() == 0);
if (!predecessorVectors.growBy(length))
return false;
for (uint32_t i = 0; i < length - 1; i++) {
auto& node = postOrder[i];
MOZ_ASSERT(node != root,
"Only the last node should be root, since this was a post order traversal.");
auto ptr = predecessorSets.lookup(node);
MOZ_ASSERT(ptr,
"Because this isn't the root, it had better have predecessors, or else how "
"did we even find it.");
auto& predecessors = ptr->value();
if (!predecessorVectors[i].reserve(predecessors->count()))
return false;
for (auto range = predecessors->all(); !range.empty(); range.popFront()) {
auto ptr = nodeToPostOrderIndex.lookup(range.front());
MOZ_ASSERT(ptr);
predecessorVectors[i].infallibleAppend(ptr->value());
}
}
predecessorSets.finish();
return true;
}
// Initialize `doms` such that the immediate dominator of the `root` is the
// `root` itself and all others are `UNDEFINED`.
static bool initializeDominators(mozilla::Vector<uint32_t>& doms, uint32_t length) {
MOZ_ASSERT(doms.length() == 0);
if (!doms.growByUninitialized(length))
return false;
doms[length - 1] = length - 1;
for (uint32_t i = 0; i < length - 1; i++)
doms[i] = UNDEFINED;
return true;
}
void assertSanity() const {
MOZ_ASSERT(postOrder.length() == doms.length());
MOZ_ASSERT(postOrder.length() == nodeToPostOrderIndex.count());
MOZ_ASSERT_IF(retainedSizes.isSome(), postOrder.length() == retainedSizes->length());
}
bool computeRetainedSizes(mozilla::MallocSizeOf mallocSizeOf) {
MOZ_ASSERT(retainedSizes.isNothing());
auto length = postOrder.length();
retainedSizes.emplace();
if (!retainedSizes->growBy(length)) {
retainedSizes = mozilla::Nothing();
return false;
}
// Iterate in forward order so that we know all of a node's children in
// the dominator tree have already had their retained size
// computed. Then we can simply say that the retained size of a node is
// its shallow size (JS::ubi::Node::size) plus the retained sizes of its
// immediate children in the tree.
for (uint32_t i = 0; i < length; i++) {
auto size = postOrder[i].size(mallocSizeOf);
for (const auto& dominated : dominatedSets.dominatedSet(postOrder, i)) {
// The root node dominates itself, but shouldn't contribute to
// its own retained size.
if (dominated == postOrder[length - 1]) {
MOZ_ASSERT(i == length - 1);
continue;
}
auto ptr = nodeToPostOrderIndex.lookup(dominated);
MOZ_ASSERT(ptr);
auto idxOfDominated = ptr->value();
MOZ_ASSERT(idxOfDominated < i);
size += retainedSizes.ref()[idxOfDominated];
}
retainedSizes.ref()[i] = size;
}
return true;
}
public:
// DominatorTree is not copy-able.
DominatorTree(const DominatorTree&) = delete;
DominatorTree& operator=(const DominatorTree&) = delete;
// DominatorTree is move-able.
DominatorTree(DominatorTree&& rhs)
: postOrder(mozilla::Move(rhs.postOrder))
, nodeToPostOrderIndex(mozilla::Move(rhs.nodeToPostOrderIndex))
, doms(mozilla::Move(rhs.doms))
, dominatedSets(mozilla::Move(rhs.dominatedSets))
, retainedSizes(mozilla::Move(rhs.retainedSizes))
{
MOZ_ASSERT(this != &rhs, "self-move is not allowed");
}
DominatorTree& operator=(DominatorTree&& rhs) {
this->~DominatorTree();
new (this) DominatorTree(mozilla::Move(rhs));
return *this;
}
/**
* Construct a `DominatorTree` of the heap graph visible from `root`. The
* `root` is also used as the root of the resulting dominator tree.
*
* The resulting `DominatorTree` instance must not outlive the
* `JS::ubi::Node` graph it was constructed from.
*
* - For `JS::ubi::Node` graphs backed by the live heap graph, this means
* that the `DominatorTree`'s lifetime _must_ be contained within the
* scope of the provided `AutoCheckCannotGC` reference because a GC will
* invalidate the nodes.
*
* - For `JS::ubi::Node` graphs backed by some other offline structure
* provided by the embedder, the resulting `DominatorTree`'s lifetime is
* bounded by that offline structure's lifetime.
*
* In practice, this means that within SpiderMonkey we must treat
* `DominatorTree` as if it were backed by the live heap graph and trust
* that embedders with knowledge of the graph's implementation will do the
* Right Thing.
*
* Returns `mozilla::Nothing()` on OOM failure. It is the caller's
* responsibility to handle and report the OOM.
*/
static mozilla::Maybe<DominatorTree>
Create(JSRuntime* rt, AutoCheckCannotGC& noGC, const Node& root) {
mozilla::Vector<Node> postOrder;
PredecessorSets predecessorSets;
if (!predecessorSets.init() || !doTraversal(rt, noGC, root, postOrder, predecessorSets))
return mozilla::Nothing();
MOZ_ASSERT(postOrder.length() < UINT32_MAX);
uint32_t length = postOrder.length();
MOZ_ASSERT(postOrder[length - 1] == root);
// From here on out we wish to avoid hash table lookups, and we use
// indices into `postOrder` instead of actual nodes wherever
// possible. This greatly improves the performance of this
// implementation, but we have to pay a little bit of upfront cost to
// convert our data structures to play along first.
NodeToIndexMap nodeToPostOrderIndex;
if (!mapNodesToTheirIndices(postOrder, nodeToPostOrderIndex))
return mozilla::Nothing();
mozilla::Vector<mozilla::Vector<uint32_t>> predecessorVectors;
if (!convertPredecessorSetsToVectors(root, postOrder, predecessorSets, nodeToPostOrderIndex,
predecessorVectors))
return mozilla::Nothing();
mozilla::Vector<uint32_t> doms;
if (!initializeDominators(doms, length))
return mozilla::Nothing();
bool changed = true;
while (changed) {
changed = false;
// Iterate over the non-root nodes in reverse post order.
for (uint32_t indexPlusOne = length - 1; indexPlusOne > 0; indexPlusOne--) {
MOZ_ASSERT(postOrder[indexPlusOne - 1] != root);
// Take the intersection of every predecessor's dominator set;
// that is the current best guess at the immediate dominator for
// this node.
uint32_t newIDomIdx = UNDEFINED;
auto& predecessors = predecessorVectors[indexPlusOne - 1];
auto range = predecessors.all();
for ( ; !range.empty(); range.popFront()) {
auto idx = range.front();
if (doms[idx] != UNDEFINED) {
newIDomIdx = idx;
break;
}
}
MOZ_ASSERT(newIDomIdx != UNDEFINED,
"Because the root is initialized to dominate itself and is the first "
"node in every path, there must exist a predecessor to this node that "
"also has a dominator.");
for ( ; !range.empty(); range.popFront()) {
auto idx = range.front();
if (doms[idx] != UNDEFINED)
newIDomIdx = intersect(doms, newIDomIdx, idx);
}
// If the immediate dominator changed, we will have to do
// another pass of the outer while loop to continue the forward
// dataflow.
if (newIDomIdx != doms[indexPlusOne - 1]) {
doms[indexPlusOne - 1] = newIDomIdx;
changed = true;
}
}
}
auto maybeDominatedSets = DominatedSets::Create(doms);
if (maybeDominatedSets.isNothing())
return mozilla::Nothing();
return mozilla::Some(DominatorTree(mozilla::Move(postOrder),
mozilla::Move(nodeToPostOrderIndex),
mozilla::Move(doms),
mozilla::Move(*maybeDominatedSets)));
}
/**
* Get the root node for this dominator tree.
*/
const Node& root() const {
return postOrder[postOrder.length() - 1];
}
/**
* Return the immediate dominator of the given `node`. If `node` was not
* reachable from the `root` that this dominator tree was constructed from,
* then return the null `JS::ubi::Node`.
*/
Node getImmediateDominator(const Node& node) const {
assertSanity();
auto ptr = nodeToPostOrderIndex.lookup(node);
if (!ptr)
return Node();
auto idx = ptr->value();
MOZ_ASSERT(idx < postOrder.length());
return postOrder[doms[idx]];
}
/**
* Get the set of nodes immediately dominated by the given `node`. If `node`
* is not a member of this dominator tree, return `Nothing`.
*
* Example usage:
*
* mozilla::Maybe<DominatedSetRange> range = myDominatorTree.getDominatedSet(myNode);
* if (range.isNothing()) {
* // Handle unknown node however you see fit...
* return false;
* }
*
* for (const JS::ubi::Node& dominatedNode : *range) {
* // Do something with each immediately dominated node...
* }
*/
mozilla::Maybe<DominatedSetRange> getDominatedSet(const Node& node) {
assertSanity();
auto ptr = nodeToPostOrderIndex.lookup(node);
if (!ptr)
return mozilla::Nothing();
auto idx = ptr->value();
MOZ_ASSERT(idx < postOrder.length());
return mozilla::Some(dominatedSets.dominatedSet(postOrder, idx));
}
/**
* Get the retained size of the given `node`. The size is placed in
* `outSize`, or 0 if `node` is not a member of the dominator tree. Returns
* false on OOM failure, leaving `outSize` unchanged.
*/
bool getRetainedSize(const Node& node, mozilla::MallocSizeOf mallocSizeOf,
Node::Size& outSize) {
assertSanity();
auto ptr = nodeToPostOrderIndex.lookup(node);
if (!ptr) {
outSize = 0;
return true;
}
if (retainedSizes.isNothing() && !computeRetainedSizes(mallocSizeOf))
return false;
auto idx = ptr->value();
MOZ_ASSERT(idx < postOrder.length());
outSize = retainedSizes.ref()[idx];
return true;
}
};
} // namespace ubi
} // namespace JS
#endif // js_UbiNodeDominatorTree_h