Files
klubhaus-doorbell/libraries/FastLED/ci/util/esp32_symbol_analysis.py
2026-02-12 00:45:31 -08:00

381 lines
12 KiB
Python

#!/usr/bin/env python3
# pyright: reportUnknownMemberType=false, reportMissingParameterType=false, reportUnknownLambdaType=false, reportArgumentType=false
"""
ESP32 Symbol Analysis Tool
Analyzes the ESP32 ELF file to identify symbols that can be eliminated for binary size reduction.
"""
import json
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Tuple
@dataclass
class SymbolInfo:
"""Represents a symbol in the ESP32 binary"""
address: str
size: int
type: str
name: str
demangled_name: str
source: str # STRICT: NO defaults - all callers must provide explicit source
def run_command(cmd: str) -> str:
"""Run a command and return stdout"""
try:
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True, check=True
)
return result.stdout
except subprocess.CalledProcessError as e:
print(f"Error running command: {cmd}")
print(f"Error: {e.stderr}")
return ""
def demangle_symbol(mangled_name: str, cppfilt_path: str) -> str:
"""Demangle a C++ symbol using c++filt"""
try:
cmd = f'echo "{mangled_name}" | "{cppfilt_path}"'
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True, check=True
)
demangled = result.stdout.strip()
# If demangling failed, c++filt returns the original name
return demangled if demangled != mangled_name else mangled_name
except Exception as e:
print(f"Error demangling symbol: {mangled_name}")
print(f"Error: {e}")
return mangled_name
def analyze_symbols(
elf_file: str, nm_path: str, cppfilt_path: str
) -> Tuple[List[SymbolInfo], List[SymbolInfo], List[SymbolInfo]]:
"""Analyze symbols in ELF file using nm with C++ demangling"""
print("Analyzing symbols...")
# Get all symbols with sizes
cmd = f'"{nm_path}" --print-size --size-sort --radix=d "{elf_file}"'
output = run_command(cmd)
symbols: List[SymbolInfo] = []
fastled_symbols: List[SymbolInfo] = []
large_symbols: List[SymbolInfo] = []
print("Demangling C++ symbols...")
for line in output.strip().split("\n"):
if not line.strip():
continue
parts = line.split()
if len(parts) >= 4:
addr = parts[0]
size = int(parts[1])
symbol_type = parts[2]
mangled_name = " ".join(parts[3:])
# Demangle the symbol name
demangled_name = demangle_symbol(mangled_name, cppfilt_path)
symbol_info = SymbolInfo(
address=addr,
size=size,
type=symbol_type,
name=mangled_name,
demangled_name=demangled_name,
source="esp32_nm",
)
symbols.append(symbol_info)
# Identify FastLED-related symbols using demangled names
search_text = demangled_name.lower()
if any(
keyword in search_text
for keyword in [
"fastled",
"cfastled",
"crgb",
"hsv",
"pixel",
"controller",
"led",
"rmt",
"strip",
"neopixel",
"ws2812",
"apa102",
]
):
fastled_symbols.append(symbol_info)
# Identify large symbols (>100 bytes)
if size > 100:
large_symbols.append(symbol_info)
return symbols, fastled_symbols, large_symbols
def analyze_map_file(map_file: str) -> Dict[str, List[str]]:
"""Analyze the map file to understand module dependencies"""
print("Analyzing map file...")
dependencies: Dict[str, List[str]] = {}
current_archive = None
try:
with open(map_file, "r") as f:
for line in f:
line = line.strip()
# Look for archive member includes
if line.startswith(".pio/build/esp32dev/liba4c/libsrc.a("):
# Extract module name
start = line.find("(") + 1
end = line.find(")")
if start > 0 and end > start:
current_archive = line[start:end]
dependencies[current_archive] = []
elif current_archive and line and not line.startswith(".pio"):
# This line shows what pulled in the module
if "(" in line and ")" in line:
# Extract the symbol that caused the inclusion
symbol_start = line.find("(") + 1
symbol_end = line.find(")")
if symbol_start > 0 and symbol_end > symbol_start:
symbol = line[symbol_start:symbol_end]
dependencies[current_archive].append(symbol)
current_archive = None
except FileNotFoundError:
print(f"Map file not found: {map_file}")
return {}
return dependencies
def generate_report(
symbols: List[SymbolInfo],
fastled_symbols: List[SymbolInfo],
large_symbols: List[SymbolInfo],
dependencies: Dict[str, List[str]],
) -> Dict[str, Any]:
"""Generate a comprehensive report"""
print("\n" + "=" * 80)
print("ESP32 FASTLED SYMBOL ANALYSIS REPORT")
print("=" * 80)
# Summary statistics
total_symbols = len(symbols)
total_fastled = len(fastled_symbols)
fastled_size = sum(s.size for s in fastled_symbols)
print("\nSUMMARY:")
print(f" Total symbols: {total_symbols}")
print(f" FastLED symbols: {total_fastled}")
print(f" Total FastLED size: {fastled_size} bytes ({fastled_size / 1024:.1f} KB)")
# Largest FastLED symbols
print("\nLARGEST FASTLED SYMBOLS (potential elimination targets):")
fastled_sorted: List[SymbolInfo] = sorted(
fastled_symbols, key=lambda x: x.size, reverse=True
)
for i, sym in enumerate(fastled_sorted[:20]):
display_name = sym.demangled_name
print(f" {i + 1:2d}. {sym.size:6d} bytes - {display_name}")
if sym.demangled_name != sym.name:
print(
f" (mangled: {sym.name[:80]}{'...' if len(sym.name) > 80 else ''})"
)
# FastLED modules analysis
print("\nFASTLED MODULES PULLED IN:")
fastled_modules: List[str] = [
mod
for mod in dependencies.keys()
if any(kw in mod.lower() for kw in ["fastled", "crgb", "led", "rmt", "strip"])
]
for module in sorted(fastled_modules):
print(f" {module}:")
for symbol in dependencies[module][:5]: # Show first 5 symbols
print(f" - {symbol}")
if len(dependencies[module]) > 5:
print(f" ... and {len(dependencies[module]) - 5} more")
# Largest overall symbols (non-FastLED)
print("\nLARGEST NON-FASTLED SYMBOLS:")
non_fastled: List[SymbolInfo] = [
s
for s in large_symbols
if not any(
keyword in s.demangled_name.lower()
for keyword in ["fastled", "cfastled", "crgb", "hsv"]
)
]
non_fastled_sorted: List[SymbolInfo] = sorted(
non_fastled, key=lambda x: x.size, reverse=True
)
for i, sym in enumerate(non_fastled_sorted[:15]):
display_name = sym.demangled_name
print(f" {i + 1:2d}. {sym.size:6d} bytes - {display_name}")
# Recommendations
print("\nRECOMMENDATIONS FOR SIZE REDUCTION:")
# Identify unused features
# unused_features = []
feature_patterns = {
"JSON functionality": ["json", "Json"],
"Audio processing": ["audio", "fft", "Audio"],
"2D effects": ["2d", "noise", "matrix"],
"Video functionality": ["video", "Video"],
"UI components": ["ui", "button", "slider"],
"File system": ["file", "File", "fs_"],
"Mathematical functions": ["sqrt", "sin", "cos", "math"],
"String processing": ["string", "str", "String"],
}
for feature, patterns in feature_patterns.items():
feature_symbols: List[SymbolInfo] = [
s for s in fastled_symbols if any(p in s.demangled_name for p in patterns)
]
if feature_symbols:
total_size = sum(s.size for s in feature_symbols)
print(f" - {feature}: {len(feature_symbols)} symbols, {total_size} bytes")
if total_size > 1000: # Only show features with >1KB
print(
f" Consider removing if not needed (could save ~{total_size / 1024:.1f} KB)"
)
# Show a few example symbols
for sym in feature_symbols[:3]:
display_name = sym.demangled_name[:60]
print(f" * {sym.size} bytes: {display_name}")
if len(feature_symbols) > 3:
print(f" ... and {len(feature_symbols) - 3} more")
return {
"total_symbols": total_symbols,
"fastled_symbols": total_fastled,
"fastled_size": fastled_size,
"largest_fastled": fastled_sorted[:10],
"dependencies": fastled_modules,
}
def main():
# Detect build directory and board - try multiple possible locations
possible_build_dirs = [
Path("../../.build"),
Path("../.build"),
Path(".build"),
Path("../../build"),
Path("../build"),
Path("build"),
]
build_dir = None
for build_path in possible_build_dirs:
if build_path.exists():
build_dir = build_path
break
if not build_dir:
print("Error: Could not find build directory (.build)")
sys.exit(1)
# Find ESP32 board directory
esp32_boards = [
"esp32dev",
"esp32",
"esp32s2",
"esp32s3",
"esp32c3",
"esp32c6",
"esp32h2",
"esp32p4",
"esp32c2",
]
board_dir = None
board_name = None
for board in esp32_boards:
candidate_dir = build_dir / board
if candidate_dir.exists():
build_info_file = candidate_dir / "build_info.json"
if build_info_file.exists():
board_dir = candidate_dir
board_name = board
break
if not board_dir:
print("Error: No ESP32 board with build_info.json found in build directory")
print(f"Searched in: {build_dir}")
print(f"Looking for boards: {esp32_boards}")
sys.exit(1)
build_info_path = board_dir / "build_info.json"
print(f"Found ESP32 build info for {board_name}: {build_info_path}")
with open(build_info_path) as f:
build_info = json.load(f)
# Use the detected board name instead of hardcoded "esp32dev"
esp32_info = build_info[board_name]
nm_path = esp32_info["aliases"]["nm"]
elf_file = esp32_info["prog_path"]
# Find map file
map_file = Path(elf_file).with_suffix(".map")
print(f"Analyzing ELF file: {elf_file}")
print(f"Using nm tool: {nm_path}")
print(f"Map file: {map_file}")
# Analyze symbols
cppfilt_path = esp32_info["aliases"]["c++filt"]
symbols, fastled_symbols, large_symbols = analyze_symbols(
elf_file, nm_path, cppfilt_path
)
# Analyze dependencies
dependencies = analyze_map_file(map_file)
# Generate report
report = generate_report(symbols, fastled_symbols, large_symbols, dependencies)
# Save detailed data to JSON (sorted by size, largest first)
# Use board-specific filename and place it relative to build directory
output_file = build_dir / f"{board_name}_symbol_analysis.json"
detailed_data = {
"summary": report,
"all_fastled_symbols": sorted(
fastled_symbols, key=lambda x: x.size, reverse=True
),
"all_symbols_sorted_by_size": sorted(
symbols, key=lambda x: x.size, reverse=True
)[:100], # Top 100 largest symbols
"dependencies": dependencies,
"large_symbols": sorted(large_symbols, key=lambda x: x.size, reverse=True)[
:50
], # Top 50 largest symbols
}
with open(output_file, "w") as f:
json.dump(detailed_data, f, indent=2)
print(f"\nDetailed analysis saved to: {output_file}")
print("You can examine this file to identify specific symbols to eliminate.")
if __name__ == "__main__":
main()