Skip to content

Commit 7e3050c

Browse files
author
Zachary Turner
committed
[analyze-project-deps.py] Add the ability to list all cycles.
This analyzes the dependency graph and computes all minimal cycles. Equivalent cycles that differ only by rotation are excluded, as are cycles that are "super-cycles" of other smaller cycles. For example, if we discover the cycle A -> C -> A, and then later A -> B -> C -> D -> A, this latter cycle is not considered. Thus, it is possible that after eliminating some cycles, new ones will appear. However, this is the only way to make the algorithm terminate in a reasonable amount of time. llvm-svn: 298324
1 parent b57e496 commit 7e3050c

File tree

1 file changed

+77
-1
lines changed

1 file changed

+77
-1
lines changed

lldb/scripts/analyze-project-deps.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
description='Analyze LLDB project #include dependencies.')
99
parser.add_argument('--show-counts', default=False, action='store_true',
1010
help='When true, show the number of dependencies from each subproject')
11+
parser.add_argument('--discover-cycles', default=False, action='store_true',
12+
help='When true, find and display all project dependency cycles. Note,'
13+
'this option is very slow')
14+
1115
args = parser.parse_args()
1216

1317
src_dir = os.path.join(lldb_root, "source")
@@ -17,9 +21,17 @@
1721

1822
include_regex = re.compile('#include \"((lldb|Plugins|clang)(.*/)+).*\"')
1923

24+
def is_sublist(small, big):
25+
it = iter(big)
26+
return all(c in it for c in small)
27+
2028
def normalize_host(str):
2129
if str.startswith("lldb/Host"):
2230
return "lldb/Host"
31+
if str.startswith("Plugins"):
32+
return "lldb/" + str
33+
if str.startswith("lldb/../../source"):
34+
return str.replace("lldb/../../source", "lldb")
2335
return str
2436

2537
def scan_deps(this_dir, file):
@@ -40,7 +52,7 @@ def scan_deps(this_dir, file):
4052
relative = normalize_host(relative)
4153
if relative in deps:
4254
deps[relative] += 1
43-
else:
55+
elif relative != this_dir:
4456
deps[relative] = 1
4557
if this_dir not in src_map and len(deps) > 0:
4658
src_map[this_dir] = deps
@@ -65,6 +77,57 @@ def scan_deps(this_dir, file):
6577
scan_deps(norm_base_path, src_path)
6678
pass
6779

80+
def is_existing_cycle(path, cycles):
81+
# If we have a cycle like # A -> B -> C (with an implicit -> A at the end)
82+
# then we don't just want to check for an occurrence of A -> B -> C in the
83+
# list of known cycles, but every possible rotation of A -> B -> C. For
84+
# example, if we previously encountered B -> C -> A (with an implicit -> B
85+
# at the end), then A -> B -> C is also a cycle. This is an important
86+
# optimization which reduces the search space by multiple orders of
87+
# magnitude.
88+
for i in xrange(0,len(path)):
89+
if any(is_sublist(x, path) for x in cycles):
90+
return True
91+
path = [path[-1]] + path[0:-1]
92+
return False
93+
94+
def expand(path_queue, path_lengths, cycles, src_map):
95+
# We do a breadth first search, to make sure we visit all paths in order
96+
# of ascending length. This is an important optimization to make sure that
97+
# short cycles are discovered first, which will allow us to discard longer
98+
# cycles which grow the search space exponentially the longer they get.
99+
while len(path_queue) > 0:
100+
cur_path = path_queue.pop(0)
101+
if is_existing_cycle(cur_path, cycles):
102+
continue
103+
104+
next_len = path_lengths.pop(0) + 1
105+
106+
last_component = cur_path[-1]
107+
108+
for item in src_map[last_component]:
109+
if item.startswith("clang"):
110+
continue
111+
112+
if item in cur_path:
113+
# This is a cycle. Minimize it and then check if the result is
114+
# already in the list of cycles. Insert it (or not) and then
115+
# exit.
116+
new_index = cur_path.index(item)
117+
cycle = cur_path[new_index:]
118+
if not is_existing_cycle(cycle, cycles):
119+
cycles.append(cycle)
120+
continue
121+
122+
path_lengths.append(next_len)
123+
path_queue.append(cur_path + [item])
124+
pass
125+
126+
cycles = []
127+
128+
path_queue = [[x] for x in src_map.iterkeys()]
129+
path_lens = [1] * len(path_queue)
130+
68131
items = list(src_map.iteritems())
69132
items.sort(lambda A, B : cmp(A[0], B[0]))
70133

@@ -79,4 +142,17 @@ def scan_deps(this_dir, file):
79142
sorted_deps.sort(lambda A, B: cmp(A[0], B[0]))
80143
for dep in sorted_deps:
81144
print "\t{}".format(dep[0])
145+
146+
if args.discover_cycles:
147+
print "Analyzing cycles..."
148+
149+
expand(path_queue, path_lens, cycles, src_map)
150+
151+
average = sum([len(x)+1 for x in cycles]) / len(cycles)
152+
153+
print "Found {} cycles. Average cycle length = {}.".format(len(cycles), average)
154+
for cycle in cycles:
155+
cycle.append(cycle[0])
156+
print " -> ".join(cycle)
157+
82158
pass

0 commit comments

Comments
 (0)