mirror of
https://github.com/esphome/esphome.git
synced 2026-06-01 01:19:45 +08:00
[analyze_memory] Add nRF52/Zephyr platform support for memory analysis (#13249)
This commit is contained in:
@@ -22,7 +22,7 @@ from .helpers import (
|
|||||||
map_section_name,
|
map_section_name,
|
||||||
parse_symbol_line,
|
parse_symbol_line,
|
||||||
)
|
)
|
||||||
from .toolchain import find_tool, run_tool
|
from .toolchain import find_tool, resolve_tool_path, run_tool
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from esphome.platformio_api import IDEData
|
from esphome.platformio_api import IDEData
|
||||||
@@ -132,6 +132,12 @@ class MemoryAnalyzer:
|
|||||||
readelf_path = readelf_path or idedata.readelf_path
|
readelf_path = readelf_path or idedata.readelf_path
|
||||||
_LOGGER.debug("Using toolchain paths from PlatformIO idedata")
|
_LOGGER.debug("Using toolchain paths from PlatformIO idedata")
|
||||||
|
|
||||||
|
# Validate paths exist, fall back to find_tool if they don't
|
||||||
|
# This handles cases like Zephyr where cc_path doesn't include full path
|
||||||
|
# and the toolchain prefix may differ (e.g., arm-zephyr-eabi- vs arm-none-eabi-)
|
||||||
|
objdump_path = resolve_tool_path("objdump", objdump_path, objdump_path)
|
||||||
|
readelf_path = resolve_tool_path("readelf", readelf_path, objdump_path)
|
||||||
|
|
||||||
self.objdump_path = objdump_path or "objdump"
|
self.objdump_path = objdump_path or "objdump"
|
||||||
self.readelf_path = readelf_path or "readelf"
|
self.readelf_path = readelf_path or "readelf"
|
||||||
self.external_components = external_components or set()
|
self.external_components = external_components or set()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::")
|
|||||||
# - LibreTiny RTL87xx: .xip.code_* (flash), .ram.code_* (RAM)
|
# - LibreTiny RTL87xx: .xip.code_* (flash), .ram.code_* (RAM)
|
||||||
# - LibreTiny BK7231: .itcm.code (fast RAM), .vectors (interrupt vectors)
|
# - LibreTiny BK7231: .itcm.code (fast RAM), .vectors (interrupt vectors)
|
||||||
# - LibreTiny LN882X: .flash_text, .flash_copy* (flash code)
|
# - LibreTiny LN882X: .flash_text, .flash_copy* (flash code)
|
||||||
|
# - Zephyr/nRF52: text, rodata, datas, bss (no leading dots)
|
||||||
SECTION_MAPPING = {
|
SECTION_MAPPING = {
|
||||||
".text": frozenset(
|
".text": frozenset(
|
||||||
[
|
[
|
||||||
@@ -30,6 +31,9 @@ SECTION_MAPPING = {
|
|||||||
# LibreTiny LN882X flash code
|
# LibreTiny LN882X flash code
|
||||||
".flash_text",
|
".flash_text",
|
||||||
".flash_copy",
|
".flash_copy",
|
||||||
|
# Zephyr/nRF52 sections (no leading dots)
|
||||||
|
"text",
|
||||||
|
"rom_start",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
".rodata": frozenset(
|
".rodata": frozenset(
|
||||||
@@ -37,6 +41,8 @@ SECTION_MAPPING = {
|
|||||||
".rodata",
|
".rodata",
|
||||||
# LibreTiny RTL87xx read-only data in RAM
|
# LibreTiny RTL87xx read-only data in RAM
|
||||||
".ram.code_rodata",
|
".ram.code_rodata",
|
||||||
|
# Zephyr/nRF52 sections (no leading dots)
|
||||||
|
"rodata",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
# .bss patterns - must be before .data to catch ".dram0.bss"
|
# .bss patterns - must be before .data to catch ".dram0.bss"
|
||||||
@@ -45,9 +51,19 @@ SECTION_MAPPING = {
|
|||||||
".bss",
|
".bss",
|
||||||
# LibreTiny LN882X BSS
|
# LibreTiny LN882X BSS
|
||||||
".bss_ram",
|
".bss_ram",
|
||||||
|
# Zephyr/nRF52 sections (no leading dots)
|
||||||
|
"bss",
|
||||||
|
"noinit",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
".data": frozenset(
|
||||||
|
[
|
||||||
|
".data",
|
||||||
|
".dram",
|
||||||
|
# Zephyr/nRF52 sections (no leading dots)
|
||||||
|
"datas",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
".data": frozenset([".data", ".dram"]),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Section to ComponentMemory attribute mapping
|
# Section to ComponentMemory attribute mapping
|
||||||
|
|||||||
@@ -94,13 +94,13 @@ def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Find section, size, and name
|
# Find section, size, and name
|
||||||
|
# Try each part as a potential section name
|
||||||
for i, part in enumerate(parts):
|
for i, part in enumerate(parts):
|
||||||
if not part.startswith("."):
|
# Skip parts that are clearly flags, addresses, or other metadata
|
||||||
continue
|
# Sections start with '.' (standard ELF) or are known section names (Zephyr)
|
||||||
|
|
||||||
section = map_section_name(part)
|
section = map_section_name(part)
|
||||||
if not section:
|
if not section:
|
||||||
break
|
continue
|
||||||
|
|
||||||
# Need at least size field after section
|
# Need at least size field after section
|
||||||
if i + 1 >= len(parts):
|
if i + 1 >= len(parts):
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@@ -17,10 +18,82 @@ TOOLCHAIN_PREFIXES = [
|
|||||||
"xtensa-lx106-elf-", # ESP8266
|
"xtensa-lx106-elf-", # ESP8266
|
||||||
"xtensa-esp32-elf-", # ESP32
|
"xtensa-esp32-elf-", # ESP32
|
||||||
"xtensa-esp-elf-", # ESP32 (newer IDF)
|
"xtensa-esp-elf-", # ESP32 (newer IDF)
|
||||||
|
"arm-zephyr-eabi-", # nRF52/Zephyr SDK
|
||||||
|
"arm-none-eabi-", # Generic ARM (RP2040, etc.)
|
||||||
"", # System default (no prefix)
|
"", # System default (no prefix)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _find_in_platformio_packages(tool_name: str) -> str | None:
|
||||||
|
"""Search for a tool in PlatformIO package directories.
|
||||||
|
|
||||||
|
This handles cases like Zephyr SDK where tools are installed in nested
|
||||||
|
directories that aren't in PATH.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tool_name: Name of the tool (e.g., "readelf", "objdump")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Full path to the tool or None if not found
|
||||||
|
"""
|
||||||
|
# Get PlatformIO packages directory
|
||||||
|
platformio_home = Path(os.path.expanduser("~/.platformio/packages"))
|
||||||
|
if not platformio_home.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Search patterns for toolchains that might contain the tool
|
||||||
|
# Order matters - more specific patterns first
|
||||||
|
search_patterns = [
|
||||||
|
# Zephyr SDK deeply nested structure (4 levels)
|
||||||
|
# e.g., toolchain-gccarmnoneeabi/zephyr-sdk-0.17.4/arm-zephyr-eabi/bin/arm-zephyr-eabi-objdump
|
||||||
|
f"toolchain-*/*/*/bin/*-{tool_name}",
|
||||||
|
# Zephyr SDK nested structure (3 levels)
|
||||||
|
f"toolchain-*/*/bin/*-{tool_name}",
|
||||||
|
f"toolchain-*/bin/*-{tool_name}",
|
||||||
|
# Standard PlatformIO toolchain structure
|
||||||
|
f"toolchain-*/bin/*{tool_name}",
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in search_patterns:
|
||||||
|
matches = list(platformio_home.glob(pattern))
|
||||||
|
if matches:
|
||||||
|
# Sort to get consistent results, prefer arm-zephyr-eabi over arm-none-eabi
|
||||||
|
matches.sort(key=lambda p: ("zephyr" not in str(p), str(p)))
|
||||||
|
tool_path = str(matches[0])
|
||||||
|
_LOGGER.debug("Found %s in PlatformIO packages: %s", tool_name, tool_path)
|
||||||
|
return tool_path
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_tool_path(
|
||||||
|
tool_name: str,
|
||||||
|
derived_path: str | None,
|
||||||
|
objdump_path: str | None = None,
|
||||||
|
) -> str | None:
|
||||||
|
"""Resolve a tool path, falling back to find_tool if derived path doesn't exist.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tool_name: Name of the tool (e.g., "objdump", "readelf")
|
||||||
|
derived_path: Path derived from idedata (may not exist for some platforms)
|
||||||
|
objdump_path: Path to objdump binary to derive other tool paths from
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Resolved path to the tool, or the original derived_path if it exists
|
||||||
|
"""
|
||||||
|
if derived_path and not Path(derived_path).exists():
|
||||||
|
found = find_tool(tool_name, objdump_path)
|
||||||
|
if found:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Derived %s path %s not found, using %s",
|
||||||
|
tool_name,
|
||||||
|
derived_path,
|
||||||
|
found,
|
||||||
|
)
|
||||||
|
return found
|
||||||
|
return derived_path
|
||||||
|
|
||||||
|
|
||||||
def find_tool(
|
def find_tool(
|
||||||
tool_name: str,
|
tool_name: str,
|
||||||
objdump_path: str | None = None,
|
objdump_path: str | None = None,
|
||||||
@@ -28,7 +101,8 @@ def find_tool(
|
|||||||
"""Find a toolchain tool by name.
|
"""Find a toolchain tool by name.
|
||||||
|
|
||||||
First tries to derive the tool path from objdump_path (if provided),
|
First tries to derive the tool path from objdump_path (if provided),
|
||||||
then falls back to searching for platform-specific tools.
|
then searches PlatformIO package directories (for cross-compile toolchains),
|
||||||
|
and finally falls back to searching for platform-specific tools in PATH.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt")
|
tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt")
|
||||||
@@ -47,7 +121,13 @@ def find_tool(
|
|||||||
_LOGGER.debug("Found %s at: %s", tool_name, potential_path)
|
_LOGGER.debug("Found %s at: %s", tool_name, potential_path)
|
||||||
return potential_path
|
return potential_path
|
||||||
|
|
||||||
# Try platform-specific tools
|
# Search in PlatformIO packages directory first (handles Zephyr SDK, etc.)
|
||||||
|
# This must come before PATH search because system tools (e.g., /usr/bin/objdump)
|
||||||
|
# are for the host architecture, not the target (ARM, Xtensa, etc.)
|
||||||
|
if found := _find_in_platformio_packages(tool_name):
|
||||||
|
return found
|
||||||
|
|
||||||
|
# Try platform-specific tools in PATH (fallback for when tools are installed globally)
|
||||||
for prefix in TOOLCHAIN_PREFIXES:
|
for prefix in TOOLCHAIN_PREFIXES:
|
||||||
cmd = f"{prefix}{tool_name}"
|
cmd = f"{prefix}{tool_name}"
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ class Platform(StrEnum):
|
|||||||
RTL87XX_ARD = "rtl87xx-ard" # LibreTiny RTL8720x
|
RTL87XX_ARD = "rtl87xx-ard" # LibreTiny RTL8720x
|
||||||
LN882X_ARD = "ln882x-ard" # LibreTiny LN882x
|
LN882X_ARD = "ln882x-ard" # LibreTiny LN882x
|
||||||
RP2040_ARD = "rp2040-ard" # Raspberry Pi Pico
|
RP2040_ARD = "rp2040-ard" # Raspberry Pi Pico
|
||||||
|
NRF52_ZEPHYR = "nrf52-adafruit" # Nordic nRF52 (Zephyr)
|
||||||
|
|
||||||
|
|
||||||
# Memory impact analysis constants
|
# Memory impact analysis constants
|
||||||
@@ -112,7 +113,7 @@ PLATFORM_SPECIFIC_COMPONENTS = frozenset(
|
|||||||
"rtl87xx", # Realtek RTL87xx platform implementation (uses LibreTiny)
|
"rtl87xx", # Realtek RTL87xx platform implementation (uses LibreTiny)
|
||||||
"ln882x", # Winner Micro LN882x platform implementation (uses LibreTiny)
|
"ln882x", # Winner Micro LN882x platform implementation (uses LibreTiny)
|
||||||
"host", # Host platform (for testing on development machine)
|
"host", # Host platform (for testing on development machine)
|
||||||
"nrf52", # Nordic nRF52 platform implementation
|
"nrf52", # Nordic nRF52 platform implementation (uses Zephyr)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -126,6 +127,7 @@ PLATFORM_SPECIFIC_COMPONENTS = frozenset(
|
|||||||
# 4-6. Other ESP32 variants - Less commonly used but still supported
|
# 4-6. Other ESP32 variants - Less commonly used but still supported
|
||||||
# 7-9. LibreTiny platforms (BK72XX, RTL87XX, LN882X) - good for detecting LibreTiny-specific changes
|
# 7-9. LibreTiny platforms (BK72XX, RTL87XX, LN882X) - good for detecting LibreTiny-specific changes
|
||||||
# 10. RP2040 - Raspberry Pi Pico platform
|
# 10. RP2040 - Raspberry Pi Pico platform
|
||||||
|
# 11. nRF52 - Nordic nRF52 with Zephyr (good for detecting Zephyr-specific changes)
|
||||||
MEMORY_IMPACT_PLATFORM_PREFERENCE = [
|
MEMORY_IMPACT_PLATFORM_PREFERENCE = [
|
||||||
Platform.ESP32_C6_IDF, # ESP32-C6 IDF (newest, supports Thread/Zigbee)
|
Platform.ESP32_C6_IDF, # ESP32-C6 IDF (newest, supports Thread/Zigbee)
|
||||||
Platform.ESP8266_ARD, # ESP8266 Arduino (most memory constrained, fastest builds)
|
Platform.ESP8266_ARD, # ESP8266 Arduino (most memory constrained, fastest builds)
|
||||||
@@ -137,6 +139,7 @@ MEMORY_IMPACT_PLATFORM_PREFERENCE = [
|
|||||||
Platform.RTL87XX_ARD, # LibreTiny RTL8720x
|
Platform.RTL87XX_ARD, # LibreTiny RTL8720x
|
||||||
Platform.LN882X_ARD, # LibreTiny LN882x
|
Platform.LN882X_ARD, # LibreTiny LN882x
|
||||||
Platform.RP2040_ARD, # Raspberry Pi Pico
|
Platform.RP2040_ARD, # Raspberry Pi Pico
|
||||||
|
Platform.NRF52_ZEPHYR, # Nordic nRF52 (Zephyr)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -463,6 +466,10 @@ def _detect_platform_hint_from_filename(filename: str) -> Platform | None:
|
|||||||
if "pico" in filename_lower or "rp2040" in filename_lower:
|
if "pico" in filename_lower or "rp2040" in filename_lower:
|
||||||
return Platform.RP2040_ARD
|
return Platform.RP2040_ARD
|
||||||
|
|
||||||
|
# nRF52 / Zephyr
|
||||||
|
if "nrf52" in filename_lower or "zephyr" in filename_lower:
|
||||||
|
return Platform.NRF52_ZEPHYR
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1499,6 +1499,23 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) ->
|
|||||||
"tests/components/rp2040/test.rp2040-ard.yaml",
|
"tests/components/rp2040/test.rp2040-ard.yaml",
|
||||||
determine_jobs.Platform.RP2040_ARD,
|
determine_jobs.Platform.RP2040_ARD,
|
||||||
),
|
),
|
||||||
|
# nRF52 / Zephyr detection
|
||||||
|
(
|
||||||
|
"tests/components/logger/test.nrf52-adafruit.yaml",
|
||||||
|
determine_jobs.Platform.NRF52_ZEPHYR,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"esphome/components/nrf52/gpio.cpp",
|
||||||
|
determine_jobs.Platform.NRF52_ZEPHYR,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"esphome/components/zephyr/core.cpp",
|
||||||
|
determine_jobs.Platform.NRF52_ZEPHYR,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"esphome/components/zephyr_ble_server/ble_server.cpp",
|
||||||
|
determine_jobs.Platform.NRF52_ZEPHYR,
|
||||||
|
),
|
||||||
# No platform hint (generic files)
|
# No platform hint (generic files)
|
||||||
("esphome/components/wifi/wifi.cpp", None),
|
("esphome/components/wifi/wifi.cpp", None),
|
||||||
("esphome/components/sensor/sensor.h", None),
|
("esphome/components/sensor/sensor.h", None),
|
||||||
@@ -1528,6 +1545,10 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) ->
|
|||||||
"pico_i2c",
|
"pico_i2c",
|
||||||
"pico_spi",
|
"pico_spi",
|
||||||
"rp2040_test_yaml",
|
"rp2040_test_yaml",
|
||||||
|
"nrf52_test_yaml",
|
||||||
|
"nrf52_gpio",
|
||||||
|
"zephyr_core",
|
||||||
|
"zephyr_ble_server",
|
||||||
"generic_wifi_no_hint",
|
"generic_wifi_no_hint",
|
||||||
"generic_sensor_no_hint",
|
"generic_sensor_no_hint",
|
||||||
"core_helpers_no_hint",
|
"core_helpers_no_hint",
|
||||||
@@ -1554,6 +1575,11 @@ def test_detect_platform_hint_from_filename(
|
|||||||
("file_ESP8266.cpp", determine_jobs.Platform.ESP8266_ARD),
|
("file_ESP8266.cpp", determine_jobs.Platform.ESP8266_ARD),
|
||||||
# ESP32 with different cases
|
# ESP32 with different cases
|
||||||
("file_ESP32.cpp", determine_jobs.Platform.ESP32_IDF),
|
("file_ESP32.cpp", determine_jobs.Platform.ESP32_IDF),
|
||||||
|
# nRF52/Zephyr with different cases
|
||||||
|
("file_NRF52.cpp", determine_jobs.Platform.NRF52_ZEPHYR),
|
||||||
|
("file_Nrf52.cpp", determine_jobs.Platform.NRF52_ZEPHYR),
|
||||||
|
("file_ZEPHYR.cpp", determine_jobs.Platform.NRF52_ZEPHYR),
|
||||||
|
("file_Zephyr.cpp", determine_jobs.Platform.NRF52_ZEPHYR),
|
||||||
],
|
],
|
||||||
ids=[
|
ids=[
|
||||||
"rp2040_uppercase",
|
"rp2040_uppercase",
|
||||||
@@ -1562,6 +1588,10 @@ def test_detect_platform_hint_from_filename(
|
|||||||
"pico_titlecase",
|
"pico_titlecase",
|
||||||
"esp8266_uppercase",
|
"esp8266_uppercase",
|
||||||
"esp32_uppercase",
|
"esp32_uppercase",
|
||||||
|
"nrf52_uppercase",
|
||||||
|
"nrf52_mixedcase",
|
||||||
|
"zephyr_uppercase",
|
||||||
|
"zephyr_titlecase",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_detect_platform_hint_from_filename_case_insensitive(
|
def test_detect_platform_hint_from_filename_case_insensitive(
|
||||||
|
|||||||
Reference in New Issue
Block a user