xref: /aosp_15_r20/external/skia/tests/IncrTopoSortTest.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1 /*
2  * Copyright 2018 Google Inc.
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include "include/core/SkRefCnt.h"
9 #include "include/core/SkString.h"
10 #include "include/core/SkTypes.h"
11 #include "include/private/base/SkDebug.h"
12 #include "include/private/base/SkTArray.h"
13 #include "include/private/base/SkTDArray.h"
14 #include "src/base/SkTSort.h"
15 #include "tests/Test.h"
16 
17 #include <algorithm>
18 #include <cstdint>
19 
20 using namespace skia_private;
21 
22 // A node in the graph. This corresponds to an opsTask in the MDB world.
23 class Node : public SkRefCnt {
24 public:
id() const25     char id() const { return fID; }
indexInSort() const26     int indexInSort() const { SkASSERT(fIndexInSort >= 0); return fIndexInSort; }
visited() const27     bool visited() const { return fVisited; }
28 
29 #ifdef SK_DEBUG
print() const30     void print() const {
31         SkDebugf("%d: id %c", fIndexInSort, fID);
32         if (fNodesIDependOn.size()) {
33             SkDebugf(" I depend on (%d): ", fNodesIDependOn.size());
34             for (Node* tmp : fNodesIDependOn) {
35                 SkDebugf("%c, ", tmp->id());
36             }
37         }
38         if (fNodesThatDependOnMe.size()) {
39             SkDebugf(" (%d) depend on me: ", fNodesThatDependOnMe.size());
40             for (Node* tmp : fNodesThatDependOnMe) {
41                 SkDebugf("%c, ", tmp->id());
42             }
43         }
44         SkDebugf("\n");
45     }
46 #endif
47 
validate(skiatest::Reporter * reporter) const48     void validate(skiatest::Reporter* reporter) const {
49         for (Node* dependedOn : fNodesIDependOn) {
50             REPORTER_ASSERT(reporter, dependedOn->indexInSort() < this->indexInSort());
51         }
52         for (Node* dependent : fNodesThatDependOnMe) {
53             REPORTER_ASSERT(reporter, this->indexInSort() < dependent->indexInSort());
54         }
55         REPORTER_ASSERT(reporter, !fVisited); // this flag should only be true w/in depEdges()
56     }
57 
CompareIndicesGT(Node * const & a,Node * const & b)58     static bool CompareIndicesGT(Node* const& a, Node* const& b) {
59         return a->indexInSort() > b->indexInSort();
60     }
61 
numDependents() const62     int numDependents() const { return fNodesThatDependOnMe.size(); }
dependent(int index) const63     Node* dependent(int index) const {
64         SkASSERT(0 <= index && index < fNodesThatDependOnMe.size());
65         return fNodesThatDependOnMe[index];
66     }
67 
68 private:
69     friend class Graph;
70 
Node(char id)71     explicit Node(char id) : fID(id), fIndexInSort(-1), fVisited(false) {}
72 
setIndexInSort(int indexInSort)73     void setIndexInSort(int indexInSort) { fIndexInSort = indexInSort; }
setVisited(bool visited)74     void setVisited(bool visited) { fVisited = visited; }
addDependency(Node * dependedOn)75     void addDependency(Node* dependedOn) {
76         fNodesIDependOn.push_back(dependedOn);
77 
78         dependedOn->addDependent(this);
79     }
addDependent(Node * dependent)80     void addDependent(Node* dependent) {
81         fNodesThatDependOnMe.push_back(dependent);
82     }
83 
84     char             fID;
85     SkTDArray<Node*> fNodesIDependOn;      // These nodes must appear before this one in the sort
86     SkTDArray<Node*> fNodesThatDependOnMe; // These ones must appear after this one
87     int              fIndexInSort;
88     bool             fVisited;             // only used in addEdges()
89 };
90 
91 // The DAG driving the incremental topological sort. This corresponds to the opsTask DAG in
92 // the MDB world.
93 class Graph {
94 public:
Graph(int numNodesToReserve,skiatest::Reporter * reporter)95     Graph(int numNodesToReserve, skiatest::Reporter* reporter)
96             : fNodes(numNodesToReserve)
97             , fReporter(reporter) {
98     }
99 
addNode(uint32_t id)100     Node* addNode(uint32_t id) {
101         this->validate();
102         sk_sp<Node> tmp(new Node(id));
103 
104         fNodes.push_back(tmp);       // The graph gets the creation ref
105         tmp->setIndexInSort(fNodes.size()-1);
106         this->validate();
107         return tmp.get();
108     }
109 
110     // 'dependedOn' must appear before 'dependent' in the sort
addEdge(Node * dependedOn,Node * dependent)111     void addEdge(Node* dependedOn, Node* dependent) {
112         // TODO: this would be faster if all the SkTDArray code was stripped out of
113         // addEdges but, when used in MDB sorting, this entry point will never be used.
114         SkTDArray<Node*> tmp(&dependedOn, 1);
115         this->addEdges(&tmp, dependent);
116     }
117 
118     // All the nodes in 'dependedOn' must appear before 'dependent' in the sort.
119     // This is O(v + e + cost_of_sorting(b)) where:
120     //    v: number of nodes
121     //    e: number of edges
122     //    b: number of new edges in 'dependedOn'
123     //
124     // The algorithm works by first finding the "affected region" that contains all the
125     // nodes whose position in the topological sort is invalidated by the addition of the new
126     // edges. It then traverses the affected region from left to right, temporarily removing
127     // invalid nodes from 'fNodes' and shifting valid nodes left to fill in the gaps. In this
128     // left to right traversal, when a node is shifted to the left the current set of invalid
129     // nodes is examined to see if any needed to be moved to the right of that node. If so,
130     // they are reintroduced to the 'fNodes' array but now in the appropriate position. The
131     // separation of the algorithm into search (the dfs method) and readjustment (the shift
132     // method) means that each node affected by the new edges is only ever moved once.
addEdges(SkTDArray<Node * > * dependedOn,Node * dependent)133     void addEdges(SkTDArray<Node*>* dependedOn, Node* dependent) {
134         this->validate();
135 
136         // remove any of the new dependencies that are already satisfied
137         for (int i = 0; i < dependedOn->size(); ++i) {
138             if ((*dependedOn)[i]->indexInSort() < dependent->indexInSort()) {
139                 dependent->addDependency((*dependedOn)[i]);
140                 dependedOn->removeShuffle(i);
141                 i--;
142             } else {
143                 dependent->addDependency((*dependedOn)[i]);
144             }
145         }
146 
147         if (dependedOn->empty()) {
148             return;
149         }
150 
151         // Sort the remaining dependencies into descending order based on their indices in the
152         // sort. This means that we will be proceeding from right to left in the sort when
153         // correcting the order.
154         // TODO: QSort is waaay overkill here!
155         SkTQSort<Node*>(dependedOn->begin(), dependedOn->end(), Node::CompareIndicesGT);
156 
157         // TODO: although this is the general algorithm, I think this can be simplified for our
158         // use case (i.e., the same dependent for all the new edges).
159 
160         int lowerBound = fNodes.size();  // 'lowerBound' tracks the left of the affected region
161         for (int i = 0; i < dependedOn->size(); ++i) {
162             if ((*dependedOn)[i]->indexInSort() < lowerBound) {
163                 this->shift(lowerBound);
164             }
165 
166             if (!dependent->visited()) {
167                 this->dfs(dependent, (*dependedOn)[i]->indexInSort());
168             }
169 
170             lowerBound = std::min(dependent->indexInSort(), lowerBound);
171         }
172 
173         this->shift(lowerBound);
174 
175         this->validate();
176     }
177 
178     // Get the list of node ids in the current sorted order
getActual(SkString * actual) const179     void getActual(SkString* actual) const {
180         this->validate();
181 
182         for (int i = 0; i < fNodes.size(); ++i) {
183             (*actual) += fNodes[i]->id();
184             if (i < fNodes.size()-1) {
185                 (*actual) += ',';
186             }
187         }
188     }
189 
190 #ifdef SK_DEBUG
print() const191     void print() const {
192         SkDebugf("-------------------\n");
193         for (int i = 0; i < fNodes.size(); ++i) {
194             if (fNodes[i]) {
195                 SkDebugf("%c ", fNodes[i]->id());
196             } else {
197                 SkDebugf("0 ");
198             }
199         }
200         SkDebugf("\n");
201 
202         for (int i = 0; i < fNodes.size(); ++i) {
203             if (fNodes[i]) {
204                 fNodes[i]->print();
205             }
206         }
207 
208         SkDebugf("Stack: ");
209         for (int i = 0; i < fStack.size(); ++i) {
210            SkDebugf("%c/%c ", fStack[i].fNode->id(), fStack[i].fDest->id());
211         }
212         SkDebugf("\n");
213     }
214 #endif
215 
216 private:
validate() const217     void validate() const {
218         REPORTER_ASSERT(fReporter, fStack.empty());
219 
220         for (int i = 0; i < fNodes.size(); ++i) {
221             REPORTER_ASSERT(fReporter, fNodes[i]->indexInSort() == i);
222 
223             fNodes[i]->validate(fReporter);
224         }
225 
226         // All the nodes in the Queue had better have been marked as visited
227         for (int i = 0; i < fStack.size(); ++i) {
228             SkASSERT(fStack[i].fNode->visited());
229         }
230     }
231 
232     // Collect the nodes that need to be moved within the affected region. All the nodes found
233     // to be in violation of the topological constraints are placed in 'fStack'.
dfs(Node * node,int upperBound)234     void dfs(Node* node, int upperBound) {
235         node->setVisited(true);
236 
237         for (int i = 0; i < node->numDependents(); ++i) {
238             Node* dependent = node->dependent(i);
239 
240             SkASSERT(dependent->indexInSort() != upperBound); // this would be a cycle
241 
242             if (!dependent->visited() && dependent->indexInSort() < upperBound) {
243                 this->dfs(dependent, upperBound);
244             }
245         }
246 
247         fStack.push_back({ sk_ref_sp(node), fNodes[upperBound].get() });
248     }
249 
250     // Move 'node' to the index-th slot of the sort. The index-th slot should not have a current
251     // occupant.
moveNodeInSort(const sk_sp<Node> & node,int index)252     void moveNodeInSort(const sk_sp<Node>& node, int index) {
253         SkASSERT(!fNodes[index]);
254         fNodes[index] = node;
255         node->setIndexInSort(index);
256     }
257 
258 #ifdef SK_DEBUG
259     // Does 'fStack' have 'node'? That is, was 'node' discovered to be in violation of the
260     // topological constraints?
stackContains(Node * node)261     bool stackContains(Node* node) {
262         for (int i = 0; i < fStack.size(); ++i) {
263             if (node == fStack[i].fNode.get()) {
264                 return true;
265             }
266         }
267         return false;
268     }
269 #endif
270 
271     // The 'shift' method iterates through the affected area from left to right moving Nodes that
272     // were found to be in violation of the topological order (i.e., in 'fStack') to their correct
273     // locations and shifting the non-violating nodes left, into the holes the violating nodes left
274     // behind.
shift(int index)275     void shift(int index) {
276         int numRemoved = 0;
277         while (!fStack.empty()) {
278             sk_sp<Node> node = fNodes[index];
279 
280             if (node->visited()) {
281                 // This node is in 'fStack' and was found to be in violation of the topological
282                 // constraints. Remove it from 'fNodes' so non-violating nodes can be shifted
283                 // left.
284                 SkASSERT(this->stackContains(node.get()));
285                 node->setVisited(false);  // reset for future use
286                 fNodes[index] = nullptr;
287                 numRemoved++;
288             } else {
289                 // This node was found to not be in violation of any topological constraints but
290                 // must be moved left to fill in for those that were.
291                 SkASSERT(!this->stackContains(node.get()));
292                 SkASSERT(numRemoved); // should be moving left
293 
294                 this->moveNodeInSort(node, index - numRemoved);
295                 fNodes[index] = nullptr;
296             }
297 
298             while (!fStack.empty() && node.get() == fStack.back().fDest) {
299                 // The left to right loop has finally encountered the destination for one or more
300                 // of the nodes in 'fStack'. Place them to the right of 'node' in the sort. Note
301                 // that because the violating nodes were already removed from 'fNodes' there
302                 // should be enough empty space for them to be placed now.
303                 numRemoved--;
304 
305                 this->moveNodeInSort(fStack.back().fNode, index - numRemoved);
306 
307                 fStack.pop_back();
308             }
309 
310             index++;
311         }
312     }
313 
314     TArray<sk_sp<Node>> fNodes;
315 
316     struct StackInfo {
317         sk_sp<Node> fNode;  // This gets a ref bc, in 'shift' it will be pulled out of 'fNodes'
318         Node*       fDest;
319     };
320 
321     TArray<StackInfo>   fStack;     // only used in addEdges()
322 
323     skiatest::Reporter*   fReporter;
324 };
325 
326 // This test adds a single invalidating edge.
test_1(skiatest::Reporter * reporter)327 static void test_1(skiatest::Reporter* reporter) {
328     Graph g(10, reporter);
329 
330     Node* nodeQ = g.addNode('q');
331     Node* nodeY = g.addNode('y');
332     Node* nodeA = g.addNode('a');
333     Node* nodeZ = g.addNode('z');
334     Node* nodeB = g.addNode('b');
335     /*Node* nodeC =*/ g.addNode('c');
336     Node* nodeW = g.addNode('w');
337     Node* nodeD = g.addNode('d');
338     Node* nodeX = g.addNode('x');
339     Node* nodeR = g.addNode('r');
340 
341     // All these edge are non-invalidating
342     g.addEdge(nodeD, nodeR);
343     g.addEdge(nodeZ, nodeW);
344     g.addEdge(nodeA, nodeB);
345     g.addEdge(nodeY, nodeZ);
346     g.addEdge(nodeQ, nodeA);
347 
348     {
349         const SkString kExpectedInitialState("q,y,a,z,b,c,w,d,x,r");
350         SkString actualInitialState;
351         g.getActual(&actualInitialState);
352         REPORTER_ASSERT(reporter, kExpectedInitialState == actualInitialState);
353     }
354 
355     // Add the invalidating edge
356     g.addEdge(nodeX, nodeY);
357 
358     {
359         const SkString kExpectedFinalState("q,a,b,c,d,x,y,z,w,r");
360         SkString actualFinalState;
361         g.getActual(&actualFinalState);
362         REPORTER_ASSERT(reporter, kExpectedFinalState == actualFinalState);
363     }
364 }
365 
366 // This test adds two invalidating edge sequentially
test_2(skiatest::Reporter * reporter)367 static void test_2(skiatest::Reporter* reporter) {
368     Graph g(10, reporter);
369 
370     Node* nodeY = g.addNode('y');
371     /*Node* nodeA =*/ g.addNode('a');
372     Node* nodeW = g.addNode('w');
373     /*Node* nodeB =*/ g.addNode('b');
374     Node* nodeZ = g.addNode('z');
375     Node* nodeU = g.addNode('u');
376     /*Node* nodeC =*/ g.addNode('c');
377     Node* nodeX = g.addNode('x');
378     /*Node* nodeD =*/ g.addNode('d');
379     Node* nodeV = g.addNode('v');
380 
381     // All these edge are non-invalidating
382     g.addEdge(nodeU, nodeX);
383     g.addEdge(nodeW, nodeU);
384     g.addEdge(nodeW, nodeZ);
385     g.addEdge(nodeY, nodeZ);
386 
387     {
388         const SkString kExpectedInitialState("y,a,w,b,z,u,c,x,d,v");
389         SkString actualInitialState;
390         g.getActual(&actualInitialState);
391         REPORTER_ASSERT(reporter, kExpectedInitialState == actualInitialState);
392     }
393 
394     // Add the first invalidating edge
395     g.addEdge(nodeX, nodeY);
396 
397     {
398         const SkString kExpectedFirstState("a,w,b,u,c,x,y,z,d,v");
399         SkString actualFirstState;
400         g.getActual(&actualFirstState);
401         REPORTER_ASSERT(reporter, kExpectedFirstState == actualFirstState);
402     }
403 
404     // Add the second invalidating edge
405     g.addEdge(nodeV, nodeW);
406 
407     {
408         const SkString kExpectedSecondState("a,b,c,d,v,w,u,x,y,z");
409         SkString actualSecondState;
410         g.getActual(&actualSecondState);
411         REPORTER_ASSERT(reporter, kExpectedSecondState == actualSecondState);
412     }
413 }
414 
test_diamond(skiatest::Reporter * reporter)415 static void test_diamond(skiatest::Reporter* reporter) {
416     Graph g(4, reporter);
417 
418     /* Create the graph (the '.' is the pointy side of the arrow):
419      *        b
420      *      .    \
421      *     /      .
422      *   a          d
423      *     \      .
424      *       .   /
425      *         c
426      * Possible topological orders are [a,c,b,d] and [a,b,c,d].
427      */
428 
429     Node* nodeD = g.addNode('d');
430     Node* nodeC = g.addNode('c');
431     Node* nodeB = g.addNode('b');
432 
433     {
434         SkTDArray<Node*> dependedOn;
435         dependedOn.push_back(nodeB);
436         dependedOn.push_back(nodeC);
437 
438         g.addEdges(&dependedOn, nodeD); // nodes B and C must come before node D
439     }
440 
441     Node* nodeA = g.addNode('a');
442     g.addEdge(nodeA, nodeB);            // node A must come before node B
443     g.addEdge(nodeA, nodeC);            // node A must come before node C
444 
445     const SkString kExpected0("a,c,b,d");
446     const SkString kExpected1("a,b,c,d");
447     SkString actual;
448     g.getActual(&actual);
449 
450     REPORTER_ASSERT(reporter, kExpected0 == actual || kExpected1 == actual);
451 }
452 
test_lopsided_binary_tree(skiatest::Reporter * reporter)453 static void test_lopsided_binary_tree(skiatest::Reporter* reporter) {
454     Graph g(7, reporter);
455 
456     /* Create the graph (the '.' is the pointy side of the arrow):
457      *        a
458      *       /  \
459      *      .    .
460      *     b      c
461      *           /  \
462      *          .    .
463      *         d      e
464      *               /  \
465      *              .    .
466      *             f      g
467      *
468      * Possible topological order is: [a,b,c,d,e,f,g].
469      */
470 
471     Node* nodeG = g.addNode('g');
472     Node* nodeF = g.addNode('f');
473     Node* nodeE = g.addNode('e');
474     Node* nodeD = g.addNode('d');
475     Node* nodeC = g.addNode('c');
476     Node* nodeB = g.addNode('b');
477     Node* nodeA = g.addNode('a');
478 
479     g.addEdge(nodeE, nodeG);
480     g.addEdge(nodeE, nodeF);
481     g.addEdge(nodeC, nodeE);
482     g.addEdge(nodeC, nodeD);
483     g.addEdge(nodeA, nodeC);
484     g.addEdge(nodeA, nodeB);
485 
486     const SkString kExpected("a,b,c,d,e,f,g");
487     SkString actual;
488     g.getActual(&actual);
489 
490     REPORTER_ASSERT(reporter, kExpected == actual);
491 }
492 
DEF_TEST(IncrTopoSort,reporter)493 DEF_TEST(IncrTopoSort, reporter) {
494     test_1(reporter);
495     test_2(reporter);
496     test_diamond(reporter);
497     test_lopsided_binary_tree(reporter);
498 }
499