emdbg.debug.px4.utils

  1# Copyright (c) 2023, Auterion AG
  2# SPDX-License-Identifier: BSD-3-Clause
  3
  4from __future__ import annotations
  5import re
  6import math
  7
  8from datetime import datetime
  9from itertools import zip_longest
 10from pathlib import Path
 11
 12
 13# -----------------------------------------------------------------------------
 14def gdb_getfield(value: "gdb.Value", name: str, default=None):
 15    """Find the field of a struct/class by name"""
 16    for f in value.type.fields():
 17        if name == f.name:
 18            return value[name]
 19    return default
 20
 21
 22def gdb_iter(obj):
 23    """yields the values in an array or value with a range"""
 24    if hasattr(obj, "value"):
 25        obj = obj.value()
 26    if hasattr(obj.type, "range"):
 27        for ii in range(*obj.type.range()):
 28            yield obj[ii]
 29    else:
 30        return []
 31
 32def gdb_len(obj) -> int:
 33    """Computes the length of a gdb object"""
 34    if hasattr(obj.type, "range"):
 35        start, stop = obj.type.range()
 36        return stop - start
 37    else:
 38        return 1
 39
 40def gdb_backtrace(gdb) -> str:
 41    """
 42    Unfortunately the built-in gdb command `backtrace` often crashes when
 43    trying to resolve function arguments whose memory is inaccessible due to
 44    optimizations or whose type is too complex.
 45    Therefore this is a simpler implementation in Python to avoid GDB crashing.
 46
 47    ```
 48    (gdb) px4_backtrace
 49    #0  0x0800b3be in sched_unlock() at platforms/nuttx/NuttX/nuttx/sched/sched/sched_unlock.c:272
 50    #1  0x0800b59e in nxsem_post() at platforms/nuttx/NuttX/nuttx/sched/semaphore/sem_post.c:175
 51    #2  0x0800b5b6 in sem_post() at platforms/nuttx/NuttX/nuttx/sched/semaphore/sem_post.c:220
 52    #3  0x08171570 in px4::WorkQueue::SignalWorkerThread() at platforms/common/px4_work_queue/WorkQueue.cpp:151
 53    #4  0x08171570 in px4::WorkQueue::Add(px4::WorkItem*) at platforms/common/px4_work_queue/WorkQueue.cpp:143
 54    #5  0x0816f8dc in px4::WorkItem::ScheduleNow() at platforms/common/include/px4_platform_common/px4_work_queue/WorkItem.hpp:69
 55    #6  0x0816f8dc in uORB::SubscriptionCallbackWorkItem::call() at platforms/common/uORB/SubscriptionCallback.hpp:169
 56    #7  0x0816f8dc in uORB::DeviceNode::write(file*, char const*, unsigned int) at platforms/common/uORB/uORBDeviceNode.cpp:221
 57    #8  0x0816faca in uORB::DeviceNode::publish(orb_metadata const*, void*, void const*) at platforms/common/uORB/uORBDeviceNode.cpp:295
 58    #9  0x081700bc in uORB::Manager::orb_publish(orb_metadata const*, void*, void const*) at platforms/common/uORB/uORBManager.cpp:409
 59    #10 0x0816e6d8 in orb_publish(orb_metadata const*, orb_advert_t, void const*) at platforms/common/uORB/uORBManager.hpp:193
 60    #11 0x08160912 in uORB::PublicationMulti<sensor_gyro_fifo_s, (unsigned char)4>::publish(sensor_gyro_fifo_s const&) at platforms/common/uORB/PublicationMulti.hpp:92
 61    #12 0x08160912 in PX4Gyroscope::updateFIFO(sensor_gyro_fifo_s&) at src/lib/drivers/gyroscope/PX4Gyroscope.cpp:149
 62    #13 0x08040e74 in Bosch::BMI088::Gyroscope::BMI088_Gyroscope::FIFORead(unsigned long long const&, unsigned char) at src/drivers/imu/bosch/bmi088/BMI088_Gyroscope.cpp:447
 63    #14 0x08041102 in Bosch::BMI088::Gyroscope::BMI088_Gyroscope::RunImpl() at src/drivers/imu/bosch/bmi088/BMI088_Gyroscope.cpp:224
 64    #15 0x0803fb7a in I2CSPIDriver<BMI088>::Run() at platforms/common/include/px4_platform_common/i2c_spi_buses.h:343
 65    #16 0x0817164c in px4::WorkQueue::Run() at platforms/common/px4_work_queue/WorkQueue.cpp:187
 66    #17 0x08171798 in px4::WorkQueueRunner(void*) at platforms/common/px4_work_queue/WorkQueueManager.cpp:236
 67    #18 0x08014ccc in pthread_startup() at platforms/nuttx/NuttX/nuttx/libs/libc/pthread/pthread_create.c:59
 68    ```
 69
 70    :return: the selected frame's backtrace without resolving function argument
 71    """
 72    frame = gdb.selected_frame()
 73    index = 0
 74    output = []
 75    while(frame and frame.is_valid()):
 76        pc = frame.pc()
 77        if pc > 0xffff_ff00:
 78            output.append(f"#{index: <2} <signal handler called>")
 79        else:
 80            line = "??"
 81            file = "??"
 82            if sal := frame.find_sal():
 83                line = sal.line
 84                if sal.symtab:
 85                    file = sal.symtab.fullname()
 86            if func := frame.function():
 87                func = func.print_name
 88                if not func.endswith(")"): func += "()"
 89            else:
 90                func = "??"
 91            output.append(f"#{index: <2} 0x{pc:08x} in {func} at {file}:{line}")
 92        frame = frame.older()
 93        index += 1
 94    return "\n".join(output)
 95
 96def gdb_relative_location(gdb, location) -> str:
 97    """
 98    GDB can only place breakpoint on specific line number inside a file.
 99    However, if the file changes the line numbers can shift around, which makes
100    this method brittle. Therefore this function finds the function inside
101    the file and applies a offset or searches for a pattern inside that function
102    to determine the correct line number.
103
104    :param location:
105        Location `function:+offset` or `function:regex`. In case the function
106        uses static linkage you may also need to provide a unique part of the
107        filename path to arbitrate multiple identically named static functions
108        `file:function:+offset` or `file:function:regex`.
109
110    :return: absolute location string `file:line_number`
111    """
112    parts = location.split(":")
113    file_name = None
114    if len(parts) == 3:
115        file_name, function_name, line_pattern = parts
116    elif len(parts) == 2:
117        function_name, line_pattern = parts
118    else:
119        raise ValueError(f"Unknown location format '{location}'!")()
120
121    function = gdb.lookup_global_symbol(function_name, gdb.SYMBOL_VAR_DOMAIN)
122    if function is None:
123        # Multiple static symbols may exists, we use the filename to arbitrate
124        if functions := gdb.lookup_static_symbols(function_name, gdb.SYMBOL_VAR_DOMAIN):
125            file_functions = [(f.symtab.fullname(), f) for f in functions]
126            # Arbitrate using file name hint
127            if file_name is not None:
128                file_functions = [f for f in file_functions if file_name in f[0]]
129            if len(file_functions) == 1:
130                function = file_functions[0][1]
131            else:
132                raise ValueError("Multiple functions found:\n - " + "\n - ".join(functions))
133    if function is None:
134        raise ValueError(f"Cannot find function name '{function_name}'")
135    assert function.is_function
136
137    # Find source file and line numbers: how to use file_name?
138    file = function.symtab.fullname()
139    line_numbers = function.symtab.linetable().source_lines()
140    lmin, lmax = min(line_numbers), max(line_numbers)
141
142    if line_pattern.startswith("+"):
143        # line offset relative to function
144        line = int(line_pattern[1:]) + function.line
145    else:
146        # regex line pattern, read source file and find the line
147        lines = Path(file).read_text().splitlines()
148        lines = list(enumerate(lines[lmin:lmax]))
149        for ii, line in lines:
150            if re.search(line_pattern, line):
151                line = lmin + ii
152                break
153        else:
154            lines = "\n  ".join(f"{lmin+l[0]:>4}: {l[1]}" for l in lines)
155            raise ValueError(f"Cannot find source line for '{line_pattern}'!\n"
156                f"Function '{function_name}' stretches over lines {lmin}-{lmax}.\n")
157                # f"Available source lines are:\n  {lines}")
158
159    return f"{file}:{line}"
160
161
162# -----------------------------------------------------------------------------
163def _binary_search(array, value, lo: int, hi: int, direction: int):
164    middle = (lo + hi) // 2
165    if hi - lo <= 1: return middle
166
167    nslice = [(lo, middle), (middle, hi)]
168    pick_lower_half = array[middle] == value
169    if direction > 0: pick_lower_half = 1 - pick_lower_half
170
171    lo, hi = nslice[pick_lower_half]
172    return _binary_search(array, value, lo, hi, direction)
173
174def binary_search_last(array, value, lo: int = None, hi: int = None):
175    """Binary search the last occurrance of value in an array"""
176    if lo is None: lo = 0
177    if hi is None: hi = len(array)
178    return _binary_search(array, value, lo, hi, direction=-1)
179
180def binary_search_first(array, value, lo: int = None, hi: int = None):
181    """Binary search the first occurrance of value in an array"""
182    if lo is None: lo = 0
183    if hi is None: hi = len(array)
184    return _binary_search(array, value, lo, hi, direction=1)
185
186
187# -----------------------------------------------------------------------------
188def chunks(iterable, chunk_size: int, fill=None):
189    """Convert a iterable into a list of chunks and fill the rest with a value"""
190    args = [iter(iterable)] * chunk_size
191    return zip_longest(*args, fillvalue=fill)
192
193
194def add_datetime(filename: str|Path):
195    """
196    Appends a filename with the current date and time:
197    `Year_Month_Day_Hour_Minute_Second`
198
199    Example: `path/name.txt` -> `path/name_2023_04_14_15_03_24.txt`
200    """
201    filename = Path(filename)
202    return filename.with_stem(f"{filename.stem}_{datetime.now().strftime('%Y_%m_%d_%H_%M_%S')}")
203
204
205def format_units(value: int | float | None, prefixes: dict[str, int] | str,
206                 unit: str = None, fmt: str = None, if_zero: str = None) -> str:
207    """
208    Format a value with the largest prefix.
209
210    The value is divided by the list of prefixes until it is smaller than the
211    next largest prefix. Trailing zeros are replaced by spaces and padding is
212    applied to align the prefixes and units. If the value is zero, the
213    `if_zero` string is returned if defined.
214
215    Predefined prefixes can be passed as a `group:input-prefix`:
216    - `t`: time prefixes from nanoseconds to days.
217    - `si`: SI prefixes from nano to Tera.
218
219    .. note:: The micro prefix is µ, not u.
220
221    Example:
222
223    ```py
224    format_units(123456, "t:µs", fmt=".1f")    # "123.5ms"
225    format_units(0, "t:s", if_zero="-")        # "-"
226    format_units(1234, "si:", "Hz", fmt=".2f") # "1.23kHz"
227    format_units(1001, "si:", "Hz", fmt=".2f") # "1   kHz"
228    format_units(2345, {"k": 1e3, "M": 1e3}, "Si", fmt=".1f") # "2.3MSi"
229    ```
230
231    :param value: An integer or floating point value. If None, an empty string is returned.
232    :param prefixes:
233        A dictionary of prefix string to ratio of the next largest prefix. The
234        dictionary must be sorted from smallest to largest prefix. The prefix of
235        the input value must be the first entry.
236    :param unit: A unit string to be appended to the formatted value.
237    :param fmt: A format specifier to be applied when formatting the value.
238    :param if_zero: A string to be returned when the value is zero.
239    """
240    if value is None: return ""
241    if if_zero is not None and value == 0: return if_zero
242
243    # Find the correct prefix from a list of predefined common prefixes
244    _found = False
245    if prefixes.startswith("t:"):
246        time_units = {"ns": 1e3, "µs": 1e3, "ms": 1e3, "s": 60, "m": 60, "h": 24, "d": 365.25/12}
247        prefixes = prefixes.split(":")[1]
248        prefixes = {k:v for k, v in time_units.items() if _found or (_found := (k == prefixes))}
249    elif prefixes.startswith("si:"):
250        prefixes = prefixes.split(":")[1]
251        prefixes = {k:1e3 for k in ["n", "µ", "m", "", "k", "M", "G", "T"]
252                    if _found or (_found := (k == prefixes))}
253
254    # Divide the value until it is smaller than the next largest prefix
255    for prefix, factor in prefixes.items():
256        if value < factor: break
257        value /= factor
258
259    # Format the value
260    value = f"{value:{fmt or ''}}"
261    value_stripped = value.rstrip("0").rstrip(".")
262    if if_zero is not None and value_stripped == "0": return if_zero
263    # pad the value to the right to align it
264    padding = max(len(p) for p in prefixes.keys()) - len(prefix)
265    padding += len(value) - len(value_stripped)
266    return f"{value_stripped}{padding * ' '}{prefix}{unit or ''}"
267
268
269# -----------------------------------------------------------------------------
270def format_table(fmtstr: str, header: list[str], rows: list[list[str]], columns: int = 1) -> str:
271    """
272    DEPRECATED: Use `rich.table.Table` instead!
273
274    Formats a list of rows into a table of multiple meta-columns based on the format string.
275    Example for formatting an array of registers into a table with three meta-columns:
276
277    ```py
278    fmtstr = "{:%d}  {:>%d}  {:>%d}"
279    header = ["NAME", "HEX VALUE", "INT VALUE"]
280    rows = [[reg, hex(value), value] for reg, value in registers.items()]
281    table = utils.format_table(fmtstr, header, rows, 3)
282    ```
283
284    :param fmtstr: A string describing the spacing between columns and their
285                   alignment. Must have exactly as many entries as the header a
286                   rows. Example: two columns, aligned right and left with a
287                   brace formatter: `"{:>%d} ({:%d})"` The `%d` is replaced by
288                   the max column width by this function.
289    :param header: A list of names for the header row. If you specify more than
290                   one column, the header will be duplicated.
291    :param rows: a list of lists of entries in the table. Each entry will be
292                 converted to `str` to count the maximal length of each column.
293    :param columns: If a table is very long, the header can be duplicated to the
294                    right side of the table to fill more screen space.
295    """
296    # duplicate and join the format string for each column
297    split_horizontal = columns > 0
298    columns = abs(columns)
299    fmtstr = "      :      ".join([fmtstr] * columns)
300    fmtcnt = fmtstr.count("%d")
301    column_width = [0] * fmtcnt
302
303    # Interleave the rows for the later chunking
304    fill = [""] * (fmtcnt // columns)
305    if split_horizontal:
306        rows = [val for tup in zip(*chunks(rows, math.ceil(len(rows) / columns), fill)) for val in tup]
307    # prepend the duplicated header before the rows
308    if header is not None:
309        rows = [header] * columns + rows
310    # Group the individual rows into multiple columns per row
311    rows = [sum(rs, []) for rs in chunks(rows, columns, fill)]
312
313    # collect each line and compute the column width
314    lines = []
315    for row in rows:
316        if len(row) != fmtcnt:
317            raise ValueError("Each row have the same number of entries as the format string")
318        line = [str(l) for l in row]
319        lines.append(line)
320        column_width = [max(w, len(l)) for w, l in zip(column_width, line)]
321
322    # Format the format string with the column width first
323    fmtstr = fmtstr % tuple(column_width)
324    # Now format the actual lines with the formatted format string
325    return "\n".join(fmtstr.format(*line).rstrip() for line in lines) + "\n"
326
327
328# -----------------------------------------------------------------------------
329class _Singleton(type):
330    _instances = {}
331    def __call__(cls, *args, **kwargs):
332        if cls not in cls._instances:
333            cls._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs)
334        return cls._instances[cls]
def gdb_getfield(value: "'gdb.Value'", name: str, default=None):
15def gdb_getfield(value: "gdb.Value", name: str, default=None):
16    """Find the field of a struct/class by name"""
17    for f in value.type.fields():
18        if name == f.name:
19            return value[name]
20    return default

Find the field of a struct/class by name

def gdb_iter(obj):
23def gdb_iter(obj):
24    """yields the values in an array or value with a range"""
25    if hasattr(obj, "value"):
26        obj = obj.value()
27    if hasattr(obj.type, "range"):
28        for ii in range(*obj.type.range()):
29            yield obj[ii]
30    else:
31        return []

yields the values in an array or value with a range

def gdb_len(obj) -> int:
33def gdb_len(obj) -> int:
34    """Computes the length of a gdb object"""
35    if hasattr(obj.type, "range"):
36        start, stop = obj.type.range()
37        return stop - start
38    else:
39        return 1

Computes the length of a gdb object

def gdb_backtrace(gdb) -> str:
41def gdb_backtrace(gdb) -> str:
42    """
43    Unfortunately the built-in gdb command `backtrace` often crashes when
44    trying to resolve function arguments whose memory is inaccessible due to
45    optimizations or whose type is too complex.
46    Therefore this is a simpler implementation in Python to avoid GDB crashing.
47
48    ```
49    (gdb) px4_backtrace
50    #0  0x0800b3be in sched_unlock() at platforms/nuttx/NuttX/nuttx/sched/sched/sched_unlock.c:272
51    #1  0x0800b59e in nxsem_post() at platforms/nuttx/NuttX/nuttx/sched/semaphore/sem_post.c:175
52    #2  0x0800b5b6 in sem_post() at platforms/nuttx/NuttX/nuttx/sched/semaphore/sem_post.c:220
53    #3  0x08171570 in px4::WorkQueue::SignalWorkerThread() at platforms/common/px4_work_queue/WorkQueue.cpp:151
54    #4  0x08171570 in px4::WorkQueue::Add(px4::WorkItem*) at platforms/common/px4_work_queue/WorkQueue.cpp:143
55    #5  0x0816f8dc in px4::WorkItem::ScheduleNow() at platforms/common/include/px4_platform_common/px4_work_queue/WorkItem.hpp:69
56    #6  0x0816f8dc in uORB::SubscriptionCallbackWorkItem::call() at platforms/common/uORB/SubscriptionCallback.hpp:169
57    #7  0x0816f8dc in uORB::DeviceNode::write(file*, char const*, unsigned int) at platforms/common/uORB/uORBDeviceNode.cpp:221
58    #8  0x0816faca in uORB::DeviceNode::publish(orb_metadata const*, void*, void const*) at platforms/common/uORB/uORBDeviceNode.cpp:295
59    #9  0x081700bc in uORB::Manager::orb_publish(orb_metadata const*, void*, void const*) at platforms/common/uORB/uORBManager.cpp:409
60    #10 0x0816e6d8 in orb_publish(orb_metadata const*, orb_advert_t, void const*) at platforms/common/uORB/uORBManager.hpp:193
61    #11 0x08160912 in uORB::PublicationMulti<sensor_gyro_fifo_s, (unsigned char)4>::publish(sensor_gyro_fifo_s const&) at platforms/common/uORB/PublicationMulti.hpp:92
62    #12 0x08160912 in PX4Gyroscope::updateFIFO(sensor_gyro_fifo_s&) at src/lib/drivers/gyroscope/PX4Gyroscope.cpp:149
63    #13 0x08040e74 in Bosch::BMI088::Gyroscope::BMI088_Gyroscope::FIFORead(unsigned long long const&, unsigned char) at src/drivers/imu/bosch/bmi088/BMI088_Gyroscope.cpp:447
64    #14 0x08041102 in Bosch::BMI088::Gyroscope::BMI088_Gyroscope::RunImpl() at src/drivers/imu/bosch/bmi088/BMI088_Gyroscope.cpp:224
65    #15 0x0803fb7a in I2CSPIDriver<BMI088>::Run() at platforms/common/include/px4_platform_common/i2c_spi_buses.h:343
66    #16 0x0817164c in px4::WorkQueue::Run() at platforms/common/px4_work_queue/WorkQueue.cpp:187
67    #17 0x08171798 in px4::WorkQueueRunner(void*) at platforms/common/px4_work_queue/WorkQueueManager.cpp:236
68    #18 0x08014ccc in pthread_startup() at platforms/nuttx/NuttX/nuttx/libs/libc/pthread/pthread_create.c:59
69    ```
70
71    :return: the selected frame's backtrace without resolving function argument
72    """
73    frame = gdb.selected_frame()
74    index = 0
75    output = []
76    while(frame and frame.is_valid()):
77        pc = frame.pc()
78        if pc > 0xffff_ff00:
79            output.append(f"#{index: <2} <signal handler called>")
80        else:
81            line = "??"
82            file = "??"
83            if sal := frame.find_sal():
84                line = sal.line
85                if sal.symtab:
86                    file = sal.symtab.fullname()
87            if func := frame.function():
88                func = func.print_name
89                if not func.endswith(")"): func += "()"
90            else:
91                func = "??"
92            output.append(f"#{index: <2} 0x{pc:08x} in {func} at {file}:{line}")
93        frame = frame.older()
94        index += 1
95    return "\n".join(output)

Unfortunately the built-in gdb command backtrace often crashes when trying to resolve function arguments whose memory is inaccessible due to optimizations or whose type is too complex. Therefore this is a simpler implementation in Python to avoid GDB crashing.

(gdb) px4_backtrace
#0  0x0800b3be in sched_unlock() at platforms/nuttx/NuttX/nuttx/sched/sched/sched_unlock.c:272
#1  0x0800b59e in nxsem_post() at platforms/nuttx/NuttX/nuttx/sched/semaphore/sem_post.c:175
#2  0x0800b5b6 in sem_post() at platforms/nuttx/NuttX/nuttx/sched/semaphore/sem_post.c:220
#3  0x08171570 in px4::WorkQueue::SignalWorkerThread() at platforms/common/px4_work_queue/WorkQueue.cpp:151
#4  0x08171570 in px4::WorkQueue::Add(px4::WorkItem*) at platforms/common/px4_work_queue/WorkQueue.cpp:143
#5  0x0816f8dc in px4::WorkItem::ScheduleNow() at platforms/common/include/px4_platform_common/px4_work_queue/WorkItem.hpp:69
#6  0x0816f8dc in uORB::SubscriptionCallbackWorkItem::call() at platforms/common/uORB/SubscriptionCallback.hpp:169
#7  0x0816f8dc in uORB::DeviceNode::write(file*, char const*, unsigned int) at platforms/common/uORB/uORBDeviceNode.cpp:221
#8  0x0816faca in uORB::DeviceNode::publish(orb_metadata const*, void*, void const*) at platforms/common/uORB/uORBDeviceNode.cpp:295
#9  0x081700bc in uORB::Manager::orb_publish(orb_metadata const*, void*, void const*) at platforms/common/uORB/uORBManager.cpp:409
#10 0x0816e6d8 in orb_publish(orb_metadata const*, orb_advert_t, void const*) at platforms/common/uORB/uORBManager.hpp:193
#11 0x08160912 in uORB::PublicationMulti<sensor_gyro_fifo_s, (unsigned char)4>::publish(sensor_gyro_fifo_s const&) at platforms/common/uORB/PublicationMulti.hpp:92
#12 0x08160912 in PX4Gyroscope::updateFIFO(sensor_gyro_fifo_s&) at src/lib/drivers/gyroscope/PX4Gyroscope.cpp:149
#13 0x08040e74 in Bosch::BMI088::Gyroscope::BMI088_Gyroscope::FIFORead(unsigned long long const&, unsigned char) at src/drivers/imu/bosch/bmi088/BMI088_Gyroscope.cpp:447
#14 0x08041102 in Bosch::BMI088::Gyroscope::BMI088_Gyroscope::RunImpl() at src/drivers/imu/bosch/bmi088/BMI088_Gyroscope.cpp:224
#15 0x0803fb7a in I2CSPIDriver<BMI088>::Run() at platforms/common/include/px4_platform_common/i2c_spi_buses.h:343
#16 0x0817164c in px4::WorkQueue::Run() at platforms/common/px4_work_queue/WorkQueue.cpp:187
#17 0x08171798 in px4::WorkQueueRunner(void*) at platforms/common/px4_work_queue/WorkQueueManager.cpp:236
#18 0x08014ccc in pthread_startup() at platforms/nuttx/NuttX/nuttx/libs/libc/pthread/pthread_create.c:59
Returns

the selected frame's backtrace without resolving function argument

def gdb_relative_location(gdb, location) -> str:
 97def gdb_relative_location(gdb, location) -> str:
 98    """
 99    GDB can only place breakpoint on specific line number inside a file.
100    However, if the file changes the line numbers can shift around, which makes
101    this method brittle. Therefore this function finds the function inside
102    the file and applies a offset or searches for a pattern inside that function
103    to determine the correct line number.
104
105    :param location:
106        Location `function:+offset` or `function:regex`. In case the function
107        uses static linkage you may also need to provide a unique part of the
108        filename path to arbitrate multiple identically named static functions
109        `file:function:+offset` or `file:function:regex`.
110
111    :return: absolute location string `file:line_number`
112    """
113    parts = location.split(":")
114    file_name = None
115    if len(parts) == 3:
116        file_name, function_name, line_pattern = parts
117    elif len(parts) == 2:
118        function_name, line_pattern = parts
119    else:
120        raise ValueError(f"Unknown location format '{location}'!")()
121
122    function = gdb.lookup_global_symbol(function_name, gdb.SYMBOL_VAR_DOMAIN)
123    if function is None:
124        # Multiple static symbols may exists, we use the filename to arbitrate
125        if functions := gdb.lookup_static_symbols(function_name, gdb.SYMBOL_VAR_DOMAIN):
126            file_functions = [(f.symtab.fullname(), f) for f in functions]
127            # Arbitrate using file name hint
128            if file_name is not None:
129                file_functions = [f for f in file_functions if file_name in f[0]]
130            if len(file_functions) == 1:
131                function = file_functions[0][1]
132            else:
133                raise ValueError("Multiple functions found:\n - " + "\n - ".join(functions))
134    if function is None:
135        raise ValueError(f"Cannot find function name '{function_name}'")
136    assert function.is_function
137
138    # Find source file and line numbers: how to use file_name?
139    file = function.symtab.fullname()
140    line_numbers = function.symtab.linetable().source_lines()
141    lmin, lmax = min(line_numbers), max(line_numbers)
142
143    if line_pattern.startswith("+"):
144        # line offset relative to function
145        line = int(line_pattern[1:]) + function.line
146    else:
147        # regex line pattern, read source file and find the line
148        lines = Path(file).read_text().splitlines()
149        lines = list(enumerate(lines[lmin:lmax]))
150        for ii, line in lines:
151            if re.search(line_pattern, line):
152                line = lmin + ii
153                break
154        else:
155            lines = "\n  ".join(f"{lmin+l[0]:>4}: {l[1]}" for l in lines)
156            raise ValueError(f"Cannot find source line for '{line_pattern}'!\n"
157                f"Function '{function_name}' stretches over lines {lmin}-{lmax}.\n")
158                # f"Available source lines are:\n  {lines}")
159
160    return f"{file}:{line}"

GDB can only place breakpoint on specific line number inside a file. However, if the file changes the line numbers can shift around, which makes this method brittle. Therefore this function finds the function inside the file and applies a offset or searches for a pattern inside that function to determine the correct line number.

Parameters
  • location: Location function:+offset or function:regex. In case the function uses static linkage you may also need to provide a unique part of the filename path to arbitrate multiple identically named static functions file:function:+offset or file:function:regex.
Returns

absolute location string file:line_number

def binary_search_last(array, value, lo: int = None, hi: int = None):
175def binary_search_last(array, value, lo: int = None, hi: int = None):
176    """Binary search the last occurrance of value in an array"""
177    if lo is None: lo = 0
178    if hi is None: hi = len(array)
179    return _binary_search(array, value, lo, hi, direction=-1)

Binary search the last occurrance of value in an array

def binary_search_first(array, value, lo: int = None, hi: int = None):
181def binary_search_first(array, value, lo: int = None, hi: int = None):
182    """Binary search the first occurrance of value in an array"""
183    if lo is None: lo = 0
184    if hi is None: hi = len(array)
185    return _binary_search(array, value, lo, hi, direction=1)

Binary search the first occurrance of value in an array

def chunks(iterable, chunk_size: int, fill=None):
189def chunks(iterable, chunk_size: int, fill=None):
190    """Convert a iterable into a list of chunks and fill the rest with a value"""
191    args = [iter(iterable)] * chunk_size
192    return zip_longest(*args, fillvalue=fill)

Convert a iterable into a list of chunks and fill the rest with a value

def add_datetime(filename: str | pathlib.Path):
195def add_datetime(filename: str|Path):
196    """
197    Appends a filename with the current date and time:
198    `Year_Month_Day_Hour_Minute_Second`
199
200    Example: `path/name.txt` -> `path/name_2023_04_14_15_03_24.txt`
201    """
202    filename = Path(filename)
203    return filename.with_stem(f"{filename.stem}_{datetime.now().strftime('%Y_%m_%d_%H_%M_%S')}")

Appends a filename with the current date and time: Year_Month_Day_Hour_Minute_Second

Example: path/name.txt -> path/name_2023_04_14_15_03_24.txt

def format_units( value: int | float | None, prefixes: dict[str, int] | str, unit: str = None, fmt: str = None, if_zero: str = None) -> str:
206def format_units(value: int | float | None, prefixes: dict[str, int] | str,
207                 unit: str = None, fmt: str = None, if_zero: str = None) -> str:
208    """
209    Format a value with the largest prefix.
210
211    The value is divided by the list of prefixes until it is smaller than the
212    next largest prefix. Trailing zeros are replaced by spaces and padding is
213    applied to align the prefixes and units. If the value is zero, the
214    `if_zero` string is returned if defined.
215
216    Predefined prefixes can be passed as a `group:input-prefix`:
217    - `t`: time prefixes from nanoseconds to days.
218    - `si`: SI prefixes from nano to Tera.
219
220    .. note:: The micro prefix is µ, not u.
221
222    Example:
223
224    ```py
225    format_units(123456, "t:µs", fmt=".1f")    # "123.5ms"
226    format_units(0, "t:s", if_zero="-")        # "-"
227    format_units(1234, "si:", "Hz", fmt=".2f") # "1.23kHz"
228    format_units(1001, "si:", "Hz", fmt=".2f") # "1   kHz"
229    format_units(2345, {"k": 1e3, "M": 1e3}, "Si", fmt=".1f") # "2.3MSi"
230    ```
231
232    :param value: An integer or floating point value. If None, an empty string is returned.
233    :param prefixes:
234        A dictionary of prefix string to ratio of the next largest prefix. The
235        dictionary must be sorted from smallest to largest prefix. The prefix of
236        the input value must be the first entry.
237    :param unit: A unit string to be appended to the formatted value.
238    :param fmt: A format specifier to be applied when formatting the value.
239    :param if_zero: A string to be returned when the value is zero.
240    """
241    if value is None: return ""
242    if if_zero is not None and value == 0: return if_zero
243
244    # Find the correct prefix from a list of predefined common prefixes
245    _found = False
246    if prefixes.startswith("t:"):
247        time_units = {"ns": 1e3, "µs": 1e3, "ms": 1e3, "s": 60, "m": 60, "h": 24, "d": 365.25/12}
248        prefixes = prefixes.split(":")[1]
249        prefixes = {k:v for k, v in time_units.items() if _found or (_found := (k == prefixes))}
250    elif prefixes.startswith("si:"):
251        prefixes = prefixes.split(":")[1]
252        prefixes = {k:1e3 for k in ["n", "µ", "m", "", "k", "M", "G", "T"]
253                    if _found or (_found := (k == prefixes))}
254
255    # Divide the value until it is smaller than the next largest prefix
256    for prefix, factor in prefixes.items():
257        if value < factor: break
258        value /= factor
259
260    # Format the value
261    value = f"{value:{fmt or ''}}"
262    value_stripped = value.rstrip("0").rstrip(".")
263    if if_zero is not None and value_stripped == "0": return if_zero
264    # pad the value to the right to align it
265    padding = max(len(p) for p in prefixes.keys()) - len(prefix)
266    padding += len(value) - len(value_stripped)
267    return f"{value_stripped}{padding * ' '}{prefix}{unit or ''}"

Format a value with the largest prefix.

The value is divided by the list of prefixes until it is smaller than the next largest prefix. Trailing zeros are replaced by spaces and padding is applied to align the prefixes and units. If the value is zero, the if_zero string is returned if defined.

Predefined prefixes can be passed as a group:input-prefix:

  • t: time prefixes from nanoseconds to days.
  • si: SI prefixes from nano to Tera.
The micro prefix is µ, not u.

Example:

format_units(123456, "t:µs", fmt=".1f")    # "123.5ms"
format_units(0, "t:s", if_zero="-")        # "-"
format_units(1234, "si:", "Hz", fmt=".2f") # "1.23kHz"
format_units(1001, "si:", "Hz", fmt=".2f") # "1   kHz"
format_units(2345, {"k": 1e3, "M": 1e3}, "Si", fmt=".1f") # "2.3MSi"
Parameters
  • value: An integer or floating point value. If None, an empty string is returned.
  • prefixes: A dictionary of prefix string to ratio of the next largest prefix. The dictionary must be sorted from smallest to largest prefix. The prefix of the input value must be the first entry.
  • unit: A unit string to be appended to the formatted value.
  • fmt: A format specifier to be applied when formatting the value.
  • if_zero: A string to be returned when the value is zero.
def format_table( fmtstr: str, header: list[str], rows: list[list[str]], columns: int = 1) -> str:
271def format_table(fmtstr: str, header: list[str], rows: list[list[str]], columns: int = 1) -> str:
272    """
273    DEPRECATED: Use `rich.table.Table` instead!
274
275    Formats a list of rows into a table of multiple meta-columns based on the format string.
276    Example for formatting an array of registers into a table with three meta-columns:
277
278    ```py
279    fmtstr = "{:%d}  {:>%d}  {:>%d}"
280    header = ["NAME", "HEX VALUE", "INT VALUE"]
281    rows = [[reg, hex(value), value] for reg, value in registers.items()]
282    table = utils.format_table(fmtstr, header, rows, 3)
283    ```
284
285    :param fmtstr: A string describing the spacing between columns and their
286                   alignment. Must have exactly as many entries as the header a
287                   rows. Example: two columns, aligned right and left with a
288                   brace formatter: `"{:>%d} ({:%d})"` The `%d` is replaced by
289                   the max column width by this function.
290    :param header: A list of names for the header row. If you specify more than
291                   one column, the header will be duplicated.
292    :param rows: a list of lists of entries in the table. Each entry will be
293                 converted to `str` to count the maximal length of each column.
294    :param columns: If a table is very long, the header can be duplicated to the
295                    right side of the table to fill more screen space.
296    """
297    # duplicate and join the format string for each column
298    split_horizontal = columns > 0
299    columns = abs(columns)
300    fmtstr = "      :      ".join([fmtstr] * columns)
301    fmtcnt = fmtstr.count("%d")
302    column_width = [0] * fmtcnt
303
304    # Interleave the rows for the later chunking
305    fill = [""] * (fmtcnt // columns)
306    if split_horizontal:
307        rows = [val for tup in zip(*chunks(rows, math.ceil(len(rows) / columns), fill)) for val in tup]
308    # prepend the duplicated header before the rows
309    if header is not None:
310        rows = [header] * columns + rows
311    # Group the individual rows into multiple columns per row
312    rows = [sum(rs, []) for rs in chunks(rows, columns, fill)]
313
314    # collect each line and compute the column width
315    lines = []
316    for row in rows:
317        if len(row) != fmtcnt:
318            raise ValueError("Each row have the same number of entries as the format string")
319        line = [str(l) for l in row]
320        lines.append(line)
321        column_width = [max(w, len(l)) for w, l in zip(column_width, line)]
322
323    # Format the format string with the column width first
324    fmtstr = fmtstr % tuple(column_width)
325    # Now format the actual lines with the formatted format string
326    return "\n".join(fmtstr.format(*line).rstrip() for line in lines) + "\n"

DEPRECATED: Use rich.table.Table instead!

Formats a list of rows into a table of multiple meta-columns based on the format string. Example for formatting an array of registers into a table with three meta-columns:

fmtstr = "{:%d}  {:>%d}  {:>%d}"
header = ["NAME", "HEX VALUE", "INT VALUE"]
rows = [[reg, hex(value), value] for reg, value in registers.items()]
table = utils.format_table(fmtstr, header, rows, 3)
Parameters
  • fmtstr: A string describing the spacing between columns and their alignment. Must have exactly as many entries as the header a rows. Example: two columns, aligned right and left with a brace formatter: "{:>%d} ({:%d})" The %d is replaced by the max column width by this function.
  • header: A list of names for the header row. If you specify more than one column, the header will be duplicated.
  • rows: a list of lists of entries in the table. Each entry will be converted to str to count the maximal length of each column.
  • columns: If a table is very long, the header can be duplicated to the right side of the table to fill more screen space.