1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-16 15:37:51 +02:00

[ci] Automatic Flash/RAM impact analysis

This commit is contained in:
J. Nick Koston 2025-10-14 13:09:01 -10:00
parent c4eeed7f7e
commit 25a6202bb9
No known key found for this signature in database
4 changed files with 215 additions and 285 deletions

View File

@ -179,6 +179,7 @@ jobs:
changed-components: ${{ steps.determine.outputs.changed-components }}
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
component-test-count: ${{ steps.determine.outputs.component-test-count }}
memory_impact: ${{ steps.determine.outputs.memory-impact }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@ -207,6 +208,7 @@ jobs:
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
integration-tests:
name: Run integration tests
@ -510,6 +512,118 @@ jobs:
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
if: always()
memory-impact-target-branch:
name: Build target branch for memory impact
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true'
outputs:
ram_usage: ${{ steps.extract.outputs.ram_usage }}
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out target branch
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.base_ref }}
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
- name: Compile test configuration and extract memory usage
id: extract
run: |
. venv/bin/activate
component="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).component }}"
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
test_file="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).test_file }}"
echo "Compiling $component for $platform using $test_file"
python script/test_build_components.py -e compile -c "$component" -t "$platform" --no-grouping 2>&1 | \
python script/ci_memory_impact_extract.py --output-env
memory-impact-pr-branch:
name: Build PR branch for memory impact
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true'
outputs:
ram_usage: ${{ steps.extract.outputs.ram_usage }}
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out PR branch
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
- name: Compile test configuration and extract memory usage
id: extract
run: |
. venv/bin/activate
component="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).component }}"
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
test_file="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).test_file }}"
echo "Compiling $component for $platform using $test_file"
python script/test_build_components.py -e compile -c "$component" -t "$platform" --no-grouping 2>&1 | \
python script/ci_memory_impact_extract.py --output-env
memory-impact-comment:
name: Comment memory impact
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
- memory-impact-target-branch
- memory-impact-pr-branch
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true'
permissions:
contents: read
pull-requests: write
steps:
- name: Check out code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Post or update PR comment
env:
GH_TOKEN: ${{ github.token }}
COMPONENT: ${{ fromJSON(needs.determine-jobs.outputs.memory_impact).component }}
PLATFORM: ${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}
TARGET_RAM: ${{ needs.memory-impact-target-branch.outputs.ram_usage }}
TARGET_FLASH: ${{ needs.memory-impact-target-branch.outputs.flash_usage }}
PR_RAM: ${{ needs.memory-impact-pr-branch.outputs.ram_usage }}
PR_FLASH: ${{ needs.memory-impact-pr-branch.outputs.flash_usage }}
run: |
. venv/bin/activate
python script/ci_memory_impact_comment.py \
--pr-number "${{ github.event.pull_request.number }}" \
--component "$COMPONENT" \
--platform "$PLATFORM" \
--target-ram "$TARGET_RAM" \
--target-flash "$TARGET_FLASH" \
--pr-ram "$PR_RAM" \
--pr-flash "$PR_FLASH"
ci-status:
name: CI Status
runs-on: ubuntu-24.04
@ -525,6 +639,9 @@ jobs:
- test-build-components-splitter
- test-build-components-split
- pre-commit-ci-lite
- memory-impact-target-branch
- memory-impact-pr-branch
- memory-impact-comment
if: always()
steps:
- name: Success

View File

@ -1,150 +0,0 @@
---
name: Memory Impact Analysis
on:
pull_request:
paths:
- "esphome/components/**"
- "esphome/core/**"
permissions:
contents: read
pull-requests: write
env:
DEFAULT_PYTHON: "3.11"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
detect-single-component:
name: Detect single component change
runs-on: ubuntu-24.04
outputs:
should_run: ${{ steps.detect.outputs.should_run }}
component: ${{ steps.detect.outputs.component }}
test_file: ${{ steps.detect.outputs.test_file }}
platform: ${{ steps.detect.outputs.platform }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install PyYAML
- name: Detect single component change
id: detect
run: |
python script/ci_memory_impact_detector.py
build-target-branch:
name: Build target branch
runs-on: ubuntu-24.04
needs: detect-single-component
if: needs.detect-single-component.outputs.should_run == 'true'
outputs:
ram_usage: ${{ steps.extract.outputs.ram_usage }}
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out target branch
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.base_ref }}
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Install ESPHome
run: |
pip install -e .
- name: Cache platformio
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-memory-${{ needs.detect-single-component.outputs.platform }}-${{ hashFiles('platformio.ini') }}
- name: Compile test configuration and extract memory usage
id: extract
run: |
component="${{ needs.detect-single-component.outputs.component }}"
platform="${{ needs.detect-single-component.outputs.platform }}"
test_file="${{ needs.detect-single-component.outputs.test_file }}"
echo "Compiling $component for $platform using $test_file"
python script/test_build_components.py -e compile -c "$component" -t "$platform" --no-grouping 2>&1 | \
python script/ci_memory_impact_extract.py --output-env
build-pr-branch:
name: Build PR branch
runs-on: ubuntu-24.04
needs: detect-single-component
if: needs.detect-single-component.outputs.should_run == 'true'
outputs:
ram_usage: ${{ steps.extract.outputs.ram_usage }}
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out PR branch
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Install ESPHome
run: |
pip install -e .
- name: Cache platformio
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-memory-${{ needs.detect-single-component.outputs.platform }}-${{ hashFiles('platformio.ini') }}
- name: Compile test configuration and extract memory usage
id: extract
run: |
component="${{ needs.detect-single-component.outputs.component }}"
platform="${{ needs.detect-single-component.outputs.platform }}"
test_file="${{ needs.detect-single-component.outputs.test_file }}"
echo "Compiling $component for $platform using $test_file"
python script/test_build_components.py -e compile -c "$component" -t "$platform" --no-grouping 2>&1 | \
python script/ci_memory_impact_extract.py --output-env
comment-results:
name: Comment memory impact
runs-on: ubuntu-24.04
needs:
- detect-single-component
- build-target-branch
- build-pr-branch
if: needs.detect-single-component.outputs.should_run == 'true'
steps:
- name: Check out code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Post or update PR comment
env:
GH_TOKEN: ${{ github.token }}
COMPONENT: ${{ needs.detect-single-component.outputs.component }}
PLATFORM: ${{ needs.detect-single-component.outputs.platform }}
TARGET_RAM: ${{ needs.build-target-branch.outputs.ram_usage }}
TARGET_FLASH: ${{ needs.build-target-branch.outputs.flash_usage }}
PR_RAM: ${{ needs.build-pr-branch.outputs.ram_usage }}
PR_FLASH: ${{ needs.build-pr-branch.outputs.flash_usage }}
run: |
python script/ci_memory_impact_comment.py \
--pr-number "${{ github.event.pull_request.number }}" \
--component "$COMPONENT" \
--platform "$PLATFORM" \
--target-ram "$TARGET_RAM" \
--target-flash "$TARGET_FLASH" \
--pr-ram "$PR_RAM" \
--pr-flash "$PR_FLASH"

View File

@ -1,134 +0,0 @@
#!/usr/bin/env python3
"""Detect if a PR changes exactly one component for memory impact analysis.
This script is used by the CI workflow to determine if a PR should trigger
memory impact analysis. The analysis only runs when:
1. Exactly one component has changed (not counting core changes)
2. The component has at least one test configuration
The script outputs GitHub Actions environment variables to control the workflow.
"""
from __future__ import annotations
from pathlib import Path
import sys
# Add esphome to path
sys.path.insert(0, str(Path(__file__).parent.parent))
# pylint: disable=wrong-import-position
from script.ci_helpers import write_github_output
from script.helpers import ESPHOME_COMPONENTS_PATH, changed_files
# Platform preference order for memory impact analysis
# Ordered by production relevance and memory constraint importance
PLATFORM_PREFERENCE = [
"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)
]
def find_test_for_component(component: str) -> tuple[str | None, str | None]:
"""Find a test configuration for the given component.
Prefers platforms based on PLATFORM_PREFERENCE order.
Args:
component: Component name
Returns:
Tuple of (test_file_name, platform) or (None, None) if no test found
"""
tests_dir = Path(__file__).parent.parent / "tests" / "components" / component
if not tests_dir.exists():
return None, None
# Look for test files
test_files = list(tests_dir.glob("test.*.yaml"))
if not test_files:
return None, None
# Try each preferred platform in order
for preferred_platform in PLATFORM_PREFERENCE:
for test_file in test_files:
parts = test_file.stem.split(".")
if len(parts) >= 2:
platform = parts[1]
if platform == preferred_platform:
return test_file.name, platform
# 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 test_file.name, platform
def detect_single_component_change() -> None:
"""Detect if exactly one component changed and output GitHub Actions variables."""
files = changed_files()
# Find all changed components (excluding core)
changed_components = set()
for file in files:
if file.startswith(ESPHOME_COMPONENTS_PATH):
parts = file.split("/")
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"]:
changed_components.add(component)
# Only proceed if exactly one component changed
if len(changed_components) != 1:
print(
f"Found {len(changed_components)} component(s) changed, skipping memory analysis"
)
write_github_output({"should_run": "false"})
return
component = list(changed_components)[0]
print(f"Detected single component change: {component}")
# Find a test configuration for this component
test_file, platform = find_test_for_component(component)
if not test_file:
print(f"No test configuration found for {component}, skipping memory analysis")
write_github_output({"should_run": "false"})
return
print(f"Found test: {test_file} for platform: {platform}")
print("Memory impact analysis will run")
write_github_output(
{
"should_run": "true",
"component": component,
"test_file": test_file,
"platform": platform,
}
)
if __name__ == "__main__":
detect_single_component_change()

View File

@ -10,7 +10,13 @@ what files have changed. It outputs JSON with the following structure:
"clang_format": true/false,
"python_linters": true/false,
"changed_components": ["component1", "component2", ...],
"component_test_count": 5
"component_test_count": 5,
"memory_impact": {
"should_run": "true/false",
"component": "component_name",
"test_file": "test.esp32-idf.yaml",
"platform": "esp32-idf"
}
}
The CI workflow uses this information to:
@ -20,6 +26,7 @@ The CI workflow uses this information to:
- Skip or run Python linters (ruff, flake8, pylint, pyupgrade)
- Determine which components to test individually
- Decide how to split component tests (if there are many)
- Run memory impact analysis when exactly one component changes
Usage:
python script/determine-jobs.py [-b BRANCH]
@ -212,6 +219,92 @@ def _any_changed_file_endswith(branch: str | None, extensions: tuple[str, ...])
return any(file.endswith(extensions) for file in changed_files(branch))
def detect_single_component_for_memory_impact(
changed_components: list[str],
) -> dict[str, Any]:
"""Detect if exactly one component changed for memory impact analysis.
Args:
changed_components: List of changed component names
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)
"""
# Platform preference order for memory impact analysis
# Ordered by production relevance and memory constraint importance
PLATFORM_PREFERENCE = [
"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)
]
# Skip base bus components as they're used across many builds
filtered_components = [
c for c in changed_components if c not in ["i2c", "spi", "uart", "modbus"]
]
# Only proceed if exactly one component changed
if len(filtered_components) != 1:
return {"should_run": "false"}
component = filtered_components[0]
# Find a test configuration for this component
tests_dir = Path(root_path) / "tests" / "components" / component
if not tests_dir.exists():
return {"should_run": "false"}
# 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:
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,
}
# 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,
"platform": platform,
}
def main() -> None:
"""Main function that determines which CI jobs to run."""
parser = argparse.ArgumentParser(
@ -247,6 +340,9 @@ def main() -> None:
and any(component_test_dir.glob("test.*.yaml"))
]
# Detect single component change for memory impact analysis
memory_impact = detect_single_component_for_memory_impact(changed_components)
# Build output
output: dict[str, Any] = {
"integration_tests": run_integration,
@ -256,6 +352,7 @@ def main() -> None:
"changed_components": changed_components,
"changed_components_with_tests": changed_components_with_tests,
"component_test_count": len(changed_components_with_tests),
"memory_impact": memory_impact,
}
# Output as JSON