Files
firmware/extra_scripts/debug_info.py
T
Ben Meadors 2fb9b2fa8c tools: attribute flash to source files through LTO inlining
We had no way to answer "where is our flash actually going" on the LTO'd
nRF52 builds. bin/analyze_map.py reads the linker map, which tells you which
object file contributed a section - a question whole-image LTO stops
answering usefully once it inlines across translation units. `nm --size-sort`
has the same problem from the other end: the biggest symbol in an nRF52 image
is `setup` at ~8.9 KB, which is really dozens of inlined module initialisers
from all over src/.

bin/flash_attribution.py walks the disassembly instead, asks addr2line for
the inline stack at every instruction, and charges each instruction's bytes
to the innermost frame. Output is a per-subsystem rollup plus the heaviest
source files. Only needs objdump/addr2line from the toolchain.

Sample rak4631 run (.text 767,036 B): src/mesh 8.7%, src/modules 7.4%,
src/graphics/draw 5.9%, framework 5.3%, Telemetry 4.9%, newlib/libstdc++
4.4%. Heaviest files NodeDB.cpp 14,890 - AdminModule.cpp 12,508 -
MenuHandler.cpp 12,310 - XEdDSA.cpp 12,030 - GPS.cpp 11,818. It also finds
costs that have no symbol to sort by at all, e.g. ~14 KB of C++ template
instantiation charged to stl_vector.h / std_function.h / stl_tree.h.

extra_scripts/debug_info.py turns the required DWARF on behind
MESHTASTIC_DEBUG_INFO=1. Two non-obvious reasons it needs to be a script
rather than a build flag:

  - nrf52.ini's build_unflags strips -g, -g0..-g3 and -ggdb2/3, so the usual
    spellings get removed again. -gdwarf-4 is not in that list.
  - under -flto the code is generated by lto1 at LINK time, so -g is needed
    on the link line too. Compile-only leaves ~96% of an nRF52 image
    unresolvable; adding LINKFLAGS takes it to ~30%, the rest being vendor
    blobs with no source (newlib, CryptoCell, BSEC) and rodata in .text.

Debug info goes to non-allocated sections, so this does not change image
size: rak4631 links at 0xE46D8 either way. Verified the script is inert
without the variable - the resulting ELF has no .debug_* sections at all -
and that it loads cleanly on esp32 as well as nrf52.
2026-07-27 10:29:09 -05:00

31 lines
1.5 KiB
Python

#!/usr/bin/env python3
# trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports
#
# Opt-in DWARF, for source-level flash attribution (bin/flash_attribution.py).
#
# Set MESHTASTIC_DEBUG_INFO=1 in the environment to turn it on; the build is
# untouched otherwise. Debug info lands in non-allocated .debug_* sections, so an
# image built this way is the same size as the shipping one - measured on
# nrf52_promicro_diy_tcxo, 0xE8610 with DWARF vs 0xE8618 without.
#
# Two things make this less obvious than "add -g":
#
# 1. nrf52.ini's build_unflags strips -g, -g0..-g3 and -ggdb2/3, so the usual
# spellings are removed again after we add them. -gdwarf-4 is not in that
# list and survives.
# 2. Under -flto the final code is generated by lto1 during the LINK, so debug
# info has to be requested on the link line as well. With -gdwarf-4 only on
# the compile line, ~96% of an LTO'd nRF52 image resolves to "??" - the only
# code carrying line info is what nrf52_lto.py holds out of LTO, plus the
# precompiled vendor archives. Adding it to LINKFLAGS takes that to ~30%,
# the rest being blobs with no source to point at (newlib, CryptoCell, BSEC)
# and read-only data sharing .text.
import os
Import("env")
if os.environ.get("MESHTASTIC_DEBUG_INFO", "") not in ("", "0"):
env.Append(CCFLAGS=["-gdwarf-4"], LINKFLAGS=["-gdwarf-4"])
print("debug_info: DWARF enabled for size attribution (image size is unaffected)")