mirror of
https://github.com/esphome/esphome.git
synced 2025-10-20 18:53:47 +01:00
merge
This commit is contained in:
@@ -24,6 +24,57 @@ from esphome.analyze_memory import MemoryAnalyzer # noqa: E402
|
||||
COMMENT_MARKER = "<!-- esphome-memory-impact-analysis -->"
|
||||
|
||||
|
||||
def get_platform_toolchain(platform: str) -> tuple[str | None, str | None]:
|
||||
"""Get platform-specific objdump and readelf paths.
|
||||
|
||||
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
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
|
||||
|
||||
def format_bytes(bytes_value: int) -> str:
|
||||
"""Format bytes value with comma separators.
|
||||
|
||||
@@ -314,7 +365,7 @@ def create_detailed_breakdown_table(
|
||||
|
||||
|
||||
def create_comment_body(
|
||||
component: str,
|
||||
components: list[str],
|
||||
platform: str,
|
||||
target_ram: int,
|
||||
target_flash: int,
|
||||
@@ -328,7 +379,7 @@ def create_comment_body(
|
||||
"""Create the comment body with memory impact analysis.
|
||||
|
||||
Args:
|
||||
component: Component name
|
||||
components: List of component names (merged config)
|
||||
platform: Platform name
|
||||
target_ram: RAM usage in target branch
|
||||
target_flash: Flash usage in target branch
|
||||
@@ -374,10 +425,18 @@ def create_comment_body(
|
||||
else:
|
||||
print("No ELF files provided, skipping detailed analysis", file=sys.stderr)
|
||||
|
||||
# Format components list
|
||||
if len(components) == 1:
|
||||
components_str = f"`{components[0]}`"
|
||||
config_note = "a representative test configuration"
|
||||
else:
|
||||
components_str = ", ".join(f"`{c}`" for c in sorted(components))
|
||||
config_note = f"a merged configuration with {len(components)} components"
|
||||
|
||||
return f"""{COMMENT_MARKER}
|
||||
## Memory Impact Analysis
|
||||
|
||||
**Component:** `{component}`
|
||||
**Components:** {components_str}
|
||||
**Platform:** `{platform}`
|
||||
|
||||
| Metric | Target Branch | This PR | Change |
|
||||
@@ -386,7 +445,7 @@ def create_comment_body(
|
||||
| **Flash** | {format_bytes(target_flash)} | {format_bytes(pr_flash)} | {flash_change} |
|
||||
{component_breakdown}{symbol_changes}
|
||||
---
|
||||
*This analysis runs automatically when a single component changes. Memory usage is measured from a representative test configuration.*
|
||||
*This analysis runs automatically when components change. Memory usage is measured from {config_note}.*
|
||||
"""
|
||||
|
||||
|
||||
@@ -537,7 +596,11 @@ def main() -> int:
|
||||
description="Post or update PR comment with memory impact analysis"
|
||||
)
|
||||
parser.add_argument("--pr-number", required=True, help="PR number")
|
||||
parser.add_argument("--component", required=True, help="Component name")
|
||||
parser.add_argument(
|
||||
"--components",
|
||||
required=True,
|
||||
help='JSON array of component names (e.g., \'["api", "wifi"]\')',
|
||||
)
|
||||
parser.add_argument("--platform", required=True, help="Platform name")
|
||||
parser.add_argument(
|
||||
"--target-ram", type=int, required=True, help="Target branch RAM usage"
|
||||
@@ -560,9 +623,29 @@ def main() -> int:
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Parse components from JSON
|
||||
try:
|
||||
components = json.loads(args.components)
|
||||
if not isinstance(components, list):
|
||||
print("Error: --components must be a JSON array", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as e:
|
||||
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
|
||||
|
||||
if not objdump_path or not readelf_path:
|
||||
# Auto-detect based on platform
|
||||
objdump_path, readelf_path = get_platform_toolchain(args.platform)
|
||||
|
||||
# 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.
|
||||
comment_body = create_comment_body(
|
||||
component=args.component,
|
||||
components=components,
|
||||
platform=args.platform,
|
||||
target_ram=args.target_ram,
|
||||
target_flash=args.target_flash,
|
||||
@@ -570,8 +653,8 @@ def main() -> int:
|
||||
pr_flash=args.pr_flash,
|
||||
target_elf=args.target_elf,
|
||||
pr_elf=args.pr_elf,
|
||||
objdump_path=args.objdump_path,
|
||||
readelf_path=args.readelf_path,
|
||||
objdump_path=objdump_path,
|
||||
readelf_path=readelf_path,
|
||||
)
|
||||
|
||||
# Post or update comment
|
||||
|
@@ -28,27 +28,36 @@ from script.ci_helpers import write_github_output
|
||||
def extract_from_compile_output(output_text: str) -> tuple[int | None, int | None]:
|
||||
"""Extract memory usage from PlatformIO compile output.
|
||||
|
||||
Supports multiple builds (for component groups or isolated components).
|
||||
When test_build_components.py creates multiple builds, this sums the
|
||||
memory usage across all builds.
|
||||
|
||||
Looks for lines like:
|
||||
RAM: [==== ] 36.1% (used 29548 bytes from 81920 bytes)
|
||||
Flash: [=== ] 34.0% (used 348511 bytes from 1023984 bytes)
|
||||
|
||||
Args:
|
||||
output_text: Compile output text
|
||||
output_text: Compile output text (may contain multiple builds)
|
||||
|
||||
Returns:
|
||||
Tuple of (ram_bytes, flash_bytes) or (None, None) if not found
|
||||
Tuple of (total_ram_bytes, total_flash_bytes) or (None, None) if not found
|
||||
"""
|
||||
ram_match = re.search(
|
||||
# Find all RAM and Flash matches (may be multiple builds)
|
||||
ram_matches = re.findall(
|
||||
r"RAM:\s+\[.*?\]\s+\d+\.\d+%\s+\(used\s+(\d+)\s+bytes", output_text
|
||||
)
|
||||
flash_match = re.search(
|
||||
flash_matches = re.findall(
|
||||
r"Flash:\s+\[.*?\]\s+\d+\.\d+%\s+\(used\s+(\d+)\s+bytes", output_text
|
||||
)
|
||||
|
||||
if ram_match and flash_match:
|
||||
return int(ram_match.group(1)), int(flash_match.group(1))
|
||||
if not ram_matches or not flash_matches:
|
||||
return None, None
|
||||
|
||||
return None, None
|
||||
# Sum all builds (handles multiple component groups)
|
||||
total_ram = sum(int(match) for match in ram_matches)
|
||||
total_flash = sum(int(match) for match in flash_matches)
|
||||
|
||||
return total_ram, total_flash
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -83,8 +92,21 @@ def main() -> int:
|
||||
)
|
||||
return 1
|
||||
|
||||
print(f"RAM: {ram_bytes} bytes", file=sys.stderr)
|
||||
print(f"Flash: {flash_bytes} bytes", file=sys.stderr)
|
||||
# Count how many builds were found
|
||||
num_builds = len(
|
||||
re.findall(
|
||||
r"RAM:\s+\[.*?\]\s+\d+\.\d+%\s+\(used\s+(\d+)\s+bytes", compile_output
|
||||
)
|
||||
)
|
||||
|
||||
if num_builds > 1:
|
||||
print(
|
||||
f"Found {num_builds} builds - summing memory usage across all builds",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
print(f"Total RAM: {ram_bytes} bytes", file=sys.stderr)
|
||||
print(f"Total Flash: {flash_bytes} bytes", file=sys.stderr)
|
||||
|
||||
if args.output_env:
|
||||
# Output to GitHub Actions
|
||||
|
@@ -237,14 +237,14 @@ def _component_has_tests(component: str) -> bool:
|
||||
return any(tests_dir.glob("test.*.yaml"))
|
||||
|
||||
|
||||
def detect_single_component_for_memory_impact(
|
||||
def detect_memory_impact_config(
|
||||
branch: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Detect if exactly one component changed for memory impact analysis.
|
||||
"""Determine memory impact analysis configuration.
|
||||
|
||||
This analyzes the actual changed files (not dependencies) to determine if
|
||||
exactly one component has been modified. This is different from the
|
||||
changed_components list which includes all dependencies.
|
||||
Always runs memory impact analysis when there are changed components,
|
||||
building a merged configuration with all changed components (like
|
||||
test_build_components.py does) to get comprehensive memory analysis.
|
||||
|
||||
Args:
|
||||
branch: Branch to compare against
|
||||
@@ -252,37 +252,25 @@ def detect_single_component_for_memory_impact(
|
||||
Returns:
|
||||
Dictionary with memory impact analysis parameters:
|
||||
- should_run: "true" or "false"
|
||||
- component: component name (if should_run is true)
|
||||
- test_file: test file name (if should_run is true)
|
||||
- platform: platform name (if should_run is true)
|
||||
- components: list of component names to analyze
|
||||
- platform: platform name for the merged build
|
||||
- use_merged_config: "true" (always use merged config)
|
||||
"""
|
||||
# Platform preference order for memory impact analysis
|
||||
# Ordered by production relevance and memory constraint importance
|
||||
# Prefer ESP8266 for memory impact as it's the most constrained platform
|
||||
PLATFORM_PREFERENCE = [
|
||||
"esp8266-ard", # ESP8266 Arduino (most memory constrained - best for impact analysis)
|
||||
"esp32-idf", # Primary ESP32 IDF platform
|
||||
"esp32-c3-idf", # ESP32-C3 IDF
|
||||
"esp32-c6-idf", # ESP32-C6 IDF
|
||||
"esp32-s2-idf", # ESP32-S2 IDF
|
||||
"esp32-s3-idf", # ESP32-S3 IDF
|
||||
"esp32-c2-idf", # ESP32-C2 IDF
|
||||
"esp32-c5-idf", # ESP32-C5 IDF
|
||||
"esp32-h2-idf", # ESP32-H2 IDF
|
||||
"esp32-p4-idf", # ESP32-P4 IDF
|
||||
"esp8266-ard", # ESP8266 Arduino (memory constrained)
|
||||
"esp32-ard", # ESP32 Arduino
|
||||
"esp32-c3-ard", # ESP32-C3 Arduino
|
||||
"esp32-s2-ard", # ESP32-S2 Arduino
|
||||
"esp32-s3-ard", # ESP32-S3 Arduino
|
||||
"bk72xx-ard", # BK72xx Arduino
|
||||
"rp2040-ard", # RP2040 Arduino
|
||||
"nrf52-adafruit", # nRF52 Adafruit
|
||||
"host", # Host platform (development/testing)
|
||||
]
|
||||
|
||||
# Get actually changed files (not dependencies)
|
||||
files = changed_files(branch)
|
||||
|
||||
# Find all changed components (excluding core)
|
||||
# Find all changed components (excluding core and base bus components)
|
||||
changed_component_set = set()
|
||||
|
||||
for file in files:
|
||||
@@ -291,49 +279,53 @@ def detect_single_component_for_memory_impact(
|
||||
if len(parts) >= 3:
|
||||
component = parts[2]
|
||||
# Skip base bus components as they're used across many builds
|
||||
if component not in ["i2c", "spi", "uart", "modbus"]:
|
||||
if component not in ["i2c", "spi", "uart", "modbus", "canbus"]:
|
||||
changed_component_set.add(component)
|
||||
|
||||
# Only proceed if exactly one component changed
|
||||
if len(changed_component_set) != 1:
|
||||
# If no components changed, don't run memory impact
|
||||
if not changed_component_set:
|
||||
return {"should_run": "false"}
|
||||
|
||||
component = list(changed_component_set)[0]
|
||||
# Find components that have tests on the preferred platform
|
||||
components_with_tests = []
|
||||
selected_platform = None
|
||||
|
||||
# Find a test configuration for this component
|
||||
tests_dir = Path(root_path) / "tests" / "components" / component
|
||||
for component in sorted(changed_component_set):
|
||||
tests_dir = Path(root_path) / "tests" / "components" / component
|
||||
if not tests_dir.exists():
|
||||
continue
|
||||
|
||||
if not tests_dir.exists():
|
||||
return {"should_run": "false"}
|
||||
# Look for test files on preferred platforms
|
||||
test_files = list(tests_dir.glob("test.*.yaml"))
|
||||
if not test_files:
|
||||
continue
|
||||
|
||||
# Look for test files
|
||||
test_files = list(tests_dir.glob("test.*.yaml"))
|
||||
if not test_files:
|
||||
return {"should_run": "false"}
|
||||
|
||||
# Try each preferred platform in order
|
||||
for preferred_platform in PLATFORM_PREFERENCE:
|
||||
# Check if component has tests for any preferred platform
|
||||
for test_file in test_files:
|
||||
parts = test_file.stem.split(".")
|
||||
if len(parts) >= 2:
|
||||
platform = parts[1]
|
||||
if platform == preferred_platform:
|
||||
return {
|
||||
"should_run": "true",
|
||||
"component": component,
|
||||
"test_file": test_file.name,
|
||||
"platform": platform,
|
||||
}
|
||||
if platform in PLATFORM_PREFERENCE:
|
||||
components_with_tests.append(component)
|
||||
# Select the most preferred platform across all components
|
||||
if selected_platform is None or PLATFORM_PREFERENCE.index(
|
||||
platform
|
||||
) < PLATFORM_PREFERENCE.index(selected_platform):
|
||||
selected_platform = platform
|
||||
break
|
||||
|
||||
# If no components have tests, don't run memory impact
|
||||
if not components_with_tests:
|
||||
return {"should_run": "false"}
|
||||
|
||||
# Use the most preferred platform found, or fall back to esp8266-ard
|
||||
platform = selected_platform or "esp8266-ard"
|
||||
|
||||
# Fall back to first test file
|
||||
test_file = test_files[0]
|
||||
parts = test_file.stem.split(".")
|
||||
platform = parts[1] if len(parts) >= 2 else "esp32-idf"
|
||||
return {
|
||||
"should_run": "true",
|
||||
"component": component,
|
||||
"test_file": test_file.name,
|
||||
"components": components_with_tests,
|
||||
"platform": platform,
|
||||
"use_merged_config": "true",
|
||||
}
|
||||
|
||||
|
||||
@@ -386,8 +378,8 @@ def main() -> None:
|
||||
if component not in directly_changed_components
|
||||
]
|
||||
|
||||
# Detect single component change for memory impact analysis
|
||||
memory_impact = detect_single_component_for_memory_impact(args.branch)
|
||||
# Detect components for memory impact analysis (merged config)
|
||||
memory_impact = detect_memory_impact_config(args.branch)
|
||||
|
||||
# Build output
|
||||
output: dict[str, Any] = {
|
||||
|
Reference in New Issue
Block a user