diff --git a/js/src/Makefile.in b/js/src/Makefile.in index 0fa1cd658c6..cbca4656273 100644 --- a/js/src/Makefile.in +++ b/js/src/Makefile.in @@ -146,6 +146,7 @@ CPPSRCS = \ Memory.cpp \ Statistics.cpp \ StoreBuffer.cpp \ + FindSCCs.cpp \ StringBuffer.cpp \ Unicode.cpp \ Xdr.cpp \ diff --git a/js/src/gc/FindSCCs.cpp b/js/src/gc/FindSCCs.cpp new file mode 100644 index 00000000000..505279fe2ed --- /dev/null +++ b/js/src/gc/FindSCCs.cpp @@ -0,0 +1,176 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + */ +/* 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/. */ + +#include "FindSCCs.h" + +#include "jsfriendapi.h" + +namespace js { +namespace gc { + +ComponentFinder::ComponentFinder(uintptr_t sl) + : clock(1), + stack(NULL), + firstComponent(NULL), + cur(NULL), + stackLimit(sl), + stackFull(false) +{ +} + +ComponentFinder::~ComponentFinder() +{ + JS_ASSERT(!stack); + JS_ASSERT(!firstComponent); +} + +void +ComponentFinder::addNode(GraphNodeBase *v) +{ + if (v->gcDiscoveryTime == Undefined) { + JS_ASSERT(v->gcLowLink == Undefined); + JS_ASSERT(!v->gcNextGraphNode); + processNode(v); + } +} + +void +ComponentFinder::checkStackFull() +{ + /* + * Check for exceeding the size of the C stack. + * + * If this happens we give up and return all vertices in one group, by + * pushing them onto the output list with lowLink set to 1. + */ + + if (!stackFull) { + int stackDummy; + if (!JS_CHECK_STACK_SIZE(stackLimit, &stackDummy)) + stackFull = true; + } + + if (stackFull) { + GraphNodeBase *w; + while (stack) { + w = stack; + stack = w->gcNextGraphNode; + + w->gcLowLink = 1; + w->gcNextGraphNode = firstComponent; + firstComponent = w; + } + } +} + +void +ComponentFinder::processNode(GraphNodeBase *v) +{ + v->gcDiscoveryTime = clock; + v->gcLowLink = clock; + ++clock; + + JS_ASSERT(!v->gcNextGraphNode); + v->gcNextGraphNode = stack; + stack = v; + + checkStackFull(); + if (stackFull) + return; + + GraphNodeBase *old = cur; + cur = v; + cur->findOutgoingEdges(*this); + cur = old; + + if (stackFull) { + JS_ASSERT(!stack); + return; + } + + if (v->gcLowLink == v->gcDiscoveryTime) { + GraphNodeBase *w; + do { + JS_ASSERT(stack); + w = stack; + stack = w->gcNextGraphNode; + + /* + * Record the elements of a component by setting all their gcLowLink + * fields to the same value. + */ + w->gcLowLink = v->gcDiscoveryTime; + + /* + * Record that the element is no longer on the stack by setting the + * discovery time to a special value that's not Undefined. + */ + w->gcDiscoveryTime = Finished; + + /* + * Prepend the component to the beginning of the output list to + * reverse the list and achieve the desired order. + */ + w->gcNextGraphNode = firstComponent; + firstComponent = w; + } while (w != v); + } +} + +void +ComponentFinder::addEdgeTo(GraphNodeBase *w) +{ + if (w->gcDiscoveryTime == Undefined) { + processNode(w); + cur->gcLowLink = Min(cur->gcLowLink, w->gcLowLink); + } else if (w->gcDiscoveryTime != Finished) { + cur->gcLowLink = Min(cur->gcLowLink, w->gcDiscoveryTime); + } +} + +GraphNodeBase * +ComponentFinder::getResultsList() +{ + JS_ASSERT(!stack); + GraphNodeBase *result = firstComponent; + firstComponent = NULL; + return result; +} + +GraphNodeBase * +ComponentFinder::removeFirstGroup(GraphNodeBase *resultsList) +{ + /* Remove the first group from resultsList and return the new list head. */ + + JS_ASSERT(resultsList); + + GraphNodeBase *v = resultsList; + unsigned lowLink = v->gcLowLink; + + GraphNodeBase *last; + do { + v->gcDiscoveryTime = Undefined; + v->gcLowLink = Undefined; + last = v; + v = v->gcNextGraphNode; + } + while (v && v->gcLowLink == lowLink); + + last->gcNextGraphNode = NULL; + return v; +} + +void +ComponentFinder::removeAllRemaining(GraphNodeBase *resultsList) +{ + for (GraphNodeBase *v = resultsList; v; v = v->gcNextGraphNode) { + v->gcDiscoveryTime = Undefined; + v->gcLowLink = Undefined; + } +} + +} /* namespace gc */ +} /* namespace js */ diff --git a/js/src/gc/FindSCCs.h b/js/src/gc/FindSCCs.h new file mode 100644 index 00000000000..571f77b737d --- /dev/null +++ b/js/src/gc/FindSCCs.h @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + */ +/* 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 gc_findsccs_h___ +#define gc_findsccs_h___ + +#include "jsutil.h" + +namespace js { +namespace gc { + +class ComponentFinder; + +struct GraphNodeBase { + GraphNodeBase *gcNextGraphNode; + unsigned gcDiscoveryTime; + unsigned gcLowLink; + + GraphNodeBase() + : gcNextGraphNode(NULL), + gcDiscoveryTime(0), + gcLowLink(0) {} + + virtual ~GraphNodeBase() {} + virtual void findOutgoingEdges(ComponentFinder& finder) = 0; +}; + +template static T * +NextGraphNode(const T *current) +{ + const GraphNodeBase *node = current; + return static_cast(node->gcNextGraphNode); +} + +template void +AddGraphNode(T *&listHead, T *newFirstNode) +{ + GraphNodeBase *node = newFirstNode; + JS_ASSERT(!node->gcNextGraphNode); + node->gcNextGraphNode = listHead; + listHead = newFirstNode; +} + +template static T * +RemoveGraphNode(T *&listHead) +{ + GraphNodeBase *node = listHead; + if (!node) + return NULL; + + T *result = listHead; + listHead = static_cast(node->gcNextGraphNode); + node->gcNextGraphNode = NULL; + return result; +} + +/* + * Find the strongly connected components of a graph using Tarjan's algorithm, + * and return them in topological order. + * + * Nodes derive from GraphNodeBase and implement findGraphEdges, which calls + * finder.addEdgeTo to describe the outgoing edges from that node: + * + * struct MyGraphNode : public GraphNodeBase + * { + * void findOutgoingEdges(ComponentFinder& finder) + * { + * for edge in my_outgoing_edges: + * if is_relevant(edge): + * finder.addEdgeTo(edge.destination) + * } + * } + */ +class ComponentFinder +{ + public: + ComponentFinder(uintptr_t stackLimit); + ~ComponentFinder(); + void addNode(GraphNodeBase *v); + GraphNodeBase *getResultsList(); + + template static T * + getNextGroup(T *&resultsList) { + T *group = resultsList; + if (resultsList) + resultsList = static_cast(removeFirstGroup(resultsList)); + return group; + } + + template static T * + getAllRemaining(T *&resultsList) { + T *all = resultsList; + removeAllRemaining(resultsList); + resultsList = NULL; + return all; + } + + private: + static GraphNodeBase *removeFirstGroup(GraphNodeBase *resultsList); + static void removeAllRemaining(GraphNodeBase *resultsList); + + public: + /* Call from implementation of GraphNodeBase::findOutgoingEdges(). */ + void addEdgeTo(GraphNodeBase *w); + + private: + /* Constant used to indicate an unprocessed vertex. */ + static const unsigned Undefined = 0; + + /* Constant used to indicate an processed vertex that is no longer on the stack. */ + static const unsigned Finished = (unsigned)-1; + + void processNode(GraphNodeBase *v); + void checkStackFull(); + + private: + unsigned clock; + GraphNodeBase *stack; + GraphNodeBase *firstComponent; + GraphNodeBase *cur; + uintptr_t stackLimit; + bool stackFull; +}; + +} /* namespace gc */ +} /* namespace js */ + +#endif /* gc_findsccs_h___ */ diff --git a/js/src/jsapi-tests/Makefile.in b/js/src/jsapi-tests/Makefile.in index 51f3260f2be..3c0a6b5c98b 100644 --- a/js/src/jsapi-tests/Makefile.in +++ b/js/src/jsapi-tests/Makefile.in @@ -67,6 +67,7 @@ CPPSRCS = \ testJSEvaluateScript.cpp \ testErrorCopying.cpp \ testEnclosingFunction.cpp \ + testFindSCCs.cpp \ $(NULL) # Disabled: an entirely unrelated test seems to cause this to fail. Moreover, diff --git a/js/src/jsapi-tests/testFindSCCs.cpp b/js/src/jsapi-tests/testFindSCCs.cpp new file mode 100644 index 00000000000..9df1e781ec6 --- /dev/null +++ b/js/src/jsapi-tests/testFindSCCs.cpp @@ -0,0 +1,263 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sw=4 et 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/. */ + + +#include "tests.h" +#include +#include + +#include "../gc/FindSCCs.h" +#include "jscntxt.h" +#include "jsgc.h" + +static const unsigned MaxVertices = 10; + +using js::gc::GraphNodeBase; +using js::gc::ComponentFinder; + +struct TestNode : public GraphNodeBase +{ + unsigned index; + bool hasEdge[MaxVertices]; + + void findOutgoingEdges(ComponentFinder& finder); +}; + +static TestNode Vertex[MaxVertices]; + +void +TestNode::findOutgoingEdges(ComponentFinder& finder) +{ + for (unsigned i = 0; i < MaxVertices; ++i) { + if (hasEdge[i]) + finder.addEdgeTo(&Vertex[i]); + } +} + +BEGIN_TEST(testFindSCCs) +{ + // no vertices + + setup(0); + run(); + CHECK(end()); + + // no edges + + setup(1); + run(); + CHECK(group(0, -1)); + CHECK(end()); + + setup(3); + run(); + CHECK(group(2, -1)); + CHECK(group(1, -1)); + CHECK(group(0, -1)); + CHECK(end()); + + // linear + + setup(3); + edge(0, 1); + edge(1, 2); + run(); + CHECK(group(0, -1)); + CHECK(group(1, -1)); + CHECK(group(2, -1)); + CHECK(end()); + + // tree + + setup(3); + edge(0, 1); + edge(0, 2); + run(); + CHECK(group(0, -1)); + CHECK(group(2, -1)); + CHECK(group(1, -1)); + CHECK(end()); + + // cycles + + setup(3); + edge(0, 1); + edge(1, 2); + edge(2, 0); + run(); + CHECK(group(0, 1, 2, -1)); + CHECK(end()); + + setup(4); + edge(0, 1); + edge(1, 2); + edge(2, 1); + edge(2, 3); + run(); + CHECK(group(0, -1)); + CHECK(group(1, 2, -1)); + CHECK(group(3, -1)); + CHECK(end()); + + // remaining + + setup(2); + edge(0, 1); + run(); + CHECK(remaining(0, 1, -1)); + CHECK(end()); + + setup(2); + edge(0, 1); + run(); + CHECK(group(0, -1)); + CHECK(remaining(1, -1)); + CHECK(end()); + + setup(2); + edge(0, 1); + run(); + CHECK(group(0, -1)); + CHECK(group(1, -1)); + CHECK(remaining(-1)); + CHECK(end()); + + return true; +} + +unsigned vertex_count; +ComponentFinder *finder; +GraphNodeBase *resultsList; + +void setup(unsigned count) +{ + vertex_count = count; + for (unsigned i = 0; i < MaxVertices; ++i) { + TestNode &v = Vertex[i]; + v.gcNextGraphNode = NULL; + v.index = i; + memset(&v.hasEdge, 0, sizeof(v.hasEdge)); + } +} + +void edge(unsigned src_index, unsigned dest_index) +{ + Vertex[src_index].hasEdge[dest_index] = true; +} + +void run() +{ + finder = new ComponentFinder(rt->nativeStackLimit); + for (unsigned i = 0; i < vertex_count; ++i) + finder->addNode(&Vertex[i]); + resultsList = finder->getResultsList(); +} + +bool group(int vertex, ...) +{ + TestNode *v = (TestNode *)ComponentFinder::getNextGroup(resultsList); + + va_list ap; + va_start(ap, vertex); + while (vertex != -1) { + CHECK(v != NULL); + CHECK(v->index == vertex); + v = (TestNode *)v->gcNextGraphNode; + vertex = va_arg(ap, int); + } + va_end(ap); + + CHECK(v == NULL); + return true; +} + +bool remaining(int vertex, ...) +{ + TestNode *v = (TestNode *)ComponentFinder::getAllRemaining(resultsList); + + va_list ap; + va_start(ap, vertex); + while (vertex != -1) { + CHECK(v != NULL); + CHECK(v->index == vertex); + v = (TestNode *)v->gcNextGraphNode; + vertex = va_arg(ap, int); + } + va_end(ap); + + CHECK(v == NULL); + return true; +} + +bool end() +{ + CHECK(resultsList == NULL); + + delete finder; + finder = NULL; + return true; +} +END_TEST(testFindSCCs) + +struct TestNode2 : public GraphNodeBase +{ + TestNode2 *edge; + + TestNode2() : + edge(NULL) + { + } + + void + findOutgoingEdges(ComponentFinder& finder) + { + if (edge) + finder.addEdgeTo(edge); + } +}; + +BEGIN_TEST(testFindSCCsStackLimit) +{ + /* + * Test what happens if recusion causes the stack to become full while + * traversing the graph. + * + * The test case is a large number of vertices, almost all of which are + * arranged in a linear chain. The last few are left unlinked to exercise + * adding vertices after the stack full condition has already been detected. + * + * Such an arrangement with no cycles would normally result in one group for + * each vertex, but since the stack is exhasted in processing a single group + * is returned containing all the vertices. + */ + const unsigned max = 1000000; + + TestNode2 *vertices = new TestNode2[max](); + for (unsigned i = 0; i < (max - 10); ++i) + vertices[i].edge = &vertices[i + 1]; + + ComponentFinder finder(rt->nativeStackLimit); + for (unsigned i = 0; i < max; ++i) + finder.addNode(&vertices[i]); + + GraphNodeBase *r = finder.getResultsList(); + CHECK(r); + GraphNodeBase *v = finder.getNextGroup(r); + CHECK(v); + + unsigned count = 0; + while (v) { + ++count; + v = v->gcNextGraphNode; + } + CHECK(count == max); + CHECK(finder.getNextGroup(r) == NULL); + + delete [] vertices; + return true; +} +END_TEST(testFindSCCsStackLimit)