emdbg.analyze.priority

You can generate semaphore boosts logs either at runtime using the emdbg.patch.set.semaphore_boostlog patch, or using the px4_calltrace_semaphore_boosts command of the emdbg.bench.skynode module at debug time.

Command Line Interface

You can summarize boost logs:

python3 -m emdbg.analyze.priority log_semaphore_boosts.txt

You can also generate a callgraph with boost operations:

python3 -m emdbg.analyze.callgraph log_semaphore_boosts.txt --svg --type Semaphore
  1# Copyright (c) 2023, Auterion AG
  2# SPDX-License-Identifier: BSD-3-Clause
  3
  4"""
  5You can generate semaphore boosts logs either at runtime using the
  6`emdbg.patch.set.semaphore_boostlog` patch, or using the
  7`px4_calltrace_semaphore_boosts` command of the `emdbg.bench.skynode` module
  8at debug time.
  9
 10
 11## Command Line Interface
 12
 13You can summarize boost logs:
 14
 15```sh
 16python3 -m emdbg.analyze.priority log_semaphore_boosts.txt
 17```
 18
 19You can also generate a callgraph with boost operations:
 20
 21```sh
 22python3 -m emdbg.analyze.callgraph log_semaphore_boosts.txt --svg --type Semaphore
 23```
 24"""
 25
 26from __future__ import annotations
 27import re
 28from pathlib import Path
 29import statistics
 30import itertools
 31from collections import defaultdict
 32from ..debug.px4.utils import format_table
 33from .utils import read_gdb_log
 34
 35class _BoostOperation:
 36    PATTERN = re.compile(r"L(\d+) +(\d+)us> +(0x[0-9a-f]+|<opti-out>): +(.*?) +(\d+) +([\\/_]+) +(\d+) *(.*)")
 37
 38    def __init__(self, description):
 39        self.is_valid = False
 40        if match := self.PATTERN.search(description):
 41            self.line = int(match.group(1))
 42            self.uptime = int(match.group(2))
 43            try:
 44                self.semaphore = int(match.group(3), 16)
 45            except:
 46                self.semaphore = None
 47            self.task = match.group(4)
 48            self.prio_from = int(match.group(5))
 49            self.operation = match.group(6)
 50            self.prio_to = int(match.group(7))
 51            self.reason = None if not match.group(8) else match.group(8)
 52            self.is_valid = True
 53
 54    def __repr__(self) -> str:
 55        return f"{self.task} {self.prio_from} {self.operation} {self.prio_to}"
 56
 57
 58def summarize_semaphore_boostlog(logfile: Path) -> str:
 59    """
 60    Analyze a boost log and create a statistical summary and return it.
 61
 62    :param logfile: The boostlog file to analyze
 63    :return: A formatted string containing the analysis.
 64    """
 65    output = []
 66
 67    ops = []
 68    for line in read_gdb_log(logfile).splitlines():
 69        op = _BoostOperation(line)
 70        if op.is_valid:
 71            ops.append(op)
 72        # else:
 73        #     print(line)
 74    if not ops: return "No task boosts found!"
 75    ops.sort(key=lambda o: o.uptime)
 76
 77
 78    uptime = ops[-1].uptime
 79    sample_time = max(o.uptime for o in ops) - min(o.uptime for o in ops)
 80    output.append(f"Uptime: {uptime/1e6:.1f}s = {uptime/6e7:.1f}min")
 81    output.append(f"Sample time: {sample_time/1e6:.1f}s = {sample_time/6e7:.1f}min")
 82
 83    # Print summary statistics
 84    prios = []
 85    def _mean(values):
 86        return int((statistics.fmean(values) if len(values) else 0) / 1e3)
 87    def _stdev(values):
 88        if len(values) >= 2: return int(statistics.stdev(values) / 1e3)
 89        return values[0] if len(values) else 0
 90
 91    diffs = [n.uptime - p.uptime for p,n in itertools.pairwise(ops)]
 92    prios.append(["All", len(ops), _mean(diffs), _stdev(diffs)])
 93
 94    boost_up = [op for op in ops if op.operation == "_/"]
 95    diffs_up = [n.uptime - p.uptime for p,n in itertools.pairwise(boost_up)]
 96    prios.append(["Up", len(boost_up), _mean(diffs_up), _stdev(diffs_up)])
 97
 98    boost_down = [op for op in ops if op.operation == "\\_"]
 99    diffs_down = [n.uptime - p.uptime for p,n in itertools.pairwise(boost_down)]
100    prios.append(["Down", len(boost_down), _mean(diffs_down), _stdev(diffs_down)])
101
102    boost_255 = [op for op in ops if op.operation == "_/" and op.prio_to == 255]
103    diffs_255 = [n.uptime - p.uptime for p,n in itertools.pairwise(boost_255)]
104    prios.append(["255", len(boost_255), _mean(diffs_255), _stdev(diffs_255)])
105
106    fmtstr = "{:%d}  {:>%d} ~{:>%d}ms ±{:>%d}ms"
107    header = ["KIND", "#BOOSTS", "MEAN", "STDEV"]
108    output.append("")
109    output.append(format_table(fmtstr, header, prios))
110
111    # Print summary statistics per task
112    fmtstr = "{:%d}  {:>%d} {:>%d}%%  [{:>%d}, {:>%d}]   {:%d}   {:>%d}  {:%d}"
113    header = ["TASK", "#BOOSTS", "PCT", "MIN", "MAX", "REASONS", "#MAX", "REASONS FOR MAX"]
114    tasks = defaultdict(list)
115    for op in ops:
116        tasks[op.task].append(op)
117    task_prios = [[
118            task,
119            boosts := sum(1 for o in ops if o if o.operation == "_/"),
120            f"{boosts * 100 / len(boost_up):.1f}",
121            min(o.prio_from for o in ops),
122            max_prio := max(o.prio_to for o in ops),
123            ", ".join(sorted({o.reason for o in ops if o.reason is not None})),
124            sum(1 for o in ops if o if o.operation == "_/" and o.prio_to == max_prio),
125            ", ".join(sorted({o.reason for o in ops if o.reason is not None and o.prio_to == max_prio}))
126        ]
127        for task, ops in tasks.items()]
128    task_prios.sort(key=lambda t: (t[4], t[3], t[0], t[1]))
129    output.append("")
130    output.append(format_table(fmtstr, header, task_prios))
131
132    semtasks = defaultdict(list)
133    semtasksr = defaultdict(list)
134    for op in ops:
135        if op.semaphore is not None:
136            semtasks[op.semaphore].append(op.task)
137            if op.reason: semtasksr[op.semaphore].append(op.reason)
138    semtasks = [[hex(s), len(ts), ", ".join(sorted(set(ts))), ", ".join(sorted(set(semtasksr.get(s, []))))] for s, ts in semtasks.items()]
139    semtasks.sort(key=lambda s: -s[1])
140    fmtstr = "{:%d}  {:>%d}  {:%d}  {:%d}"
141    header = ["SEMAPHORE", "#BOOSTS", "BOOSTS THESE TASKS", "BECAUSE OF THESE TASKS"]
142    output.append("")
143    output.append(format_table(fmtstr, header, semtasks))
144
145    return "\n".join(output)
146
147
148# -----------------------------------------------------------------------------
149if __name__ == "__main__":
150    import argparse
151
152    parser = argparse.ArgumentParser(description="Priority Ceiling Analyzer")
153    parser.add_argument(
154        "file",
155        type=Path,
156        help="The GDB log containing the semaphore boost trace.")
157    args = parser.parse_args()
158
159    print(summarize_semaphore_boostlog(args.file))
def summarize_semaphore_boostlog(logfile: pathlib.Path) -> str:
 59def summarize_semaphore_boostlog(logfile: Path) -> str:
 60    """
 61    Analyze a boost log and create a statistical summary and return it.
 62
 63    :param logfile: The boostlog file to analyze
 64    :return: A formatted string containing the analysis.
 65    """
 66    output = []
 67
 68    ops = []
 69    for line in read_gdb_log(logfile).splitlines():
 70        op = _BoostOperation(line)
 71        if op.is_valid:
 72            ops.append(op)
 73        # else:
 74        #     print(line)
 75    if not ops: return "No task boosts found!"
 76    ops.sort(key=lambda o: o.uptime)
 77
 78
 79    uptime = ops[-1].uptime
 80    sample_time = max(o.uptime for o in ops) - min(o.uptime for o in ops)
 81    output.append(f"Uptime: {uptime/1e6:.1f}s = {uptime/6e7:.1f}min")
 82    output.append(f"Sample time: {sample_time/1e6:.1f}s = {sample_time/6e7:.1f}min")
 83
 84    # Print summary statistics
 85    prios = []
 86    def _mean(values):
 87        return int((statistics.fmean(values) if len(values) else 0) / 1e3)
 88    def _stdev(values):
 89        if len(values) >= 2: return int(statistics.stdev(values) / 1e3)
 90        return values[0] if len(values) else 0
 91
 92    diffs = [n.uptime - p.uptime for p,n in itertools.pairwise(ops)]
 93    prios.append(["All", len(ops), _mean(diffs), _stdev(diffs)])
 94
 95    boost_up = [op for op in ops if op.operation == "_/"]
 96    diffs_up = [n.uptime - p.uptime for p,n in itertools.pairwise(boost_up)]
 97    prios.append(["Up", len(boost_up), _mean(diffs_up), _stdev(diffs_up)])
 98
 99    boost_down = [op for op in ops if op.operation == "\\_"]
100    diffs_down = [n.uptime - p.uptime for p,n in itertools.pairwise(boost_down)]
101    prios.append(["Down", len(boost_down), _mean(diffs_down), _stdev(diffs_down)])
102
103    boost_255 = [op for op in ops if op.operation == "_/" and op.prio_to == 255]
104    diffs_255 = [n.uptime - p.uptime for p,n in itertools.pairwise(boost_255)]
105    prios.append(["255", len(boost_255), _mean(diffs_255), _stdev(diffs_255)])
106
107    fmtstr = "{:%d}  {:>%d} ~{:>%d}ms ±{:>%d}ms"
108    header = ["KIND", "#BOOSTS", "MEAN", "STDEV"]
109    output.append("")
110    output.append(format_table(fmtstr, header, prios))
111
112    # Print summary statistics per task
113    fmtstr = "{:%d}  {:>%d} {:>%d}%%  [{:>%d}, {:>%d}]   {:%d}   {:>%d}  {:%d}"
114    header = ["TASK", "#BOOSTS", "PCT", "MIN", "MAX", "REASONS", "#MAX", "REASONS FOR MAX"]
115    tasks = defaultdict(list)
116    for op in ops:
117        tasks[op.task].append(op)
118    task_prios = [[
119            task,
120            boosts := sum(1 for o in ops if o if o.operation == "_/"),
121            f"{boosts * 100 / len(boost_up):.1f}",
122            min(o.prio_from for o in ops),
123            max_prio := max(o.prio_to for o in ops),
124            ", ".join(sorted({o.reason for o in ops if o.reason is not None})),
125            sum(1 for o in ops if o if o.operation == "_/" and o.prio_to == max_prio),
126            ", ".join(sorted({o.reason for o in ops if o.reason is not None and o.prio_to == max_prio}))
127        ]
128        for task, ops in tasks.items()]
129    task_prios.sort(key=lambda t: (t[4], t[3], t[0], t[1]))
130    output.append("")
131    output.append(format_table(fmtstr, header, task_prios))
132
133    semtasks = defaultdict(list)
134    semtasksr = defaultdict(list)
135    for op in ops:
136        if op.semaphore is not None:
137            semtasks[op.semaphore].append(op.task)
138            if op.reason: semtasksr[op.semaphore].append(op.reason)
139    semtasks = [[hex(s), len(ts), ", ".join(sorted(set(ts))), ", ".join(sorted(set(semtasksr.get(s, []))))] for s, ts in semtasks.items()]
140    semtasks.sort(key=lambda s: -s[1])
141    fmtstr = "{:%d}  {:>%d}  {:%d}  {:%d}"
142    header = ["SEMAPHORE", "#BOOSTS", "BOOSTS THESE TASKS", "BECAUSE OF THESE TASKS"]
143    output.append("")
144    output.append(format_table(fmtstr, header, semtasks))
145
146    return "\n".join(output)

Analyze a boost log and create a statistical summary and return it.

Parameters
  • logfile: The boostlog file to analyze
Returns

A formatted string containing the analysis.