mirror of
https://github.com/esphome/esphome.git
synced 2025-08-06 18:37:47 +00:00
analyze_memory
This commit is contained in:
parent
fae96e279c
commit
b8a75bc925
@ -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",
|
||||
|
714
esphome/analyze_memory.py
Normal file
714
esphome/analyze_memory.py
Normal file
@ -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 <elf_file>")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
report = analyze_elf(sys.argv[1])
|
||||
print(report)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user