mirror of
https://github.com/esphome/esphome.git
synced 2025-10-20 18:53:47 +01:00
tweak
This commit is contained in:
134
.github/workflows/ci.yml
vendored
134
.github/workflows/ci.yml
vendored
@@ -548,7 +548,7 @@ jobs:
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
- name: Build and compile with test_build_components
|
||||
- name: Build, compile, and analyze memory
|
||||
id: extract
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -563,38 +563,32 @@ jobs:
|
||||
component_list=$(echo "$components" | jq -r 'join(",")')
|
||||
|
||||
echo "Compiling with test_build_components.py..."
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
python script/ci_memory_impact_extract.py --output-env
|
||||
- name: Find and upload final ELF file
|
||||
run: |
|
||||
# Note: test_build_components.py may run multiple builds, but each overwrites
|
||||
# the previous firmware.elf. The memory totals (RAM/Flash) are already summed
|
||||
# by ci_memory_impact_extract.py. This ELF is from the last build and is used
|
||||
# for detailed component breakdown (if available).
|
||||
mkdir -p ./elf-artifacts/target
|
||||
|
||||
# Find the most recent firmware.elf
|
||||
if [ -d ~/.esphome/build ]; then
|
||||
elf_file=$(find ~/.esphome/build -name "firmware.elf" -type f -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2-)
|
||||
# Find most recent build directory for detailed analysis
|
||||
build_dir=$(find ~/.esphome/build -type d -maxdepth 1 -mindepth 1 -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2- || echo "")
|
||||
|
||||
if [ -n "$elf_file" ] && [ -f "$elf_file" ]; then
|
||||
echo "Found final ELF file: $elf_file"
|
||||
cp "$elf_file" "./elf-artifacts/target/firmware.elf"
|
||||
else
|
||||
echo "Warning: No ELF file found in ~/.esphome/build"
|
||||
ls -la ~/.esphome/build/ || true
|
||||
fi
|
||||
# Run build and extract memory, with optional detailed analysis
|
||||
if [ -n "$build_dir" ]; then
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
python script/ci_memory_impact_extract.py \
|
||||
--output-env \
|
||||
--build-dir "$build_dir" \
|
||||
--output-json memory-analysis-target.json
|
||||
else
|
||||
echo "Warning: ~/.esphome/build directory not found"
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
python script/ci_memory_impact_extract.py --output-env
|
||||
fi
|
||||
- name: Upload ELF artifact
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: memory-impact-target-elf
|
||||
path: ./elf-artifacts/target/firmware.elf
|
||||
name: memory-analysis-target
|
||||
path: memory-analysis-target.json
|
||||
if-no-files-found: warn
|
||||
retention-days: 1
|
||||
|
||||
@@ -621,7 +615,7 @@ jobs:
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
- name: Build and compile with test_build_components
|
||||
- name: Build, compile, and analyze memory
|
||||
id: extract
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -636,38 +630,32 @@ jobs:
|
||||
component_list=$(echo "$components" | jq -r 'join(",")')
|
||||
|
||||
echo "Compiling with test_build_components.py..."
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
python script/ci_memory_impact_extract.py --output-env
|
||||
- name: Find and upload final ELF file
|
||||
run: |
|
||||
# Note: test_build_components.py may run multiple builds, but each overwrites
|
||||
# the previous firmware.elf. The memory totals (RAM/Flash) are already summed
|
||||
# by ci_memory_impact_extract.py. This ELF is from the last build and is used
|
||||
# for detailed component breakdown (if available).
|
||||
mkdir -p ./elf-artifacts/pr
|
||||
|
||||
# Find the most recent firmware.elf
|
||||
if [ -d ~/.esphome/build ]; then
|
||||
elf_file=$(find ~/.esphome/build -name "firmware.elf" -type f -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2-)
|
||||
# Find most recent build directory for detailed analysis
|
||||
build_dir=$(find ~/.esphome/build -type d -maxdepth 1 -mindepth 1 -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2- || echo "")
|
||||
|
||||
if [ -n "$elf_file" ] && [ -f "$elf_file" ]; then
|
||||
echo "Found final ELF file: $elf_file"
|
||||
cp "$elf_file" "./elf-artifacts/pr/firmware.elf"
|
||||
else
|
||||
echo "Warning: No ELF file found in ~/.esphome/build"
|
||||
ls -la ~/.esphome/build/ || true
|
||||
fi
|
||||
# Run build and extract memory, with optional detailed analysis
|
||||
if [ -n "$build_dir" ]; then
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
python script/ci_memory_impact_extract.py \
|
||||
--output-env \
|
||||
--build-dir "$build_dir" \
|
||||
--output-json memory-analysis-pr.json
|
||||
else
|
||||
echo "Warning: ~/.esphome/build directory not found"
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
python script/ci_memory_impact_extract.py --output-env
|
||||
fi
|
||||
- name: Upload ELF artifact
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: memory-impact-pr-elf
|
||||
path: ./elf-artifacts/pr/firmware.elf
|
||||
name: memory-analysis-pr
|
||||
path: memory-analysis-pr.json
|
||||
if-no-files-found: warn
|
||||
retention-days: 1
|
||||
|
||||
@@ -691,17 +679,17 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Download target ELF artifact
|
||||
- name: Download target analysis JSON
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: memory-impact-target-elf
|
||||
path: ./elf-artifacts/target
|
||||
name: memory-analysis-target
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Download PR ELF artifact
|
||||
- name: Download PR analysis JSON
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: memory-impact-pr-elf
|
||||
path: ./elf-artifacts/pr
|
||||
name: memory-analysis-pr
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Post or update PR comment
|
||||
env:
|
||||
@@ -715,22 +703,22 @@ jobs:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
|
||||
# Check if ELF files exist (from final build)
|
||||
target_elf_arg=""
|
||||
pr_elf_arg=""
|
||||
# Check if analysis JSON files exist
|
||||
target_json_arg=""
|
||||
pr_json_arg=""
|
||||
|
||||
if [ -f ./elf-artifacts/target/firmware.elf ]; then
|
||||
echo "Found target ELF file"
|
||||
target_elf_arg="--target-elf ./elf-artifacts/target/firmware.elf"
|
||||
if [ -f ./memory-analysis/memory-analysis-target.json ]; then
|
||||
echo "Found target analysis JSON"
|
||||
target_json_arg="--target-json ./memory-analysis/memory-analysis-target.json"
|
||||
else
|
||||
echo "No target ELF file found"
|
||||
echo "No target analysis JSON found"
|
||||
fi
|
||||
|
||||
if [ -f ./elf-artifacts/pr/firmware.elf ]; then
|
||||
echo "Found PR ELF file"
|
||||
pr_elf_arg="--pr-elf ./elf-artifacts/pr/firmware.elf"
|
||||
if [ -f ./memory-analysis/memory-analysis-pr.json ]; then
|
||||
echo "Found PR analysis JSON"
|
||||
pr_json_arg="--pr-json ./memory-analysis/memory-analysis-pr.json"
|
||||
else
|
||||
echo "No PR ELF file found"
|
||||
echo "No PR analysis JSON found"
|
||||
fi
|
||||
|
||||
python script/ci_memory_impact_comment.py \
|
||||
@@ -741,8 +729,8 @@ jobs:
|
||||
--target-flash "$TARGET_FLASH" \
|
||||
--pr-ram "$PR_RAM" \
|
||||
--pr-flash "$PR_FLASH" \
|
||||
$target_elf_arg \
|
||||
$pr_elf_arg
|
||||
$target_json_arg \
|
||||
$pr_json_arg
|
||||
|
||||
ci-status:
|
||||
name: CI Status
|
||||
|
@@ -7,6 +7,7 @@ import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .const import (
|
||||
CORE_SUBCATEGORY_PATTERNS,
|
||||
@@ -22,9 +23,65 @@ from .helpers import (
|
||||
parse_symbol_line,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esphome.platformio_api import IDEData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_toolchain_for_platform(platform: str) -> tuple[str | None, str | None]:
|
||||
"""Get objdump and readelf paths for a given platform.
|
||||
|
||||
This function auto-detects the correct toolchain based on the platform name,
|
||||
using the same detection logic as PlatformIO's IDEData class.
|
||||
|
||||
Args:
|
||||
platform: Platform name (e.g., "esp8266-ard", "esp32-idf", "esp32-c3-idf")
|
||||
|
||||
Returns:
|
||||
Tuple of (objdump_path, readelf_path) or (None, None) if not found/supported
|
||||
"""
|
||||
home = Path.home()
|
||||
platformio_packages = home / ".platformio" / "packages"
|
||||
|
||||
# Map platform to toolchain and prefix (same logic as PlatformIO uses)
|
||||
toolchain = None
|
||||
prefix = None
|
||||
|
||||
if "esp8266" in platform:
|
||||
toolchain = "toolchain-xtensa"
|
||||
prefix = "xtensa-lx106-elf"
|
||||
elif "esp32-c" in platform or "esp32-h" in platform or "esp32-p4" in platform:
|
||||
# RISC-V variants (C2, C3, C5, C6, H2, P4)
|
||||
toolchain = "toolchain-riscv32-esp"
|
||||
prefix = "riscv32-esp-elf"
|
||||
elif "esp32" in platform:
|
||||
# Xtensa variants (original, S2, S3)
|
||||
toolchain = "toolchain-xtensa-esp-elf"
|
||||
if "s2" in platform:
|
||||
prefix = "xtensa-esp32s2-elf"
|
||||
elif "s3" in platform:
|
||||
prefix = "xtensa-esp32s3-elf"
|
||||
else:
|
||||
prefix = "xtensa-esp32-elf"
|
||||
else:
|
||||
# Other platforms (RP2040, LibreTiny, etc.) - not supported for ELF analysis
|
||||
_LOGGER.debug("Platform %s not supported for ELF analysis", platform)
|
||||
return None, None
|
||||
|
||||
# Construct paths (same pattern as IDEData.objdump_path/readelf_path)
|
||||
toolchain_path = platformio_packages / toolchain / "bin"
|
||||
objdump_path = toolchain_path / f"{prefix}-objdump"
|
||||
readelf_path = toolchain_path / f"{prefix}-readelf"
|
||||
|
||||
if objdump_path.exists() and readelf_path.exists():
|
||||
_LOGGER.debug("Found %s toolchain: %s", platform, prefix)
|
||||
return str(objdump_path), str(readelf_path)
|
||||
|
||||
_LOGGER.warning("Toolchain not found at %s", toolchain_path)
|
||||
return None, None
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemorySection:
|
||||
"""Represents a memory section with its symbols."""
|
||||
@@ -67,11 +124,27 @@ class MemoryAnalyzer:
|
||||
objdump_path: str | None = None,
|
||||
readelf_path: str | None = None,
|
||||
external_components: set[str] | None = None,
|
||||
idedata: "IDEData | None" = None,
|
||||
):
|
||||
"""Initialize memory analyzer.
|
||||
|
||||
Args:
|
||||
elf_path: Path to ELF file to analyze
|
||||
objdump_path: Path to objdump binary (auto-detected from idedata if not provided)
|
||||
readelf_path: Path to readelf binary (auto-detected from idedata if not provided)
|
||||
external_components: Set of external component names
|
||||
idedata: Optional PlatformIO IDEData object to auto-detect toolchain paths
|
||||
"""
|
||||
self.elf_path = Path(elf_path)
|
||||
if not self.elf_path.exists():
|
||||
raise FileNotFoundError(f"ELF file not found: {elf_path}")
|
||||
|
||||
# Auto-detect toolchain paths from idedata if not provided
|
||||
if idedata is not None and (objdump_path is None or readelf_path is None):
|
||||
objdump_path = objdump_path or idedata.objdump_path
|
||||
readelf_path = readelf_path or idedata.readelf_path
|
||||
_LOGGER.debug("Using toolchain paths from PlatformIO idedata")
|
||||
|
||||
self.objdump_path = objdump_path or "objdump"
|
||||
self.readelf_path = readelf_path or "readelf"
|
||||
self.external_components = external_components or set()
|
||||
|
@@ -1,7 +1,6 @@
|
||||
"""CLI interface for memory analysis with report generation."""
|
||||
|
||||
from collections import defaultdict
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from . import MemoryAnalyzer
|
||||
@@ -313,51 +312,91 @@ def analyze_elf(
|
||||
def main():
|
||||
"""CLI entrypoint for memory analysis."""
|
||||
if len(sys.argv) < 2:
|
||||
print(
|
||||
"Usage: python -m esphome.analyze_memory <elf_file> [objdump_path] [readelf_path]"
|
||||
)
|
||||
print("\nIf objdump/readelf paths are not provided, you must specify them.")
|
||||
print("\nExample for ESP8266:")
|
||||
print(" python -m esphome.analyze_memory firmware.elf \\")
|
||||
print(
|
||||
" ~/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-objdump \\"
|
||||
)
|
||||
print(
|
||||
" ~/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-readelf"
|
||||
)
|
||||
print("\nExample for ESP32:")
|
||||
print(" python -m esphome.analyze_memory firmware.elf \\")
|
||||
print(
|
||||
" ~/.platformio/packages/toolchain-xtensa-esp-elf/bin/xtensa-esp32-elf-objdump \\"
|
||||
)
|
||||
print(
|
||||
" ~/.platformio/packages/toolchain-xtensa-esp-elf/bin/xtensa-esp32-elf-readelf"
|
||||
)
|
||||
print("\nExample for ESP32-C3 (RISC-V):")
|
||||
print(" python -m esphome.analyze_memory firmware.elf \\")
|
||||
print(
|
||||
" ~/.platformio/packages/toolchain-riscv32-esp/bin/riscv32-esp-elf-objdump \\"
|
||||
)
|
||||
print(
|
||||
" ~/.platformio/packages/toolchain-riscv32-esp/bin/riscv32-esp-elf-readelf"
|
||||
)
|
||||
print("Usage: python -m esphome.analyze_memory <build_directory>")
|
||||
print("\nAnalyze memory usage from an ESPHome build directory.")
|
||||
print("The build directory should contain firmware.elf and idedata will be")
|
||||
print("loaded from ~/.esphome/.internal/idedata/<device>.json")
|
||||
print("\nExamples:")
|
||||
print(" python -m esphome.analyze_memory ~/.esphome/build/my-device")
|
||||
print(" python -m esphome.analyze_memory .esphome/build/my-device")
|
||||
print(" python -m esphome.analyze_memory my-device # Short form")
|
||||
sys.exit(1)
|
||||
|
||||
elf_file = sys.argv[1]
|
||||
objdump_path = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
readelf_path = sys.argv[3] if len(sys.argv) > 3 else None
|
||||
build_dir = sys.argv[1]
|
||||
|
||||
# Load build directory
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.platformio_api import IDEData
|
||||
|
||||
build_path = Path(build_dir)
|
||||
|
||||
# If no path separator in name, assume it's a device name
|
||||
if "/" not in build_dir and not build_path.is_dir():
|
||||
# Try current directory first
|
||||
cwd_path = Path.cwd() / ".esphome" / "build" / build_dir
|
||||
if cwd_path.is_dir():
|
||||
build_path = cwd_path
|
||||
print(f"Using build directory: {build_path}", file=sys.stderr)
|
||||
else:
|
||||
# Fall back to home directory
|
||||
build_path = Path.home() / ".esphome" / "build" / build_dir
|
||||
print(f"Using build directory: {build_path}", file=sys.stderr)
|
||||
|
||||
if not build_path.is_dir():
|
||||
print(f"Error: {build_path} is not a directory", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Find firmware.elf
|
||||
elf_file = None
|
||||
for elf_candidate in [
|
||||
build_path / "firmware.elf",
|
||||
build_path / ".pioenvs" / build_path.name / "firmware.elf",
|
||||
]:
|
||||
if elf_candidate.exists():
|
||||
elf_file = str(elf_candidate)
|
||||
break
|
||||
|
||||
if not elf_file:
|
||||
print(f"Error: firmware.elf not found in {build_dir}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Find idedata.json - check current directory first, then home
|
||||
device_name = build_path.name
|
||||
idedata_candidates = [
|
||||
Path.cwd() / ".esphome" / "idedata" / f"{device_name}.json",
|
||||
Path.home() / ".esphome" / "idedata" / f"{device_name}.json",
|
||||
]
|
||||
|
||||
idedata = None
|
||||
for idedata_path in idedata_candidates:
|
||||
if idedata_path.exists():
|
||||
try:
|
||||
with open(idedata_path, encoding="utf-8") as f:
|
||||
raw_data = json.load(f)
|
||||
idedata = IDEData(raw_data)
|
||||
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)
|
||||
break
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print(f"Warning: Failed to load idedata: {e}", file=sys.stderr)
|
||||
|
||||
if not idedata:
|
||||
print(
|
||||
f"Warning: idedata not found (searched {idedata_candidates[0]} and {idedata_candidates[1]})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
try:
|
||||
report = analyze_elf(elf_file, objdump_path, readelf_path)
|
||||
analyzer = MemoryAnalyzerCLI(elf_file, idedata=idedata)
|
||||
analyzer.analyze()
|
||||
report = analyzer.generate_report()
|
||||
print(report)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, OSError) as e:
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
if "readelf" in str(e) or "objdump" in str(e):
|
||||
print(
|
||||
"\nHint: You need to specify the toolchain-specific tools.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("See usage above for examples.", file=sys.stderr)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
@@ -412,10 +412,8 @@ def analyze_memory_usage(config: dict[str, Any]) -> None:
|
||||
|
||||
idedata = get_idedata(config)
|
||||
|
||||
# Get paths to tools
|
||||
# Get ELF path
|
||||
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)
|
||||
@@ -457,7 +455,10 @@ def analyze_memory_usage(config: dict[str, Any]) -> None:
|
||||
_LOGGER.debug("Detected external components: %s", external_components)
|
||||
|
||||
# Create analyzer and run analysis
|
||||
analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path, external_components)
|
||||
# Pass idedata to auto-detect toolchain paths
|
||||
analyzer = MemoryAnalyzer(
|
||||
elf_path, external_components=external_components, idedata=idedata
|
||||
)
|
||||
analyzer.analyze()
|
||||
|
||||
# Generate and print report
|
||||
|
@@ -18,61 +18,31 @@ import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from esphome.analyze_memory import MemoryAnalyzer # noqa: E402
|
||||
|
||||
# Comment marker to identify our memory impact comments
|
||||
COMMENT_MARKER = "<!-- esphome-memory-impact-analysis -->"
|
||||
|
||||
|
||||
def get_platform_toolchain(platform: str) -> tuple[str | None, str | None]:
|
||||
"""Get platform-specific objdump and readelf paths.
|
||||
def load_analysis_json(json_path: str) -> dict | None:
|
||||
"""Load memory analysis results from JSON file.
|
||||
|
||||
Args:
|
||||
platform: Platform name (e.g., "esp8266-ard", "esp32-idf", "esp32-c3-idf")
|
||||
json_path: Path to analysis JSON file
|
||||
|
||||
Returns:
|
||||
Tuple of (objdump_path, readelf_path) or (None, None) if not found/supported
|
||||
Dictionary with analysis results or None if file doesn't exist/can't be loaded
|
||||
"""
|
||||
from pathlib import Path
|
||||
json_file = Path(json_path)
|
||||
if not json_file.exists():
|
||||
print(f"Analysis JSON not found: {json_path}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
home = Path.home()
|
||||
platformio_packages = home / ".platformio" / "packages"
|
||||
|
||||
# Map platform to toolchain
|
||||
toolchain = None
|
||||
prefix = None
|
||||
|
||||
if "esp8266" in platform:
|
||||
toolchain = "toolchain-xtensa"
|
||||
prefix = "xtensa-lx106-elf"
|
||||
elif "esp32-c" in platform or "esp32-h" in platform or "esp32-p4" in platform:
|
||||
# RISC-V variants (C2, C3, C5, C6, H2, P4)
|
||||
toolchain = "toolchain-riscv32-esp"
|
||||
prefix = "riscv32-esp-elf"
|
||||
elif "esp32" in platform:
|
||||
# Xtensa variants (original, S2, S3)
|
||||
toolchain = "toolchain-xtensa-esp-elf"
|
||||
if "s2" in platform:
|
||||
prefix = "xtensa-esp32s2-elf"
|
||||
elif "s3" in platform:
|
||||
prefix = "xtensa-esp32s3-elf"
|
||||
else:
|
||||
prefix = "xtensa-esp32-elf"
|
||||
else:
|
||||
# Other platforms (RP2040, LibreTiny, etc.) - not supported
|
||||
print(f"Platform {platform} not supported for ELF analysis", file=sys.stderr)
|
||||
return None, None
|
||||
|
||||
toolchain_path = platformio_packages / toolchain / "bin"
|
||||
objdump_path = toolchain_path / f"{prefix}-objdump"
|
||||
readelf_path = toolchain_path / f"{prefix}-readelf"
|
||||
|
||||
if objdump_path.exists() and readelf_path.exists():
|
||||
print(f"Using {platform} toolchain: {prefix}", file=sys.stderr)
|
||||
return str(objdump_path), str(readelf_path)
|
||||
|
||||
print(f"Warning: Toolchain not found at {toolchain_path}", file=sys.stderr)
|
||||
return None, None
|
||||
try:
|
||||
with open(json_file, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print(f"Failed to load analysis JSON: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def format_bytes(bytes_value: int) -> str:
|
||||
@@ -122,56 +92,6 @@ def format_change(before: int, after: int) -> str:
|
||||
return f"{emoji} {delta_str} ({pct_str})"
|
||||
|
||||
|
||||
def run_detailed_analysis(
|
||||
elf_path: str, objdump_path: str | None = None, readelf_path: str | None = None
|
||||
) -> tuple[dict | None, dict | None]:
|
||||
"""Run detailed memory analysis on an ELF file.
|
||||
|
||||
Args:
|
||||
elf_path: Path to ELF file
|
||||
objdump_path: Optional path to objdump tool
|
||||
readelf_path: Optional path to readelf tool
|
||||
|
||||
Returns:
|
||||
Tuple of (component_breakdown, symbol_map) or (None, None) if analysis fails
|
||||
component_breakdown: Dictionary with component memory breakdown
|
||||
symbol_map: Dictionary mapping symbol names to their sizes
|
||||
"""
|
||||
try:
|
||||
analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path)
|
||||
components = analyzer.analyze()
|
||||
|
||||
# Convert ComponentMemory objects to dictionaries
|
||||
component_result = {}
|
||||
for name, mem in components.items():
|
||||
component_result[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,
|
||||
}
|
||||
|
||||
# Build symbol map from all sections
|
||||
symbol_map = {}
|
||||
for section in analyzer.sections.values():
|
||||
for symbol_name, size, _ in section.symbols:
|
||||
if size > 0: # Only track non-zero sized symbols
|
||||
# Demangle the symbol for better readability
|
||||
demangled = analyzer._demangle_symbol(symbol_name)
|
||||
symbol_map[demangled] = size
|
||||
|
||||
return component_result, symbol_map
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to run detailed analysis: {e}", file=sys.stderr)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
return None, None
|
||||
|
||||
|
||||
def create_symbol_changes_table(
|
||||
target_symbols: dict | None, pr_symbols: dict | None
|
||||
) -> str:
|
||||
@@ -371,10 +291,10 @@ def create_comment_body(
|
||||
target_flash: int,
|
||||
pr_ram: int,
|
||||
pr_flash: int,
|
||||
target_elf: str | None = None,
|
||||
pr_elf: str | None = None,
|
||||
objdump_path: str | None = None,
|
||||
readelf_path: str | None = None,
|
||||
target_analysis: dict | None = None,
|
||||
pr_analysis: dict | None = None,
|
||||
target_symbols: dict | None = None,
|
||||
pr_symbols: dict | None = None,
|
||||
) -> str:
|
||||
"""Create the comment body with memory impact analysis.
|
||||
|
||||
@@ -385,10 +305,10 @@ def create_comment_body(
|
||||
target_flash: Flash usage in target branch
|
||||
pr_ram: RAM usage in PR branch
|
||||
pr_flash: Flash usage in PR branch
|
||||
target_elf: Optional path to target branch ELF file
|
||||
pr_elf: Optional path to PR branch ELF file
|
||||
objdump_path: Optional path to objdump tool
|
||||
readelf_path: Optional path to readelf tool
|
||||
target_analysis: Optional component breakdown for target branch
|
||||
pr_analysis: Optional component breakdown for PR branch
|
||||
target_symbols: Optional symbol map for target branch
|
||||
pr_symbols: Optional symbol map for PR branch
|
||||
|
||||
Returns:
|
||||
Formatted comment body
|
||||
@@ -396,29 +316,14 @@ def create_comment_body(
|
||||
ram_change = format_change(target_ram, pr_ram)
|
||||
flash_change = format_change(target_flash, pr_flash)
|
||||
|
||||
# Run detailed analysis if ELF files are provided
|
||||
target_analysis = None
|
||||
pr_analysis = None
|
||||
target_symbols = None
|
||||
pr_symbols = None
|
||||
# Use provided analysis data if available
|
||||
component_breakdown = ""
|
||||
symbol_changes = ""
|
||||
|
||||
if target_elf and pr_elf:
|
||||
print(
|
||||
f"Running detailed analysis on {target_elf} and {pr_elf}", file=sys.stderr
|
||||
if target_analysis and pr_analysis:
|
||||
component_breakdown = create_detailed_breakdown_table(
|
||||
target_analysis, pr_analysis
|
||||
)
|
||||
target_analysis, target_symbols = run_detailed_analysis(
|
||||
target_elf, objdump_path, readelf_path
|
||||
)
|
||||
pr_analysis, pr_symbols = run_detailed_analysis(
|
||||
pr_elf, objdump_path, readelf_path
|
||||
)
|
||||
|
||||
if target_analysis and pr_analysis:
|
||||
component_breakdown = create_detailed_breakdown_table(
|
||||
target_analysis, pr_analysis
|
||||
)
|
||||
|
||||
if target_symbols and pr_symbols:
|
||||
symbol_changes = create_symbol_changes_table(target_symbols, pr_symbols)
|
||||
@@ -612,13 +517,13 @@ def main() -> int:
|
||||
parser.add_argument(
|
||||
"--pr-flash", type=int, required=True, help="PR branch flash usage"
|
||||
)
|
||||
parser.add_argument("--target-elf", help="Optional path to target branch ELF file")
|
||||
parser.add_argument("--pr-elf", help="Optional path to PR branch ELF file")
|
||||
parser.add_argument(
|
||||
"--objdump-path", help="Optional path to objdump tool for detailed analysis"
|
||||
"--target-json",
|
||||
help="Optional path to target branch analysis JSON (for detailed analysis)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--readelf-path", help="Optional path to readelf tool for detailed analysis"
|
||||
"--pr-json",
|
||||
help="Optional path to PR branch analysis JSON (for detailed analysis)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
@@ -633,17 +538,26 @@ def main() -> int:
|
||||
print(f"Error parsing --components JSON: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Detect platform-specific toolchain paths
|
||||
objdump_path = args.objdump_path
|
||||
readelf_path = args.readelf_path
|
||||
# Load analysis JSON files
|
||||
target_analysis = None
|
||||
pr_analysis = None
|
||||
target_symbols = None
|
||||
pr_symbols = None
|
||||
|
||||
if not objdump_path or not readelf_path:
|
||||
# Auto-detect based on platform
|
||||
objdump_path, readelf_path = get_platform_toolchain(args.platform)
|
||||
if args.target_json:
|
||||
target_data = load_analysis_json(args.target_json)
|
||||
if target_data and target_data.get("detailed_analysis"):
|
||||
target_analysis = target_data["detailed_analysis"].get("components")
|
||||
target_symbols = target_data["detailed_analysis"].get("symbols")
|
||||
|
||||
if args.pr_json:
|
||||
pr_data = load_analysis_json(args.pr_json)
|
||||
if pr_data and pr_data.get("detailed_analysis"):
|
||||
pr_analysis = pr_data["detailed_analysis"].get("components")
|
||||
pr_symbols = pr_data["detailed_analysis"].get("symbols")
|
||||
|
||||
# Create comment body
|
||||
# Note: ELF files (if provided) are from the final build when test_build_components
|
||||
# runs multiple builds. Memory totals (RAM/Flash) are already summed across all builds.
|
||||
# Note: Memory totals (RAM/Flash) are summed across all builds if multiple were run.
|
||||
comment_body = create_comment_body(
|
||||
components=components,
|
||||
platform=args.platform,
|
||||
@@ -651,10 +565,10 @@ def main() -> int:
|
||||
target_flash=args.target_flash,
|
||||
pr_ram=args.pr_ram,
|
||||
pr_flash=args.pr_flash,
|
||||
target_elf=args.target_elf,
|
||||
pr_elf=args.pr_elf,
|
||||
objdump_path=objdump_path,
|
||||
readelf_path=readelf_path,
|
||||
target_analysis=target_analysis,
|
||||
pr_analysis=pr_analysis,
|
||||
target_symbols=target_symbols,
|
||||
pr_symbols=pr_symbols,
|
||||
)
|
||||
|
||||
# Post or update comment
|
||||
|
@@ -9,11 +9,14 @@ The script reads compile output from stdin and looks for the standard
|
||||
PlatformIO output format:
|
||||
RAM: [==== ] 36.1% (used 29548 bytes from 81920 bytes)
|
||||
Flash: [=== ] 34.0% (used 348511 bytes from 1023984 bytes)
|
||||
|
||||
Optionally performs detailed memory analysis if a build directory is provided.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
@@ -60,6 +63,87 @@ def extract_from_compile_output(output_text: str) -> tuple[int | None, int | Non
|
||||
return total_ram, total_flash
|
||||
|
||||
|
||||
def run_detailed_analysis(build_dir: str) -> dict | None:
|
||||
"""Run detailed memory analysis on build directory.
|
||||
|
||||
Args:
|
||||
build_dir: Path to ESPHome build directory
|
||||
|
||||
Returns:
|
||||
Dictionary with analysis results or None if analysis fails
|
||||
"""
|
||||
from esphome.analyze_memory import MemoryAnalyzer
|
||||
from esphome.platformio_api import IDEData
|
||||
|
||||
build_path = Path(build_dir)
|
||||
if not build_path.exists():
|
||||
print(f"Build directory not found: {build_dir}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
# Find firmware.elf
|
||||
elf_path = None
|
||||
for elf_candidate in [
|
||||
build_path / "firmware.elf",
|
||||
build_path / ".pioenvs" / build_path.name / "firmware.elf",
|
||||
]:
|
||||
if elf_candidate.exists():
|
||||
elf_path = str(elf_candidate)
|
||||
break
|
||||
|
||||
if not elf_path:
|
||||
print(f"firmware.elf not found in {build_dir}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
# Find idedata.json
|
||||
device_name = build_path.name
|
||||
idedata_path = Path.home() / ".esphome" / "idedata" / f"{device_name}.json"
|
||||
|
||||
idedata = None
|
||||
if idedata_path.exists():
|
||||
try:
|
||||
with open(idedata_path, encoding="utf-8") as f:
|
||||
raw_data = json.load(f)
|
||||
idedata = IDEData(raw_data)
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print(f"Warning: Failed to load idedata: {e}", file=sys.stderr)
|
||||
|
||||
try:
|
||||
analyzer = MemoryAnalyzer(elf_path, idedata=idedata)
|
||||
components = analyzer.analyze()
|
||||
|
||||
# Convert to JSON-serializable format
|
||||
result = {
|
||||
"components": {},
|
||||
"symbols": {},
|
||||
}
|
||||
|
||||
for name, mem in components.items():
|
||||
result["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,
|
||||
}
|
||||
|
||||
# Build symbol map
|
||||
for section in analyzer.sections.values():
|
||||
for symbol_name, size, _ in section.symbols:
|
||||
if size > 0:
|
||||
demangled = analyzer._demangle_symbol(symbol_name)
|
||||
result["symbols"][demangled] = size
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to run detailed analysis: {e}", file=sys.stderr)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
@@ -70,6 +154,14 @@ def main() -> int:
|
||||
action="store_true",
|
||||
help="Output to GITHUB_OUTPUT environment file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--build-dir",
|
||||
help="Optional build directory for detailed memory analysis",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-json",
|
||||
help="Optional path to save detailed analysis JSON",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -108,6 +200,26 @@ def main() -> int:
|
||||
print(f"Total RAM: {ram_bytes} bytes", file=sys.stderr)
|
||||
print(f"Total Flash: {flash_bytes} bytes", file=sys.stderr)
|
||||
|
||||
# Run detailed analysis if build directory provided
|
||||
detailed_analysis = None
|
||||
if args.build_dir:
|
||||
print(f"Running detailed analysis on {args.build_dir}", file=sys.stderr)
|
||||
detailed_analysis = run_detailed_analysis(args.build_dir)
|
||||
|
||||
# Save JSON output if requested
|
||||
if args.output_json:
|
||||
output_data = {
|
||||
"ram_bytes": ram_bytes,
|
||||
"flash_bytes": flash_bytes,
|
||||
"detailed_analysis": detailed_analysis,
|
||||
}
|
||||
|
||||
output_path = Path(args.output_json)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(output_data, f, indent=2)
|
||||
print(f"Saved analysis to {args.output_json}", file=sys.stderr)
|
||||
|
||||
if args.output_env:
|
||||
# Output to GitHub Actions
|
||||
write_github_output(
|
||||
|
Reference in New Issue
Block a user