Skip to content

Commit a934cce

Browse files
authored
Merge pull request swiftlang#32226 from gottesmm/pr-12d3f8c7e3ae40ee63271019b129f8436acd493e
[build-script] Add a really simple build scheduler that assumes/enforces a DAG build graph.
2 parents 29d3cc4 + 8effe49 commit a934cce

File tree

2 files changed

+223
-0
lines changed

2 files changed

+223
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# swift_build_support/build_graph.py ----------------------------*- python -*-
2+
#
3+
# This source file is part of the Swift.org open source project
4+
#
5+
# Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
6+
# Licensed under Apache License v2.0 with Runtime Library Exception
7+
#
8+
# See https://swift.org/LICENSE.txt for license information
9+
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
#
11+
# ----------------------------------------------------------------------------
12+
#
13+
# This is a simple implementation of an acyclic build graph. We require no
14+
# cycles, so we just perform a reverse post order traversal to get a topological
15+
# ordering. We check during the reverse post order traversal that we do not
16+
# visit any node multiple times.
17+
#
18+
# Nodes are assumed to be a product's class.
19+
#
20+
# ----------------------------------------------------------------------------
21+
22+
23+
def _get_po_ordered_nodes(root, invertedDepMap):
24+
# Then setup our worklist/visited node set.
25+
worklist = [root]
26+
visitedNodes = set([])
27+
# TODO: Can we unify po_ordered_nodes and visitedNodes in some way?
28+
po_ordered_nodes = []
29+
30+
# Until we no longer have nodes to visit...
31+
while not len(worklist) == 0:
32+
# First grab the last element of the worklist. If we have already
33+
# visited this node, just pop it and skip it.
34+
#
35+
# DISCUSSION: Consider the following build graph:
36+
#
37+
# A -> [C, B]
38+
# B -> [C]
39+
#
40+
# In this case, we will most likely get the following worklist
41+
# before actually processing anything:
42+
#
43+
# A, C, B, C
44+
#
45+
# In this case, we want to ignore the initial C pushed onto the
46+
# worklist by visiting A since we will have visited C already due to
47+
# the edge from B -> C.
48+
node = worklist[-1]
49+
if node in visitedNodes:
50+
worklist.pop()
51+
continue
52+
53+
# Then grab the dependents of our node.
54+
deps = invertedDepMap.get(node, set([]))
55+
assert(isinstance(deps, set))
56+
57+
# Then visit those and see if we have not visited any of them. Push
58+
# any such nodes onto the worklist and continue. If we have already
59+
# visited all of our dependents, then we can actually process this
60+
# node.
61+
foundDep = False
62+
for d in deps:
63+
if d not in visitedNodes:
64+
foundDep = True
65+
worklist.append(d)
66+
if foundDep:
67+
continue
68+
69+
# Now process the node by popping it off the worklist, adding it to
70+
# the visited nodes set, and append it to the po_ordered_nodes in
71+
# its final position.
72+
worklist.pop()
73+
visitedNodes.add(node)
74+
po_ordered_nodes.append(node)
75+
return po_ordered_nodes
76+
77+
78+
class BuildDAG(object):
79+
80+
def __init__(self):
81+
self.root = None
82+
83+
# A map from a node to a list of nodes that depend on the given node.
84+
#
85+
# NOTE: This is an inverted dependency map implying that the root will
86+
# be a "final element" of the graph.
87+
self.invertedDepMap = {}
88+
89+
def add_edge(self, pred, succ):
90+
self.invertedDepMap.setdefault(pred, set([succ])) \
91+
.add(succ)
92+
93+
def set_root(self, root):
94+
# Assert that we always only have one root.
95+
assert(self.root is None)
96+
self.root = root
97+
98+
def produce_schedule(self):
99+
# Grab the root and make sure it is not None
100+
root = self.root
101+
assert(root is not None)
102+
103+
# Then perform a post order traversal from root using our inverted
104+
# dependency map to compute a list of our nodes in post order.
105+
#
106+
# NOTE: The index of each node in this list is the post order number of
107+
# the node.
108+
po_ordered_nodes = _get_po_ordered_nodes(root, self.invertedDepMap)
109+
110+
# Ok, we have our post order list. We want to provide our user a reverse
111+
# post order, so we take our array and construct a dictionary of an
112+
# enumeration of the list. This will give us a dictionary mapping our
113+
# product names to their reverse post order number.
114+
rpo_ordered_nodes = list(reversed(po_ordered_nodes))
115+
node_to_rpot_map = dict((y, x) for x, y in enumerate(rpo_ordered_nodes))
116+
117+
# Now before we return our rpo_ordered_nodes and our node_to_rpot_map, lets
118+
# verify that we didn't find any cycles. We can do this by traversing
119+
# our dependency graph in reverse post order and making sure all
120+
# dependencies of each node we visit has a later reverse post order
121+
# number than the node we are checking.
122+
for n, node in enumerate(rpo_ordered_nodes):
123+
for dep in self.invertedDepMap.get(node, []):
124+
if node_to_rpot_map[dep] < n:
125+
print('n: {}. node: {}.'.format(n, node))
126+
print('dep: {}.'.format(dep))
127+
print('inverted dependency map: {}'.format(self.invertedDepMap))
128+
print('rpo ordered nodes: {}'.format(rpo_ordered_nodes))
129+
print('rpo node to rpo number map: {}'.format(node_to_rpot_map))
130+
raise RuntimeError('Found cycle in build graph!')
131+
132+
return (rpo_ordered_nodes, node_to_rpot_map)
133+
134+
135+
def produce_scheduled_build(input_product_classes):
136+
"""For a given a subset input_input_product_classes of
137+
all_input_product_classes, compute a topological ordering of the
138+
input_input_product_classes + topological closures that respects the
139+
dependency graph.
140+
"""
141+
dag = BuildDAG()
142+
worklist = list(input_product_classes)
143+
visited = set(input_product_classes)
144+
145+
# Construct the DAG.
146+
while len(worklist) > 0:
147+
entry = worklist.pop()
148+
deps = entry.get_dependencies()
149+
if len(deps) == 0:
150+
dag.set_root(entry)
151+
for d in deps:
152+
dag.add_edge(d, entry)
153+
if d not in visited:
154+
worklist.append(d)
155+
visited = visited.union(deps)
156+
157+
# Then produce the schedule.
158+
schedule = dag.produce_schedule()
159+
160+
# Finally check that all of our input_product_classes are in the schedule.
161+
if len(set(input_product_classes) - set(schedule[0])) != 0:
162+
raise RuntimeError('Found disconnected graph?!')
163+
164+
return schedule
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# test_build_graph.py - Test the build_graph using mocks --------*- python -*-
2+
#
3+
# This source file is part of the Swift.org open source project
4+
#
5+
# Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
# Licensed under Apache License v2.0 with Runtime Library Exception
7+
#
8+
# See https://swift.org/LICENSE.txt for license information
9+
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
11+
12+
from __future__ import absolute_import, unicode_literals
13+
14+
import unittest
15+
16+
from swift_build_support import build_graph
17+
18+
19+
class ProductMock(object):
20+
def __init__(self, name):
21+
self.name = name
22+
self.deps = []
23+
24+
def get_dependencies(self):
25+
return self.deps
26+
27+
def __repr__(self):
28+
return "<ProductMock: {}>".format(self.name)
29+
30+
31+
def get_products():
32+
products = {
33+
"cmark": ProductMock("cmark"),
34+
"llvm": ProductMock("llvm"),
35+
"swift": ProductMock("swift"),
36+
"swiftpm": ProductMock("swiftpm"),
37+
"libMockSwiftPM": ProductMock("libMockSwiftPM"),
38+
"libMockCMark": ProductMock("libMockCMark"),
39+
"libMockSwiftPM2": ProductMock("libMockSwiftPM2"),
40+
}
41+
42+
products['llvm'].deps.extend([products['cmark']])
43+
products['swift'].deps.extend([products['llvm']])
44+
products['swiftpm'].deps.extend([products['llvm'], products['swift']])
45+
products['libMockSwiftPM'].deps.extend([products['swiftpm']])
46+
products['libMockCMark'].deps.extend([products['cmark']])
47+
products['libMockSwiftPM2'].deps.extend([products['swiftpm'], products['cmark']])
48+
49+
return products
50+
51+
52+
class BuildGraphTestCase(unittest.TestCase):
53+
54+
def test_simple_build_graph(self):
55+
products = get_products()
56+
selectedProducts = [products['swiftpm']]
57+
schedule = build_graph.produce_scheduled_build(selectedProducts)
58+
names = [x.name for x in schedule[0]]
59+
self.assertEquals(['cmark', 'llvm', 'swift', 'swiftpm'], names)

0 commit comments

Comments
 (0)