import os
import sys
import graphviz
skip_functions = [
"call_extension_method", "do_with_logging", "do_with_hooks", "do_with_compile_wrapper",
"write_hook_point_metadata"
]
skip_groups = ["logging"]
def get_group_from_filename(file):
if file == "<extension_magic>":
return "extension_magic"
if file == "<START_HERE>":
return "start_here"
if file.startswith("lib/functions/general/extensions.sh"):
return "extensions_infra"
if file.startswith("extensions/"):
return "core_extensions"
if file.startswith("config/sources/families"):
return "family_code"
if file.startswith("config/"):
return "config_code"
if file.startswith("lib/functions/compilation/uboot.sh"):
return "compilation-u-boot"
if file.startswith("lib/functions/compilation/kernel-debs.sh"):
return "compilation-kernel"
if file.startswith("lib/functions/compilation/kernel.sh"):
return "compilation-kernel"
if file.startswith("lib/functions/"):
components = file.split("/")
return "/".join(components[2:-1])
return "unknown"
def prepare_file_for_screen(file):
if file.startswith("lib/functions/"):
return file[14:]
return file
def cleanup_filename(filename, common_prefix):
real_filename = filename.replace(common_prefix, "")
if real_filename.startswith(".tmp/") and real_filename.endswith("/extension_function_definition.sh"):
real_filename = "<extension_magic>"
return real_filename
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def split_by_spaces(value):
return value.split(" ")
file_handle = open("output/call-traces/calls.txt", 'r')
file_lines = file_handle.readlines()
file_handle.close()
eprint("Number of lines: " + str(len(file_lines)))
bare_calls = []
for line in file_lines:
line_parts = line.split("|", maxsplit=3)
if len(line_parts) != 4:
eprint("Error: line_parts length is not 4")
eprint("line_parts: " + str(line_parts))
continue
function_names = split_by_spaces(line_parts[0])
line_numbers = split_by_spaces(line_parts[1])
file_names = split_by_spaces(line_parts[2])
current_line_no = int(line_parts[3])
stack = []
for i in range(len(function_names) - 1):
stack.append({
"function": function_names[i],
"called_by_line": line_numbers[i],
"called_by_file": file_names[i + 1]
})
if len(stack) == 1:
caller = "<START_HERE>"
else:
caller = (stack[1]["function"])
bare_calls.append({
"callee": (stack[0]["function"]),
"caller": caller,
"callpoint_file": (stack[0]["called_by_file"]),
"callpoint_line": (int(stack[0]["called_by_line"])),
"callee_return_line": current_line_no,
"callee_return_file": file_names[0].strip(),
"stack": stack
})
absolute_calls = []
for call in bare_calls:
if call["callpoint_file"].startswith("/"):
absolute_calls.append(call)
common_prefix = os.path.commonpath([call["callpoint_file"] for call in absolute_calls]) + "/"
eprint("Common prefix: " + common_prefix)
for call in bare_calls:
call["callpoint_file"] = cleanup_filename(call["callpoint_file"], common_prefix)
call["callee_return_file"] = cleanup_filename(call["callee_return_file"], common_prefix)
for stack_item in call["stack"]:
stack_item["called_by_file"] = cleanup_filename(stack_item["called_by_file"], common_prefix)
function_to_file = {}
for call in bare_calls:
if call["callee"] not in function_to_file:
function_to_file[call["callee"]] = {
"def_file": call["callee_return_file"],
"def_line": call["callee_return_line"],
"group": get_group_from_filename(call["callee_return_file"])
}
for call in bare_calls:
for stack_member in call["stack"]:
func = stack_member["function"]
if function_to_file.get(func) is not None:
func2file = function_to_file[func]
stack_member["def_file"] = func2file["def_file"]
stack_member["def_line"] = func2file["def_line"]
stack_member["group"] = func2file["group"]
else:
eprint("Error: function not found in function_to_file: " + func)
raise Exception("Error: function not found in function_to_file: " + func)
calls = []
for call in bare_calls:
if call["stack"][0]["group"] in skip_groups:
continue
if call["stack"][0]["function"] in skip_functions:
continue
size_pre = len(call["stack"])
original_stack = call["stack"]
new_stack = []
stack_counter = 0
previous_stack = None
for stack_item in call["stack"]:
if (stack_item["group"] not in skip_groups) and (stack_item["function"] not in skip_functions):
new_stack.append(stack_item)
else:
if previous_stack is not None:
previous_stack["called_by_line"] = stack_item["called_by_line"]
previous_stack["called_by_file"] = stack_item["called_by_file"]
stack_counter += 1
previous_stack = stack_item
stack = new_stack
call["stack"] = new_stack
size_post = len(stack)
if len(stack) == 0:
eprint("Empty stack, skipping")
continue
if len(stack) == 1:
caller = "<START_HERE>"
else:
caller = (stack[1]["function"])
calls.append({
"callee": (stack[0]["function"]),
"caller": caller,
"callpoint_file": (stack[0]["called_by_file"]),
"callpoint_line": (int(stack[0]["called_by_line"])),
"callee_return_line": (int(stack[0]["def_line"])),
"callee_return_file": ((stack[0]["def_file"])),
"callee_group": stack[0]["group"],
})
bare_calls = calls
bare_calls = [dict(t) for t in {tuple(d.items()) for d in bare_calls}]
nodes = {}
for call in bare_calls:
if call["callee"] not in nodes:
nodes[call["callee"]] = {
"function": call["callee"],
"definition_file": call["callee_return_file"], "definition_line": call["callee_return_line"]
}
grouped_nodes = {}
for node in nodes:
file = nodes[node]["definition_file"]
group = get_group_from_filename(file)
if group not in grouped_nodes:
grouped_nodes[group] = []
grouped_nodes[group].append(node)
dot = graphviz.Digraph(comment='Armbian build system call graph')
colors = [
"#ff0000", "#00ff00", "#0000ff", "#ffff00", "#00ffff", "#ff00ff", "#ff8000", "#ff0080", "#0080ff", "#8000ff",
"#008080", "#800080", "#808000", "#008000", "#800000", "#000080", "#808080"
]
color_pairs = []
for color in colors:
if (int(color[1:3], 16) + int(color[3:5], 16) + int(color[5:7], 16)) < 400:
color_pairs.append({"back": color, "fore": "#ffffff"})
else:
color_pairs.append({"back": color, "fore": "#000000"})
group_counter = 0
node_colors = {}
for node_group in grouped_nodes:
group_counter += 1
color = color_pairs[group_counter % len(colors)]
for node in grouped_nodes[node_group]:
label = nodes[node]["function"] + "()" + "\n" + prepare_file_for_screen(
nodes[node]["definition_file"]) + ":" + str(
nodes[node]["definition_line"]) + "\n" + "[" + node_group + "]"
dot.node(node, label, style="filled", fillcolor=color["back"], fontcolor=color["fore"])
node_colors[node] = color["back"]
for call in bare_calls:
edge_label = prepare_file_for_screen(call["callpoint_file"]) + ":" + str(call["callpoint_line"])
edge_color = node_colors.get(call["caller"])
if edge_color is None:
edge_color = "black"
dot.edge(call["caller"], call["callee"], label=edge_label, color=edge_color, fontcolor="black")
dot = dot.unflatten(stagger=3, fanout=True, chain=13)
dot.format = 'svg'
dot.render('output/call-traces/call-graph.dot', view=True)
dot.format = 'pdf'
dot.render('output/call-traces/call-graph.dot.exp', view=True)
dot.format = 'png'
dot.render('output/call-traces/call-graph.dot.img', view=True)
eprint("Done")