1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-22 03:33:52 +01:00
This commit is contained in:
J. Nick Koston
2025-10-17 14:09:08 -10:00
parent 86c12079b4
commit 25fe4a1476
3 changed files with 100 additions and 95 deletions

View File

@@ -2,7 +2,6 @@
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import cache
import json import json
import logging import logging
from pathlib import Path from pathlib import Path
@@ -16,52 +15,16 @@ from .const import (
SECTION_TO_ATTR, SECTION_TO_ATTR,
SYMBOL_PATTERNS, SYMBOL_PATTERNS,
) )
from .helpers import map_section_name, parse_symbol_line from .helpers import (
get_component_class_patterns,
get_esphome_components,
map_section_name,
parse_symbol_line,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Get the list of actual ESPHome components by scanning the components directory
@cache
def get_esphome_components():
"""Get set of actual ESPHome components from the components directory."""
# Find the components directory relative to this file
# Go up two levels from analyze_memory/__init__.py to esphome/
current_dir = Path(__file__).parent.parent
components_dir = current_dir / "components"
if not components_dir.exists() or not components_dir.is_dir():
return frozenset()
return frozenset(
item.name
for item in components_dir.iterdir()
if item.is_dir()
and not item.name.startswith(".")
and not item.name.startswith("__")
)
@cache
def get_component_class_patterns(component_name: str) -> list[str]:
"""Generate component class name patterns for symbol matching.
Args:
component_name: The component name (e.g., "ota", "wifi", "api")
Returns:
List of pattern strings to match against demangled symbols
"""
component_upper = component_name.upper()
component_camel = component_name.replace("_", "").title()
return [
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
]
@dataclass @dataclass
class MemorySection: class MemorySection:
"""Represents a memory section with its symbols.""" """Represents a memory section with its symbols."""
@@ -146,23 +109,26 @@ class MemoryAnalyzer:
# Parse section headers # Parse section headers
for line in result.stdout.splitlines(): for line in result.stdout.splitlines():
# Look for section entries # Look for section entries
match = re.match( if not (
r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)", match := re.match(
line, 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) continue
size = int(size_hex, 16)
# Map to standard section name section_name = match.group(1)
mapped_section = map_section_name(section_name) size_hex = match.group(2)
if mapped_section: size = int(size_hex, 16)
if mapped_section not in self.sections:
self.sections[mapped_section] = MemorySection( # Map to standard section name
mapped_section mapped_section = map_section_name(section_name)
) if not mapped_section:
self.sections[mapped_section].total_size += size continue
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: except subprocess.CalledProcessError as e:
_LOGGER.error("Failed to parse sections: %s", e) _LOGGER.error("Failed to parse sections: %s", e)
@@ -201,10 +167,11 @@ class MemoryAnalyzer:
def _categorize_symbols(self) -> None: def _categorize_symbols(self) -> None:
"""Categorize symbols by component.""" """Categorize symbols by component."""
# First, collect all unique symbol names for batch demangling # First, collect all unique symbol names for batch demangling
all_symbols = set() all_symbols = {
for section in self.sections.values(): symbol_name
for symbol_name, _, _ in section.symbols: for section in self.sections.values()
all_symbols.add(symbol_name) for symbol_name, _, _ in section.symbols
}
# Batch demangle all symbols at once # Batch demangle all symbols at once
self._batch_demangle_symbols(list(all_symbols)) self._batch_demangle_symbols(list(all_symbols))

View File

@@ -231,39 +231,33 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
if components_to_analyze: if components_to_analyze:
for comp_name, comp_mem in components_to_analyze: for comp_name, comp_mem in components_to_analyze:
comp_symbols = self._component_symbols.get(comp_name, []) if not (comp_symbols := self._component_symbols.get(comp_name, [])):
if comp_symbols: continue
lines.append("") lines.append("")
lines.append("=" * self.TABLE_WIDTH) lines.append("=" * self.TABLE_WIDTH)
lines.append( lines.append(f"{comp_name} Detailed Analysis".center(self.TABLE_WIDTH))
f"{comp_name} Detailed Analysis".center(self.TABLE_WIDTH) lines.append("=" * self.TABLE_WIDTH)
) lines.append("")
lines.append("=" * self.TABLE_WIDTH)
lines.append("")
# Sort symbols by size # Sort symbols by size
sorted_symbols = sorted( sorted_symbols = sorted(comp_symbols, key=lambda x: x[2], reverse=True)
comp_symbols, key=lambda x: x[2], reverse=True
)
lines.append(f"Total symbols: {len(sorted_symbols)}") lines.append(f"Total symbols: {len(sorted_symbols)}")
lines.append(f"Total size: {comp_mem.flash_total:,} B") lines.append(f"Total size: {comp_mem.flash_total:,} B")
lines.append("") lines.append("")
# Show all symbols > 100 bytes for better visibility # Show all symbols > 100 bytes for better visibility
large_symbols = [ large_symbols = [
(sym, dem, size) (sym, dem, size) for sym, dem, size in sorted_symbols if size > 100
for sym, dem, size in sorted_symbols ]
if size > 100
]
lines.append( lines.append(
f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):" f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):"
) )
for i, (symbol, demangled, size) in enumerate(large_symbols): for i, (symbol, demangled, size) in enumerate(large_symbols):
lines.append(f"{i + 1}. {demangled} ({size:,} B)") lines.append(f"{i + 1}. {demangled} ({size:,} B)")
lines.append("=" * self.TABLE_WIDTH) lines.append("=" * self.TABLE_WIDTH)
return "\n".join(lines) return "\n".join(lines)
@@ -284,10 +278,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append("-" * 10 + "-+-" + "-" * 60 + "-+-" + "-" * 40) lines.append("-" * 10 + "-+-" + "-" * 60 + "-+-" + "-" * 40)
for symbol, demangled, size in sorted_symbols[:100]: # Top 100 for symbol, demangled, size in sorted_symbols[:100]: # Top 100
if symbol != demangled: demangled_display = (
lines.append(f"{size:>10,} | {symbol[:60]:<60} | {demangled[:100]}") demangled[:100] if symbol != demangled else "[not demangled]"
else: )
lines.append(f"{size:>10,} | {symbol[:60]:<60} | [not demangled]") lines.append(f"{size:>10,} | {symbol[:60]:<60} | {demangled_display}")
if len(sorted_symbols) > 100: if len(sorted_symbols) > 100:
lines.append(f"\n... and {len(sorted_symbols) - 100} more symbols") lines.append(f"\n... and {len(sorted_symbols) - 100} more symbols")

View File

@@ -1,8 +1,52 @@
"""Helper functions for memory analysis.""" """Helper functions for memory analysis."""
from functools import cache
from pathlib import Path
from .const import SECTION_MAPPING from .const import SECTION_MAPPING
# Get the list of actual ESPHome components by scanning the components directory
@cache
def get_esphome_components():
"""Get set of actual ESPHome components from the components directory."""
# Find the components directory relative to this file
# Go up two levels from analyze_memory/helpers.py to esphome/
current_dir = Path(__file__).parent.parent
components_dir = current_dir / "components"
if not components_dir.exists() or not components_dir.is_dir():
return frozenset()
return frozenset(
item.name
for item in components_dir.iterdir()
if item.is_dir()
and not item.name.startswith(".")
and not item.name.startswith("__")
)
@cache
def get_component_class_patterns(component_name: str) -> list[str]:
"""Generate component class name patterns for symbol matching.
Args:
component_name: The component name (e.g., "ota", "wifi", "api")
Returns:
List of pattern strings to match against demangled symbols
"""
component_upper = component_name.upper()
component_camel = component_name.replace("_", "").title()
return [
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
]
def map_section_name(raw_section: str) -> str | None: def map_section_name(raw_section: str) -> str | None:
"""Map raw section name to standard section. """Map raw section name to standard section.