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, the dot command is used to generate a SVG file instead.
  • output_pyvis: Output path to a pyvis file. (Requires the pyvis module to be installed).