From 30558262d84350e144c77ad228701b524cf8c93d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Feb 2026 14:27:09 -0600 Subject: [PATCH] fix dupes --- esphome/analyze_memory/__init__.py | 100 ++++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 17 deletions(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 62aa59e22c6..751f4d5c7f7 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -189,6 +189,8 @@ class MemoryAnalyzer: self._lib_symbol_map: dict[str, str] = {} # PlatformIO library hash to name mapping: "lib641" -> "espsoftwareserial" self._lib_hash_to_name: dict[str, str] = {} + # Heuristic category to library redirect: "mdns_lib" -> "[lib]mdns" + self._heuristic_to_lib: dict[str, str] = {} def analyze(self) -> dict[str, ComponentMemory]: """Analyze the ELF file and return component memory usage.""" @@ -346,12 +348,12 @@ class MemoryAnalyzer: # Check against symbol patterns for component, patterns in SYMBOL_PATTERNS.items(): if any(pattern in symbol_name for pattern in patterns): - return component + return self._heuristic_to_lib.get(component, component) # Check against demangled patterns for component, patterns in DEMANGLED_PATTERNS.items(): if any(pattern in demangled for pattern in patterns): - return component + return self._heuristic_to_lib.get(component, component) # Special cases that need more complex logic @@ -401,16 +403,18 @@ class MemoryAnalyzer: def _discover_pio_libraries( self, - libraries: dict[str, Path], + libraries: dict[str, list[Path]], hash_to_name: dict[str, str], ) -> None: """Discover PlatformIO third-party libraries from the build directory. Scans ``lib/`` directories under ``.pioenvs//`` to find - library names and their ``.a`` archive paths. + library names and their ``.a`` archive or ``.o`` file paths. Args: - libraries: Dict to populate with library name -> ``.a`` path mappings. + libraries: Dict to populate with library name -> file path list mappings. + Prefers ``.a`` archives when available, falls back to ``.o`` files + (e.g., pioarduino ESP32 Arduino builds only produce ``.o`` files). hash_to_name: Dict to populate with dir name -> library name mappings for CSWTCH attribution (e.g., ``lib641`` -> ``espsoftwareserial``). """ @@ -442,17 +446,28 @@ class MemoryAnalyzer: archives = list(entry.glob("*.a")) archive = archives[0] if archives else None if archive and archive.exists(): - libraries[lib_name] = archive + libraries[lib_name] = [archive] hash_to_name[entry.name] = lib_name _LOGGER.debug( "Discovered PlatformIO library: %s -> %s", lib_subdir.name, archive, ) + else: + # No .a archive (e.g., pioarduino CMake builds) - use .o files + obj_files = sorted(lib_subdir.rglob("*.o")) + if obj_files: + libraries[lib_name] = obj_files + hash_to_name[entry.name] = lib_name + _LOGGER.debug( + "Discovered PlatformIO library (objects): %s -> %d .o files", + lib_subdir.name, + len(obj_files), + ) def _discover_idf_managed_components( self, - libraries: dict[str, Path], + libraries: dict[str, list[Path]], hash_to_name: dict[str, str], ) -> None: """Discover ESP-IDF managed component libraries from the build directory. @@ -463,7 +478,7 @@ class MemoryAnalyzer: ``esp-idf/__/lib__.a``. Args: - libraries: Dict to populate with library name -> ``.a`` path mappings. + libraries: Dict to populate with library name -> file path list mappings. hash_to_name: Dict to populate with dir name -> library name mappings for CSWTCH attribution (e.g., ``espressif__mdns`` -> ``mdns``). """ @@ -486,7 +501,7 @@ class MemoryAnalyzer: # Find the .a archive under esp-idf/__/ archive = espidf_dir / full_name / f"lib{full_name}.a" if archive.exists(): - libraries[short_name] = archive + libraries[short_name] = [archive] hash_to_name[full_name] = short_name _LOGGER.debug( "Discovered IDF managed component: %s -> %s", @@ -494,14 +509,17 @@ class MemoryAnalyzer: archive, ) - def _build_library_symbol_map(self, libraries: dict[str, Path]) -> dict[str, str]: - """Build a symbol-to-library mapping from library archives. + def _build_library_symbol_map( + self, libraries: dict[str, list[Path]] + ) -> dict[str, str]: + """Build a symbol-to-library mapping from library archives or object files. - Runs ``nm --defined-only`` on each ``.a`` file to collect global and - weak defined symbols. + Runs ``nm --defined-only`` on each ``.a`` or ``.o`` file to collect + global and weak defined symbols. Args: - libraries: Dictionary mapping library name to ``.a`` file path. + libraries: Dictionary mapping library name to list of file paths + (``.a`` archives or ``.o`` object files). Returns: Dictionary mapping symbol name to library name. @@ -511,9 +529,9 @@ class MemoryAnalyzer: if not self.nm_path: return symbol_map - for lib_name, archive_path in libraries.items(): + for lib_name, file_paths in libraries.items(): result = run_tool( - [self.nm_path, "--defined-only", str(archive_path)], + [self.nm_path, "--defined-only", *(str(p) for p in file_paths)], timeout=10, ) if result is None or result.returncode != 0: @@ -533,6 +551,51 @@ class MemoryAnalyzer: return symbol_map + @staticmethod + def _build_heuristic_to_lib_mapping( + library_names: set[str], + ) -> dict[str, str]: + """Build mapping from heuristic pattern categories to discovered libraries. + + Heuristic categories like ``mdns_lib``, ``web_server_lib``, ``async_tcp`` + exist as approximations for library attribution. When we discover the + actual library, symbols matching those heuristics should be redirected + to the ``[lib]`` category instead. + + The mapping is built by checking if the normalized category name + (stripped of ``_lib`` suffix and underscores) appears as a substring + of any discovered library name. + + Examples:: + + mdns_lib -> mdns -> in "mdns" or "esp8266mdns" -> [lib]mdns + web_server_lib -> webserver -> in "espasyncwebserver" -> [lib]espasyncwebserver + async_tcp -> asynctcp -> in "espasynctcp" -> [lib]espasynctcp + + Args: + library_names: Set of discovered library names (lowercase). + + Returns: + Dictionary mapping heuristic category to ``[lib]`` string. + """ + mapping: dict[str, str] = {} + all_categories = set(SYMBOL_PATTERNS) | set(DEMANGLED_PATTERNS) + + for category in all_categories: + base = category.removesuffix("_lib").replace("_", "") + for lib_name in library_names: + if base in lib_name: + mapping[category] = f"{_COMPONENT_PREFIX_LIB}{lib_name}" + break + + if mapping: + _LOGGER.debug( + "Heuristic-to-library redirects: %s", + ", ".join(f"{k} -> {v}" for k, v in sorted(mapping.items())), + ) + + return mapping + def _scan_pio_libraries(self) -> None: """Discover third-party libraries and build symbol mapping. @@ -541,7 +604,7 @@ class MemoryAnalyzer: Orchestrates symbol map construction and hash-to-name mapping for CSWTCH attribution. """ - libraries: dict[str, Path] = {} + libraries: dict[str, list[Path]] = {} self._discover_pio_libraries(libraries, self._lib_hash_to_name) self._discover_idf_managed_components(libraries, self._lib_hash_to_name) @@ -556,6 +619,9 @@ class MemoryAnalyzer: ) self._lib_symbol_map = self._build_library_symbol_map(libraries) + self._heuristic_to_lib = self._build_heuristic_to_lib_mapping( + set(libraries.keys()) + ) _LOGGER.info( "Built library symbol map: %d symbols from %d libraries",