mirror of
https://github.com/esphome/esphome.git
synced 2026-03-26 17:50:28 +08:00
[core] Attribute placement new storage symbols to components (#15092)
This commit is contained in:
@@ -56,6 +56,10 @@ _COMPONENT_PREFIX_LIB = "[lib]"
|
||||
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
|
||||
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
|
||||
|
||||
# Placement new storage suffix (generated by codegen Pvariable)
|
||||
_PSTORAGE_SUFFIX = "__pstorage"
|
||||
|
||||
|
||||
# C++ namespace prefixes
|
||||
_NAMESPACE_ESPHOME = "esphome::"
|
||||
_NAMESPACE_STD = "std::"
|
||||
@@ -332,6 +336,13 @@ class MemoryAnalyzer:
|
||||
# Demangle C++ names if needed
|
||||
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)
|
||||
# This handles cases like esphome::ESPHomeOTAComponent which should map to ota
|
||||
if _NAMESPACE_ESPHOME in demangled:
|
||||
@@ -399,6 +410,24 @@ class MemoryAnalyzer:
|
||||
# Track uncategorized symbols for analysis
|
||||
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:
|
||||
"""Batch demangle C++ symbol names for efficiency."""
|
||||
if not symbols:
|
||||
|
||||
@@ -15,6 +15,7 @@ from . import (
|
||||
_COMPONENT_PREFIX_ESPHOME,
|
||||
_COMPONENT_PREFIX_EXTERNAL,
|
||||
_COMPONENT_PREFIX_LIB,
|
||||
_PSTORAGE_SUFFIX,
|
||||
RAM_SECTIONS,
|
||||
MemoryAnalyzer,
|
||||
)
|
||||
@@ -23,6 +24,17 @@ if TYPE_CHECKING:
|
||||
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):
|
||||
"""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
|
||||
" [data]" or " [bss]" is appended. For non-RAM sections or when
|
||||
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 = ""
|
||||
if section in RAM_SECTIONS:
|
||||
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:
|
||||
"""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):
|
||||
# Format section label
|
||||
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 = (
|
||||
f"{demangled[:truncate_limit]}..."
|
||||
if len(demangled) > self.COL_TOP_SYMBOL_NAME
|
||||
else demangled
|
||||
f"{display_name[:truncate_limit]}..."
|
||||
if len(display_name) > self.COL_TOP_SYMBOL_NAME
|
||||
else display_name
|
||||
)
|
||||
lines.append(
|
||||
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("")
|
||||
|
||||
# Show all symbols above threshold for better visibility
|
||||
# Show symbols above threshold, always include storage symbols
|
||||
large_symbols = [
|
||||
(sym, dem, size, sec)
|
||||
for sym, dem, size, sec in sorted_symbols
|
||||
if size > self.SYMBOL_SIZE_THRESHOLD
|
||||
or dem.endswith(_PSTORAGE_SUFFIX)
|
||||
]
|
||||
|
||||
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):
|
||||
lines.append(
|
||||
@@ -604,7 +622,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
# Sort by size descending
|
||||
sorted_ram_syms = sorted(ram_syms, key=lambda x: x[2], reverse=True)
|
||||
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):")
|
||||
@@ -622,13 +643,14 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
for symbol, demangled, size, section in large_ram_syms[:10]:
|
||||
# Format section label consistently by stripping leading dot
|
||||
section_label = section.lstrip(".") if section else ""
|
||||
display_name = _format_pstorage_name(demangled)
|
||||
# Add ellipsis if name is truncated
|
||||
demangled_display = (
|
||||
f"{demangled[:70]}..." if len(demangled) > 70 else demangled
|
||||
)
|
||||
lines.append(
|
||||
f" {size:>6,} B [{section_label}] {demangled_display}"
|
||||
display_name = (
|
||||
f"{display_name[:70]}..."
|
||||
if len(display_name) > 70
|
||||
else display_name
|
||||
)
|
||||
lines.append(f" {size:>6,} B [{section_label}] {display_name}")
|
||||
if len(large_ram_syms) > 10:
|
||||
lines.append(f" ... and {len(large_ram_syms) - 10} more")
|
||||
lines.append("")
|
||||
|
||||
@@ -584,7 +584,16 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
|
||||
# For 'new' allocations, use placement new into static storage
|
||||
# to avoid heap fragmentation on embedded devices.
|
||||
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
|
||||
CORE.add_global(
|
||||
|
||||
@@ -8,7 +8,7 @@ def test_deep_sleep_setup(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml")
|
||||
|
||||
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
|
||||
)
|
||||
assert "new(deepsleep) deep_sleep::DeepSleepComponent();" in main_cpp
|
||||
|
||||
@@ -242,11 +242,11 @@ def test_image_generation(
|
||||
main_cpp = generate_main(component_config_path("image_test.yaml"))
|
||||
assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
assert (
|
||||
|
||||
@@ -119,11 +119,11 @@ def test_code_generation(
|
||||
|
||||
main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml"))
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
assert (
|
||||
|
||||
@@ -13,11 +13,11 @@ def test_status_led_generation(
|
||||
"""Test status_led generation."""
|
||||
main_cpp = generate_main(component_config_path("status_led_test.yaml"))
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
assert "new(status_led_statusled_id) status_led::StatusLED(" in main_cpp
|
||||
|
||||
90
tests/unit_tests/analyze_memory/test_pstorage_attribution.py
Normal file
90
tests/unit_tests/analyze_memory/test_pstorage_attribution.py
Normal 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"
|
||||
Reference in New Issue
Block a user