diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index f56d720ec2a..33854ac2896 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -793,8 +793,11 @@ class MemoryAnalyzer: """Scan ESPHome source object files to map extern "C" symbols to components. When no linker map file is available, this uses ``nm`` to scan ``.o`` files - under ``src/esphome/`` and build a symbol-to-component mapping. This catches - ``extern "C"`` functions and other symbols that lack C++ namespace prefixes. + under ``src/`` (including ``src/main.cpp.o`` and everything beneath + ``src/esphome/``) and build a symbol-to-component mapping. This catches + ``extern "C"`` functions, the ESPHome-generated ``setup()``/``loop()`` + entry points in ``main.cpp``, and other symbols that lack C++ namespace + prefixes. Skips scanning if ``_source_symbol_map`` was already populated by ``_parse_map_file()``. @@ -806,12 +809,12 @@ class MemoryAnalyzer: if obj_dir is None: return - # Find ESPHome source object files - esphome_src_dir = obj_dir / "src" / "esphome" - if not esphome_src_dir.is_dir(): + # Scan all ESPHome-owned source object files: src/main.cpp.o and src/esphome/... + src_dir = obj_dir / "src" + if not src_dir.is_dir(): return - obj_files = sorted(esphome_src_dir.rglob("*.o")) + obj_files = sorted(src_dir.rglob("*.o")) if not obj_files: return @@ -1064,6 +1067,10 @@ class MemoryAnalyzer: if component_name in self.external_components: return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}" + # ESPHome-generated entry point: src/main.cpp.o (contains setup()/loop()) + if len(parts) >= 2 and parts[-2:] == ("src", "main.cpp.o"): + return _COMPONENT_CORE + # ESPHome core: src/esphome/core/... or src/esphome/... if "core" in parts and "esphome" in parts: return _COMPONENT_CORE diff --git a/tests/unit_tests/analyze_memory/test_source_file_attribution.py b/tests/unit_tests/analyze_memory/test_source_file_attribution.py new file mode 100644 index 00000000000..2793f41bd0e --- /dev/null +++ b/tests/unit_tests/analyze_memory/test_source_file_attribution.py @@ -0,0 +1,43 @@ +"""Tests for source-file-to-component attribution in memory analyzer.""" + +from unittest.mock import patch + +from esphome.analyze_memory import 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() + analyzer._lib_hash_to_name = {} + return analyzer + + +def test_source_file_to_component_main_cpp_relative() -> None: + """ESPHome-generated src/main.cpp.o (nm path form) attributes to core.""" + analyzer = _make_analyzer() + assert analyzer._source_file_to_component("src/main.cpp.o") == "[esphome]core" + + +def test_source_file_to_component_main_cpp_pioenvs_path() -> None: + """Linker map paths like .pioenvs//src/main.cpp.o attribute to core.""" + analyzer = _make_analyzer() + result = analyzer._source_file_to_component(".pioenvs/drivewaygate/src/main.cpp.o") + assert result == "[esphome]core" + + +def test_source_file_to_component_esphome_core() -> None: + """Sources under src/esphome/core/ attribute to core.""" + analyzer = _make_analyzer() + result = analyzer._source_file_to_component("src/esphome/core/application.cpp.o") + assert result == "[esphome]core" + + +def test_source_file_to_component_known_component() -> None: + """Known ESPHome components attribute to their component name.""" + analyzer = _make_analyzer() + result = analyzer._source_file_to_component( + "src/esphome/components/wifi/wifi_component.cpp.o" + ) + assert result == "[esphome]wifi"