886a904595
C++ algorithms have largely standardized on a [begin, end) half-open range, as seen in standard library containers. SkTQSort now adheres to this model, and takes vec.begin() and vec.end() as its inputs. To avoid confusion between inclusive and half-open ranges inside the implementation, internal helper functions now take "left" and "count" arguments instead of "left"/"right" or "begin"/"end". This avoids any ambiguity. (Although performance was not the main goal, this CL appears to slightly improve our sorting benchmark on my machine.) Change-Id: I5e96b6730be96cf23d001ee0915c69764b2c024a Reviewed-on: https://skia-review.googlesource.com/c/skia/+/302579 Reviewed-by: Mike Klein <mtklein@google.com> Commit-Queue: John Stiles <johnstiles@google.com>
491 lines
16 KiB
C++
491 lines
16 KiB
C++
/*
|
|
* Copyright 2018 Google Inc.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license that can be
|
|
* found in the LICENSE file.
|
|
*/
|
|
|
|
#include "include/core/SkRefCnt.h"
|
|
#include "src/core/SkTSort.h"
|
|
#include "tests/Test.h"
|
|
|
|
#include "tools/ToolUtils.h"
|
|
|
|
// A node in the graph. This corresponds to an opsTask in the MDB world.
|
|
class Node : public SkRefCnt {
|
|
public:
|
|
char id() const { return fID; }
|
|
int indexInSort() const { SkASSERT(fIndexInSort >= 0); return fIndexInSort; }
|
|
bool visited() const { return fVisited; }
|
|
|
|
#ifdef SK_DEBUG
|
|
void print() const {
|
|
SkDebugf("%d: id %c", fIndexInSort, fID);
|
|
if (fNodesIDependOn.count()) {
|
|
SkDebugf(" I depend on (%d): ", fNodesIDependOn.count());
|
|
for (Node* tmp : fNodesIDependOn) {
|
|
SkDebugf("%c, ", tmp->id());
|
|
}
|
|
}
|
|
if (fNodesThatDependOnMe.count()) {
|
|
SkDebugf(" (%d) depend on me: ", fNodesThatDependOnMe.count());
|
|
for (Node* tmp : fNodesThatDependOnMe) {
|
|
SkDebugf("%c, ", tmp->id());
|
|
}
|
|
}
|
|
SkDebugf("\n");
|
|
}
|
|
#endif
|
|
|
|
void validate(skiatest::Reporter* reporter) const {
|
|
for (Node* dependedOn : fNodesIDependOn) {
|
|
REPORTER_ASSERT(reporter, dependedOn->indexInSort() < this->indexInSort());
|
|
}
|
|
for (Node* dependent : fNodesThatDependOnMe) {
|
|
REPORTER_ASSERT(reporter, this->indexInSort() < dependent->indexInSort());
|
|
}
|
|
REPORTER_ASSERT(reporter, !fVisited); // this flag should only be true w/in depEdges()
|
|
}
|
|
|
|
static bool CompareIndicesGT(Node* const& a, Node* const& b) {
|
|
return a->indexInSort() > b->indexInSort();
|
|
}
|
|
|
|
int numDependents() const { return fNodesThatDependOnMe.count(); }
|
|
Node* dependent(int index) const {
|
|
SkASSERT(0 <= index && index < fNodesThatDependOnMe.count());
|
|
return fNodesThatDependOnMe[index];
|
|
}
|
|
|
|
private:
|
|
friend class Graph;
|
|
|
|
explicit Node(char id) : fID(id), fIndexInSort(-1), fVisited(false) {}
|
|
|
|
void setIndexInSort(int indexInSort) { fIndexInSort = indexInSort; }
|
|
void setVisited(bool visited) { fVisited = visited; }
|
|
void addDependency(Node* dependedOn) {
|
|
fNodesIDependOn.push_back(dependedOn);
|
|
|
|
dependedOn->addDependent(this);
|
|
}
|
|
void addDependent(Node* dependent) {
|
|
fNodesThatDependOnMe.push_back(dependent);
|
|
}
|
|
|
|
char fID;
|
|
SkTDArray<Node*> fNodesIDependOn; // These nodes must appear before this one in the sort
|
|
SkTDArray<Node*> fNodesThatDependOnMe; // These ones must appear after this one
|
|
int fIndexInSort;
|
|
bool fVisited; // only used in addEdges()
|
|
};
|
|
|
|
// The DAG driving the incremental topological sort. This corresponds to the opsTask DAG in
|
|
// the MDB world.
|
|
class Graph {
|
|
public:
|
|
Graph(int numNodesToReserve, skiatest::Reporter* reporter)
|
|
: fNodes(numNodesToReserve)
|
|
, fReporter(reporter) {
|
|
}
|
|
|
|
Node* addNode(uint32_t id) {
|
|
this->validate();
|
|
sk_sp<Node> tmp(new Node(id));
|
|
|
|
fNodes.push_back(tmp); // The graph gets the creation ref
|
|
tmp->setIndexInSort(fNodes.count()-1);
|
|
this->validate();
|
|
return tmp.get();
|
|
}
|
|
|
|
// 'dependedOn' must appear before 'dependent' in the sort
|
|
void addEdge(Node* dependedOn, Node* dependent) {
|
|
// TODO: this would be faster if all the SkTDArray code was stripped out of
|
|
// addEdges but, when used in MDB sorting, this entry point will never be used.
|
|
SkTDArray<Node*> tmp(&dependedOn, 1);
|
|
this->addEdges(&tmp, dependent);
|
|
}
|
|
|
|
// All the nodes in 'dependedOn' must appear before 'dependent' in the sort.
|
|
// This is O(v + e + cost_of_sorting(b)) where:
|
|
// v: number of nodes
|
|
// e: number of edges
|
|
// b: number of new edges in 'dependedOn'
|
|
//
|
|
// The algorithm works by first finding the "affected region" that contains all the
|
|
// nodes whose position in the topological sort is invalidated by the addition of the new
|
|
// edges. It then traverses the affected region from left to right, temporarily removing
|
|
// invalid nodes from 'fNodes' and shifting valid nodes left to fill in the gaps. In this
|
|
// left to right traversal, when a node is shifted to the left the current set of invalid
|
|
// nodes is examined to see if any needed to be moved to the right of that node. If so,
|
|
// they are reintroduced to the 'fNodes' array but now in the appropriate position. The
|
|
// separation of the algorithm into search (the dfs method) and readjustment (the shift
|
|
// method) means that each node affected by the new edges is only ever moved once.
|
|
void addEdges(SkTDArray<Node*>* dependedOn, Node* dependent) {
|
|
this->validate();
|
|
|
|
// remove any of the new dependencies that are already satisfied
|
|
for (int i = 0; i < dependedOn->count(); ++i) {
|
|
if ((*dependedOn)[i]->indexInSort() < dependent->indexInSort()) {
|
|
dependent->addDependency((*dependedOn)[i]);
|
|
dependedOn->removeShuffle(i);
|
|
i--;
|
|
} else {
|
|
dependent->addDependency((*dependedOn)[i]);
|
|
}
|
|
}
|
|
|
|
if (dependedOn->isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// Sort the remaining dependencies into descending order based on their indices in the
|
|
// sort. This means that we will be proceeding from right to left in the sort when
|
|
// correcting the order.
|
|
// TODO: QSort is waaay overkill here!
|
|
SkTQSort<Node*>(dependedOn->begin(), dependedOn->end(), Node::CompareIndicesGT);
|
|
|
|
// TODO: although this is the general algorithm, I think this can be simplified for our
|
|
// use case (i.e., the same dependent for all the new edges).
|
|
|
|
int lowerBound = fNodes.count(); // 'lowerBound' tracks the left of the affected region
|
|
for (int i = 0; i < dependedOn->count(); ++i) {
|
|
if ((*dependedOn)[i]->indexInSort() < lowerBound) {
|
|
this->shift(lowerBound);
|
|
}
|
|
|
|
if (!dependent->visited()) {
|
|
this->dfs(dependent, (*dependedOn)[i]->indexInSort());
|
|
}
|
|
|
|
lowerBound = std::min(dependent->indexInSort(), lowerBound);
|
|
}
|
|
|
|
this->shift(lowerBound);
|
|
|
|
this->validate();
|
|
}
|
|
|
|
// Get the list of node ids in the current sorted order
|
|
void getActual(SkString* actual) const {
|
|
this->validate();
|
|
|
|
for (int i = 0; i < fNodes.count(); ++i) {
|
|
(*actual) += fNodes[i]->id();
|
|
if (i < fNodes.count()-1) {
|
|
(*actual) += ',';
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef SK_DEBUG
|
|
void print() const {
|
|
SkDebugf("-------------------\n");
|
|
for (int i = 0; i < fNodes.count(); ++i) {
|
|
if (fNodes[i]) {
|
|
SkDebugf("%c ", fNodes[i]->id());
|
|
} else {
|
|
SkDebugf("0 ");
|
|
}
|
|
}
|
|
SkDebugf("\n");
|
|
|
|
for (int i = 0; i < fNodes.count(); ++i) {
|
|
if (fNodes[i]) {
|
|
fNodes[i]->print();
|
|
}
|
|
}
|
|
|
|
SkDebugf("Stack: ");
|
|
for (int i = 0; i < fStack.count(); ++i) {
|
|
SkDebugf("%c/%c ", fStack[i].fNode->id(), fStack[i].fDest->id());
|
|
}
|
|
SkDebugf("\n");
|
|
}
|
|
#endif
|
|
|
|
private:
|
|
void validate() const {
|
|
REPORTER_ASSERT(fReporter, fStack.empty());
|
|
|
|
for (int i = 0; i < fNodes.count(); ++i) {
|
|
REPORTER_ASSERT(fReporter, fNodes[i]->indexInSort() == i);
|
|
|
|
fNodes[i]->validate(fReporter);
|
|
}
|
|
|
|
// All the nodes in the Queue had better have been marked as visited
|
|
for (int i = 0; i < fStack.count(); ++i) {
|
|
SkASSERT(fStack[i].fNode->visited());
|
|
}
|
|
}
|
|
|
|
// Collect the nodes that need to be moved within the affected region. All the nodes found
|
|
// to be in violation of the topological constraints are placed in 'fStack'.
|
|
void dfs(Node* node, int upperBound) {
|
|
node->setVisited(true);
|
|
|
|
for (int i = 0; i < node->numDependents(); ++i) {
|
|
Node* dependent = node->dependent(i);
|
|
|
|
SkASSERT(dependent->indexInSort() != upperBound); // this would be a cycle
|
|
|
|
if (!dependent->visited() && dependent->indexInSort() < upperBound) {
|
|
this->dfs(dependent, upperBound);
|
|
}
|
|
}
|
|
|
|
fStack.push_back({ sk_ref_sp(node), fNodes[upperBound].get() });
|
|
}
|
|
|
|
// Move 'node' to the index-th slot of the sort. The index-th slot should not have a current
|
|
// occupant.
|
|
void moveNodeInSort(sk_sp<Node> node, int index) {
|
|
SkASSERT(!fNodes[index]);
|
|
fNodes[index] = node;
|
|
node->setIndexInSort(index);
|
|
}
|
|
|
|
#ifdef SK_DEBUG
|
|
// Does 'fStack' have 'node'? That is, was 'node' discovered to be in violation of the
|
|
// topological constraints?
|
|
bool stackContains(Node* node) {
|
|
for (int i = 0; i < fStack.count(); ++i) {
|
|
if (node == fStack[i].fNode.get()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
// The 'shift' method iterates through the affected area from left to right moving Nodes that
|
|
// were found to be in violation of the topological order (i.e., in 'fStack') to their correct
|
|
// locations and shifting the non-violating nodes left, into the holes the violating nodes left
|
|
// behind.
|
|
void shift(int index) {
|
|
int numRemoved = 0;
|
|
while (!fStack.empty()) {
|
|
sk_sp<Node> node = fNodes[index];
|
|
|
|
if (node->visited()) {
|
|
// This node is in 'fStack' and was found to be in violation of the topological
|
|
// constraints. Remove it from 'fNodes' so non-violating nodes can be shifted
|
|
// left.
|
|
SkASSERT(this->stackContains(node.get()));
|
|
node->setVisited(false); // reset for future use
|
|
fNodes[index] = nullptr;
|
|
numRemoved++;
|
|
} else {
|
|
// This node was found to not be in violation of any topological constraints but
|
|
// must be moved left to fill in for those that were.
|
|
SkASSERT(!this->stackContains(node.get()));
|
|
SkASSERT(numRemoved); // should be moving left
|
|
|
|
this->moveNodeInSort(node, index - numRemoved);
|
|
fNodes[index] = nullptr;
|
|
}
|
|
|
|
while (!fStack.empty() && node.get() == fStack.back().fDest) {
|
|
// The left to right loop has finally encountered the destination for one or more
|
|
// of the nodes in 'fStack'. Place them to the right of 'node' in the sort. Note
|
|
// that because the violating nodes were already removed from 'fNodes' there
|
|
// should be enough empty space for them to be placed now.
|
|
numRemoved--;
|
|
|
|
this->moveNodeInSort(fStack.back().fNode, index - numRemoved);
|
|
|
|
fStack.pop_back();
|
|
}
|
|
|
|
index++;
|
|
}
|
|
}
|
|
|
|
SkTArray<sk_sp<Node>> fNodes;
|
|
|
|
struct StackInfo {
|
|
sk_sp<Node> fNode; // This gets a ref bc, in 'shift' it will be pulled out of 'fNodes'
|
|
Node* fDest;
|
|
};
|
|
|
|
SkTArray<StackInfo> fStack; // only used in addEdges()
|
|
|
|
skiatest::Reporter* fReporter;
|
|
};
|
|
|
|
// This test adds a single invalidating edge.
|
|
static void test_1(skiatest::Reporter* reporter) {
|
|
Graph g(10, reporter);
|
|
|
|
Node* nodeQ = g.addNode('q');
|
|
Node* nodeY = g.addNode('y');
|
|
Node* nodeA = g.addNode('a');
|
|
Node* nodeZ = g.addNode('z');
|
|
Node* nodeB = g.addNode('b');
|
|
/*Node* nodeC =*/ g.addNode('c');
|
|
Node* nodeW = g.addNode('w');
|
|
Node* nodeD = g.addNode('d');
|
|
Node* nodeX = g.addNode('x');
|
|
Node* nodeR = g.addNode('r');
|
|
|
|
// All these edge are non-invalidating
|
|
g.addEdge(nodeD, nodeR);
|
|
g.addEdge(nodeZ, nodeW);
|
|
g.addEdge(nodeA, nodeB);
|
|
g.addEdge(nodeY, nodeZ);
|
|
g.addEdge(nodeQ, nodeA);
|
|
|
|
{
|
|
const SkString kExpectedInitialState("q,y,a,z,b,c,w,d,x,r");
|
|
SkString actualInitialState;
|
|
g.getActual(&actualInitialState);
|
|
REPORTER_ASSERT(reporter, kExpectedInitialState == actualInitialState);
|
|
}
|
|
|
|
// Add the invalidating edge
|
|
g.addEdge(nodeX, nodeY);
|
|
|
|
{
|
|
const SkString kExpectedFinalState("q,a,b,c,d,x,y,z,w,r");
|
|
SkString actualFinalState;
|
|
g.getActual(&actualFinalState);
|
|
REPORTER_ASSERT(reporter, kExpectedFinalState == actualFinalState);
|
|
}
|
|
}
|
|
|
|
// This test adds two invalidating edge sequentially
|
|
static void test_2(skiatest::Reporter* reporter) {
|
|
Graph g(10, reporter);
|
|
|
|
Node* nodeY = g.addNode('y');
|
|
/*Node* nodeA =*/ g.addNode('a');
|
|
Node* nodeW = g.addNode('w');
|
|
/*Node* nodeB =*/ g.addNode('b');
|
|
Node* nodeZ = g.addNode('z');
|
|
Node* nodeU = g.addNode('u');
|
|
/*Node* nodeC =*/ g.addNode('c');
|
|
Node* nodeX = g.addNode('x');
|
|
/*Node* nodeD =*/ g.addNode('d');
|
|
Node* nodeV = g.addNode('v');
|
|
|
|
// All these edge are non-invalidating
|
|
g.addEdge(nodeU, nodeX);
|
|
g.addEdge(nodeW, nodeU);
|
|
g.addEdge(nodeW, nodeZ);
|
|
g.addEdge(nodeY, nodeZ);
|
|
|
|
{
|
|
const SkString kExpectedInitialState("y,a,w,b,z,u,c,x,d,v");
|
|
SkString actualInitialState;
|
|
g.getActual(&actualInitialState);
|
|
REPORTER_ASSERT(reporter, kExpectedInitialState == actualInitialState);
|
|
}
|
|
|
|
// Add the first invalidating edge
|
|
g.addEdge(nodeX, nodeY);
|
|
|
|
{
|
|
const SkString kExpectedFirstState("a,w,b,u,c,x,y,z,d,v");
|
|
SkString actualFirstState;
|
|
g.getActual(&actualFirstState);
|
|
REPORTER_ASSERT(reporter, kExpectedFirstState == actualFirstState);
|
|
}
|
|
|
|
// Add the second invalidating edge
|
|
g.addEdge(nodeV, nodeW);
|
|
|
|
{
|
|
const SkString kExpectedSecondState("a,b,c,d,v,w,u,x,y,z");
|
|
SkString actualSecondState;
|
|
g.getActual(&actualSecondState);
|
|
REPORTER_ASSERT(reporter, kExpectedSecondState == actualSecondState);
|
|
}
|
|
}
|
|
|
|
static void test_diamond(skiatest::Reporter* reporter) {
|
|
Graph g(4, reporter);
|
|
|
|
/* Create the graph (the '.' is the pointy side of the arrow):
|
|
* b
|
|
* . \
|
|
* / .
|
|
* a d
|
|
* \ .
|
|
* . /
|
|
* c
|
|
* Possible topological orders are [a,c,b,d] and [a,b,c,d].
|
|
*/
|
|
|
|
Node* nodeD = g.addNode('d');
|
|
Node* nodeC = g.addNode('c');
|
|
Node* nodeB = g.addNode('b');
|
|
|
|
{
|
|
SkTDArray<Node*> dependedOn;
|
|
dependedOn.push_back(nodeB);
|
|
dependedOn.push_back(nodeC);
|
|
|
|
g.addEdges(&dependedOn, nodeD); // nodes B and C must come before node D
|
|
}
|
|
|
|
Node* nodeA = g.addNode('a');
|
|
g.addEdge(nodeA, nodeB); // node A must come before node B
|
|
g.addEdge(nodeA, nodeC); // node A must come before node C
|
|
|
|
const SkString kExpected0("a,c,b,d");
|
|
const SkString kExpected1("a,b,c,d");
|
|
SkString actual;
|
|
g.getActual(&actual);
|
|
|
|
REPORTER_ASSERT(reporter, kExpected0 == actual || kExpected1 == actual);
|
|
}
|
|
|
|
static void test_lopsided_binary_tree(skiatest::Reporter* reporter) {
|
|
Graph g(7, reporter);
|
|
|
|
/* Create the graph (the '.' is the pointy side of the arrow):
|
|
* a
|
|
* / \
|
|
* . .
|
|
* b c
|
|
* / \
|
|
* . .
|
|
* d e
|
|
* / \
|
|
* . .
|
|
* f g
|
|
*
|
|
* Possible topological order is: [a,b,c,d,e,f,g].
|
|
*/
|
|
|
|
Node* nodeG = g.addNode('g');
|
|
Node* nodeF = g.addNode('f');
|
|
Node* nodeE = g.addNode('e');
|
|
Node* nodeD = g.addNode('d');
|
|
Node* nodeC = g.addNode('c');
|
|
Node* nodeB = g.addNode('b');
|
|
Node* nodeA = g.addNode('a');
|
|
|
|
g.addEdge(nodeE, nodeG);
|
|
g.addEdge(nodeE, nodeF);
|
|
g.addEdge(nodeC, nodeE);
|
|
g.addEdge(nodeC, nodeD);
|
|
g.addEdge(nodeA, nodeC);
|
|
g.addEdge(nodeA, nodeB);
|
|
|
|
const SkString kExpected("a,b,c,d,e,f,g");
|
|
SkString actual;
|
|
g.getActual(&actual);
|
|
|
|
REPORTER_ASSERT(reporter, kExpected == actual);
|
|
}
|
|
|
|
DEF_TEST(IncrTopoSort, reporter) {
|
|
test_1(reporter);
|
|
test_2(reporter);
|
|
test_diamond(reporter);
|
|
test_lopsided_binary_tree(reporter);
|
|
}
|