381 lines
12 KiB
Python
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()
|