From b8a75bc9252da0b07b8005948c266aca0c891fbf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Jul 2025 07:30:17 -0500 Subject: [PATCH 01/16] analyze_memory --- esphome/__main__.py | 18 + esphome/analyze_memory.py | 714 ++++++++++++++++++++++++++++++++++++++ esphome/platformio_api.py | 77 +++- 3 files changed, 808 insertions(+), 1 deletion(-) create mode 100644 esphome/analyze_memory.py diff --git a/esphome/__main__.py b/esphome/__main__.py index d8a79c018a..f4e110bc60 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -458,6 +458,13 @@ def command_vscode(args): def command_compile(args, config): + # Set memory analysis options in config + if args.analyze_memory: + config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True + + if args.memory_report: + config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report + exit_code = write_cpp(config) if exit_code != 0: return exit_code @@ -837,6 +844,17 @@ def parse_args(argv): help="Only generate source code, do not compile.", action="store_true", ) + parser_compile.add_argument( + "--analyze-memory", + help="Analyze and display memory usage by component after compilation.", + action="store_true", + ) + parser_compile.add_argument( + "--memory-report", + help="Save memory analysis report to a file (supports .json or .txt).", + type=str, + metavar="FILE", + ) parser_upload = subparsers.add_parser( "upload", diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py new file mode 100644 index 0000000000..6e63c4875d --- /dev/null +++ b/esphome/analyze_memory.py @@ -0,0 +1,714 @@ +"""Memory usage analyzer for ESPHome compiled binaries.""" + +from collections import defaultdict +import json +import logging +from pathlib import Path +import re +import subprocess + +_LOGGER = logging.getLogger(__name__) + +# Component namespace patterns +COMPONENT_PATTERNS = { + "api": re.compile(r"esphome::api::"), + "wifi": re.compile(r"esphome::wifi::"), + "mqtt": re.compile(r"esphome::mqtt::"), + "web_server": re.compile(r"esphome::web_server::"), + "sensor": re.compile(r"esphome::sensor::"), + "binary_sensor": re.compile(r"esphome::binary_sensor::"), + "switch": re.compile(r"esphome::switch_::"), + "light": re.compile(r"esphome::light::"), + "cover": re.compile(r"esphome::cover::"), + "climate": re.compile(r"esphome::climate::"), + "fan": re.compile(r"esphome::fan::"), + "display": re.compile(r"esphome::display::"), + "logger": re.compile(r"esphome::logger::"), + "ota": re.compile(r"esphome::ota::"), + "time": re.compile(r"esphome::time::"), + "sun": re.compile(r"esphome::sun::"), + "text_sensor": re.compile(r"esphome::text_sensor::"), + "script": re.compile(r"esphome::script::"), + "interval": re.compile(r"esphome::interval::"), + "json": re.compile(r"esphome::json::"), + "network": re.compile(r"esphome::network::"), + "mdns": re.compile(r"esphome::mdns::"), + "i2c": re.compile(r"esphome::i2c::"), + "spi": re.compile(r"esphome::spi::"), + "uart": re.compile(r"esphome::uart::"), + "dallas": re.compile(r"esphome::dallas::"), + "dht": re.compile(r"esphome::dht::"), + "adc": re.compile(r"esphome::adc::"), + "pwm": re.compile(r"esphome::pwm::"), + "ledc": re.compile(r"esphome::ledc::"), + "gpio": re.compile(r"esphome::gpio::"), + "esp32": re.compile(r"esphome::esp32::"), + "esp8266": re.compile(r"esphome::esp8266::"), + "remote": re.compile(r"esphome::remote_"), + "rf_bridge": re.compile(r"esphome::rf_bridge::"), + "captive_portal": re.compile(r"esphome::captive_portal::"), + "deep_sleep": re.compile(r"esphome::deep_sleep::"), + "bluetooth_proxy": re.compile(r"esphome::bluetooth_proxy::"), + "esp32_ble": re.compile(r"esphome::esp32_ble::"), + "esp32_ble_tracker": re.compile(r"esphome::esp32_ble_tracker::"), + "ethernet": re.compile(r"esphome::ethernet::"), + "core": re.compile( + r"esphome::(?!api::|wifi::|mqtt::|web_server::|sensor::|binary_sensor::|switch_::|light::|cover::|climate::|fan::|display::|logger::|ota::|time::|sun::|text_sensor::|script::|interval::|json::|network::|mdns::|i2c::|spi::|uart::|dallas::|dht::|adc::|pwm::|ledc::|gpio::|esp32::|esp8266::|remote_|rf_bridge::|captive_portal::|deep_sleep::|bluetooth_proxy::|esp32_ble::|esp32_ble_tracker::|ethernet::)" + ), +} + + +class MemorySection: + """Represents a memory section with its symbols.""" + + def __init__(self, name: str): + self.name = name + self.symbols: list[tuple[str, int, str]] = [] # (symbol_name, size, component) + self.total_size = 0 + + +class ComponentMemory: + """Tracks memory usage for a component.""" + + def __init__(self, name: str): + self.name = name + self.text_size = 0 # Code in flash + self.rodata_size = 0 # Read-only data in flash + self.data_size = 0 # Initialized data (flash + ram) + self.bss_size = 0 # Uninitialized data (ram only) + self.symbol_count = 0 + + @property + def flash_total(self) -> int: + return self.text_size + self.rodata_size + self.data_size + + @property + def ram_total(self) -> int: + return self.data_size + self.bss_size + + +class MemoryAnalyzer: + """Analyzes memory usage from ELF files.""" + + def __init__( + self, + elf_path: str, + objdump_path: str | None = None, + readelf_path: str | None = None, + ): + self.elf_path = Path(elf_path) + if not self.elf_path.exists(): + raise FileNotFoundError(f"ELF file not found: {elf_path}") + + self.objdump_path = objdump_path or "objdump" + self.readelf_path = readelf_path or "readelf" + + self.sections: dict[str, MemorySection] = {} + self.components: dict[str, ComponentMemory] = defaultdict( + lambda: ComponentMemory("") + ) + self._demangle_cache: dict[str, str] = {} + + def analyze(self) -> dict[str, ComponentMemory]: + """Analyze the ELF file and return component memory usage.""" + self._parse_sections() + self._parse_symbols() + self._categorize_symbols() + return dict(self.components) + + def _parse_sections(self): + """Parse section headers from ELF file.""" + try: + result = subprocess.run( + [self.readelf_path, "-S", str(self.elf_path)], + capture_output=True, + text=True, + check=True, + ) + + # Parse section headers + for line in result.stdout.splitlines(): + # Look for section entries + match = re.match( + r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)", + line, + ) + if match: + section_name = match.group(1) + size_hex = match.group(2) + size = int(size_hex, 16) + + # Map various section names to standard categories + mapped_section = None + if ".text" in section_name or ".iram" in section_name: + mapped_section = ".text" + elif ".rodata" in section_name: + mapped_section = ".rodata" + elif ".data" in section_name and "bss" not in section_name: + mapped_section = ".data" + elif ".bss" in section_name: + mapped_section = ".bss" + + if mapped_section: + if mapped_section not in self.sections: + self.sections[mapped_section] = MemorySection( + mapped_section + ) + self.sections[mapped_section].total_size += size + + except subprocess.CalledProcessError as e: + _LOGGER.error(f"Failed to parse sections: {e}") + raise + + def _parse_symbols(self): + """Parse symbols from ELF file.""" + try: + result = subprocess.run( + [self.objdump_path, "-t", str(self.elf_path)], + capture_output=True, + text=True, + check=True, + ) + + for line in result.stdout.splitlines(): + # Parse symbol table entries + # Format: address l/g w/d F/O section size name + # Example: 40084870 l F .iram0.text 00000000 _xt_user_exc + parts = line.split() + if len(parts) >= 5: + try: + # Check if this looks like a symbol entry + int(parts[0], 16) + + # Look for F (function) or O (object) flag + if "F" in parts or "O" in parts: + # Find the section name + section = None + size = 0 + name = None + + for i, part in enumerate(parts): + if part.startswith("."): + # Map section names + if ".text" in part or ".iram" in part: + section = ".text" + elif ".rodata" in part: + section = ".rodata" + elif ".data" in part or ".dram" in part: + section = ".data" + elif ".bss" in part: + section = ".bss" + + if section and i + 1 < len(parts): + try: + # Next field should be size + size = int(parts[i + 1], 16) + # Rest is the symbol name + if i + 2 < len(parts): + name = " ".join(parts[i + 2 :]) + except ValueError: + pass + break + + if section and name and size > 0: + if section in self.sections: + self.sections[section].symbols.append( + (name, size, "") + ) + + except ValueError: + # Not a valid address, skip + continue + + except subprocess.CalledProcessError as e: + _LOGGER.error(f"Failed to parse symbols: {e}") + raise + + def _categorize_symbols(self): + """Categorize symbols by component.""" + # First, collect all unique symbol names for batch demangling + all_symbols = set() + for section in self.sections.values(): + for symbol_name, _, _ in section.symbols: + all_symbols.add(symbol_name) + + # Batch demangle all symbols at once + self._batch_demangle_symbols(list(all_symbols)) + + # Now categorize with cached demangled names + for section_name, section in self.sections.items(): + for symbol_name, size, _ in section.symbols: + component = self._identify_component(symbol_name) + + if component not in self.components: + self.components[component] = ComponentMemory(component) + + comp_mem = self.components[component] + comp_mem.symbol_count += 1 + + if section_name == ".text": + comp_mem.text_size += size + elif section_name == ".rodata": + comp_mem.rodata_size += size + elif section_name == ".data": + comp_mem.data_size += size + elif section_name == ".bss": + comp_mem.bss_size += size + + def _identify_component(self, symbol_name: str) -> str: + """Identify which component a symbol belongs to.""" + # Demangle C++ names if needed + demangled = self._demangle_symbol(symbol_name) + + # Check against component patterns + for component, pattern in COMPONENT_PATTERNS.items(): + if pattern.search(demangled): + return f"[esphome]{component}" + + # Check for web server related code + if ( + "AsyncWebServer" in demangled + or "AsyncWebHandler" in demangled + or "WebServer" in demangled + ): + return "web_server_lib" + elif "AsyncClient" in demangled or "AsyncServer" in demangled: + return "async_tcp" + + # Check for FreeRTOS/ESP-IDF components + if any( + prefix in symbol_name + for prefix in [ + "vTask", + "xTask", + "xQueue", + "pvPort", + "vPort", + "uxTask", + "pcTask", + ] + ): + return "freertos" + elif "xt_" in symbol_name or "_xt_" in symbol_name: + return "xtensa" + elif "heap_" in symbol_name or "multi_heap" in demangled: + return "heap" + elif "spi_flash" in symbol_name: + return "spi_flash" + elif "rtc_" in symbol_name: + return "rtc" + elif "gpio_" in symbol_name or "GPIO" in demangled: + return "gpio_driver" + elif "uart_" in symbol_name or "UART" in demangled: + return "uart_driver" + elif "timer_" in symbol_name or "esp_timer" in symbol_name: + return "timer" + elif "periph_" in symbol_name: + return "peripherals" + + # C++ standard library + if any(ns in demangled for ns in ["std::", "__gnu_cxx::", "__cxxabiv"]): + return "cpp_stdlib" + elif "_GLOBAL__N_" in symbol_name: + return "cpp_anonymous" + + # Platform/system code + if "esp_" in demangled or "ESP" in demangled: + return "esp_system" + elif "app_" in symbol_name: + return "app_framework" + elif "arduino" in demangled.lower(): + return "arduino" + + # Network stack components + if any( + net in demangled + for net in [ + "lwip", + "tcp", + "udp", + "ip4", + "ip6", + "dhcp", + "dns", + "netif", + "ethernet", + "ppp", + "slip", + ] + ): + return "network_stack" + elif "vj_compress" in symbol_name: # Van Jacobson TCP compression + return "network_stack" + + # WiFi/802.11 stack + if any( + wifi in symbol_name + for wifi in [ + "ieee80211", + "hostap", + "sta_", + "ap_", + "scan_", + "wifi_", + "wpa_", + "wps_", + "esp_wifi", + ] + ): + return "wifi_stack" + elif "NetworkInterface" in demangled: + return "wifi_stack" + + # mDNS specific + if ( + "mdns" in symbol_name or "mdns" in demangled + ) and "esphome" not in demangled: + return "mdns_lib" + + # Cryptography + if any( + crypto in demangled + for crypto in [ + "mbedtls", + "crypto", + "sha", + "aes", + "rsa", + "ecc", + "tls", + "ssl", + ] + ): + return "crypto" + + # C library functions + if any( + libc in symbol_name + for libc in [ + "printf", + "scanf", + "malloc", + "free", + "memcpy", + "memset", + "strcpy", + "strlen", + "_dtoa", + "_fopen", + ] + ): + return "libc" + elif symbol_name.startswith("_") and symbol_name[1:].replace("_r", "").replace( + "v", "" + ).replace("s", "") in ["printf", "fprintf", "sprintf", "scanf"]: + return "libc" + + # IPv6 specific + if "nd6_" in symbol_name or "ip6_" in symbol_name: + return "ipv6_stack" + + # Other system libraries + if "nvs_" in demangled: + return "nvs" + elif "spiffs" in demangled or "vfs" in demangled: + return "filesystem" + elif "newlib" in demangled: + return "libc" + elif ( + "libgcc" in demangled + or "_divdi3" in symbol_name + or "_udivdi3" in symbol_name + ): + return "libgcc" + + # Boot and startup + if any( + boot in symbol_name + for boot in ["boot", "start_cpu", "call_start", "startup", "bootloader"] + ): + return "boot_startup" + + # PHY/Radio layer + if any( + phy in symbol_name + for phy in [ + "phy_", + "rf_", + "chip_", + "register_chipv7", + "pbus_", + "bb_", + "fe_", + ] + ): + return "phy_radio" + elif any(pp in symbol_name for pp in ["pp_", "ppT", "ppR", "ppP", "ppInstall"]): + return "wifi_phy_pp" + elif "lmac" in symbol_name: + return "wifi_lmac" + elif "wdev" in symbol_name: + return "wifi_device" + + # Bluetooth/BLE + if any( + bt in symbol_name for bt in ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_"] + ): + return "bluetooth" + elif "coex" in symbol_name: + return "wifi_bt_coex" + + # Power management + if any( + pm in symbol_name + for pm in [ + "pm_", + "sleep", + "rtc_sleep", + "light_sleep", + "deep_sleep", + "power_down", + ] + ): + return "power_mgmt" + + # Logging and diagnostics + if any(log in demangled for log in ["log", "Log", "print", "Print", "diag_"]): + return "logging" + + # Memory management + if any(mem in symbol_name for mem in ["mem_", "memory_", "tlsf_", "memp_"]): + return "memory_mgmt" + + # HAL (Hardware Abstraction Layer) + if "hal_" in symbol_name: + return "hal_layer" + + # Clock management + if any( + clk in symbol_name + for clk in ["clk_", "clock_", "rtc_clk", "apb_", "cpu_freq"] + ): + return "clock_mgmt" + + # Cache management + if "cache" in symbol_name: + return "cache_mgmt" + + # Flash operations + if "flash" in symbol_name and "spi" not in symbol_name: + return "flash_ops" + + # Interrupt/Exception handling + if any( + isr in symbol_name + for isr in ["isr", "interrupt", "intr_", "exc_", "exception"] + ): + return "interrupt_handlers" + elif "_wrapper" in symbol_name: + return "wrapper_functions" + + # Error handling + if any( + err in symbol_name + for err in ["panic", "abort", "assert", "error_", "fault"] + ): + return "error_handling" + + # ECC/Crypto math + if any( + ecc in symbol_name for ecc in ["ecp_", "bignum_", "mpi_", "sswu", "modp"] + ): + return "crypto_math" + + # Authentication + if "checkDigestAuthentication" in demangled or "auth" in symbol_name.lower(): + return "authentication" + + # PPP protocol + if any(ppp in symbol_name for ppp in ["ppp", "ipcp_", "lcp_", "chap_"]): + return "ppp_protocol" + + # DHCP + if "dhcp" in symbol_name or "handle_dhcp" in symbol_name: + return "dhcp" + + return "other" + + def _batch_demangle_symbols(self, symbols: list[str]) -> None: + """Batch demangle C++ symbol names for efficiency.""" + if not symbols: + return + + try: + # Send all symbols to c++filt at once + result = subprocess.run( + ["c++filt"], + input="\n".join(symbols), + capture_output=True, + text=True, + check=False, + ) + if result.returncode == 0: + demangled_lines = result.stdout.strip().split("\n") + # Map original to demangled names + for original, demangled in zip(symbols, demangled_lines): + self._demangle_cache[original] = demangled + else: + # If batch fails, cache originals + for symbol in symbols: + self._demangle_cache[symbol] = symbol + except Exception: + # On error, cache originals + for symbol in symbols: + self._demangle_cache[symbol] = symbol + + def _demangle_symbol(self, symbol: str) -> str: + """Get demangled C++ symbol name from cache.""" + return self._demangle_cache.get(symbol, symbol) + + def generate_report(self, detailed: bool = False) -> str: + """Generate a formatted memory report.""" + components = sorted( + self.components.items(), key=lambda x: x[1].flash_total, reverse=True + ) + + # Calculate totals + total_flash = sum(c.flash_total for _, c in components) + total_ram = sum(c.ram_total for _, c in components) + + # Build report + lines = [] + lines.append("=" * 108) + lines.append(" Component Memory Analysis") + lines.append("=" * 108) + lines.append("") + + # Main table + lines.append( + f"{'Component':<28} | {'Flash (text)':<12} | {'Flash (data)':<12} | {'RAM (data)':<10} | {'RAM (bss)':<10} | {'Total Flash':<12} | {'Total RAM':<10}" + ) + lines.append( + "-" * 28 + + "-+-" + + "-" * 12 + + "-+-" + + "-" * 12 + + "-+-" + + "-" * 10 + + "-+-" + + "-" * 10 + + "-+-" + + "-" * 12 + + "-+-" + + "-" * 10 + ) + + for name, mem in components: + if mem.flash_total > 0 or mem.ram_total > 0: + flash_rodata = mem.rodata_size + mem.data_size + lines.append( + f"{name:<28} | {mem.text_size:>11,} B | {flash_rodata:>11,} B | " + f"{mem.data_size:>9,} B | {mem.bss_size:>9,} B | " + f"{mem.flash_total:>11,} B | {mem.ram_total:>9,} B" + ) + + lines.append( + "-" * 28 + + "-+-" + + "-" * 12 + + "-+-" + + "-" * 12 + + "-+-" + + "-" * 10 + + "-+-" + + "-" * 10 + + "-+-" + + "-" * 12 + + "-+-" + + "-" * 10 + ) + lines.append( + f"{'TOTAL':<28} | {' ':>11} | {' ':>11} | " + f"{' ':>9} | {' ':>9} | " + f"{total_flash:>11,} B | {total_ram:>9,} B" + ) + + # Top consumers + lines.append("") + lines.append("Top Flash Consumers:") + for i, (name, mem) in enumerate(components[:10]): + if mem.flash_total > 0: + percentage = ( + (mem.flash_total / total_flash * 100) if total_flash > 0 else 0 + ) + lines.append( + f"{i + 1}. {name} ({mem.flash_total:,} B) - {percentage:.1f}% of analyzed flash" + ) + + lines.append("") + lines.append("Top RAM Consumers:") + ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True) + for i, (name, mem) in enumerate(ram_components[:10]): + if mem.ram_total > 0: + percentage = (mem.ram_total / total_ram * 100) if total_ram > 0 else 0 + lines.append( + f"{i + 1}. {name} ({mem.ram_total:,} B) - {percentage:.1f}% of analyzed RAM" + ) + + lines.append("") + lines.append( + "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included." + ) + lines.append("=" * 108) + + return "\n".join(lines) + + def to_json(self) -> str: + """Export analysis results as JSON.""" + data = { + "components": { + name: { + "text": mem.text_size, + "rodata": mem.rodata_size, + "data": mem.data_size, + "bss": mem.bss_size, + "flash_total": mem.flash_total, + "ram_total": mem.ram_total, + "symbol_count": mem.symbol_count, + } + for name, mem in self.components.items() + }, + "totals": { + "flash": sum(c.flash_total for c in self.components.values()), + "ram": sum(c.ram_total for c in self.components.values()), + }, + } + return json.dumps(data, indent=2) + + +def analyze_elf( + elf_path: str, + objdump_path: str | None = None, + readelf_path: str | None = None, + detailed: bool = False, +) -> str: + """Analyze an ELF file and return a memory report.""" + analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path) + analyzer.analyze() + return analyzer.generate_report(detailed) + + +if __name__ == "__main__": + import sys + + if len(sys.argv) < 2: + print("Usage: analyze_memory.py ") + sys.exit(1) + + try: + report = analyze_elf(sys.argv[1]) + print(report) + except Exception as e: + print(f"Error: {e}") + sys.exit(1) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 808db03231..96e746fa8d 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -5,6 +5,7 @@ import os from pathlib import Path import re import subprocess +from typing import Any from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE from esphome.core import CORE, EsphomeError @@ -104,7 +105,16 @@ def run_compile(config, verbose): args = [] if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]: args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"] - return run_platformio_cli_run(config, verbose, *args) + result = run_platformio_cli_run(config, verbose, *args) + + # Run memory analysis if enabled + if config.get(CONF_ESPHOME, {}).get("analyze_memory", False): + try: + analyze_memory_usage(config) + except Exception as e: + _LOGGER.warning("Failed to analyze memory usage: %s", e) + + return result def _run_idedata(config): @@ -331,3 +341,68 @@ class IDEData: return f"{self.cc_path[:-7]}addr2line.exe" return f"{self.cc_path[:-3]}addr2line" + + @property + def objdump_path(self) -> str: + # replace gcc at end with objdump + + # Windows + if self.cc_path.endswith(".exe"): + return f"{self.cc_path[:-7]}objdump.exe" + + return f"{self.cc_path[:-3]}objdump" + + @property + def readelf_path(self) -> str: + # replace gcc at end with readelf + + # Windows + if self.cc_path.endswith(".exe"): + return f"{self.cc_path[:-7]}readelf.exe" + + return f"{self.cc_path[:-3]}readelf" + + +def analyze_memory_usage(config: dict[str, Any]) -> None: + """Analyze memory usage by component after compilation.""" + # Lazy import to avoid overhead when not needed + from esphome.analyze_memory import MemoryAnalyzer + + idedata = get_idedata(config) + + # Get paths to tools + elf_path = idedata.firmware_elf_path + objdump_path = idedata.objdump_path + readelf_path = idedata.readelf_path + + # Debug logging + _LOGGER.debug("ELF path from idedata: %s", elf_path) + + # Check if file exists + if not Path(elf_path).exists(): + # Try alternate path + alt_path = Path(CORE.relative_build_path(".pioenvs", CORE.name, "firmware.elf")) + if alt_path.exists(): + elf_path = str(alt_path) + _LOGGER.debug("Using alternate ELF path: %s", elf_path) + else: + _LOGGER.warning("ELF file not found at %s or %s", elf_path, alt_path) + return + + # Create analyzer and run analysis + analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path) + analyzer.analyze() + + # Generate and print report + report = analyzer.generate_report() + _LOGGER.info("\n%s", report) + + # Optionally save to file + if config.get(CONF_ESPHOME, {}).get("memory_report_file"): + report_file = Path(config[CONF_ESPHOME]["memory_report_file"]) + if report_file.suffix == ".json": + report_file.write_text(analyzer.to_json()) + _LOGGER.info("Memory report saved to %s", report_file) + else: + report_file.write_text(report) + _LOGGER.info("Memory report saved to %s", report_file) From 85049611c3ea064ab219106d7805d767f67cd31b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Jul 2025 07:48:55 -0500 Subject: [PATCH 02/16] wip --- esphome/analyze_memory.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py index 6e63c4875d..1d08af05cb 100644 --- a/esphome/analyze_memory.py +++ b/esphome/analyze_memory.py @@ -52,9 +52,6 @@ COMPONENT_PATTERNS = { "esp32_ble": re.compile(r"esphome::esp32_ble::"), "esp32_ble_tracker": re.compile(r"esphome::esp32_ble_tracker::"), "ethernet": re.compile(r"esphome::ethernet::"), - "core": re.compile( - r"esphome::(?!api::|wifi::|mqtt::|web_server::|sensor::|binary_sensor::|switch_::|light::|cover::|climate::|fan::|display::|logger::|ota::|time::|sun::|text_sensor::|script::|interval::|json::|network::|mdns::|i2c::|spi::|uart::|dallas::|dht::|adc::|pwm::|ledc::|gpio::|esp32::|esp8266::|remote_|rf_bridge::|captive_portal::|deep_sleep::|bluetooth_proxy::|esp32_ble::|esp32_ble_tracker::|ethernet::)" - ), } @@ -260,11 +257,17 @@ class MemoryAnalyzer: # Demangle C++ names if needed demangled = self._demangle_symbol(symbol_name) - # Check against component patterns + # Check against specific component patterns first (skip 'core') for component, pattern in COMPONENT_PATTERNS.items(): + if component == "core": + continue if pattern.search(demangled): return f"[esphome]{component}" + # Check for esphome core namespace last + if "esphome::" in demangled: + return "[esphome]core" + # Check for web server related code if ( "AsyncWebServer" in demangled @@ -540,10 +543,20 @@ class MemoryAnalyzer: if not symbols: return + # Try to find the appropriate c++filt for the platform + cppfilt_cmd = "c++filt" + + # Check if we have a toolchain-specific c++filt + if self.objdump_path and self.objdump_path != "objdump": + # Replace objdump with c++filt in the path + potential_cppfilt = self.objdump_path.replace("objdump", "c++filt") + if Path(potential_cppfilt).exists(): + cppfilt_cmd = potential_cppfilt + try: # Send all symbols to c++filt at once result = subprocess.run( - ["c++filt"], + [cppfilt_cmd], input="\n".join(symbols), capture_output=True, text=True, From 548cd39496c99bae0cae57df6fa64eedd8e48024 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Jul 2025 07:51:04 -0500 Subject: [PATCH 03/16] wip --- esphome/analyze_memory.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py index 1d08af05cb..c5542c3362 100644 --- a/esphome/analyze_memory.py +++ b/esphome/analyze_memory.py @@ -52,6 +52,26 @@ COMPONENT_PATTERNS = { "esp32_ble": re.compile(r"esphome::esp32_ble::"), "esp32_ble_tracker": re.compile(r"esphome::esp32_ble_tracker::"), "ethernet": re.compile(r"esphome::ethernet::"), + "valve": re.compile(r"esphome::valve::"), + "lock": re.compile(r"esphome::lock::"), + "alarm_control_panel": re.compile(r"esphome::alarm_control_panel::"), + "number": re.compile(r"esphome::number::"), + "select": re.compile(r"esphome::select::"), + "button": re.compile(r"esphome::button::"), + "datetime": re.compile(r"esphome::datetime::"), + "text": re.compile(r"esphome::text::"), + "media_player": re.compile(r"esphome::media_player::"), + "microphone": re.compile(r"esphome::microphone::"), + "speaker": re.compile(r"esphome::speaker::"), + "voice_assistant": re.compile(r"esphome::voice_assistant::"), + "update": re.compile(r"esphome::update::"), + "image": re.compile(r"esphome::image::"), + "font": re.compile(r"esphome::font::"), + "color": re.compile(r"esphome::color::"), + "graph": re.compile(r"esphome::graph::"), + "qr_code": re.compile(r"esphome::qr_code::"), + "touchscreen": re.compile(r"esphome::touchscreen::"), + "lvgl": re.compile(r"esphome::lvgl::"), } From 40d9c0a3db9696d3eb53a6f821b9091928562855 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Jul 2025 07:58:35 -0500 Subject: [PATCH 04/16] wip --- esphome/analyze_memory.py | 283 ++++++++++++++++++++++++++++---------- 1 file changed, 212 insertions(+), 71 deletions(-) diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py index c5542c3362..91b3d3228b 100644 --- a/esphome/analyze_memory.py +++ b/esphome/analyze_memory.py @@ -9,70 +9,33 @@ import subprocess _LOGGER = logging.getLogger(__name__) -# Component namespace patterns -COMPONENT_PATTERNS = { - "api": re.compile(r"esphome::api::"), - "wifi": re.compile(r"esphome::wifi::"), - "mqtt": re.compile(r"esphome::mqtt::"), - "web_server": re.compile(r"esphome::web_server::"), - "sensor": re.compile(r"esphome::sensor::"), - "binary_sensor": re.compile(r"esphome::binary_sensor::"), - "switch": re.compile(r"esphome::switch_::"), - "light": re.compile(r"esphome::light::"), - "cover": re.compile(r"esphome::cover::"), - "climate": re.compile(r"esphome::climate::"), - "fan": re.compile(r"esphome::fan::"), - "display": re.compile(r"esphome::display::"), - "logger": re.compile(r"esphome::logger::"), - "ota": re.compile(r"esphome::ota::"), - "time": re.compile(r"esphome::time::"), - "sun": re.compile(r"esphome::sun::"), - "text_sensor": re.compile(r"esphome::text_sensor::"), - "script": re.compile(r"esphome::script::"), - "interval": re.compile(r"esphome::interval::"), - "json": re.compile(r"esphome::json::"), - "network": re.compile(r"esphome::network::"), - "mdns": re.compile(r"esphome::mdns::"), - "i2c": re.compile(r"esphome::i2c::"), - "spi": re.compile(r"esphome::spi::"), - "uart": re.compile(r"esphome::uart::"), - "dallas": re.compile(r"esphome::dallas::"), - "dht": re.compile(r"esphome::dht::"), - "adc": re.compile(r"esphome::adc::"), - "pwm": re.compile(r"esphome::pwm::"), - "ledc": re.compile(r"esphome::ledc::"), - "gpio": re.compile(r"esphome::gpio::"), - "esp32": re.compile(r"esphome::esp32::"), - "esp8266": re.compile(r"esphome::esp8266::"), - "remote": re.compile(r"esphome::remote_"), - "rf_bridge": re.compile(r"esphome::rf_bridge::"), - "captive_portal": re.compile(r"esphome::captive_portal::"), - "deep_sleep": re.compile(r"esphome::deep_sleep::"), - "bluetooth_proxy": re.compile(r"esphome::bluetooth_proxy::"), - "esp32_ble": re.compile(r"esphome::esp32_ble::"), - "esp32_ble_tracker": re.compile(r"esphome::esp32_ble_tracker::"), - "ethernet": re.compile(r"esphome::ethernet::"), - "valve": re.compile(r"esphome::valve::"), - "lock": re.compile(r"esphome::lock::"), - "alarm_control_panel": re.compile(r"esphome::alarm_control_panel::"), - "number": re.compile(r"esphome::number::"), - "select": re.compile(r"esphome::select::"), - "button": re.compile(r"esphome::button::"), - "datetime": re.compile(r"esphome::datetime::"), - "text": re.compile(r"esphome::text::"), - "media_player": re.compile(r"esphome::media_player::"), - "microphone": re.compile(r"esphome::microphone::"), - "speaker": re.compile(r"esphome::speaker::"), - "voice_assistant": re.compile(r"esphome::voice_assistant::"), - "update": re.compile(r"esphome::update::"), - "image": re.compile(r"esphome::image::"), - "font": re.compile(r"esphome::font::"), - "color": re.compile(r"esphome::color::"), - "graph": re.compile(r"esphome::graph::"), - "qr_code": re.compile(r"esphome::qr_code::"), - "touchscreen": re.compile(r"esphome::touchscreen::"), - "lvgl": re.compile(r"esphome::lvgl::"), -} +# Pattern to extract ESPHome component namespaces dynamically +ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::") + + +# Get the list of actual ESPHome components by scanning the components directory +def get_esphome_components(): + """Get set of actual ESPHome components from the components directory.""" + components = set() + + # Find the components directory relative to this file + current_dir = Path(__file__).parent + components_dir = current_dir / "components" + + if components_dir.exists() and components_dir.is_dir(): + for item in components_dir.iterdir(): + if ( + item.is_dir() + and not item.name.startswith(".") + and not item.name.startswith("__") + ): + components.add(item.name) + + return components + + +# Cache the component list +ESPHOME_COMPONENTS = get_esphome_components() class MemorySection: @@ -277,14 +240,22 @@ class MemoryAnalyzer: # Demangle C++ names if needed demangled = self._demangle_symbol(symbol_name) - # Check against specific component patterns first (skip 'core') - for component, pattern in COMPONENT_PATTERNS.items(): - if component == "core": - continue - if pattern.search(demangled): - return f"[esphome]{component}" + # Check for ESPHome component namespaces dynamically + # Pattern: esphome::component_name:: (with trailing ::) + match = ESPHOME_COMPONENT_PATTERN.search(demangled) + if match: + component_name = match.group(1) + # Strip trailing underscore if present (e.g., switch_ -> switch) + component_name = component_name.rstrip("_") - # Check for esphome core namespace last + # Check if this is an actual component or core + if component_name in ESPHOME_COMPONENTS: + return f"[esphome]{component_name}" + else: + return "[esphome]core" + + # Check for esphome core namespace (no component namespace) + # This catches esphome::ClassName or esphome::function_name if "esphome::" in demangled: return "[esphome]core" @@ -480,6 +451,11 @@ class MemoryAnalyzer: return "bluetooth" elif "coex" in symbol_name: return "wifi_bt_coex" + elif "r_" in symbol_name and any( + bt in symbol_name for bt in ["ble", "lld", "llc", "llm"] + ): + # ROM bluetooth functions + return "bluetooth_rom" # Power management if any( @@ -556,6 +532,171 @@ class MemoryAnalyzer: if "dhcp" in symbol_name or "handle_dhcp" in symbol_name: return "dhcp" + # JSON parsing + if any( + json in demangled + for json in [ + "ArduinoJson", + "JsonDocument", + "JsonArray", + "JsonObject", + "deserialize", + "serialize", + ] + ): + return "json_lib" + + # HTTP/Web related + if any( + http in demangled + for http in ["HTTP", "http_", "Request", "Response", "Uri", "WebSocket"] + ): + return "http_lib" + + # Ethernet PHY drivers + if any( + eth in symbol_name + for eth in [ + "emac_", + "eth_phy_", + "phy_tlk110", + "phy_lan87", + "phy_ip101", + "phy_rtl", + "phy_dp83", + "phy_ksz", + ] + ): + return "ethernet_phy" + + # Task/Thread management + if any(task in symbol_name for task in ["pthread_", "thread_", "_task_"]): + return "threading" + + # Mutex/Semaphore + if any( + sync in symbol_name + for sync in ["mutex", "semaphore", "spinlock", "portMUX"] + ): + return "synchronization" + + # String formatting + if any( + fmt in symbol_name + for fmt in [ + "snprintf", + "vsnprintf", + "sprintf", + "vsprintf", + "sscanf", + "vsscanf", + ] + ): + return "string_formatting" + + # Math functions + if ( + any( + math in symbol_name + for math in [ + "sin", + "cos", + "tan", + "sqrt", + "pow", + "exp", + "log", + "atan", + "asin", + "acos", + "floor", + "ceil", + "fabs", + "round", + ] + ) + and len(symbol_name) < 20 + ): + return "math_lib" + + # Random number generation + if any(rng in symbol_name for rng in ["rand", "random", "rng_", "prng"]): + return "random" + + # Time functions + if any( + time in symbol_name + for time in [ + "time", + "clock", + "gettimeofday", + "settimeofday", + "localtime", + "gmtime", + "mktime", + "strftime", + ] + ): + return "time_lib" + + # Console/UART output + if any( + console in symbol_name + for console in [ + "console_", + "uart_tx", + "uart_rx", + "puts", + "putchar", + "getchar", + ] + ): + return "console_io" + + # ROM functions + if symbol_name.startswith("r_") or symbol_name.startswith("rom_"): + return "rom_functions" + + # Compiler generated code + if any( + gen in symbol_name + for gen in [ + "__divdi3", + "__udivdi3", + "__moddi3", + "__muldi3", + "__ashldi3", + "__ashrdi3", + "__lshrdi3", + "__cmpdi2", + "__fixdfdi", + "__floatdidf", + ] + ): + return "compiler_runtime" + + # Exception handling + if any( + exc in symbol_name for exc in ["__cxa_", "_Unwind_", "__gcc_personality"] + ): + return "exception_handling" + + # RTTI (Run-Time Type Information) + if "__type_info" in demangled or "__class_type_info" in demangled: + return "rtti" + + # Static initializers + if "_GLOBAL__sub_I_" in symbol_name or "__static_initialization" in demangled: + return "static_init" + + # Weak symbols + if "__weak_" in symbol_name: + return "weak_symbols" + + # Compiler builtins + if "__builtin_" in symbol_name: + return "compiler_builtins" + return "other" def _batch_demangle_symbols(self, symbols: list[str]) -> None: From 1f361b07d1aa52ee704d07bd19c03c92ee34d108 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Jul 2025 08:16:35 -0500 Subject: [PATCH 05/16] wip --- esphome/analyze_memory.py | 915 ++++++++++++++++++++------------------ 1 file changed, 480 insertions(+), 435 deletions(-) diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py index 91b3d3228b..45d73ae920 100644 --- a/esphome/analyze_memory.py +++ b/esphome/analyze_memory.py @@ -12,6 +12,397 @@ _LOGGER = logging.getLogger(__name__) # Pattern to extract ESPHome component namespaces dynamically ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::") +# Component identification rules +# Symbol patterns: patterns found in raw symbol names +SYMBOL_PATTERNS = { + "freertos": [ + "vTask", + "xTask", + "xQueue", + "pvPort", + "vPort", + "uxTask", + "pcTask", + "prvTimerTask", + "prvAddNewTaskToReadyList", + "pxReadyTasksLists", + ], + "xtensa": ["xt_", "_xt_"], + "heap": ["heap_", "multi_heap"], + "spi_flash": ["spi_flash"], + "rtc": ["rtc_"], + "gpio_driver": ["gpio_", "pins"], + "uart_driver": ["uart", "_uart", "UART"], + "timer": ["timer_", "esp_timer"], + "peripherals": ["periph_", "periman"], + "network_stack": [ + "vj_compress", + "raw_sendto", + "raw_input", + "etharp_", + "icmp_input", + "socket_ipv6", + "ip_napt", + ], + "ipv6_stack": ["nd6_", "ip6_", "mld6_"], + "wifi_stack": [ + "ieee80211", + "hostap", + "sta_", + "ap_", + "scan_", + "wifi_", + "wpa_", + "wps_", + "esp_wifi", + "cnx_", + "wpa3_", + "sae_", + "wDev_", + "ic_", + "mac_", + "esf_buf", + "gWpaSm", + "sm_WPA", + "eapol_", + "owe_", + ], + "bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"], + "wifi_bt_coex": ["coex"], + "bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"], + "bluedroid_bt": ["bluedroid", "btc_", "bta_", "btm_", "btu_"], + "crypto_math": [ + "ecp_", + "bignum_", + "mpi_", + "sswu", + "modp", + "dragonfly_", + "gcm_mult", + "__multiply", + ], + "hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"], + "libc": [ + "printf", + "scanf", + "malloc", + "free", + "memcpy", + "memset", + "strcpy", + "strlen", + "_dtoa", + "_fopen", + "__sfvwrite_r", + "qsort", + ], + "string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"], + "memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"], + "file_io": ["fread", "fwrite", "fopen", "fclose", "fseek", "ftell", "fflush"], + "string_formatting": [ + "snprintf", + "vsnprintf", + "sprintf", + "vsprintf", + "sscanf", + "vsscanf", + ], + "cpp_anonymous": ["_GLOBAL__N_"], + "cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality"], + "exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"], + "static_init": ["_GLOBAL__sub_I_"], + "mdns_lib": ["mdns"], + "phy_radio": [ + "phy_", + "rf_", + "chip_", + "register_chipv7", + "pbus_", + "bb_", + "fe_", + "rfcal_", + "ram_rfcal", + "tx_pwctrl", + "rx_chan", + "set_rx_gain", + "set_chan", + "agc_reg", + "ram_txiq", + "ram_txdc", + "ram_gen_rx_gain", + ], + "wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"], + "wifi_lmac": ["lmac"], + "wifi_device": ["wdev", "wDev_"], + "power_mgmt": [ + "pm_", + "sleep", + "rtc_sleep", + "light_sleep", + "deep_sleep", + "power_down", + "g_pm", + ], + "memory_mgmt": ["mem_", "memory_", "tlsf_", "memp_"], + "hal_layer": ["hal_"], + "clock_mgmt": [ + "clk_", + "clock_", + "rtc_clk", + "apb_", + "cpu_freq", + "setCpuFrequencyMhz", + ], + "cache_mgmt": ["cache"], + "flash_ops": ["flash", "image_load"], + "interrupt_handlers": [ + "isr", + "interrupt", + "intr_", + "exc_", + "exception", + "port_IntStack", + ], + "wrapper_functions": ["_wrapper"], + "error_handling": ["panic", "abort", "assert", "error_", "fault"], + "authentication": ["auth"], + "ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_"], + "dhcp": ["dhcp", "handle_dhcp"], + "ethernet_phy": [ + "emac_", + "eth_phy_", + "phy_tlk110", + "phy_lan87", + "phy_ip101", + "phy_rtl", + "phy_dp83", + "phy_ksz", + ], + "threading": ["pthread_", "thread_", "_task_"], + "pthread": ["pthread"], + "synchronization": ["mutex", "semaphore", "spinlock", "portMUX"], + "math_lib": [ + "sin", + "cos", + "tan", + "sqrt", + "pow", + "exp", + "log", + "atan", + "asin", + "acos", + "floor", + "ceil", + "fabs", + "round", + ], + "random": ["rand", "random", "rng_", "prng"], + "time_lib": [ + "time", + "clock", + "gettimeofday", + "settimeofday", + "localtime", + "gmtime", + "mktime", + "strftime", + ], + "console_io": ["console_", "uart_tx", "uart_rx", "puts", "putchar", "getchar"], + "rom_functions": ["r_", "rom_"], + "compiler_runtime": [ + "__divdi3", + "__udivdi3", + "__moddi3", + "__muldi3", + "__ashldi3", + "__ashrdi3", + "__lshrdi3", + "__cmpdi2", + "__fixdfdi", + "__floatdidf", + ], + "libgcc": ["libgcc", "_divdi3", "_udivdi3"], + "boot_startup": ["boot", "start_cpu", "call_start", "startup", "bootloader"], + "bootloader": ["bootloader_", "esp_bootloader"], + "app_framework": ["app_", "initArduino", "setup", "loop"], + "weak_symbols": ["__weak_"], + "compiler_builtins": ["__builtin_"], + "vfs": ["vfs_", "VFS"], + "esp32_sdk": ["esp32_", "esp32c", "esp32s"], + "usb": ["usb_", "USB", "cdc_", "CDC"], + "i2c_driver": ["i2c_", "I2C"], + "i2s_driver": ["i2s_", "I2S"], + "spi_driver": ["spi_", "SPI"], + "adc_driver": ["adc_", "ADC"], + "dac_driver": ["dac_", "DAC"], + "touch_driver": ["touch_", "TOUCH"], + "pwm_driver": ["pwm_", "PWM", "ledc_", "LEDC"], + "rmt_driver": ["rmt_", "RMT"], + "pcnt_driver": ["pcnt_", "PCNT"], + "can_driver": ["can_", "CAN", "twai_", "TWAI"], + "sdmmc_driver": ["sdmmc_", "SDMMC", "sdcard", "sd_card"], + "temp_sensor": ["temp_sensor", "tsens_"], + "watchdog": ["wdt_", "WDT", "watchdog"], + "brownout": ["brownout", "bod_"], + "ulp": ["ulp_", "ULP"], + "psram": ["psram", "PSRAM", "spiram", "SPIRAM"], + "efuse": ["efuse", "EFUSE"], + "partition": ["partition", "esp_partition"], + "esp_event": ["esp_event", "event_loop", "event_callback"], + "esp_console": ["esp_console", "console_"], + "chip_specific": ["chip_", "esp_chip"], + "esp_system_utils": ["esp_system", "esp_hw", "esp_clk", "esp_sleep"], + "ipc": ["esp_ipc", "ipc_"], + "wifi_config": [ + "g_cnxMgr", + "gChmCxt", + "g_ic", + "TxRxCxt", + "s_dp", + "s_ni", + "s_reg_dump", + "packet$", + "d_mult_table", + "K", + "fcstab", + ], + "smartconfig": ["sc_ack_send"], + "rc_calibration": ["rc_cal", "rcUpdate"], + "noise_floor": ["noise_check"], + "rf_calibration": [ + "set_rx_sense", + "set_rx_gain_cal", + "set_chan_dig_gain", + "tx_pwctrl_init_cal", + "rfcal_txiq", + "set_tx_gain_table", + "correct_rfpll_offset", + "pll_correct_dcap", + "txiq_cal_init", + "pwdet_sar", + "rx_11b_opt", + ], + "wifi_crypto": [ + "pk_use_ecparams", + "process_segments", + "ccmp_", + "rc4_", + "aria_", + "mgf_mask", + "dh_group", + ], + "radio_control": ["fsm_input", "fsm_sconfreq"], + "pbuf": [ + "pbuf_", + "ppSearchTxframe", + "ppMapWaitTxq", + "ppFillAMPDUBar", + "ppCheckTxConnTrafficIdle", + ], + "ppTask": ["ppCalTkipMic"], + "event_group": ["xEventGroup"], + "ringbuffer": ["xRingbuffer", "prvSend", "prvReceive", "prvCopy"], + "provisioning": ["prov_"], + "scan": ["gScanStruct"], + "port": ["xPort"], + "elf_loader": ["elf_add", "process_image", "read_encoded"], + "socket_api": [ + "sockets", + "netconn_", + "accept_function", + "recv_raw", + "socket_ipv4_multicast", + "socket_ipv6_multicast", + ], + "igmp": ["igmp_"], + "icmp6": ["icmp6_"], + "arp": ["arp_table"], + "ampdu": ["ampdu_", "rcAmpdu", "trc_onAmpduOp"], + "ieee802_11": ["ieee802_11_"], + "rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"], + "nan": ["nan_dp_"], + "channel_mgmt": ["chm_init", "chm_set_current_channel"], + "trace": ["trc_init"], + "country_code": ["country_info"], + "multicore": ["do_multicore_settings"], + "Update_lib": ["Update"], + "stdio": [ + "__sf", + "__sflush_r", + "__srefill_r", + "_impure_data", + "_reclaim_reent", + "_open_r", + ], + "strncpy_ops": ["strncpy"], + "math_internal": ["__mdiff", "__lshift", "__mprec_tens", "quorem"], + "character_class": ["__chclass"], + "camellia": ["camellia_"], + "crypto_tables": ["FSb", "FSb2", "FSb3", "FSb4"], + "event_buffer": ["g_eb_list_desc", "eb_space"], + "base_node": ["base_node_"], + "file_descriptor": ["s_fd_table"], + "tx_delay": ["tx_delay_cfg"], + "deinit": ["deinit_functions"], + "lcp_echo": ["LcpEchoCheck"], + "raw_api": ["raw_bind", "raw_connect"], +} + +# Demangled patterns: patterns found in demangled C++ names +DEMANGLED_PATTERNS = { + "gpio_driver": ["GPIO"], + "uart_driver": ["UART"], + "network_stack": [ + "lwip", + "tcp", + "udp", + "ip4", + "ip6", + "dhcp", + "dns", + "netif", + "ethernet", + "ppp", + "slip", + ], + "wifi_stack": ["NetworkInterface"], + "nimble_bt": [ + "nimble", + "NimBLE", + "ble_hs", + "ble_gap", + "ble_gatt", + "ble_att", + "ble_l2cap", + "ble_sm", + ], + "crypto": ["mbedtls", "crypto", "sha", "aes", "rsa", "ecc", "tls", "ssl"], + "cpp_stdlib": ["std::", "__gnu_cxx::", "__cxxabiv"], + "static_init": ["__static_initialization"], + "rtti": ["__type_info", "__class_type_info"], + "web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"], + "async_tcp": ["AsyncClient", "AsyncServer"], + "mdns_lib": ["mdns"], + "json_lib": [ + "ArduinoJson", + "JsonDocument", + "JsonArray", + "JsonObject", + "deserialize", + "serialize", + ], + "http_lib": ["HTTP", "http_", "Request", "Response", "Uri", "WebSocket"], + "logging": ["log", "Log", "print", "Print", "diag_"], + "authentication": ["checkDigestAuthentication"], + "libgcc": ["libgcc"], + "esp_system": ["esp_", "ESP"], + "arduino": ["arduino"], + "nvs": ["nvs_"], + "filesystem": ["spiffs", "vfs"], + "libc": ["newlib"], +} + # Get the list of actual ESPHome components by scanning the components directory def get_esphome_components(): @@ -88,6 +479,7 @@ class MemoryAnalyzer: lambda: ComponentMemory("") ) self._demangle_cache: dict[str, str] = {} + self._uncategorized_symbols: list[tuple[str, str, int]] = [] def analyze(self) -> dict[str, ComponentMemory]: """Analyze the ELF file and return component memory usage.""" @@ -235,13 +627,17 @@ class MemoryAnalyzer: elif section_name == ".bss": comp_mem.bss_size += size + # Track uncategorized symbols + if component == "other" and size > 0: + demangled = self._demangle_symbol(symbol_name) + self._uncategorized_symbols.append((symbol_name, demangled, size)) + def _identify_component(self, symbol_name: str) -> str: """Identify which component a symbol belongs to.""" # Demangle C++ names if needed demangled = self._demangle_symbol(symbol_name) - # Check for ESPHome component namespaces dynamically - # Pattern: esphome::component_name:: (with trailing ::) + # Check for ESPHome component namespaces first match = ESPHOME_COMPONENT_PATTERN.search(demangled) if match: component_name = match.group(1) @@ -255,448 +651,64 @@ class MemoryAnalyzer: return "[esphome]core" # Check for esphome core namespace (no component namespace) - # This catches esphome::ClassName or esphome::function_name if "esphome::" in demangled: return "[esphome]core" - # Check for web server related code + # Check against symbol patterns + for component, patterns in SYMBOL_PATTERNS.items(): + if any(pattern in symbol_name for pattern in patterns): + return component + + # Check against demangled patterns + for component, patterns in DEMANGLED_PATTERNS.items(): + if any(pattern in demangled for pattern in patterns): + return component + + # Special cases that need more complex logic + + # ROM functions starting with r_ or rom_ + if symbol_name.startswith("r_") or symbol_name.startswith("rom_"): + return "rom_functions" + + # Math functions with short names + if len(symbol_name) < 20 and symbol_name in [ + "sin", + "cos", + "tan", + "sqrt", + "pow", + "exp", + "log", + "atan", + "asin", + "acos", + "floor", + "ceil", + "fabs", + "round", + ]: + return "math_lib" + + # Check if spi_flash vs spi_driver + if "spi_" in symbol_name or "SPI" in symbol_name: + if "spi_flash" in symbol_name: + return "spi_flash" + else: + return "spi_driver" + + # ESP OTA framework (exclude esphome OTA) if ( - "AsyncWebServer" in demangled - or "AsyncWebHandler" in demangled - or "WebServer" in demangled - ): - return "web_server_lib" - elif "AsyncClient" in demangled or "AsyncServer" in demangled: - return "async_tcp" - - # Check for FreeRTOS/ESP-IDF components - if any( - prefix in symbol_name - for prefix in [ - "vTask", - "xTask", - "xQueue", - "pvPort", - "vPort", - "uxTask", - "pcTask", - ] - ): - return "freertos" - elif "xt_" in symbol_name or "_xt_" in symbol_name: - return "xtensa" - elif "heap_" in symbol_name or "multi_heap" in demangled: - return "heap" - elif "spi_flash" in symbol_name: - return "spi_flash" - elif "rtc_" in symbol_name: - return "rtc" - elif "gpio_" in symbol_name or "GPIO" in demangled: - return "gpio_driver" - elif "uart_" in symbol_name or "UART" in demangled: - return "uart_driver" - elif "timer_" in symbol_name or "esp_timer" in symbol_name: - return "timer" - elif "periph_" in symbol_name: - return "peripherals" - - # C++ standard library - if any(ns in demangled for ns in ["std::", "__gnu_cxx::", "__cxxabiv"]): - return "cpp_stdlib" - elif "_GLOBAL__N_" in symbol_name: - return "cpp_anonymous" - - # Platform/system code - if "esp_" in demangled or "ESP" in demangled: - return "esp_system" - elif "app_" in symbol_name: - return "app_framework" - elif "arduino" in demangled.lower(): - return "arduino" - - # Network stack components - if any( - net in demangled - for net in [ - "lwip", - "tcp", - "udp", - "ip4", - "ip6", - "dhcp", - "dns", - "netif", - "ethernet", - "ppp", - "slip", - ] - ): - return "network_stack" - elif "vj_compress" in symbol_name: # Van Jacobson TCP compression - return "network_stack" - - # WiFi/802.11 stack - if any( - wifi in symbol_name - for wifi in [ - "ieee80211", - "hostap", - "sta_", - "ap_", - "scan_", - "wifi_", - "wpa_", - "wps_", - "esp_wifi", - ] - ): - return "wifi_stack" - elif "NetworkInterface" in demangled: - return "wifi_stack" - - # mDNS specific - if ( - "mdns" in symbol_name or "mdns" in demangled + "esp_ota" in symbol_name or "ota_" in symbol_name ) and "esphome" not in demangled: - return "mdns_lib" + return "esp_ota" - # Cryptography - if any( - crypto in demangled - for crypto in [ - "mbedtls", - "crypto", - "sha", - "aes", - "rsa", - "ecc", - "tls", - "ssl", - ] - ): - return "crypto" - - # C library functions - if any( - libc in symbol_name - for libc in [ - "printf", - "scanf", - "malloc", - "free", - "memcpy", - "memset", - "strcpy", - "strlen", - "_dtoa", - "_fopen", - ] - ): - return "libc" - elif symbol_name.startswith("_") and symbol_name[1:].replace("_r", "").replace( + # libc special printf variants + if symbol_name.startswith("_") and symbol_name[1:].replace("_r", "").replace( "v", "" ).replace("s", "") in ["printf", "fprintf", "sprintf", "scanf"]: return "libc" - # IPv6 specific - if "nd6_" in symbol_name or "ip6_" in symbol_name: - return "ipv6_stack" - - # Other system libraries - if "nvs_" in demangled: - return "nvs" - elif "spiffs" in demangled or "vfs" in demangled: - return "filesystem" - elif "newlib" in demangled: - return "libc" - elif ( - "libgcc" in demangled - or "_divdi3" in symbol_name - or "_udivdi3" in symbol_name - ): - return "libgcc" - - # Boot and startup - if any( - boot in symbol_name - for boot in ["boot", "start_cpu", "call_start", "startup", "bootloader"] - ): - return "boot_startup" - - # PHY/Radio layer - if any( - phy in symbol_name - for phy in [ - "phy_", - "rf_", - "chip_", - "register_chipv7", - "pbus_", - "bb_", - "fe_", - ] - ): - return "phy_radio" - elif any(pp in symbol_name for pp in ["pp_", "ppT", "ppR", "ppP", "ppInstall"]): - return "wifi_phy_pp" - elif "lmac" in symbol_name: - return "wifi_lmac" - elif "wdev" in symbol_name: - return "wifi_device" - - # Bluetooth/BLE - if any( - bt in symbol_name for bt in ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_"] - ): - return "bluetooth" - elif "coex" in symbol_name: - return "wifi_bt_coex" - elif "r_" in symbol_name and any( - bt in symbol_name for bt in ["ble", "lld", "llc", "llm"] - ): - # ROM bluetooth functions - return "bluetooth_rom" - - # Power management - if any( - pm in symbol_name - for pm in [ - "pm_", - "sleep", - "rtc_sleep", - "light_sleep", - "deep_sleep", - "power_down", - ] - ): - return "power_mgmt" - - # Logging and diagnostics - if any(log in demangled for log in ["log", "Log", "print", "Print", "diag_"]): - return "logging" - - # Memory management - if any(mem in symbol_name for mem in ["mem_", "memory_", "tlsf_", "memp_"]): - return "memory_mgmt" - - # HAL (Hardware Abstraction Layer) - if "hal_" in symbol_name: - return "hal_layer" - - # Clock management - if any( - clk in symbol_name - for clk in ["clk_", "clock_", "rtc_clk", "apb_", "cpu_freq"] - ): - return "clock_mgmt" - - # Cache management - if "cache" in symbol_name: - return "cache_mgmt" - - # Flash operations - if "flash" in symbol_name and "spi" not in symbol_name: - return "flash_ops" - - # Interrupt/Exception handling - if any( - isr in symbol_name - for isr in ["isr", "interrupt", "intr_", "exc_", "exception"] - ): - return "interrupt_handlers" - elif "_wrapper" in symbol_name: - return "wrapper_functions" - - # Error handling - if any( - err in symbol_name - for err in ["panic", "abort", "assert", "error_", "fault"] - ): - return "error_handling" - - # ECC/Crypto math - if any( - ecc in symbol_name for ecc in ["ecp_", "bignum_", "mpi_", "sswu", "modp"] - ): - return "crypto_math" - - # Authentication - if "checkDigestAuthentication" in demangled or "auth" in symbol_name.lower(): - return "authentication" - - # PPP protocol - if any(ppp in symbol_name for ppp in ["ppp", "ipcp_", "lcp_", "chap_"]): - return "ppp_protocol" - - # DHCP - if "dhcp" in symbol_name or "handle_dhcp" in symbol_name: - return "dhcp" - - # JSON parsing - if any( - json in demangled - for json in [ - "ArduinoJson", - "JsonDocument", - "JsonArray", - "JsonObject", - "deserialize", - "serialize", - ] - ): - return "json_lib" - - # HTTP/Web related - if any( - http in demangled - for http in ["HTTP", "http_", "Request", "Response", "Uri", "WebSocket"] - ): - return "http_lib" - - # Ethernet PHY drivers - if any( - eth in symbol_name - for eth in [ - "emac_", - "eth_phy_", - "phy_tlk110", - "phy_lan87", - "phy_ip101", - "phy_rtl", - "phy_dp83", - "phy_ksz", - ] - ): - return "ethernet_phy" - - # Task/Thread management - if any(task in symbol_name for task in ["pthread_", "thread_", "_task_"]): - return "threading" - - # Mutex/Semaphore - if any( - sync in symbol_name - for sync in ["mutex", "semaphore", "spinlock", "portMUX"] - ): - return "synchronization" - - # String formatting - if any( - fmt in symbol_name - for fmt in [ - "snprintf", - "vsnprintf", - "sprintf", - "vsprintf", - "sscanf", - "vsscanf", - ] - ): - return "string_formatting" - - # Math functions - if ( - any( - math in symbol_name - for math in [ - "sin", - "cos", - "tan", - "sqrt", - "pow", - "exp", - "log", - "atan", - "asin", - "acos", - "floor", - "ceil", - "fabs", - "round", - ] - ) - and len(symbol_name) < 20 - ): - return "math_lib" - - # Random number generation - if any(rng in symbol_name for rng in ["rand", "random", "rng_", "prng"]): - return "random" - - # Time functions - if any( - time in symbol_name - for time in [ - "time", - "clock", - "gettimeofday", - "settimeofday", - "localtime", - "gmtime", - "mktime", - "strftime", - ] - ): - return "time_lib" - - # Console/UART output - if any( - console in symbol_name - for console in [ - "console_", - "uart_tx", - "uart_rx", - "puts", - "putchar", - "getchar", - ] - ): - return "console_io" - - # ROM functions - if symbol_name.startswith("r_") or symbol_name.startswith("rom_"): - return "rom_functions" - - # Compiler generated code - if any( - gen in symbol_name - for gen in [ - "__divdi3", - "__udivdi3", - "__moddi3", - "__muldi3", - "__ashldi3", - "__ashrdi3", - "__lshrdi3", - "__cmpdi2", - "__fixdfdi", - "__floatdidf", - ] - ): - return "compiler_runtime" - - # Exception handling - if any( - exc in symbol_name for exc in ["__cxa_", "_Unwind_", "__gcc_personality"] - ): - return "exception_handling" - - # RTTI (Run-Time Type Information) - if "__type_info" in demangled or "__class_type_info" in demangled: - return "rtti" - - # Static initializers - if "_GLOBAL__sub_I_" in symbol_name or "__static_initialization" in demangled: - return "static_init" - - # Weak symbols - if "__weak_" in symbol_name: - return "weak_symbols" - - # Compiler builtins - if "__builtin_" in symbol_name: - return "compiler_builtins" - + # Track uncategorized symbols for analysis return "other" def _batch_demangle_symbols(self, symbols: list[str]) -> None: @@ -760,7 +772,7 @@ class MemoryAnalyzer: # Main table lines.append( - f"{'Component':<28} | {'Flash (text)':<12} | {'Flash (data)':<12} | {'RAM (data)':<10} | {'RAM (bss)':<10} | {'Total Flash':<12} | {'Total RAM':<10}" + f"{'Component':<28} | {'Flash (text)':>12} | {'Flash (data)':>12} | {'RAM (data)':>10} | {'RAM (bss)':>10} | {'Total Flash':>12} | {'Total RAM':>10}" ) lines.append( "-" * 28 @@ -860,6 +872,39 @@ class MemoryAnalyzer: } return json.dumps(data, indent=2) + def dump_uncategorized_symbols(self, output_file: str | None = None) -> None: + """Dump uncategorized symbols for analysis.""" + # Sort by size descending + sorted_symbols = sorted( + self._uncategorized_symbols, key=lambda x: x[2], reverse=True + ) + + lines = ["Uncategorized Symbols Analysis", "=" * 80] + lines.append(f"Total uncategorized symbols: {len(sorted_symbols)}") + lines.append( + f"Total uncategorized size: {sum(s[2] for s in sorted_symbols):,} bytes" + ) + lines.append("") + lines.append(f"{'Size':>10} | {'Symbol':<60} | Demangled") + lines.append("-" * 10 + "-+-" + "-" * 60 + "-+-" + "-" * 40) + + for symbol, demangled, size in sorted_symbols[:100]: # Top 100 + if symbol != demangled: + lines.append(f"{size:>10,} | {symbol[:60]:<60} | {demangled[:100]}") + else: + lines.append(f"{size:>10,} | {symbol[:60]:<60} | [not demangled]") + + if len(sorted_symbols) > 100: + lines.append(f"\n... and {len(sorted_symbols) - 100} more symbols") + + content = "\n".join(lines) + + if output_file: + with open(output_file, "w") as f: + f.write(content) + else: + print(content) + def analyze_elf( elf_path: str, From 06957d9895d8095d3bca2679368fe4148e32f5b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Jul 2025 08:21:36 -0500 Subject: [PATCH 06/16] wip --- esphome/analyze_memory.py | 129 ++++++++++++++++++++++++++++++++------ 1 file changed, 111 insertions(+), 18 deletions(-) diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py index 45d73ae920..bd51664271 100644 --- a/esphome/analyze_memory.py +++ b/esphome/analyze_memory.py @@ -26,8 +26,21 @@ SYMBOL_PATTERNS = { "prvTimerTask", "prvAddNewTaskToReadyList", "pxReadyTasksLists", + "prvAddCurrentTaskToDelayedList", + "xEventGroupWaitBits", + "xRingbufferSendFromISR", + "prvSendItemDoneNoSplit", + "prvReceiveGeneric", + "prvSendAcquireGeneric", + "prvCopyItemAllowSplit", + "xEventGroup", + "xRingbuffer", + "prvSend", + "prvReceive", + "prvCopy", + "xPort", ], - "xtensa": ["xt_", "_xt_"], + "xtensa": ["xt_", "_xt_", "xPortEnterCriticalTimeout"], "heap": ["heap_", "multi_heap"], "spi_flash": ["spi_flash"], "rtc": ["rtc_"], @@ -43,8 +56,21 @@ SYMBOL_PATTERNS = { "icmp_input", "socket_ipv6", "ip_napt", + "socket_ipv4_multicast", + "socket_ipv6_multicast", + "netconn_", + "recv_raw", + "accept_function", + "netconn_recv_data", + "netconn_accept", + "netconn_write_vectors_partly", + "netconn_drain", + "raw_connect", + "raw_bind", + "icmp_send_response", + "sockets", ], - "ipv6_stack": ["nd6_", "ip6_", "mld6_"], + "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], "wifi_stack": [ "ieee80211", "hostap", @@ -66,6 +92,14 @@ SYMBOL_PATTERNS = { "sm_WPA", "eapol_", "owe_", + "wifiLowLevelInit", + "s_do_mapping", + "gScanStruct", + "ppSearchTxframe", + "ppMapWaitTxq", + "ppFillAMPDUBar", + "ppCheckTxConnTrafficIdle", + "ppCalTkipMic", ], "bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"], "wifi_bt_coex": ["coex"], @@ -80,6 +114,10 @@ SYMBOL_PATTERNS = { "dragonfly_", "gcm_mult", "__multiply", + "quorem", + "__mdiff", + "__lshift", + "__mprec_tens", ], "hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"], "libc": [ @@ -95,10 +133,26 @@ SYMBOL_PATTERNS = { "_fopen", "__sfvwrite_r", "qsort", + "__sf", + "__sflush_r", + "__srefill_r", + "_impure_data", + "_reclaim_reent", + "_open_r", + "strncpy", ], "string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"], "memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"], - "file_io": ["fread", "fwrite", "fopen", "fclose", "fseek", "ftell", "fflush"], + "file_io": [ + "fread", + "fwrite", + "fopen", + "fclose", + "fseek", + "ftell", + "fflush", + "s_fd_table", + ], "string_formatting": [ "snprintf", "vsnprintf", @@ -107,8 +161,8 @@ SYMBOL_PATTERNS = { "sscanf", "vsscanf", ], - "cpp_anonymous": ["_GLOBAL__N_"], - "cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality"], + "cpp_anonymous": ["_GLOBAL__N_", "n$"], + "cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality", "_Z16"], "exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"], "static_init": ["_GLOBAL__sub_I_"], "mdns_lib": ["mdns"], @@ -130,6 +184,18 @@ SYMBOL_PATTERNS = { "ram_txiq", "ram_txdc", "ram_gen_rx_gain", + "rx_11b_opt", + "set_rx_sense", + "set_rx_gain_cal", + "set_chan_dig_gain", + "tx_pwctrl_init_cal", + "rfcal_txiq", + "set_tx_gain_table", + "correct_rfpll_offset", + "pll_correct_dcap", + "txiq_cal_init", + "pwdet_sar", + "pwdet_sar2_init", ], "wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"], "wifi_lmac": ["lmac"], @@ -143,7 +209,15 @@ SYMBOL_PATTERNS = { "power_down", "g_pm", ], - "memory_mgmt": ["mem_", "memory_", "tlsf_", "memp_"], + "memory_mgmt": [ + "mem_", + "memory_", + "tlsf_", + "memp_", + "pbuf_", + "pbuf_alloc", + "pbuf_copy_partial_pbuf", + ], "hal_layer": ["hal_"], "clock_mgmt": [ "clk_", @@ -166,7 +240,7 @@ SYMBOL_PATTERNS = { "wrapper_functions": ["_wrapper"], "error_handling": ["panic", "abort", "assert", "error_", "fault"], "authentication": ["auth"], - "ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_"], + "ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"], "dhcp": ["dhcp", "handle_dhcp"], "ethernet_phy": [ "emac_", @@ -225,7 +299,7 @@ SYMBOL_PATTERNS = { "libgcc": ["libgcc", "_divdi3", "_udivdi3"], "boot_startup": ["boot", "start_cpu", "call_start", "startup", "bootloader"], "bootloader": ["bootloader_", "esp_bootloader"], - "app_framework": ["app_", "initArduino", "setup", "loop"], + "app_framework": ["app_", "initArduino", "setup", "loop", "Update"], "weak_symbols": ["__weak_"], "compiler_builtins": ["__builtin_"], "vfs": ["vfs_", "VFS"], @@ -291,22 +365,35 @@ SYMBOL_PATTERNS = { "aria_", "mgf_mask", "dh_group", + "ccmp_aad_nonce", + "ccmp_encrypt", + "rc4_skip", + "aria_sb1", + "aria_sb2", + "aria_is1", + "aria_is2", + "aria_sl", + "aria_a", ], "radio_control": ["fsm_input", "fsm_sconfreq"], "pbuf": [ "pbuf_", - "ppSearchTxframe", - "ppMapWaitTxq", - "ppFillAMPDUBar", - "ppCheckTxConnTrafficIdle", ], - "ppTask": ["ppCalTkipMic"], "event_group": ["xEventGroup"], "ringbuffer": ["xRingbuffer", "prvSend", "prvReceive", "prvCopy"], - "provisioning": ["prov_"], + "provisioning": ["prov_", "prov_stop_and_notify"], "scan": ["gScanStruct"], "port": ["xPort"], - "elf_loader": ["elf_add", "process_image", "read_encoded"], + "elf_loader": [ + "elf_add", + "elf_add_note", + "elf_add_segment", + "process_image", + "read_encoded", + "read_encoded_value", + "read_encoded_value_with_base", + "process_image_header", + ], "socket_api": [ "sockets", "netconn_", @@ -315,11 +402,17 @@ SYMBOL_PATTERNS = { "socket_ipv4_multicast", "socket_ipv6_multicast", ], - "igmp": ["igmp_"], + "igmp": ["igmp_", "igmp_send", "igmp_input"], "icmp6": ["icmp6_"], "arp": ["arp_table"], - "ampdu": ["ampdu_", "rcAmpdu", "trc_onAmpduOp"], - "ieee802_11": ["ieee802_11_"], + "ampdu": [ + "ampdu_", + "rcAmpdu", + "trc_onAmpduOp", + "rcAmpduLowerRate", + "ampdu_dispatch_upto", + ], + "ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"], "rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"], "nan": ["nan_dp_"], "channel_mgmt": ["chm_init", "chm_set_current_channel"], From f3523a96c94c003720db37a31f43ea4ac7d98460 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Jul 2025 08:24:15 -0500 Subject: [PATCH 07/16] wip --- esphome/analyze_memory.py | 51 +++++++++++++-------------------------- 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py index bd51664271..2de90bd775 100644 --- a/esphome/analyze_memory.py +++ b/esphome/analyze_memory.py @@ -39,6 +39,15 @@ SYMBOL_PATTERNS = { "prvReceive", "prvCopy", "xPort", + "ulTaskGenericNotifyTake", + "prvIdleTask", + "prvInitialiseNewTask", + "prvIsYieldRequiredSMP", + "prvGetItemByteBuf", + "prvInitializeNewRingbuffer", + "prvAcquireItemNoSplit", + "prvNotifyQueueSetContainer", + "ucStaticTimerQueueStorage", ], "xtensa": ["xt_", "_xt_", "xPortEnterCriticalTimeout"], "heap": ["heap_", "multi_heap"], @@ -414,10 +423,10 @@ SYMBOL_PATTERNS = { ], "ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"], "rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"], - "nan": ["nan_dp_"], + "nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"], "channel_mgmt": ["chm_init", "chm_set_current_channel"], - "trace": ["trc_init"], - "country_code": ["country_info"], + "trace": ["trc_init", "trc_onAmpduOp"], + "country_code": ["country_info", "country_info_24ghz"], "multicore": ["do_multicore_settings"], "Update_lib": ["Update"], "stdio": [ @@ -431,15 +440,18 @@ SYMBOL_PATTERNS = { "strncpy_ops": ["strncpy"], "math_internal": ["__mdiff", "__lshift", "__mprec_tens", "quorem"], "character_class": ["__chclass"], - "camellia": ["camellia_"], + "camellia": ["camellia_", "camellia_feistel"], "crypto_tables": ["FSb", "FSb2", "FSb3", "FSb4"], "event_buffer": ["g_eb_list_desc", "eb_space"], - "base_node": ["base_node_"], + "base_node": ["base_node_", "base_node_add_handler"], "file_descriptor": ["s_fd_table"], "tx_delay": ["tx_delay_cfg"], "deinit": ["deinit_functions"], "lcp_echo": ["LcpEchoCheck"], "raw_api": ["raw_bind", "raw_connect"], + "checksum": ["process_checksum"], + "entry_management": ["add_entry"], + "esp_ota": ["esp_ota", "ota_"], } # Demangled patterns: patterns found in demangled C++ names @@ -759,29 +771,6 @@ class MemoryAnalyzer: # Special cases that need more complex logic - # ROM functions starting with r_ or rom_ - if symbol_name.startswith("r_") or symbol_name.startswith("rom_"): - return "rom_functions" - - # Math functions with short names - if len(symbol_name) < 20 and symbol_name in [ - "sin", - "cos", - "tan", - "sqrt", - "pow", - "exp", - "log", - "atan", - "asin", - "acos", - "floor", - "ceil", - "fabs", - "round", - ]: - return "math_lib" - # Check if spi_flash vs spi_driver if "spi_" in symbol_name or "SPI" in symbol_name: if "spi_flash" in symbol_name: @@ -789,12 +778,6 @@ class MemoryAnalyzer: else: return "spi_driver" - # ESP OTA framework (exclude esphome OTA) - if ( - "esp_ota" in symbol_name or "ota_" in symbol_name - ) and "esphome" not in demangled: - return "esp_ota" - # libc special printf variants if symbol_name.startswith("_") and symbol_name[1:].replace("_r", "").replace( "v", "" From 6f05ee74271457b65032ff742adb257f324ffd30 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Jul 2025 08:25:41 -0500 Subject: [PATCH 08/16] wip --- esphome/analyze_memory.py | 110 ++++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 51 deletions(-) diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py index 2de90bd775..aaf61e2d20 100644 --- a/esphome/analyze_memory.py +++ b/esphome/analyze_memory.py @@ -593,7 +593,7 @@ class MemoryAnalyzer: self._categorize_symbols() return dict(self.components) - def _parse_sections(self): + def _parse_sections(self) -> None: """Parse section headers from ELF file.""" try: result = subprocess.run( @@ -637,8 +637,59 @@ class MemoryAnalyzer: _LOGGER.error(f"Failed to parse sections: {e}") raise - def _parse_symbols(self): + def _parse_symbols(self) -> None: """Parse symbols from ELF file.""" + # Section mapping - centralizes the logic + SECTION_MAPPING = { + ".text": [".text", ".iram"], + ".rodata": [".rodata"], + ".data": [".data", ".dram"], + ".bss": [".bss"], + } + + def map_section_name(raw_section: str) -> str | None: + """Map raw section name to standard section.""" + for standard_section, patterns in SECTION_MAPPING.items(): + if any(pattern in raw_section for pattern in patterns): + return standard_section + return None + + def parse_symbol_line(line: str) -> tuple[str, str, int] | None: + """Parse a single symbol line from objdump output. + + Returns (section, name, size) or None if not a valid symbol. + Format: address l/g w/d F/O section size name + Example: 40084870 l F .iram0.text 00000000 _xt_user_exc + """ + parts = line.split() + if len(parts) < 5: + return None + + try: + # Validate address + int(parts[0], 16) + except ValueError: + return None + + # Look for F (function) or O (object) flag + if "F" not in parts and "O" not in parts: + return None + + # Find section, size, and name + for i, part in enumerate(parts): + if part.startswith("."): + section = map_section_name(part) + if section and i + 1 < len(parts): + try: + size = int(parts[i + 1], 16) + if i + 2 < len(parts) and size > 0: + name = " ".join(parts[i + 2 :]) + return (section, name, size) + except ValueError: + pass + break + return None + try: result = subprocess.run( [self.objdump_path, "-t", str(self.elf_path)], @@ -648,60 +699,17 @@ class MemoryAnalyzer: ) for line in result.stdout.splitlines(): - # Parse symbol table entries - # Format: address l/g w/d F/O section size name - # Example: 40084870 l F .iram0.text 00000000 _xt_user_exc - parts = line.split() - if len(parts) >= 5: - try: - # Check if this looks like a symbol entry - int(parts[0], 16) - - # Look for F (function) or O (object) flag - if "F" in parts or "O" in parts: - # Find the section name - section = None - size = 0 - name = None - - for i, part in enumerate(parts): - if part.startswith("."): - # Map section names - if ".text" in part or ".iram" in part: - section = ".text" - elif ".rodata" in part: - section = ".rodata" - elif ".data" in part or ".dram" in part: - section = ".data" - elif ".bss" in part: - section = ".bss" - - if section and i + 1 < len(parts): - try: - # Next field should be size - size = int(parts[i + 1], 16) - # Rest is the symbol name - if i + 2 < len(parts): - name = " ".join(parts[i + 2 :]) - except ValueError: - pass - break - - if section and name and size > 0: - if section in self.sections: - self.sections[section].symbols.append( - (name, size, "") - ) - - except ValueError: - # Not a valid address, skip - continue + symbol_info = parse_symbol_line(line) + if symbol_info: + section, name, size = symbol_info + if section in self.sections: + self.sections[section].symbols.append((name, size, "")) except subprocess.CalledProcessError as e: _LOGGER.error(f"Failed to parse symbols: {e}") raise - def _categorize_symbols(self): + def _categorize_symbols(self) -> None: """Categorize symbols by component.""" # First, collect all unique symbol names for batch demangling all_symbols = set() From bc9c4a8b8e8842bc2dae37987c5784725f3fead5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Jul 2025 08:35:42 -0500 Subject: [PATCH 09/16] wip --- esphome/analyze_memory.py | 212 +++++++++++++++++++++++++++++++++----- 1 file changed, 184 insertions(+), 28 deletions(-) diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py index aaf61e2d20..4b8a5d15ea 100644 --- a/esphome/analyze_memory.py +++ b/esphome/analyze_memory.py @@ -48,6 +48,9 @@ SYMBOL_PATTERNS = { "prvAcquireItemNoSplit", "prvNotifyQueueSetContainer", "ucStaticTimerQueueStorage", + "eTaskGetState", + "main_task", + "do_system_init_fn", ], "xtensa": ["xt_", "_xt_", "xPortEnterCriticalTimeout"], "heap": ["heap_", "multi_heap"], @@ -78,6 +81,11 @@ SYMBOL_PATTERNS = { "raw_bind", "icmp_send_response", "sockets", + "icmp_dest_unreach", + "inet_chksum_pseudo", + "alloc_socket", + "done_socket", + "set_global_fd_sets", ], "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], "wifi_stack": [ @@ -113,7 +121,41 @@ SYMBOL_PATTERNS = { "bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"], "wifi_bt_coex": ["coex"], "bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"], - "bluedroid_bt": ["bluedroid", "btc_", "bta_", "btm_", "btu_"], + "bluedroid_bt": [ + "bluedroid", + "btc_", + "bta_", + "btm_", + "btu_", + "BTM_", + "GATT", + "L2CA_", + "smp_", + "gatts_", + "attp_", + "l2cu_", + "l2cb", + "smp_cb", + "BTA_GATTC_", + "SMP_", + "BTU_", + "BTA_Dm", + "GAP_Ble", + "BT_tx_if", + "host_recv_pkt_cb", + "saved_local_oob_data", + "string_to_bdaddr", + "string_is_bdaddr", + "CalConnectParamTimeout", + "transmit_fragment", + "transmit_data", + "event_command_ready", + "read_command_complete_header", + "parse_read_local_extended_features_response", + "parse_read_local_version_info_response", + "should_request_high", + "btdm_wakeup_request", + ], "crypto_math": [ "ecp_", "bignum_", @@ -127,6 +169,17 @@ SYMBOL_PATTERNS = { "__mdiff", "__lshift", "__mprec_tens", + "ECC_", + "multiprecision_", + "mix_sub_columns", + "sbox", + "gfm2_sbox", + "gfm3_sbox", + "curve_p256", + "curve", + "p_256_init_curve", + "shift_sub_rows", + "rshift", ], "hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"], "libc": [ @@ -149,6 +202,30 @@ SYMBOL_PATTERNS = { "_reclaim_reent", "_open_r", "strncpy", + "_strtod_l", + "__gethex", + "__hexnan", + "_setenv_r", + "_tzset_unlocked_r", + "__tzcalc_limits", + "select", + "scalbnf", + "strtof", + "strtof_l", + "__d2b", + "__b2d", + "__s2b", + "_Balloc", + "__multadd", + "__lo0bits", + "__atexit0", + "__smakebuf_r", + "__swhatbuf_r", + "_sungetc_r", + "_close_r", + "_link_r", + "_unsetenv_r", + "_rename_r", ], "string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"], "memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"], @@ -205,6 +282,22 @@ SYMBOL_PATTERNS = { "txiq_cal_init", "pwdet_sar", "pwdet_sar2_init", + "ram_iq_est_enable", + "ram_rfpll_set_freq", + "ant_wifirx_cfg", + "ant_btrx_cfg", + "force_txrxoff", + "force_txrx_off", + "tx_paon_set", + "opt_11b_resart", + "rfpll_1p2_opt", + "ram_dc_iq_est", + "ram_start_tx_tone", + "ram_en_pwdet", + "ram_cbw2040_cfg", + "rxdc_est_min", + "i2cmst_reg_init", + "temprature_sens_read", ], "wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"], "wifi_lmac": ["lmac"], @@ -260,6 +353,13 @@ SYMBOL_PATTERNS = { "phy_rtl", "phy_dp83", "phy_ksz", + "lan87xx_", + "rtl8201_", + "ip101_", + "ksz80xx_", + "jl1101_", + "dp83848_", + "eth_on_state_changed", ], "threading": ["pthread_", "thread_", "_task_"], "pthread": ["pthread"], @@ -451,7 +551,59 @@ SYMBOL_PATTERNS = { "raw_api": ["raw_bind", "raw_connect"], "checksum": ["process_checksum"], "entry_management": ["add_entry"], - "esp_ota": ["esp_ota", "ota_"], + "esp_ota": ["esp_ota", "ota_", "read_otadata"], + "http_server": [ + "httpd_", + "parse_url_char", + "cb_headers_complete", + "delete_entry", + "validate_structure", + "config_save", + "config_new", + "verify_url", + "cb_url", + ], + "misc_system": [ + "alarm_cbs", + "start_up", + "tokens", + "unhex", + "osi_funcs_ro", + "enum_function", + "fragment_and_dispatch", + "alarm_set", + "osi_alarm_new", + "config_set_string", + "config_update_newest_section", + "config_remove_key", + "method_strings", + "interop_match", + "interop_database", + "__state_table", + "__action_table", + "s_stub_table", + "s_context", + "s_mmu_ctx", + "s_get_bus_mask", + "hli_queue_put", + "list_remove", + "list_delete", + "lock_acquire_generic", + "is_vect_desc_usable", + "io_mode_str", + "__c$20233", + ], + "bluetooth_ll": [ + "lld_pdu_", + "ld_acl_", + "lld_stop_ind_handler", + "lld_evt_winsize_change", + "config_lld_evt_funcs_reset", + "config_lld_funcs_reset", + "config_llm_funcs_reset", + "llm_set_long_adv_data", + "lld_retry_tx_prog", + ], } # Demangled patterns: patterns found in demangled C++ names @@ -849,59 +1001,63 @@ class MemoryAnalyzer: # Build report lines = [] - lines.append("=" * 108) - lines.append(" Component Memory Analysis") - lines.append("=" * 108) + + # Calculate the exact table width + table_width = 29 + 3 + 13 + 3 + 13 + 3 + 11 + 3 + 11 + 3 + 14 + 3 + 11 + + lines.append("=" * table_width) + lines.append("Component Memory Analysis".center(table_width)) + lines.append("=" * table_width) lines.append("") - # Main table + # Main table - fixed column widths lines.append( - f"{'Component':<28} | {'Flash (text)':>12} | {'Flash (data)':>12} | {'RAM (data)':>10} | {'RAM (bss)':>10} | {'Total Flash':>12} | {'Total RAM':>10}" + f"{'Component':<29} | {'Flash (text)':>13} | {'Flash (data)':>13} | {'RAM (data)':>11} | {'RAM (bss)':>11} | {'Total Flash':>14} | {'Total RAM':>11}" ) lines.append( - "-" * 28 + "-" * 29 + "-+-" - + "-" * 12 + + "-" * 13 + "-+-" - + "-" * 12 + + "-" * 13 + "-+-" - + "-" * 10 + + "-" * 11 + "-+-" - + "-" * 10 + + "-" * 11 + "-+-" - + "-" * 12 + + "-" * 14 + "-+-" - + "-" * 10 + + "-" * 11 ) for name, mem in components: if mem.flash_total > 0 or mem.ram_total > 0: flash_rodata = mem.rodata_size + mem.data_size lines.append( - f"{name:<28} | {mem.text_size:>11,} B | {flash_rodata:>11,} B | " - f"{mem.data_size:>9,} B | {mem.bss_size:>9,} B | " - f"{mem.flash_total:>11,} B | {mem.ram_total:>9,} B" + f"{name:<29} | {mem.text_size:>12,} B | {flash_rodata:>12,} B | " + f"{mem.data_size:>10,} B | {mem.bss_size:>10,} B | " + f"{mem.flash_total:>13,} B | {mem.ram_total:>10,} B" ) lines.append( - "-" * 28 + "-" * 29 + "-+-" - + "-" * 12 + + "-" * 13 + "-+-" - + "-" * 12 + + "-" * 13 + "-+-" - + "-" * 10 + + "-" * 11 + "-+-" - + "-" * 10 + + "-" * 11 + "-+-" - + "-" * 12 + + "-" * 14 + "-+-" - + "-" * 10 + + "-" * 11 ) lines.append( - f"{'TOTAL':<28} | {' ':>11} | {' ':>11} | " - f"{' ':>9} | {' ':>9} | " - f"{total_flash:>11,} B | {total_ram:>9,} B" + f"{'TOTAL':<29} | {' ':>12} | {' ':>12} | " + f"{' ':>10} | {' ':>10} | " + f"{total_flash:>13,} B | {total_ram:>10,} B" ) # Top consumers @@ -930,7 +1086,7 @@ class MemoryAnalyzer: lines.append( "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included." ) - lines.append("=" * 108) + lines.append("=" * table_width) return "\n".join(lines) From 5004f44f65b6e802a8b993489343ac00a64de6e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Jul 2025 08:42:17 -0500 Subject: [PATCH 10/16] wip --- esphome/analyze_memory.py | 199 +++++++++++++++++++++++++++++++++----- 1 file changed, 175 insertions(+), 24 deletions(-) diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py index 4b8a5d15ea..e12d30482c 100644 --- a/esphome/analyze_memory.py +++ b/esphome/analyze_memory.py @@ -51,11 +51,36 @@ SYMBOL_PATTERNS = { "eTaskGetState", "main_task", "do_system_init_fn", + "xSemaphoreCreateGenericWithCaps", + "vListInsert", + "uxListRemove", + "vRingbufferReturnItem", + "vRingbufferReturnItemFromISR", + "prvCheckItemFitsByteBuffer", + "prvGetCurMaxSizeAllowSplit", + "tick_hook", + "sys_sem_new", + "sys_arch_mbox_fetch", + "sys_arch_sem_wait", + "prvDeleteTCB", + "vQueueDeleteWithCaps", + "vRingbufferDeleteWithCaps", + "vSemaphoreDeleteWithCaps", + "prvCheckItemAvail", + "prvCheckTaskCanBeScheduledSMP", + "prvGetCurMaxSizeNoSplit", + "prvResetNextTaskUnblockTime", + "prvReturnItemByteBuf", + "vApplicationStackOverflowHook", + "vApplicationGetIdleTaskMemory", + "sys_init", + "sys_mbox_new", + "sys_arch_mbox_tryfetch", ], "xtensa": ["xt_", "_xt_", "xPortEnterCriticalTimeout"], "heap": ["heap_", "multi_heap"], "spi_flash": ["spi_flash"], - "rtc": ["rtc_"], + "rtc": ["rtc_", "rtcio_ll_"], "gpio_driver": ["gpio_", "pins"], "uart_driver": ["uart", "_uart", "UART"], "timer": ["timer_", "esp_timer"], @@ -86,6 +111,11 @@ SYMBOL_PATTERNS = { "alloc_socket", "done_socket", "set_global_fd_sets", + "inet_chksum_pbuf", + "tryget_socket_unconn_locked", + "tryget_socket_unconn", + "cs_create_ctrl_sock", + "netbuf_alloc", ], "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], "wifi_stack": [ @@ -155,6 +185,16 @@ SYMBOL_PATTERNS = { "parse_read_local_version_info_response", "should_request_high", "btdm_wakeup_request", + "BTA_SetAttributeValue", + "BTA_EnableBluetooth", + "transmit_command_futured", + "transmit_command", + "get_waiting_command", + "make_command", + "transmit_downward", + "host_recv_adv_packet", + "copy_extra_byte_in_db", + "parse_read_local_supported_commands_response", ], "crypto_math": [ "ecp_", @@ -226,6 +266,45 @@ SYMBOL_PATTERNS = { "_link_r", "_unsetenv_r", "_rename_r", + "__month_lengths", + "tzinfo", + "__ratio", + "__hi0bits", + "__ulp", + "__any_on", + "__copybits", + "L_shift", + "_fcntl_r", + "_lseek_r", + "_read_r", + "_write_r", + "_unlink_r", + "_fstat_r", + "access", + "fsync", + "tcsetattr", + "tcgetattr", + "tcflush", + "tcdrain", + "__ssrefill_r", + "_stat_r", + "__hexdig_fun", + "__mcmp", + "_fwalk_sglue", + "__fpclassifyf", + "_setlocale_r", + "_mbrtowc_r", + "fcntl", + "__match", + "_lock_close", + "__c$", + "__func__$", + "__FUNCTION__$", + "DAYS_IN_MONTH", + "_DAYS_BEFORE_MONTH", + "CSWTCH$", + "dst$", + "sulp", ], "string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"], "memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"], @@ -298,6 +377,12 @@ SYMBOL_PATTERNS = { "rxdc_est_min", "i2cmst_reg_init", "temprature_sens_read", + "ram_restart_cal", + "ram_write_gain_mem", + "ram_wait_rfpll_cal_end", + "txcal_debuge_mode", + "ant_wifitx_cfg", + "reg_init_begin", ], "wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"], "wifi_lmac": ["lmac"], @@ -592,6 +677,40 @@ SYMBOL_PATTERNS = { "is_vect_desc_usable", "io_mode_str", "__c$20233", + "interface", + "read_id_core", + "subscribe_idle", + "unsubscribe_idle", + "s_clkout_handle", + "lock_release_generic", + "config_set_int", + "config_get_int", + "config_get_string", + "config_has_key", + "config_remove_section", + "osi_alarm_init", + "osi_alarm_deinit", + "fixed_queue_enqueue", + "fixed_queue_dequeue", + "fixed_queue_new", + "fixed_pkt_queue_enqueue", + "fixed_pkt_queue_new", + "list_append", + "list_prepend", + "list_insert_after", + "list_contains", + "list_get_node", + "hash_function_blob", + "cb_no_body", + "cb_on_body", + "profile_tab", + "get_arg", + "trim", + "buf$", + "process_appended_hash_and_sig$constprop$0", + "uuidType", + "allocate_svc_db_buf", + "_hostname_is_ours", ], "bluetooth_ll": [ "lld_pdu_", @@ -603,6 +722,14 @@ SYMBOL_PATTERNS = { "config_llm_funcs_reset", "llm_set_long_adv_data", "lld_retry_tx_prog", + "llc_link_sup_to_ind_handler", + "config_llc_funcs_reset", + "lld_evt_rxwin_compute", + "config_btdm_funcs_reset", + "config_ea_funcs_reset", + "llc_defalut_state_tab_reset", + "config_rwip_funcs_reset", + "ke_lmp_rx_flooding_detect", ], } @@ -655,7 +782,7 @@ DEMANGLED_PATTERNS = { "libgcc": ["libgcc"], "esp_system": ["esp_", "ESP"], "arduino": ["arduino"], - "nvs": ["nvs_"], + "nvs": ["nvs_", "_ZTVN3nvs", "nvs::"], "filesystem": ["spiffs", "vfs"], "libc": ["newlib"], } @@ -1002,8 +1129,32 @@ class MemoryAnalyzer: # Build report lines = [] + # Column width constants + COL_COMPONENT = 29 + COL_FLASH_TEXT = 14 + COL_FLASH_DATA = 14 + COL_RAM_DATA = 12 + COL_RAM_BSS = 12 + COL_TOTAL_FLASH = 15 + COL_TOTAL_RAM = 12 + COL_SEPARATOR = 3 # " | " + # Calculate the exact table width - table_width = 29 + 3 + 13 + 3 + 13 + 3 + 11 + 3 + 11 + 3 + 14 + 3 + 11 + table_width = ( + COL_COMPONENT + + COL_SEPARATOR + + COL_FLASH_TEXT + + COL_SEPARATOR + + COL_FLASH_DATA + + COL_SEPARATOR + + COL_RAM_DATA + + COL_SEPARATOR + + COL_RAM_BSS + + COL_SEPARATOR + + COL_TOTAL_FLASH + + COL_SEPARATOR + + COL_TOTAL_RAM + ) lines.append("=" * table_width) lines.append("Component Memory Analysis".center(table_width)) @@ -1012,52 +1163,52 @@ class MemoryAnalyzer: # Main table - fixed column widths lines.append( - f"{'Component':<29} | {'Flash (text)':>13} | {'Flash (data)':>13} | {'RAM (data)':>11} | {'RAM (bss)':>11} | {'Total Flash':>14} | {'Total RAM':>11}" + f"{'Component':<{COL_COMPONENT}} | {'Flash (text)':>{COL_FLASH_TEXT}} | {'Flash (data)':>{COL_FLASH_DATA}} | {'RAM (data)':>{COL_RAM_DATA}} | {'RAM (bss)':>{COL_RAM_BSS}} | {'Total Flash':>{COL_TOTAL_FLASH}} | {'Total RAM':>{COL_TOTAL_RAM}}" ) lines.append( - "-" * 29 + "-" * COL_COMPONENT + "-+-" - + "-" * 13 + + "-" * COL_FLASH_TEXT + "-+-" - + "-" * 13 + + "-" * COL_FLASH_DATA + "-+-" - + "-" * 11 + + "-" * COL_RAM_DATA + "-+-" - + "-" * 11 + + "-" * COL_RAM_BSS + "-+-" - + "-" * 14 + + "-" * COL_TOTAL_FLASH + "-+-" - + "-" * 11 + + "-" * COL_TOTAL_RAM ) for name, mem in components: if mem.flash_total > 0 or mem.ram_total > 0: flash_rodata = mem.rodata_size + mem.data_size lines.append( - f"{name:<29} | {mem.text_size:>12,} B | {flash_rodata:>12,} B | " - f"{mem.data_size:>10,} B | {mem.bss_size:>10,} B | " - f"{mem.flash_total:>13,} B | {mem.ram_total:>10,} B" + f"{name:<{COL_COMPONENT}} | {mem.text_size:>{COL_FLASH_TEXT - 2},} B | {flash_rodata:>{COL_FLASH_DATA - 2},} B | " + f"{mem.data_size:>{COL_RAM_DATA - 2},} B | {mem.bss_size:>{COL_RAM_BSS - 2},} B | " + f"{mem.flash_total:>{COL_TOTAL_FLASH - 2},} B | {mem.ram_total:>{COL_TOTAL_RAM - 2},} B" ) lines.append( - "-" * 29 + "-" * COL_COMPONENT + "-+-" - + "-" * 13 + + "-" * COL_FLASH_TEXT + "-+-" - + "-" * 13 + + "-" * COL_FLASH_DATA + "-+-" - + "-" * 11 + + "-" * COL_RAM_DATA + "-+-" - + "-" * 11 + + "-" * COL_RAM_BSS + "-+-" - + "-" * 14 + + "-" * COL_TOTAL_FLASH + "-+-" - + "-" * 11 + + "-" * COL_TOTAL_RAM ) lines.append( - f"{'TOTAL':<29} | {' ':>12} | {' ':>12} | " - f"{' ':>10} | {' ':>10} | " - f"{total_flash:>13,} B | {total_ram:>10,} B" + f"{'TOTAL':<{COL_COMPONENT}} | {' ':>{COL_FLASH_TEXT}} | {' ':>{COL_FLASH_DATA}} | " + f"{' ':>{COL_RAM_DATA}} | {' ':>{COL_RAM_BSS}} | " + f"{total_flash:>{COL_TOTAL_FLASH - 2},} B | {total_ram:>{COL_TOTAL_RAM - 2},} B" ) # Top consumers From dd49d832c4c923c5ab505822ceaff42cd3c80f99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Jul 2025 08:56:13 -0500 Subject: [PATCH 11/16] wip --- esphome/analyze_memory.py | 199 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py index e12d30482c..cff1209624 100644 --- a/esphome/analyze_memory.py +++ b/esphome/analyze_memory.py @@ -711,6 +711,82 @@ SYMBOL_PATTERNS = { "uuidType", "allocate_svc_db_buf", "_hostname_is_ours", + "s_hli_handlers", + "tick_cb", + "idle_cb", + "input", + "entry_find", + "section_find", + "find_bucket_entry_", + "config_has_section", + "hli_queue_create", + "hli_queue_get", + "hli_c_handler", + "future_ready", + "future_await", + "future_new", + "pkt_queue_enqueue", + "pkt_queue_dequeue", + "pkt_queue_cleanup", + "pkt_queue_create", + "pkt_queue_destroy", + "fixed_pkt_queue_dequeue", + "osi_alarm_cancel", + "osi_alarm_is_active", + "osi_sem_take", + "osi_event_create", + "osi_event_bind", + "alarm_cb_handler", + "list_foreach", + "list_back", + "list_front", + "list_clear", + "fixed_queue_try_peek_first", + "translate_path", + "get_idx", + "find_key", + "init", + "end", + "start", + "set_read_value", + "copy_address_list", + "copy_and_key", + "sdk_cfg_opts", + "leftshift_onebit", + "config_section_end", + "config_section_begin", + "find_entry_and_check_all_reset", + "image_validate", + "xPendingReadyList", + "vListInitialise", + "lock_init_generic", + "ant_bttx_cfg", + "ant_dft_cfg", + "cs_send_to_ctrl_sock", + "config_llc_util_funcs_reset", + "make_set_adv_report_flow_control", + "make_set_event_mask", + "raw_new", + "raw_remove", + "BTE_InitStack", + "parse_read_local_supported_features_response", + "__math_invalidf", + "tinytens", + "__mprec_tinytens", + "__mprec_bigtens", + "vRingbufferDelete", + "vRingbufferDeleteWithCaps", + "vRingbufferReturnItem", + "vRingbufferReturnItemFromISR", + "get_acl_data_size_ble", + "get_features_ble", + "get_features_classic", + "get_acl_packet_size_ble", + "get_acl_packet_size_classic", + "supports_extended_inquiry_response", + "supports_rssi_with_inquiry_results", + "supports_interlaced_inquiry_scan", + "supports_reading_remote_extended_features", ], "bluetooth_ll": [ "lld_pdu_", @@ -864,6 +940,9 @@ class MemoryAnalyzer: ) self._demangle_cache: dict[str, str] = {} self._uncategorized_symbols: list[tuple[str, str, int]] = [] + self._esphome_core_symbols: list[ + tuple[str, str, int] + ] = [] # Track core symbols def analyze(self) -> dict[str, ComponentMemory]: """Analyze the ELF file and return component memory usage.""" @@ -1024,6 +1103,11 @@ class MemoryAnalyzer: demangled = self._demangle_symbol(symbol_name) self._uncategorized_symbols.append((symbol_name, demangled, size)) + # Track ESPHome core symbols for detailed analysis + if component == "[esphome]core" and size > 0: + demangled = self._demangle_symbol(symbol_name) + self._esphome_core_symbols.append((symbol_name, demangled, size)) + def _identify_component(self, symbol_name: str) -> str: """Identify which component a symbol belongs to.""" # Demangle C++ names if needed @@ -1116,6 +1200,51 @@ class MemoryAnalyzer: """Get demangled C++ symbol name from cache.""" return self._demangle_cache.get(symbol, symbol) + def _categorize_esphome_core_symbol(self, demangled: str) -> str: + """Categorize ESPHome core symbols into subcategories.""" + # Dictionary of patterns for core subcategories + CORE_SUBCATEGORY_PATTERNS = { + "Component Framework": ["Component"], + "Application Core": ["Application"], + "Scheduler": ["Scheduler"], + "Logging": ["Logger", "log_"], + "Preferences": ["preferences", "Preferences"], + "Synchronization": ["Mutex", "Lock"], + "Helpers": ["Helper"], + "Network Utilities": ["network", "Network"], + "Time Management": ["time", "Time"], + "String Utilities": ["str_", "string"], + "Parsing/Formatting": ["parse_", "format_"], + "Optional Types": ["optional", "Optional"], + "Callbacks": ["Callback", "callback"], + "Color Utilities": ["Color"], + "C++ Operators": ["operator"], + "Global Variables": ["global_", "_GLOBAL"], + "Setup/Loop": ["setup", "loop"], + "System Control": ["reboot", "restart"], + "GPIO Management": ["GPIO", "gpio"], + "Interrupt Handling": ["ISR", "interrupt"], + "Hooks": ["Hook", "hook"], + "Entity Base Classes": ["Entity"], + "Automation Framework": ["automation", "Automation"], + "Automation Components": ["Condition", "Action", "Trigger"], + "Lambda Support": ["lambda"], + } + + # Special patterns that need to be checked separately + if any(pattern in demangled for pattern in ["vtable", "typeinfo", "thunk"]): + return "C++ Runtime (vtables/RTTI)" + + if demangled.startswith("std::"): + return "C++ STL" + + # Check against patterns + for category, patterns in CORE_SUBCATEGORY_PATTERNS.items(): + if any(pattern in demangled for pattern in patterns): + return category + + return "Other Core" + def generate_report(self, detailed: bool = False) -> str: """Generate a formatted memory report.""" components = sorted( @@ -1139,6 +1268,12 @@ class MemoryAnalyzer: COL_TOTAL_RAM = 12 COL_SEPARATOR = 3 # " | " + # Core analysis column widths + COL_CORE_SUBCATEGORY = 30 + COL_CORE_SIZE = 12 + COL_CORE_COUNT = 6 + COL_CORE_PERCENT = 10 + # Calculate the exact table width table_width = ( COL_COMPONENT @@ -1239,6 +1374,70 @@ class MemoryAnalyzer: ) lines.append("=" * table_width) + # Add ESPHome core detailed analysis if there are core symbols + if self._esphome_core_symbols: + lines.append("") + lines.append("=" * table_width) + lines.append("[esphome]core Detailed Analysis".center(table_width)) + lines.append("=" * table_width) + lines.append("") + + # Group core symbols by subcategory + core_subcategories: dict[str, list[tuple[str, str, int]]] = defaultdict( + list + ) + + for symbol, demangled, size in self._esphome_core_symbols: + # Categorize based on demangled name patterns + subcategory = self._categorize_esphome_core_symbol(demangled) + core_subcategories[subcategory].append((symbol, demangled, size)) + + # Sort subcategories by total size + sorted_subcategories = sorted( + [ + (name, symbols, sum(s[2] for s in symbols)) + for name, symbols in core_subcategories.items() + ], + key=lambda x: x[2], + reverse=True, + ) + + lines.append( + f"{'Subcategory':<{COL_CORE_SUBCATEGORY}} | {'Size':>{COL_CORE_SIZE}} | " + f"{'Count':>{COL_CORE_COUNT}} | {'% of Core':>{COL_CORE_PERCENT}}" + ) + lines.append( + "-" * COL_CORE_SUBCATEGORY + + "-+-" + + "-" * COL_CORE_SIZE + + "-+-" + + "-" * COL_CORE_COUNT + + "-+-" + + "-" * COL_CORE_PERCENT + ) + + core_total = sum(size for _, _, size in self._esphome_core_symbols) + + for subcategory, symbols, total_size in sorted_subcategories: + percentage = (total_size / core_total * 100) if core_total > 0 else 0 + lines.append( + f"{subcategory:<{COL_CORE_SUBCATEGORY}} | {total_size:>{COL_CORE_SIZE - 2},} B | " + f"{len(symbols):>{COL_CORE_COUNT}} | {percentage:>{COL_CORE_PERCENT - 1}.1f}%" + ) + + # Top 10 largest core symbols + lines.append("") + lines.append("Top 10 Largest [esphome]core Symbols:") + sorted_core_symbols = sorted( + self._esphome_core_symbols, key=lambda x: x[2], reverse=True + ) + + MAX_SYMBOL_LENGTH = 80 + for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:10]): + lines.append(f"{i + 1}. {demangled[:MAX_SYMBOL_LENGTH]} ({size:,} B)") + + lines.append("=" * table_width) + return "\n".join(lines) def to_json(self) -> str: From ba5bb9dfa747d41aff9d222d7f5c6147e546cad5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Jul 2025 09:02:06 -0500 Subject: [PATCH 12/16] wip --- esphome/analyze_memory.py | 11 +++++++++-- esphome/platformio_api.py | 27 ++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py index cff1209624..0329013cdf 100644 --- a/esphome/analyze_memory.py +++ b/esphome/analyze_memory.py @@ -926,6 +926,7 @@ class MemoryAnalyzer: elf_path: str, objdump_path: str | None = None, readelf_path: str | None = None, + external_components: set[str] | None = None, ): self.elf_path = Path(elf_path) if not self.elf_path.exists(): @@ -933,6 +934,7 @@ class MemoryAnalyzer: self.objdump_path = objdump_path or "objdump" self.readelf_path = readelf_path or "readelf" + self.external_components = external_components or set() self.sections: dict[str, MemorySection] = {} self.components: dict[str, ComponentMemory] = defaultdict( @@ -1120,10 +1122,14 @@ class MemoryAnalyzer: # Strip trailing underscore if present (e.g., switch_ -> switch) component_name = component_name.rstrip("_") - # Check if this is an actual component or core + # Check if this is an actual component in the components directory if component_name in ESPHOME_COMPONENTS: return f"[esphome]{component_name}" + # Check if this is a known external component from the config + elif component_name in self.external_components: + return f"[external]{component_name}" else: + # Everything else in esphome:: namespace is core return "[esphome]core" # Check for esphome core namespace (no component namespace) @@ -1501,9 +1507,10 @@ def analyze_elf( objdump_path: str | None = None, readelf_path: str | None = None, detailed: bool = False, + external_components: set[str] | None = None, ) -> str: """Analyze an ELF file and return a memory report.""" - analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path) + analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path, external_components) analyzer.analyze() return analyzer.generate_report(detailed) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 96e746fa8d..7f474a1fc4 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -389,8 +389,33 @@ def analyze_memory_usage(config: dict[str, Any]) -> None: _LOGGER.warning("ELF file not found at %s or %s", elf_path, alt_path) return + # Extract external components from config + external_components = set() + + # Get the list of built-in ESPHome components + from esphome.analyze_memory import get_esphome_components + + builtin_components = get_esphome_components() + + # Special non-component keys that appear in configs + NON_COMPONENT_KEYS = { + CONF_ESPHOME, + "substitutions", + "packages", + "globals", + "<<", + } + + # Check all top-level keys in config + for key in config: + if key not in builtin_components and key not in NON_COMPONENT_KEYS: + # This is an external component + external_components.add(key) + + _LOGGER.debug("Detected external components: %s", external_components) + # Create analyzer and run analysis - analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path) + analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path, external_components) analyzer.analyze() # Generate and print report From 797d4929abfed4918c5a2813b1e04021283e2444 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Jul 2025 09:05:28 -0500 Subject: [PATCH 13/16] wip --- esphome/analyze_memory.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py index 0329013cdf..dbca665857 100644 --- a/esphome/analyze_memory.py +++ b/esphome/analyze_memory.py @@ -1134,6 +1134,32 @@ class MemoryAnalyzer: # Check for esphome core namespace (no component namespace) if "esphome::" in demangled: + # Check for special component classes that include component name in the class + # For example: esphome::ESPHomeOTAComponent -> ota component + for component_name in ESPHOME_COMPONENTS: + # Check various naming patterns + component_upper = component_name.upper() + component_camel = component_name.replace("_", "").title() + patterns = [ + f"esphome::{component_upper}", # e.g., esphome::OTA + f"esphome::ESPHome{component_upper}", # e.g., esphome::ESPHomeOTA + f"esphome::{component_camel}", # e.g., esphome::Ota + f"esphome::ESPHome{component_camel}", # e.g., esphome::ESPHomeOta + ] + + # Special handling for specific components + if component_name == "ota": + patterns.extend( + [ + "esphome::ESPHomeOTAComponent", + "esphome::OTAComponent", + ] + ) + + if any(pattern in demangled for pattern in patterns): + return f"[esphome]{component_name}" + + # If no component match found, it's core return "[esphome]core" # Check against symbol patterns From b1553807f75af2d740ca5a1ce464e3e687e6f25f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Jul 2025 09:14:26 -0500 Subject: [PATCH 14/16] wip --- esphome/analyze_memory.py | 46 +++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py index dbca665857..6bc0e0bebf 100644 --- a/esphome/analyze_memory.py +++ b/esphome/analyze_memory.py @@ -1115,7 +1115,26 @@ class MemoryAnalyzer: # Demangle C++ names if needed demangled = self._demangle_symbol(symbol_name) - # Check for ESPHome component namespaces first + # Check for special component classes first (before namespace pattern) + # This handles cases like esphome::ESPHomeOTAComponent which should map to ota + if "esphome::" in demangled: + # Check for special component classes that include component name in the class + # For example: esphome::ESPHomeOTAComponent -> ota component + for component_name in ESPHOME_COMPONENTS: + # Check various naming patterns + component_upper = component_name.upper() + component_camel = component_name.replace("_", "").title() + patterns = [ + f"esphome::{component_upper}Component", # e.g., esphome::OTAComponent + f"esphome::ESPHome{component_upper}Component", # e.g., esphome::ESPHomeOTAComponent + f"esphome::{component_camel}Component", # e.g., esphome::OtaComponent + f"esphome::ESPHome{component_camel}Component", # e.g., esphome::ESPHomeOtaComponent + ] + + if any(pattern in demangled for pattern in patterns): + return f"[esphome]{component_name}" + + # Check for ESPHome component namespaces match = ESPHOME_COMPONENT_PATTERN.search(demangled) if match: component_name = match.group(1) @@ -1134,31 +1153,6 @@ class MemoryAnalyzer: # Check for esphome core namespace (no component namespace) if "esphome::" in demangled: - # Check for special component classes that include component name in the class - # For example: esphome::ESPHomeOTAComponent -> ota component - for component_name in ESPHOME_COMPONENTS: - # Check various naming patterns - component_upper = component_name.upper() - component_camel = component_name.replace("_", "").title() - patterns = [ - f"esphome::{component_upper}", # e.g., esphome::OTA - f"esphome::ESPHome{component_upper}", # e.g., esphome::ESPHomeOTA - f"esphome::{component_camel}", # e.g., esphome::Ota - f"esphome::ESPHome{component_camel}", # e.g., esphome::ESPHomeOta - ] - - # Special handling for specific components - if component_name == "ota": - patterns.extend( - [ - "esphome::ESPHomeOTAComponent", - "esphome::OTAComponent", - ] - ) - - if any(pattern in demangled for pattern in patterns): - return f"[esphome]{component_name}" - # If no component match found, it's core return "[esphome]core" From 1a0943c9604b77c643f076ea5722b6da00a07bba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Jul 2025 10:00:20 -1000 Subject: [PATCH 15/16] add component symbols --- esphome/analyze_memory.py | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py index 6bc0e0bebf..95ec0d2214 100644 --- a/esphome/analyze_memory.py +++ b/esphome/analyze_memory.py @@ -945,6 +945,9 @@ class MemoryAnalyzer: self._esphome_core_symbols: list[ tuple[str, str, int] ] = [] # Track core symbols + self._component_symbols: dict[str, list[tuple[str, str, int]]] = defaultdict( + list + ) # Track symbols for all components def analyze(self) -> dict[str, ComponentMemory]: """Analyze the ELF file and return component memory usage.""" @@ -1110,6 +1113,13 @@ class MemoryAnalyzer: demangled = self._demangle_symbol(symbol_name) self._esphome_core_symbols.append((symbol_name, demangled, size)) + # Track all component symbols for detailed analysis + if size > 0: + demangled = self._demangle_symbol(symbol_name) + self._component_symbols[component].append( + (symbol_name, demangled, size) + ) + def _identify_component(self, symbol_name: str) -> str: """Identify which component a symbol belongs to.""" # Demangle C++ names if needed @@ -1464,6 +1474,44 @@ class MemoryAnalyzer: lines.append("=" * table_width) + # Add detailed analysis for top 5 ESPHome components + esphome_components = [ + (name, mem) + for name, mem in components + if name.startswith("[esphome]") and name != "[esphome]core" + ] + top_esphome_components = sorted( + esphome_components, key=lambda x: x[1].flash_total, reverse=True + )[:5] + + if top_esphome_components: + for comp_name, comp_mem in top_esphome_components: + comp_symbols = self._component_symbols.get(comp_name, []) + if comp_symbols: + lines.append("") + lines.append("=" * table_width) + lines.append(f"{comp_name} Detailed Analysis".center(table_width)) + lines.append("=" * table_width) + lines.append("") + + # Sort symbols by size + sorted_symbols = sorted( + comp_symbols, key=lambda x: x[2], reverse=True + ) + + lines.append(f"Total symbols: {len(sorted_symbols)}") + lines.append(f"Total size: {comp_mem.flash_total:,} B") + lines.append("") + lines.append(f"Top 10 Largest {comp_name} Symbols:") + + MAX_SYMBOL_LENGTH = 80 + for i, (symbol, demangled, size) in enumerate(sorted_symbols[:10]): + lines.append( + f"{i + 1}. {demangled[:MAX_SYMBOL_LENGTH]} ({size:,} B)" + ) + + lines.append("=" * table_width) + return "\n".join(lines) def to_json(self) -> str: From 33fb4d5d42dd2471b13c09f945e98d6ccfebc625 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Jul 2025 16:27:40 -1000 Subject: [PATCH 16/16] fixes --- esphome/analyze_memory.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py index 95ec0d2214..18dfbe564e 100644 --- a/esphome/analyze_memory.py +++ b/esphome/analyze_memory.py @@ -1017,10 +1017,10 @@ class MemoryAnalyzer: return standard_section return None - def parse_symbol_line(line: str) -> tuple[str, str, int] | None: + def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None: """Parse a single symbol line from objdump output. - Returns (section, name, size) or None if not a valid symbol. + Returns (section, name, size, address) or None if not a valid symbol. Format: address l/g w/d F/O section size name Example: 40084870 l F .iram0.text 00000000 _xt_user_exc """ @@ -1029,8 +1029,9 @@ class MemoryAnalyzer: return None try: - # Validate address - int(parts[0], 16) + # Validate and extract address + address = parts[0] + int(address, 16) except ValueError: return None @@ -1047,7 +1048,7 @@ class MemoryAnalyzer: size = int(parts[i + 1], 16) if i + 2 < len(parts) and size > 0: name = " ".join(parts[i + 2 :]) - return (section, name, size) + return (section, name, size, address) except ValueError: pass break @@ -1061,12 +1062,17 @@ class MemoryAnalyzer: check=True, ) + # Track seen addresses to avoid duplicates + seen_addresses: set[str] = set() + for line in result.stdout.splitlines(): symbol_info = parse_symbol_line(line) if symbol_info: - section, name, size = symbol_info - if section in self.sections: + section, name, size, address = symbol_info + # Skip duplicate symbols at the same address (e.g., C1/C2 constructors) + if address not in seen_addresses and section in self.sections: self.sections[section].symbols.append((name, size, "")) + seen_addresses.add(address) except subprocess.CalledProcessError as e: _LOGGER.error(f"Failed to parse symbols: {e}") @@ -1468,9 +1474,8 @@ class MemoryAnalyzer: self._esphome_core_symbols, key=lambda x: x[2], reverse=True ) - MAX_SYMBOL_LENGTH = 80 for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:10]): - lines.append(f"{i + 1}. {demangled[:MAX_SYMBOL_LENGTH]} ({size:,} B)") + lines.append(f"{i + 1}. {demangled} ({size:,} B)") lines.append("=" * table_width) @@ -1504,11 +1509,8 @@ class MemoryAnalyzer: lines.append("") lines.append(f"Top 10 Largest {comp_name} Symbols:") - MAX_SYMBOL_LENGTH = 80 for i, (symbol, demangled, size) in enumerate(sorted_symbols[:10]): - lines.append( - f"{i + 1}. {demangled[:MAX_SYMBOL_LENGTH]} ({size:,} B)" - ) + lines.append(f"{i + 1}. {demangled} ({size:,} B)") lines.append("=" * table_width)