Index: third_party/scons/scons-local/SCons/Taskmaster.py |
=================================================================== |
--- third_party/scons/scons-local/SCons/Taskmaster.py (revision 7505) |
+++ third_party/scons/scons-local/SCons/Taskmaster.py (working copy) |
@@ -48,7 +48,7 @@ |
target(s) that it decides need to be evaluated and/or built. |
""" |
-__revision__ = "src/engine/SCons/Taskmaster.py 3603 2008/10/10 05:46:45 scons" |
+__revision__ = "src/engine/SCons/Taskmaster.py 3842 2008/12/20 22:59:52 scons" |
from itertools import chain |
import operator |
@@ -138,6 +138,10 @@ |
self.node = node |
self.exc_clear() |
+ def trace_message(self, method, node, description='node'): |
+ fmt = '%-20s %s %s\n' |
+ return fmt % (method + ':', description, self.tm.trace_node(node)) |
+ |
def display(self, message): |
""" |
Hook to allow the calling interface to display a message. |
@@ -159,6 +163,8 @@ |
unlink underlying files and make all necessary directories before |
the Action is actually called to build the targets. |
""" |
+ T = self.tm.trace |
+ if T: T.write(self.trace_message('Task.prepare()', self.node)) |
# Now that it's the appropriate time, give the TaskMaster a |
# chance to raise any exceptions it encountered while preparing |
@@ -209,6 +215,8 @@ |
so only do thread safe stuff here. Do thread unsafe stuff in |
prepare(), executed() or failed(). |
""" |
+ T = self.tm.trace |
+ if T: T.write(self.trace_message('Task.execute()', self.node)) |
try: |
everything_was_cached = 1 |
@@ -225,9 +233,11 @@ |
raise |
except SCons.Errors.BuildError: |
raise |
- except: |
- raise SCons.Errors.TaskmasterException(self.targets[0], |
- sys.exc_info()) |
+ except Exception, e: |
+ buildError = SCons.Errors.convert_to_BuildError(e) |
+ buildError.node = self.targets[0] |
+ buildError.exc_info = sys.exc_info() |
+ raise buildError |
def executed_without_callbacks(self): |
""" |
@@ -235,6 +245,10 @@ |
and the Taskmaster instance doesn't want to call |
the Node's callback methods. |
""" |
+ T = self.tm.trace |
+ if T: T.write(self.trace_message('Task.executed_without_callbacks()', |
+ self.node)) |
+ |
for t in self.targets: |
if t.get_state() == NODE_EXECUTING: |
for side_effect in t.side_effects: |
@@ -254,6 +268,10 @@ |
post-visit actions that must take place regardless of whether |
or not the target was an actual built target or a source Node. |
""" |
+ T = self.tm.trace |
+ if T: T.write(self.trace_message('Task.executed_with_callbacks()', |
+ self.node)) |
+ |
for t in self.targets: |
if t.get_state() == NODE_EXECUTING: |
for side_effect in t.side_effects: |
@@ -267,17 +285,30 @@ |
def failed(self): |
""" |
Default action when a task fails: stop the build. |
+ |
+ Note: Although this function is normally invoked on nodes in |
+ the executing state, it might also be invoked on up-to-date |
+ nodes when using Configure(). |
""" |
self.fail_stop() |
def fail_stop(self): |
""" |
Explicit stop-the-build failure. |
+ |
+ This sets failure status on the target nodes and all of |
+ their dependent parent nodes. |
+ |
+ Note: Although this function is normally invoked on nodes in |
+ the executing state, it might also be invoked on up-to-date |
+ nodes when using Configure(). |
""" |
- |
+ T = self.tm.trace |
+ if T: T.write(self.trace_message('Task.failed_stop()', self.node)) |
+ |
# Invoke will_not_build() to clean-up the pending children |
# list. |
- self.tm.will_not_build(self.targets) |
+ self.tm.will_not_build(self.targets, lambda n: n.set_state(NODE_FAILED)) |
# Tell the taskmaster to not start any new tasks |
self.tm.stop() |
@@ -294,9 +325,16 @@ |
This sets failure status on the target nodes and all of |
their dependent parent nodes. |
+ |
+ Note: Although this function is normally invoked on nodes in |
+ the executing state, it might also be invoked on up-to-date |
+ nodes when using Configure(). |
""" |
- self.tm.will_not_build(self.targets) |
- |
+ T = self.tm.trace |
+ if T: T.write(self.trace_message('Task.failed_continue()', self.node)) |
+ |
+ self.tm.will_not_build(self.targets, lambda n: n.set_state(NODE_FAILED)) |
+ |
def make_ready_all(self): |
""" |
Marks all targets in a task ready for execution. |
@@ -304,6 +342,9 @@ |
This is used when the interface needs every target Node to be |
visited--the canonical example being the "scons -c" option. |
""" |
+ T = self.tm.trace |
+ if T: T.write(self.trace_message('Task.make_ready_all()', self.node)) |
+ |
self.out_of_date = self.targets[:] |
for t in self.targets: |
t.disambiguate().set_state(NODE_EXECUTING) |
@@ -317,6 +358,10 @@ |
This is the default behavior for building only what's necessary. |
""" |
+ T = self.tm.trace |
+ if T: T.write(self.trace_message('Task.make_ready_current()', |
+ self.node)) |
+ |
self.out_of_date = [] |
needs_executing = False |
for t in self.targets: |
@@ -336,7 +381,7 @@ |
t.set_state(NODE_EXECUTING) |
for s in t.side_effects: |
s.set_state(NODE_EXECUTING) |
- else: |
+ else: |
for t in self.targets: |
# We must invoke visited() to ensure that the node |
# information has been computed before allowing the |
@@ -357,6 +402,8 @@ |
waiting parent Nodes, or Nodes waiting on a common side effect, |
that can be put back on the candidates list. |
""" |
+ T = self.tm.trace |
+ if T: T.write(self.trace_message('Task.postprocess()', self.node)) |
# We may have built multiple targets, some of which may have |
# common parents waiting for this build. Count up how many |
@@ -367,8 +414,16 @@ |
targets = set(self.targets) |
+ pending_children = self.tm.pending_children |
parents = {} |
for t in targets: |
+ # A node can only be in the pending_children set if it has |
+ # some waiting_parents. |
+ if t.waiting_parents: |
+ if T: T.write(self.trace_message('Task.postprocess()', |
+ t, |
+ 'removing')) |
+ pending_children.discard(t) |
for p in t.waiting_parents: |
parents[p] = parents.get(p, 0) + 1 |
@@ -381,13 +436,14 @@ |
for p in s.waiting_s_e: |
if p.ref_count == 0: |
self.tm.candidates.append(p) |
- self.tm.pending_children.discard(p) |
for p, subtract in parents.items(): |
p.ref_count = p.ref_count - subtract |
+ if T: T.write(self.trace_message('Task.postprocess()', |
+ p, |
+ 'adjusted parent ref count')) |
if p.ref_count == 0: |
self.tm.candidates.append(p) |
- self.tm.pending_children.discard(p) |
for t in targets: |
t.postprocess() |
@@ -479,7 +535,6 @@ |
self.next_candidate = self.find_next_candidate |
self.pending_children = set() |
- |
def find_next_candidate(self): |
""" |
Returns the next candidate Node for (potential) evaluation. |
@@ -519,7 +574,7 @@ |
def no_next_candidate(self): |
""" |
Stops Taskmaster processing by not returning a next candidate. |
- |
+ |
Note that we have to clean-up the Taskmaster candidate list |
because the cycle detection depends on the fact all nodes have |
been processed somehow. |
@@ -527,9 +582,96 @@ |
while self.candidates: |
candidates = self.candidates |
self.candidates = [] |
- self.will_not_build(candidates, lambda n: n.state < NODE_UP_TO_DATE) |
+ self.will_not_build(candidates) |
return None |
+ def _validate_pending_children(self): |
+ """ |
+ Validate the content of the pending_children set. Assert if an |
+ internal error is found. |
+ |
+ This function is used strictly for debugging the taskmaster by |
+ checking that no invariants are violated. It is not used in |
+ normal operation. |
+ |
+ The pending_children set is used to detect cycles in the |
+ dependency graph. We call a "pending child" a child that is |
+ found in the "pending" state when checking the dependencies of |
+ its parent node. |
+ |
+ A pending child can occur when the Taskmaster completes a loop |
+ through a cycle. For example, lets imagine a graph made of |
+ three node (A, B and C) making a cycle. The evaluation starts |
+ at node A. The taskmaster first consider whether node A's |
+ child B is up-to-date. Then, recursively, node B needs to |
+ check whether node C is up-to-date. This leaves us with a |
+ dependency graph looking like: |
+ |
+ Next candidate \ |
+ \ |
+ Node A (Pending) --> Node B(Pending) --> Node C (NoState) |
+ ^ | |
+ | | |
+ +-------------------------------------+ |
+ |
+ Now, when the Taskmaster examines the Node C's child Node A, |
+ it finds that Node A is in the "pending" state. Therefore, |
+ Node A is a pending child of node C. |
+ |
+ Pending children indicate that the Taskmaster has potentially |
+ loop back through a cycle. We say potentially because it could |
+ also occur when a DAG is evaluated in parallel. For example, |
+ consider the following graph: |
+ |
+ |
+ Node A (Pending) --> Node B(Pending) --> Node C (Pending) --> ... |
+ | ^ |
+ | | |
+ +----------> Node D (NoState) --------+ |
+ / |
+ Next candidate / |
+ |
+ The Taskmaster first evaluates the nodes A, B, and C and |
+ starts building some children of node C. Assuming, that the |
+ maximum parallel level has not been reached, the Taskmaster |
+ will examine Node D. It will find that Node C is a pending |
+ child of Node D. |
+ |
+ In summary, evaluating a graph with a cycle will always |
+ involve a pending child at one point. A pending child might |
+ indicate either a cycle or a diamond-shaped DAG. Only a |
+ fraction of the nodes ends-up being a "pending child" of |
+ another node. This keeps the pending_children set small in |
+ practice. |
+ |
+ We can differentiate between the two cases if we wait until |
+ the end of the build. At this point, all the pending children |
+ nodes due to a diamond-shaped DAG will have been properly |
+ built (or will have failed to build). But, the pending |
+ children involved in a cycle will still be in the pending |
+ state. |
+ |
+ The taskmaster removes nodes from the pending_children set as |
+ soon as a pending_children node moves out of the pending |
+ state. This also helps to keep the pending_children set small. |
+ """ |
+ |
+ for n in self.pending_children: |
+ assert n.state in (NODE_PENDING, NODE_EXECUTING), \ |
+ (str(n), StateString[n.state]) |
+ assert len(n.waiting_parents) != 0, (str(n), len(n.waiting_parents)) |
+ for p in n.waiting_parents: |
+ assert p.ref_count > 0, (str(n), str(p), p.ref_count) |
+ |
+ |
+ def trace_message(self, message): |
+ return 'Taskmaster: %s\n' % message |
+ |
+ def trace_node(self, node): |
+ return '<%-10s %-3s %s>' % (StateString[node.get_state()], |
+ node.ref_count, |
+ repr(str(node))) |
+ |
def _find_next_ready_node(self): |
""" |
Finds the next node that is ready to be built. |
@@ -555,17 +697,25 @@ |
self.ready_exc = None |
T = self.trace |
- if T: T.write('\nTaskmaster: Looking for a node to evaluate\n') |
+ if T: T.write('\n' + self.trace_message('Looking for a node to evaluate')) |
while 1: |
node = self.next_candidate() |
if node is None: |
- if T: T.write('Taskmaster: No candidate anymore.\n\n') |
+ if T: T.write(self.trace_message('No candidate anymore.') + '\n') |
return None |
node = node.disambiguate() |
state = node.get_state() |
+ # For debugging only: |
+ # |
+ # try: |
+ # self._validate_pending_children() |
+ # except: |
+ # self.ready_exc = sys.exc_info() |
+ # return node |
+ |
if CollectStats: |
if not hasattr(node, 'stats'): |
node.stats = Stats() |
@@ -575,8 +725,7 @@ |
else: |
S = None |
- if T: T.write('Taskmaster: Considering node <%-10s %-3s %s> and its children:\n' % |
- (StateString[node.get_state()], node.ref_count, repr(str(node)))) |
+ if T: T.write(self.trace_message(' Considering node %s and its children:' % self.trace_node(node))) |
if state == NODE_NO_STATE: |
# Mark this node as being on the execution stack: |
@@ -584,7 +733,7 @@ |
elif state > NODE_PENDING: |
# Skip this node if it has already been evaluated: |
if S: S.already_handled = S.already_handled + 1 |
- if T: T.write('Taskmaster: already handled (executed)\n') |
+ if T: T.write(self.trace_message(' already handled (executed)')) |
continue |
try: |
@@ -593,7 +742,7 @@ |
exc_value = sys.exc_info()[1] |
e = SCons.Errors.ExplicitExit(node, exc_value.code) |
self.ready_exc = (SCons.Errors.ExplicitExit, e) |
- if T: T.write('Taskmaster: SystemExit\n') |
+ if T: T.write(self.trace_message(' SystemExit')) |
return node |
except Exception, e: |
# We had a problem just trying to figure out the |
@@ -602,7 +751,7 @@ |
# raise the exception when the Task is "executed." |
self.ready_exc = sys.exc_info() |
if S: S.problem = S.problem + 1 |
- if T: T.write('Taskmaster: exception %s while scanning children.\n'%s) |
+ if T: T.write(self.trace_message(' exception %s while scanning children.\n' % e)) |
return node |
children_not_visited = [] |
@@ -613,8 +762,7 @@ |
for child in chain(children,node.prerequisites): |
childstate = child.get_state() |
- if T: T.write('Taskmaster: <%-10s %-3s %s>\n' % |
- (StateString[childstate], child.ref_count, repr(str(child)))) |
+ if T: T.write(self.trace_message(' ' + self.trace_node(child))) |
if childstate == NODE_NO_STATE: |
children_not_visited.append(child) |
@@ -633,8 +781,8 @@ |
children_not_visited.reverse() |
self.candidates.extend(self.order(children_not_visited)) |
#if T and children_not_visited: |
- # T.write('Taskmaster: adding to candidates: %s\n' % map(str, children_not_visited)) |
- # T.write('Taskmaster: candidates now: %s\n' % map(str, self.candidates)) |
+ # T.write(self.trace_message(' adding to candidates: %s' % map(str, children_not_visited))) |
+ # T.write(self.trace_message(' candidates now: %s\n' % map(str, self.candidates))) |
# Skip this node if any of its children have failed. |
# |
@@ -658,8 +806,7 @@ |
node.set_state(NODE_FAILED) |
if S: S.child_failed = S.child_failed + 1 |
- if T: T.write('Taskmaster:****** <%-10s %-3s %s>\n' % |
- (StateString[node.get_state()], node.ref_count, repr(str(node)))) |
+ if T: T.write(self.trace_message('****** %s\n' % self.trace_node(node))) |
continue |
if children_not_ready: |
@@ -673,11 +820,15 @@ |
# count so we can be put back on the list for |
# re-evaluation when they've all finished. |
node.ref_count = node.ref_count + child.add_to_waiting_parents(node) |
- if T: T.write('Taskmaster: adjusting ref count: <%-10s %-3s %s>\n' % |
- (StateString[node.get_state()], node.ref_count, repr(str(node)))) |
+ if T: T.write(self.trace_message(' adjusted ref count: %s, child %s' % |
+ (self.trace_node(node), repr(str(child))))) |
+ if T: |
+ for pc in children_pending: |
+ T.write(self.trace_message(' adding %s to the pending children set\n' % |
+ self.trace_node(pc))) |
self.pending_children = self.pending_children | children_pending |
- |
+ |
continue |
# Skip this node if it has side-effects that are |
@@ -695,8 +846,17 @@ |
# The default when we've gotten through all of the checks above: |
# this node is ready to be built. |
if S: S.build = S.build + 1 |
- if T: T.write('Taskmaster: Evaluating <%-10s %-3s %s>\n' % |
- (StateString[node.get_state()], node.ref_count, repr(str(node)))) |
+ if T: T.write(self.trace_message('Evaluating %s\n' % |
+ self.trace_node(node))) |
+ |
+ # For debugging only: |
+ # |
+ # try: |
+ # self._validate_pending_children() |
+ # except: |
+ # self.ready_exc = sys.exc_info() |
+ # return node |
+ |
return node |
return None |
@@ -732,23 +892,24 @@ |
return task |
- def will_not_build(self, nodes, mark_fail=lambda n: n.state != NODE_FAILED): |
+ def will_not_build(self, nodes, node_func=lambda n: None): |
""" |
- Perform clean-up about nodes that will never be built. |
+ Perform clean-up about nodes that will never be built. Invokes |
+ a user defined function on all of these nodes (including all |
+ of their parents). |
""" |
+ T = self.trace |
+ |
pending_children = self.pending_children |
- to_visit = set() |
- for node in nodes: |
- # Set failure state on all of the parents that were dependent |
- # on this failed build. |
- if mark_fail(node): |
- node.set_state(NODE_FAILED) |
- parents = node.waiting_parents |
- to_visit = to_visit | parents |
- pending_children = pending_children - parents |
+ to_visit = set(nodes) |
+ pending_children = pending_children - to_visit |
+ if T: |
+ for n in nodes: |
+ T.write(self.trace_message(' removing node %s from the pending children set\n' % |
+ self.trace_node(n))) |
try: |
while 1: |
try: |
@@ -760,11 +921,21 @@ |
to_visit.remove(node) |
else: |
break |
- if mark_fail(node): |
- node.set_state(NODE_FAILED) |
- parents = node.waiting_parents |
- to_visit = to_visit | parents |
- pending_children = pending_children - parents |
+ |
+ node_func(node) |
+ |
+ # Prune recursion by flushing the waiting children |
+ # list immediately. |
+ parents = node.waiting_parents |
+ node.waiting_parents = set() |
+ |
+ to_visit = to_visit | parents |
+ pending_children = pending_children - parents |
+ |
+ for p in parents: |
+ p.ref_count = p.ref_count - 1 |
+ if T: T.write(self.trace_message(' removing parent %s from the pending children set\n' % |
+ self.trace_node(p))) |
except KeyError: |
# The container to_visit has been emptied. |
pass |
@@ -784,15 +955,31 @@ |
""" |
Check for dependency cycles. |
""" |
- if self.pending_children: |
- desc = 'Found dependency cycle(s):\n' |
- for node in self.pending_children: |
- cycle = find_cycle([node], set()) |
- if cycle: |
- desc = desc + " " + string.join(map(str, cycle), " -> ") + "\n" |
- else: |
- desc = desc + \ |
- " Internal Error: no cycle found for node %s (%s) in state %s\n" % \ |
- (node, repr(node), StateString[node.get_state()]) |
+ if not self.pending_children: |
+ return |
- raise SCons.Errors.UserError, desc |
+ # TODO(1.5) |
+ #nclist = [ (n, find_cycle([n], set())) for n in self.pending_children ] |
+ nclist = map(lambda n: (n, find_cycle([n], set())), self.pending_children) |
+ |
+ # TODO(1.5) |
+ #genuine_cycles = [ |
+ # node for node, cycle in nclist |
+ # if cycle or node.get_state() != NODE_EXECUTED |
+ #] |
+ genuine_cycles = filter(lambda t: t[1] or t[0].get_state() != NODE_EXECUTED, nclist) |
+ if not genuine_cycles: |
+ # All of the "cycles" found were single nodes in EXECUTED state, |
+ # which is to say, they really weren't cycles. Just return. |
+ return |
+ |
+ desc = 'Found dependency cycle(s):\n' |
+ for node, cycle in nclist: |
+ if cycle: |
+ desc = desc + " " + string.join(map(str, cycle), " -> ") + "\n" |
+ else: |
+ desc = desc + \ |
+ " Internal Error: no cycle found for node %s (%s) in state %s\n" % \ |
+ (node, repr(node), StateString[node.get_state()]) |
+ |
+ raise SCons.Errors.UserError, desc |