[core] Attribute placement new storage symbols to components (#15092)

This commit is contained in:
J. Nick Koston
2026-03-22 16:27:07 -10:00
committed by GitHub
parent fbe3e7d99c
commit 6992219e34
8 changed files with 171 additions and 21 deletions

View File

@@ -56,6 +56,10 @@ _COMPONENT_PREFIX_LIB = "[lib]"
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core" _COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api" _COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
# Placement new storage suffix (generated by codegen Pvariable)
_PSTORAGE_SUFFIX = "__pstorage"
# C++ namespace prefixes # C++ namespace prefixes
_NAMESPACE_ESPHOME = "esphome::" _NAMESPACE_ESPHOME = "esphome::"
_NAMESPACE_STD = "std::" _NAMESPACE_STD = "std::"
@@ -332,6 +336,13 @@ class MemoryAnalyzer:
# Demangle C++ names if needed # Demangle C++ names if needed
demangled = self._demangle_symbol(symbol_name) demangled = self._demangle_symbol(symbol_name)
# Check for placement new storage symbols (generated by codegen)
# Format: {component}__{id}__pstorage
if demangled.endswith(_PSTORAGE_SUFFIX) and (
component := self._match_pstorage_component(demangled)
):
return component
# Check for special component classes first (before namespace pattern) # Check for special component classes first (before namespace pattern)
# This handles cases like esphome::ESPHomeOTAComponent which should map to ota # This handles cases like esphome::ESPHomeOTAComponent which should map to ota
if _NAMESPACE_ESPHOME in demangled: if _NAMESPACE_ESPHOME in demangled:
@@ -399,6 +410,24 @@ class MemoryAnalyzer:
# Track uncategorized symbols for analysis # Track uncategorized symbols for analysis
return "other" return "other"
def _match_pstorage_component(self, symbol_name: str) -> str | None:
"""Match a __pstorage symbol to its ESPHome component.
Symbol format: {component}__{id}__pstorage
The component namespace is embedded by codegen before the double underscore.
"""
prefix = symbol_name[: -len(_PSTORAGE_SUFFIX)]
# Extract component namespace before the first double underscore
dunder_pos = prefix.find("__")
if dunder_pos == -1:
return None
component_name = prefix[:dunder_pos]
if component_name in get_esphome_components():
return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}"
if component_name in self.external_components:
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
return None
def _batch_demangle_symbols(self, symbols: list[str]) -> None: def _batch_demangle_symbols(self, symbols: list[str]) -> None:
"""Batch demangle C++ symbol names for efficiency.""" """Batch demangle C++ symbol names for efficiency."""
if not symbols: if not symbols:

View File

@@ -15,6 +15,7 @@ from . import (
_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_ESPHOME,
_COMPONENT_PREFIX_EXTERNAL, _COMPONENT_PREFIX_EXTERNAL,
_COMPONENT_PREFIX_LIB, _COMPONENT_PREFIX_LIB,
_PSTORAGE_SUFFIX,
RAM_SECTIONS, RAM_SECTIONS,
MemoryAnalyzer, MemoryAnalyzer,
) )
@@ -23,6 +24,17 @@ if TYPE_CHECKING:
from . import ComponentMemory from . import ComponentMemory
def _format_pstorage_name(name: str) -> str:
"""Format a __pstorage symbol as 'storage for {id}'."""
if not name.endswith(_PSTORAGE_SUFFIX):
return name
prefix = name[: -len(_PSTORAGE_SUFFIX)]
# Strip component namespace prefix: {component}__{id} -> {id}
dunder_pos = prefix.find("__")
var_id = prefix[dunder_pos + 2 :] if dunder_pos != -1 else prefix
return f"storage for {var_id}"
class MemoryAnalyzerCLI(MemoryAnalyzer): class MemoryAnalyzerCLI(MemoryAnalyzer):
"""Memory analyzer with CLI-specific report generation.""" """Memory analyzer with CLI-specific report generation."""
@@ -148,11 +160,14 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
If section is one of the RAM sections (.data or .bss), a label like If section is one of the RAM sections (.data or .bss), a label like
" [data]" or " [bss]" is appended. For non-RAM sections or when " [data]" or " [bss]" is appended. For non-RAM sections or when
section is None, no section label is added. section is None, no section label is added.
Placement new storage symbols are formatted as "storage for {id}".
""" """
display_name = _format_pstorage_name(demangled)
section_label = "" section_label = ""
if section in RAM_SECTIONS: if section in RAM_SECTIONS:
section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss] section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss]
return f"{demangled} ({size:,} B){section_label}" return f"{display_name} ({size:,} B){section_label}"
def _add_top_symbols(self, lines: list[str]) -> None: def _add_top_symbols(self, lines: list[str]) -> None:
"""Add a section showing the top largest symbols in the binary.""" """Add a section showing the top largest symbols in the binary."""
@@ -175,11 +190,13 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
for i, (_, demangled, size, section, component) in enumerate(top_symbols): for i, (_, demangled, size, section, component) in enumerate(top_symbols):
# Format section label # Format section label
section_label = f"[{section[1:]}]" if section else "" section_label = f"[{section[1:]}]" if section else ""
# Truncate demangled name if too long # Format storage symbols readably
display_name = _format_pstorage_name(demangled)
# Truncate if too long
demangled_display = ( demangled_display = (
f"{demangled[:truncate_limit]}..." f"{display_name[:truncate_limit]}..."
if len(demangled) > self.COL_TOP_SYMBOL_NAME if len(display_name) > self.COL_TOP_SYMBOL_NAME
else demangled else display_name
) )
lines.append( lines.append(
f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}" f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}"
@@ -573,15 +590,16 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(f"Total size: {comp_mem.flash_total:,} B") lines.append(f"Total size: {comp_mem.flash_total:,} B")
lines.append("") lines.append("")
# Show all symbols above threshold for better visibility # Show symbols above threshold, always include storage symbols
large_symbols = [ large_symbols = [
(sym, dem, size, sec) (sym, dem, size, sec)
for sym, dem, size, sec in sorted_symbols for sym, dem, size, sec in sorted_symbols
if size > self.SYMBOL_SIZE_THRESHOLD if size > self.SYMBOL_SIZE_THRESHOLD
or dem.endswith(_PSTORAGE_SUFFIX)
] ]
lines.append( lines.append(
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):" f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B & storage ({len(large_symbols)} symbols):"
) )
for i, (symbol, demangled, size, section) in enumerate(large_symbols): for i, (symbol, demangled, size, section) in enumerate(large_symbols):
lines.append( lines.append(
@@ -604,7 +622,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
# Sort by size descending # Sort by size descending
sorted_ram_syms = sorted(ram_syms, key=lambda x: x[2], reverse=True) sorted_ram_syms = sorted(ram_syms, key=lambda x: x[2], reverse=True)
large_ram_syms = [ large_ram_syms = [
s for s in sorted_ram_syms if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD s
for s in sorted_ram_syms
if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD
or s[1].endswith(_PSTORAGE_SUFFIX)
] ]
lines.append(f"{name} ({mem.ram_total:,} B total RAM):") lines.append(f"{name} ({mem.ram_total:,} B total RAM):")
@@ -622,13 +643,14 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
for symbol, demangled, size, section in large_ram_syms[:10]: for symbol, demangled, size, section in large_ram_syms[:10]:
# Format section label consistently by stripping leading dot # Format section label consistently by stripping leading dot
section_label = section.lstrip(".") if section else "" section_label = section.lstrip(".") if section else ""
display_name = _format_pstorage_name(demangled)
# Add ellipsis if name is truncated # Add ellipsis if name is truncated
demangled_display = ( display_name = (
f"{demangled[:70]}..." if len(demangled) > 70 else demangled f"{display_name[:70]}..."
) if len(display_name) > 70
lines.append( else display_name
f" {size:>6,} B [{section_label}] {demangled_display}"
) )
lines.append(f" {size:>6,} B [{section_label}] {display_name}")
if len(large_ram_syms) > 10: if len(large_ram_syms) > 10:
lines.append(f" ... and {len(large_ram_syms) - 10} more") lines.append(f" ... and {len(large_ram_syms) - 10} more")
lines.append("") lines.append("")

View File

@@ -584,7 +584,16 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
# For 'new' allocations, use placement new into static storage # For 'new' allocations, use placement new into static storage
# to avoid heap fragmentation on embedded devices. # to avoid heap fragmentation on embedded devices.
the_type = id_.type the_type = id_.type
storage_name = f"{id_.id}__pstorage" # Extract component namespace from type for memory analysis attribution
type_str = str(the_type)
# Strip leading esphome:: to get the component namespace
# e.g. esphome::dsmr::Dsmr -> dsmr, logger::Logger -> logger
bare = type_str.removeprefix("esphome::")
if "::" in bare:
component_ns = bare.split("::", maxsplit=1)[0].rstrip("_")
else:
component_ns = "esphome"
storage_name = f"{component_ns}__{id_.id}__pstorage"
# Declare aligned byte array for the object storage # Declare aligned byte array for the object storage
CORE.add_global( CORE.add_global(

View File

@@ -8,7 +8,7 @@ def test_deep_sleep_setup(generate_main):
main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml") main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml")
assert ( assert (
"static deep_sleep::DeepSleepComponent *const deepsleep = reinterpret_cast<deep_sleep::DeepSleepComponent *>(deepsleep__pstorage);" "static deep_sleep::DeepSleepComponent *const deepsleep = reinterpret_cast<deep_sleep::DeepSleepComponent *>(deep_sleep__deepsleep__pstorage);"
in main_cpp in main_cpp
) )
assert "new(deepsleep) deep_sleep::DeepSleepComponent();" in main_cpp assert "new(deepsleep) deep_sleep::DeepSleepComponent();" in main_cpp

View File

@@ -242,11 +242,11 @@ def test_image_generation(
main_cpp = generate_main(component_config_path("image_test.yaml")) main_cpp = generate_main(component_config_path("image_test.yaml"))
assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp
assert ( assert (
"alignas(image::Image) static unsigned char cat_img__pstorage[sizeof(image::Image)];" "alignas(image::Image) static unsigned char image__cat_img__pstorage[sizeof(image::Image)];"
in main_cpp in main_cpp
) )
assert ( assert (
"static image::Image *const cat_img = reinterpret_cast<image::Image *>(cat_img__pstorage);" "static image::Image *const cat_img = reinterpret_cast<image::Image *>(image__cat_img__pstorage);"
in main_cpp in main_cpp
) )
assert ( assert (

View File

@@ -119,11 +119,11 @@ def test_code_generation(
main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml")) main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml"))
assert ( assert (
"alignas(mipi_dsi::MIPI_DSI) static unsigned char p4_nano__pstorage[sizeof(mipi_dsi::MIPI_DSI)];" "alignas(mipi_dsi::MIPI_DSI) static unsigned char mipi_dsi__p4_nano__pstorage[sizeof(mipi_dsi::MIPI_DSI)];"
in main_cpp in main_cpp
) )
assert ( assert (
"static mipi_dsi::MIPI_DSI *const p4_nano = reinterpret_cast<mipi_dsi::MIPI_DSI *>(p4_nano__pstorage);" "static mipi_dsi::MIPI_DSI *const p4_nano = reinterpret_cast<mipi_dsi::MIPI_DSI *>(mipi_dsi__p4_nano__pstorage);"
in main_cpp in main_cpp
) )
assert ( assert (

View File

@@ -13,11 +13,11 @@ def test_status_led_generation(
"""Test status_led generation.""" """Test status_led generation."""
main_cpp = generate_main(component_config_path("status_led_test.yaml")) main_cpp = generate_main(component_config_path("status_led_test.yaml"))
assert ( assert (
"alignas(status_led::StatusLED) static unsigned char status_led_statusled_id__pstorage[sizeof(status_led::StatusLED)];" "alignas(status_led::StatusLED) static unsigned char status_led__status_led_statusled_id__pstorage[sizeof(status_led::StatusLED)];"
in main_cpp in main_cpp
) )
assert ( assert (
"static status_led::StatusLED *const status_led_statusled_id = reinterpret_cast<status_led::StatusLED *>(status_led_statusled_id__pstorage);" "static status_led::StatusLED *const status_led_statusled_id = reinterpret_cast<status_led::StatusLED *>(status_led__status_led_statusled_id__pstorage);"
in main_cpp in main_cpp
) )
assert "new(status_led_statusled_id) status_led::StatusLED(" in main_cpp assert "new(status_led_statusled_id) status_led::StatusLED(" in main_cpp

View File

@@ -0,0 +1,90 @@
"""Tests for __pstorage symbol attribution in memory analyzer."""
from unittest.mock import patch
from esphome.analyze_memory import _PSTORAGE_SUFFIX, MemoryAnalyzer
def _make_analyzer(external_components: set[str] | None = None) -> MemoryAnalyzer:
"""Create a MemoryAnalyzer with mocked dependencies."""
with patch.object(MemoryAnalyzer, "__init__", lambda self, *a, **kw: None):
analyzer = MemoryAnalyzer.__new__(MemoryAnalyzer)
analyzer.external_components = external_components or set()
return analyzer
def test_pstorage_suffix_constant() -> None:
"""Verify the suffix constant matches what codegen produces."""
assert _PSTORAGE_SUFFIX == "__pstorage"
def test_match_pstorage_simple_component() -> None:
"""Simple component name like 'logger'."""
analyzer = _make_analyzer()
result = analyzer._match_pstorage_component("logger__logger_id__pstorage")
assert result == "[esphome]logger"
def test_match_pstorage_underscore_component() -> None:
"""Component with underscore like 'web_server'."""
analyzer = _make_analyzer()
result = analyzer._match_pstorage_component("web_server__webserver_id__pstorage")
assert result == "[esphome]web_server"
def test_match_pstorage_api() -> None:
"""API component."""
analyzer = _make_analyzer()
result = analyzer._match_pstorage_component("api__apiserver_id__pstorage")
assert result == "[esphome]api"
def test_match_pstorage_deep_sleep() -> None:
"""Component with underscore: deep_sleep."""
analyzer = _make_analyzer()
result = analyzer._match_pstorage_component("deep_sleep__deepsleep__pstorage")
assert result == "[esphome]deep_sleep"
def test_match_pstorage_status_led() -> None:
"""Component with underscore: status_led."""
analyzer = _make_analyzer()
result = analyzer._match_pstorage_component("status_led__statusled_id__pstorage")
assert result == "[esphome]status_led"
def test_match_pstorage_external_component() -> None:
"""External component should be attributed correctly."""
analyzer = _make_analyzer(external_components={"my_custom"})
result = analyzer._match_pstorage_component("my_custom__thing_id__pstorage")
assert result == "[external]my_custom"
def test_match_pstorage_no_dunder_returns_none() -> None:
"""Symbol without double underscore separator returns None."""
analyzer = _make_analyzer()
result = analyzer._match_pstorage_component("something__pstorage")
assert result is None
def test_match_pstorage_unknown_component_returns_none() -> None:
"""Unknown component namespace returns None."""
analyzer = _make_analyzer()
result = analyzer._match_pstorage_component("nonexistent__thing_id__pstorage")
assert result is None
def test_match_pstorage_esphome_component() -> None:
"""esphome:: namespace types map to the esphome component."""
analyzer = _make_analyzer()
result = analyzer._match_pstorage_component(
"esphome__esphomeotacomponent_id__pstorage"
)
assert result == "[esphome]esphome"
def test_match_pstorage_user_id_with_component_prefix() -> None:
"""User-chosen ID that happens to contain a component name."""
analyzer = _make_analyzer()
result = analyzer._match_pstorage_component("logger__relay1__pstorage")
assert result == "[esphome]logger"