mirror of
https://github.com/PX4/PX4-Autopilot.git
synced 2026-10-06 09:02:52 +08:00
Build all targets / Scan for Board Targets (push) Canceled after 0s
Build all targets / Seed [${{ matrix.chip_family }}] (push) Canceled after 0s
Build all targets / Build [${{ matrix.runner }}][${{ matrix.group }}] (push) Canceled after 0s
Build all targets / Upload Artifacts (push) Canceled after 0s
Checks / Gate Checks [check_format] (push) Canceled after 0s
Checks / Gate Checks [check_newlines] (push) Canceled after 0s
Checks / Gate Checks [module_documentation] (push) Canceled after 0s
Checks / Gate Checks [shellcheck_all] (push) Canceled after 0s
Checks / Gate Checks [validate_module_configs] (push) Canceled after 0s
Checks / Unit Tests (push) Canceled after 0s
MacOS build / build (push) Canceled after 0s
Ubuntu environment build / Build and Test (ubuntu:22.04) (push) Canceled after 0s
Ubuntu environment build / Build and Test (ubuntu:24.04) (push) Canceled after 0s
Container build / Set Tags and Variables (push) Canceled after 0s
Container build / Build Container (amd64) (push) Canceled after 0s
Container build / Build Container (arm64) (push) Canceled after 0s
Container build / Deploy To Registry (push) Canceled after 0s
Failsafe Simulator Build / build (failsafe_web) (push) Canceled after 0s
ITCM check / Checking nxp_mr-tropic (push) Canceled after 0s
ITCM check / Checking nxp_tropic-community (push) Canceled after 0s
ITCM check / Checking px4_fmu-v5x (push) Canceled after 0s
ITCM check / Checking px4_fmu-v6xrt (push) Canceled after 0s
ROS Development Container / Build ROS Development Image (humble, amd64, ros2-dev) (push) Canceled after 0s
ROS Development Container / Build ROS Development Image (humble, amd64, ros2-gazebo-dev) (push) Canceled after 0s
ROS Development Container / Build ROS Development Image (jazzy, amd64, ros2-dev) (push) Canceled after 0s
ROS Development Container / Build ROS Development Image (jazzy, amd64, ros2-gazebo-dev) (push) Canceled after 0s
ROS Development Container / Build ROS Development Image (humble, arm64, ros2-dev) (push) Canceled after 0s
ROS Development Container / Build ROS Development Image (humble, arm64, ros2-gazebo-dev) (push) Canceled after 0s
ROS Development Container / Build ROS Development Image (jazzy, arm64, ros2-dev) (push) Canceled after 0s
ROS Development Container / Build ROS Development Image (jazzy, arm64, ros2-gazebo-dev) (push) Canceled after 0s
ROS Development Container / Publish Multi-Architecture ROS Development Image (humble, ros2-dev) (push) Canceled after 0s
ROS Development Container / Publish Multi-Architecture ROS Development Image (humble, ros2-gazebo-dev) (push) Canceled after 0s
ROS Development Container / Publish Multi-Architecture ROS Development Image (jazzy, ros2-dev) (push) Canceled after 0s
ROS Development Container / Publish Multi-Architecture ROS Development Image (jazzy, ros2-gazebo-dev) (push) Canceled after 0s
ROS Integration Tests / build (push) Canceled after 0s
ROS Translation Node Tests / Build and test [humble] (push) Canceled after 0s
ROS Translation Node Tests / Build and test [jazzy] (push) Canceled after 0s
SITL Tests / Testing PX4 quadx (push) Canceled after 0s
SITL Tests / Testing PX4 standard_vtol (push) Canceled after 0s
SITL Tests / Testing PX4 hex (push) Canceled after 0s
SITL Tests / Testing PX4 xvert (push) Canceled after 0s
FLASH usage analysis / Analyzing px4_fmu-v2 (push) Canceled after 0s
FLASH usage analysis / Analyzing px4_fmu-v5x (push) Canceled after 0s
FLASH usage analysis / Analyzing px4_fmu-v6x (push) Canceled after 0s
Python CI Checks / build (push) Canceled after 0s
FLASH usage analysis / Publish Results (push) Canceled after 0s
Static Analysis / Clang-Tidy (push) Canceled after 0s
* feat(ci): report flash and RAM use against capacity, analyze fmu-v2 The size comment showed only the delta, which does not say how close a target is to its limit. Capacity comes from the board's own linker script, because the flash-analysis build links against an oversized flash region so that a change which overflows still gets a report. fmu-v2 adds an F4 next to the F7 and H7 targets, and with 1 MB of flash it is the FMU that overflows first. Assisted-by: Claude:claude-opus-5-5 * fix(ci): shorten the size comment's usage columns Five columns with capacities spelled out made the table too wide to read at a glance. Assisted-by: Claude:claude-opus-5-5
136 lines
4.9 KiB
Python
136 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare flash image and static RAM usage in bare-metal firmware ELFs."""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
|
|
# The commit hash compiled into px4_firmware_version_binary() changes its code
|
|
# size, so identical sources on two commits can differ by up to 16 B.
|
|
MIN_REPORTED_DELTA = 30
|
|
|
|
LD_COMMENT = re.compile(r"/\*.*?\*/", re.S)
|
|
LD_MEMORY = re.compile(r"\bMEMORY\s*\{(.*?)\}", re.S)
|
|
LD_REGION = re.compile(
|
|
r"(?P<name>\w+)\s*(?:\((?P<attributes>[^)]*)\))?\s*:\s*"
|
|
r"(?:ORIGIN|org|o)\s*=\s*(?P<origin>\w+)\s*,\s*"
|
|
r"(?:LENGTH|len|l)\s*=\s*(?P<length>\w+)"
|
|
)
|
|
|
|
|
|
def memory_usage(elf: Path) -> dict[str, int]:
|
|
# Sections rather than program headers: ld may map the ELF header into the
|
|
# first LOAD segment, below the flash origin, depending on its page size.
|
|
headers = subprocess.check_output(
|
|
["arm-none-eabi-objdump", "--section-headers", "--wide", str(elf)],
|
|
text=True,
|
|
env={**os.environ, "LC_ALL": "C"},
|
|
)
|
|
image_start = None
|
|
image_end = 0
|
|
ram = 0
|
|
|
|
for line in headers.splitlines():
|
|
fields = line.split(maxsplit=7)
|
|
if len(fields) < 8 or not fields[0].isdigit():
|
|
continue
|
|
|
|
size, vma, lma = (int(value, 16) for value in fields[2:5])
|
|
flags = {flag.strip() for flag in fields[7].split(",")}
|
|
if "ALLOC" not in flags or not size:
|
|
continue
|
|
|
|
in_image = {"LOAD", "CONTENTS"} <= flags
|
|
if in_image:
|
|
image_start = lma if image_start is None else min(image_start, lma)
|
|
image_end = max(image_end, lma + size)
|
|
|
|
# Only code and constants execute in place. Initialized data and RAM
|
|
# functions are copied out of the image; NOLOAD sections reserve RAM.
|
|
if not (in_image and vma == lma):
|
|
ram += size
|
|
|
|
if image_start is None:
|
|
raise ValueError(f"{elf}: no flash load image found")
|
|
|
|
# Include alignment gaps in the programmed image, as objcopy -O binary does.
|
|
return {"flash": image_end - image_start, "ram": ram, "image_start": image_start}
|
|
|
|
|
|
def ld_number(text: str) -> int:
|
|
scale = {"K": 1 << 10, "M": 1 << 20}.get(text[-1].upper(), 1)
|
|
return int(text[:-1] if scale > 1 else text, 0) * scale
|
|
|
|
|
|
def memory_capacity(linker_script: Path, image_start: int) -> dict[str, int]:
|
|
# Read the board's own script: the analysis build links against an
|
|
# oversized flash region so that a change which overflows still links.
|
|
text = LD_COMMENT.sub("", linker_script.read_text())
|
|
regions = {}
|
|
for block in LD_MEMORY.findall(text):
|
|
if re.sub(r"[\s,]", "", LD_REGION.sub("", block)):
|
|
raise ValueError(f"{linker_script}: MEMORY entry this parser cannot read")
|
|
for region in LD_REGION.finditer(block):
|
|
writable = "w" in (region["attributes"] or "").split("!")[0].lower()
|
|
regions[region["name"]] = (writable, ld_number(region["origin"]), ld_number(region["length"]))
|
|
|
|
flash = [name for name, (_, origin, length) in regions.items()
|
|
if origin <= image_start < origin + length]
|
|
if len(flash) != 1:
|
|
raise ValueError(f"{linker_script}: no single region holds the image at {image_start:#x}")
|
|
_, origin, length = regions.pop(flash[0])
|
|
return {
|
|
"flash": origin + length - image_start,
|
|
"ram": sum(length for writable, _, length in regions.values() if writable),
|
|
}
|
|
|
|
|
|
def indicator(delta: int) -> str:
|
|
if delta > 1000:
|
|
return "🔴 "
|
|
if delta > 100:
|
|
return "🟡 "
|
|
if delta < -100:
|
|
return "🟢 "
|
|
return ""
|
|
|
|
|
|
def format_change(before: int, after: int) -> str:
|
|
delta = after - before
|
|
percentage = f"{delta / before:+.2%}" if before else "n/a"
|
|
return f"{indicator(delta)}{delta:+,} B ({percentage})"
|
|
|
|
|
|
def format_usage(used: int, capacity: int) -> str:
|
|
return f"{used / capacity:.2%}"
|
|
|
|
|
|
def summarize(before: dict[str, int], after: dict[str, int], capacity: dict[str, int]) -> dict:
|
|
return {
|
|
"flash": format_change(before["flash"], after["flash"]),
|
|
"flash_used": format_usage(after["flash"], capacity["flash"]),
|
|
"ram": format_change(before["ram"], after["ram"]),
|
|
"ram_used": format_usage(after["ram"], capacity["ram"]),
|
|
"changed": any(abs(after[key] - before[key]) >= MIN_REPORTED_DELTA
|
|
for key in ("flash", "ram")),
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--before", type=Path, required=True)
|
|
parser.add_argument("--after", type=Path, required=True)
|
|
parser.add_argument("--linker-script", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
before = memory_usage(args.before)
|
|
after = memory_usage(args.after)
|
|
capacity = memory_capacity(args.linker_script, after["image_start"])
|
|
print(json.dumps(summarize(before, after, capacity)))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|