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 dataclasses import dataclass, field
from functools import cache
import json
import logging
from pathlib import Path
@@ -16,52 +15,16 @@ from .const import (
SECTION_TO_ATTR,
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__)
# 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
class MemorySection:
"""Represents a memory section with its symbols."""
@@ -146,22 +109,25 @@ class MemoryAnalyzer:
# Parse section headers
for line in result.stdout.splitlines():
# Look for section entries
match = re.match(
if not (
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:
):
continue
section_name = match.group(1)
size_hex = match.group(2)
size = int(size_hex, 16)
# Map to standard section name
mapped_section = map_section_name(section_name)
if mapped_section:
if not mapped_section:
continue
if mapped_section not in self.sections:
self.sections[mapped_section] = MemorySection(
mapped_section
)
self.sections[mapped_section] = MemorySection(mapped_section)
self.sections[mapped_section].total_size += size
except subprocess.CalledProcessError as e:
@@ -201,10 +167,11 @@ class MemoryAnalyzer:
def _categorize_symbols(self) -> None:
"""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)
all_symbols = {
symbol_name
for section in self.sections.values()
for symbol_name, _, _ in section.symbols
}
# Batch demangle all symbols at once
self._batch_demangle_symbols(list(all_symbols))

View File

@@ -231,20 +231,16 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
if components_to_analyze:
for comp_name, comp_mem in components_to_analyze:
comp_symbols = self._component_symbols.get(comp_name, [])
if comp_symbols:
if not (comp_symbols := self._component_symbols.get(comp_name, [])):
continue
lines.append("")
lines.append("=" * self.TABLE_WIDTH)
lines.append(
f"{comp_name} Detailed Analysis".center(self.TABLE_WIDTH)
)
lines.append(f"{comp_name} Detailed Analysis".center(self.TABLE_WIDTH))
lines.append("=" * self.TABLE_WIDTH)
lines.append("")
# Sort symbols by size
sorted_symbols = sorted(
comp_symbols, key=lambda x: x[2], reverse=True
)
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")
@@ -252,9 +248,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
# Show all symbols > 100 bytes for better visibility
large_symbols = [
(sym, dem, size)
for sym, dem, size in sorted_symbols
if size > 100
(sym, dem, size) for sym, dem, size in sorted_symbols if size > 100
]
lines.append(
@@ -284,10 +278,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
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]")
demangled_display = (
demangled[:100] if symbol != demangled else "[not demangled]"
)
lines.append(f"{size:>10,} | {symbol[:60]:<60} | {demangled_display}")
if len(sorted_symbols) > 100:
lines.append(f"\n... and {len(sorted_symbols) - 100} more symbols")

View File

@@ -1,8 +1,52 @@
"""Helper functions for memory analysis."""
from functools import cache
from pathlib import Path
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:
"""Map raw section name to standard section.