emdbg.analyze.callgraph
Visualize Backtraces
You can generate backtraces using the GDB px4_backtrace
command.
To generate a log of backtrace, use the px4_breaktrace {func}
command via the
commands defined by the emdbg.bench.skynode
module.
Command Line Interface
You can convert calltraces into SVG call graphs like this:
python3 -m emdbg.analyze.callgraph calltrace_log.txt --svg --type FileSystem
You can change the type to use a specific peripheral if you traced access to that peripheral.
Installation
You need to install graphviz for the analysis functionality:
# Ubuntu
sudo apt install graphviz
# macOS
brew install graphviz
1# Copyright (c) 2023, Auterion AG 2# SPDX-License-Identifier: BSD-3-Clause 3 4""" 5.. include:: callgraph.md 6""" 7 8from __future__ import annotations 9import re, os 10import itertools 11import logging 12from pathlib import Path 13from collections import defaultdict 14from .backtrace import Backtrace 15from .utils import read_gdb_log 16LOGGER = logging.getLogger(__name__) 17 18 19def callgraph_from_backtrace(backtraces: list[str], 20 BacktraceClass: Backtrace = None, 21 output_graphviz: Path = None, 22 output_pyvis: Path = None): 23 """ 24 Convert a GDB backtrace log file into a dot, svg and pyvis file. 25 26 :param backtraces: list of strings each containing a backtrace. 27 :param BacktraceClass: One of the classes of `emdbg.analyze.backtrace`. 28 :param output_graphviz: Output path of the graphviz file (`.dot` suffix). 29 If you set its suffix to `.svg`, the `dot` command is used to generate 30 a SVG file instead. 31 :param output_pyvis: Output path to a pyvis file. (Requires the `pyvis` 32 module to be installed). 33 """ 34 35 if BacktraceClass is None: 36 BacktraceClass = Backtrace 37 38 backts = defaultdict(set) 39 for description in backtraces: 40 bt = BacktraceClass(description) 41 if bt.is_valid: 42 backts[bt.type].add(bt) 43 # if bt.type == "unknown": 44 # print(bt) 45 # print(bt.description) 46 else: 47 LOGGER.error(bt) 48 LOGGER.error(bt.description) 49 50 nodes = {} 51 edges = defaultdict(int) 52 53 def itertools_pairwise(iterable): 54 a, b = itertools.tee(iterable) 55 next(b, None) 56 return zip(a, b) 57 58 for btype, bts in backts.items(): 59 for bt in bts: 60 for frame in bt.frames: 61 nodes[frame._unique_location] = frame 62 for f1, f2 in itertools_pairwise(bt.frames): 63 edges[(f2._unique_location, f1._unique_location, str((btype or "").lower()))] += 1 64 65 sources = set(nodes) 66 sinks = set(nodes) 67 max_calls = max(edges.values()) 68 for source, sink, _ in edges.keys(): 69 sources.discard(sink) 70 sinks.discard(source) 71 72 def _n(name): 73 return re.sub(r"[, :<>]", "_", name) 74 75 if output_pyvis: 76 import pyvis 77 net = pyvis.network.Network(height="100%", width="100%", select_menu=True) 78 for node in sorted(nodes): 79 kwargs = {"label": node} 80 if node in sinks: 81 kwargs.update({"borderWidth": 3, "color": "LightBlue"}) 82 elif node in sources: 83 kwargs.update({"borderWidth": 3, "color": "LightGreen"}) 84 net.add_node(_n(node), label=node) 85 for edge in sorted(edges): 86 net.add_edge(_n(edge[0]), _n(edge[1]), label=edge[2], arrows="to") 87 net.toggle_physics(True) 88 net.show(output_pyvis, notebook=False) 89 90 if output_graphviz: 91 output_graphviz = Path(output_graphviz) 92 import graphviz 93 dot = graphviz.Digraph() 94 for node in sorted(nodes): 95 frame = nodes[node] 96 kwargs = { 97 "label": f"{frame.function}:{frame.line}", 98 "URL": f"subl://open?url={Path(frame.filename).absolute()}&line={frame.line}", 99 } 100 for pattern, style in BacktraceClass.COLORS.items(): 101 if re.search(pattern, node): 102 kwargs.update(style) 103 if node in sinks: 104 kwargs.update({"style": "bold,filled", "fillcolor": "LightBlue"}) 105 elif node in sources: 106 kwargs.update({"style": "bold,filled", "fillcolor": "LightGreen"}) 107 dot.node(_n(node), **kwargs) 108 for edge in sorted(edges): 109 dot.edge(_n(edge[0]), _n(edge[1]), label=edge[2] or str(edges[edge]), 110 penwidth=str(max(edges[edge]/max_calls * 10, 0.5))) 111 output_dot = output_graphviz.with_suffix(".dot") 112 output_dot.write_text(dot.source) 113 if output_graphviz.suffix == ".svg": 114 os.system(f"dot -Tsvg -o {output_graphviz} {output_dot}") 115 os.system(f"rm {output_dot}") 116 117 118# ----------------------------------------------------------------------------- 119if __name__ == "__main__": 120 import argparse 121 from .backtrace import * 122 123 parser = argparse.ArgumentParser(description="Backtrace Analyzer") 124 parser.add_argument( 125 "file", 126 type=Path, 127 help="The GDB log containing the backtraces.") 128 parser.add_argument( 129 "--graphviz", 130 type=Path, 131 help="The file to render the dot graph in.") 132 parser.add_argument( 133 "--svg", 134 action="store_true", 135 default=False, 136 help="Render into SVG file using the same name as the input.") 137 parser.add_argument( 138 "--pyvis", 139 type=Path, 140 help="The file to render the pyvis graph in.") 141 values = { 142 "FileSystem": FileSystemBacktrace, 143 "SPI": SpiBacktrace, 144 "I2C": I2cBacktrace, 145 "CAN": CanBacktrace, 146 "UART": UartBacktrace, 147 "Semaphore": SemaphoreBacktrace, 148 "Generic": Backtrace, 149 } 150 parser.add_argument( 151 "--type", 152 choices=values.keys(), 153 help="The backtrace class to use.") 154 args = parser.parse_args() 155 BacktraceClass = values.get(args.type) 156 157 if BacktraceClass is None: 158 if "_sdmmc" in args.file.name: 159 BacktraceClass = FileSystemBacktrace 160 elif "_spi" in args.file.name: 161 BacktraceClass = SpiBacktrace 162 elif "_i2c" in args.file.name: 163 BacktraceClass = I2cBacktrace 164 elif "_can" in args.file.name: 165 BacktraceClass = CanBacktrace 166 elif "_uart" in args.file.name: 167 BacktraceClass = UartBacktrace 168 elif "_semaphore" in args.file.name: 169 BacktraceClass = SemaphoreBacktrace 170 else: 171 BacktraceClass = Backtrace 172 173 graphviz = args.graphviz 174 if args.svg: 175 graphviz = Path(str(args.file.with_suffix(".svg")).replace("calltrace_", "callgraph_")) 176 177 backtraces = re.split(r"(?:Breakpoint|Hardware .*?watchpoint) \d", read_gdb_log(args.file)[20:]) 178 callgraph_from_backtrace(backtraces, BacktraceClass, 179 output_graphviz=graphviz, output_pyvis=args.pyvis)
LOGGER =
<Logger emdbg.analyze.callgraph (WARNING)>
def
callgraph_from_backtrace( backtraces: list[str], BacktraceClass: emdbg.analyze.backtrace.Backtrace = None, output_graphviz: pathlib.Path = None, output_pyvis: pathlib.Path = None):
20def callgraph_from_backtrace(backtraces: list[str], 21 BacktraceClass: Backtrace = None, 22 output_graphviz: Path = None, 23 output_pyvis: Path = None): 24 """ 25 Convert a GDB backtrace log file into a dot, svg and pyvis file. 26 27 :param backtraces: list of strings each containing a backtrace. 28 :param BacktraceClass: One of the classes of `emdbg.analyze.backtrace`. 29 :param output_graphviz: Output path of the graphviz file (`.dot` suffix). 30 If you set its suffix to `.svg`, the `dot` command is used to generate 31 a SVG file instead. 32 :param output_pyvis: Output path to a pyvis file. (Requires the `pyvis` 33 module to be installed). 34 """ 35 36 if BacktraceClass is None: 37 BacktraceClass = Backtrace 38 39 backts = defaultdict(set) 40 for description in backtraces: 41 bt = BacktraceClass(description) 42 if bt.is_valid: 43 backts[bt.type].add(bt) 44 # if bt.type == "unknown": 45 # print(bt) 46 # print(bt.description) 47 else: 48 LOGGER.error(bt) 49 LOGGER.error(bt.description) 50 51 nodes = {} 52 edges = defaultdict(int) 53 54 def itertools_pairwise(iterable): 55 a, b = itertools.tee(iterable) 56 next(b, None) 57 return zip(a, b) 58 59 for btype, bts in backts.items(): 60 for bt in bts: 61 for frame in bt.frames: 62 nodes[frame._unique_location] = frame 63 for f1, f2 in itertools_pairwise(bt.frames): 64 edges[(f2._unique_location, f1._unique_location, str((btype or "").lower()))] += 1 65 66 sources = set(nodes) 67 sinks = set(nodes) 68 max_calls = max(edges.values()) 69 for source, sink, _ in edges.keys(): 70 sources.discard(sink) 71 sinks.discard(source) 72 73 def _n(name): 74 return re.sub(r"[, :<>]", "_", name) 75 76 if output_pyvis: 77 import pyvis 78 net = pyvis.network.Network(height="100%", width="100%", select_menu=True) 79 for node in sorted(nodes): 80 kwargs = {"label": node} 81 if node in sinks: 82 kwargs.update({"borderWidth": 3, "color": "LightBlue"}) 83 elif node in sources: 84 kwargs.update({"borderWidth": 3, "color": "LightGreen"}) 85 net.add_node(_n(node), label=node) 86 for edge in sorted(edges): 87 net.add_edge(_n(edge[0]), _n(edge[1]), label=edge[2], arrows="to") 88 net.toggle_physics(True) 89 net.show(output_pyvis, notebook=False) 90 91 if output_graphviz: 92 output_graphviz = Path(output_graphviz) 93 import graphviz 94 dot = graphviz.Digraph() 95 for node in sorted(nodes): 96 frame = nodes[node] 97 kwargs = { 98 "label": f"{frame.function}:{frame.line}", 99 "URL": f"subl://open?url={Path(frame.filename).absolute()}&line={frame.line}", 100 } 101 for pattern, style in BacktraceClass.COLORS.items(): 102 if re.search(pattern, node): 103 kwargs.update(style) 104 if node in sinks: 105 kwargs.update({"style": "bold,filled", "fillcolor": "LightBlue"}) 106 elif node in sources: 107 kwargs.update({"style": "bold,filled", "fillcolor": "LightGreen"}) 108 dot.node(_n(node), **kwargs) 109 for edge in sorted(edges): 110 dot.edge(_n(edge[0]), _n(edge[1]), label=edge[2] or str(edges[edge]), 111 penwidth=str(max(edges[edge]/max_calls * 10, 0.5))) 112 output_dot = output_graphviz.with_suffix(".dot") 113 output_dot.write_text(dot.source) 114 if output_graphviz.suffix == ".svg": 115 os.system(f"dot -Tsvg -o {output_graphviz} {output_dot}") 116 os.system(f"rm {output_dot}")
Convert a GDB backtrace log file into a dot, svg and pyvis file.
Parameters
- backtraces: list of strings each containing a backtrace.
- BacktraceClass: One of the classes of
emdbg.analyze.backtrace
. - output_graphviz: Output path of the graphviz file (
.dot
suffix). If you set its suffix to.svg
, thedot
command is used to generate a SVG file instead. - output_pyvis: Output path to a pyvis file. (Requires the
pyvis
module to be installed).