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:
parent
72ec9b672e
commit
c4eeed7f7e
150
.github/workflows/memory-impact.yml
vendored
Normal file
150
.github/workflows/memory-impact.yml
vendored
Normal file
@ -0,0 +1,150 @@
|
||||
---
|
||||
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"
|
23
script/ci_helpers.py
Executable file
23
script/ci_helpers.py
Executable file
@ -0,0 +1,23 @@
|
||||
"""Common helper functions for CI scripts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def write_github_output(outputs: dict[str, str | int]) -> None:
|
||||
"""Write multiple outputs to GITHUB_OUTPUT or stdout.
|
||||
|
||||
When running in GitHub Actions, writes to the GITHUB_OUTPUT file.
|
||||
When running locally, writes to stdout for debugging.
|
||||
|
||||
Args:
|
||||
outputs: Dictionary of key-value pairs to write
|
||||
"""
|
||||
github_output = os.environ.get("GITHUB_OUTPUT")
|
||||
if github_output:
|
||||
with open(github_output, "a", encoding="utf-8") as f:
|
||||
f.writelines(f"{key}={value}\n" for key, value in outputs.items())
|
||||
else:
|
||||
for key, value in outputs.items():
|
||||
print(f"{key}={value}")
|
244
script/ci_memory_impact_comment.py
Executable file
244
script/ci_memory_impact_comment.py
Executable file
@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Post or update a PR comment with memory impact analysis results.
|
||||
|
||||
This script creates or updates a GitHub PR comment with memory usage changes.
|
||||
It uses the GitHub CLI (gh) to manage comments and maintains a single comment
|
||||
that gets updated on subsequent runs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Comment marker to identify our memory impact comments
|
||||
COMMENT_MARKER = "<!-- esphome-memory-impact-analysis -->"
|
||||
|
||||
|
||||
def format_bytes(bytes_value: int) -> str:
|
||||
"""Format bytes value with appropriate unit.
|
||||
|
||||
Args:
|
||||
bytes_value: Number of bytes
|
||||
|
||||
Returns:
|
||||
Formatted string (e.g., "1.5 KB", "256 bytes")
|
||||
"""
|
||||
if bytes_value < 1024:
|
||||
return f"{bytes_value} bytes"
|
||||
if bytes_value < 1024 * 1024:
|
||||
return f"{bytes_value / 1024:.2f} KB"
|
||||
return f"{bytes_value / (1024 * 1024):.2f} MB"
|
||||
|
||||
|
||||
def format_change(before: int, after: int) -> str:
|
||||
"""Format memory change with delta and percentage.
|
||||
|
||||
Args:
|
||||
before: Memory usage before change
|
||||
after: Memory usage after change
|
||||
|
||||
Returns:
|
||||
Formatted string with delta and percentage
|
||||
"""
|
||||
delta = after - before
|
||||
percentage = 0.0 if before == 0 else (delta / before) * 100
|
||||
|
||||
# Format delta with sign
|
||||
delta_str = f"+{format_bytes(delta)}" if delta >= 0 else format_bytes(delta)
|
||||
|
||||
# Format percentage with sign
|
||||
if percentage > 0:
|
||||
pct_str = f"+{percentage:.2f}%"
|
||||
elif percentage < 0:
|
||||
pct_str = f"{percentage:.2f}%"
|
||||
else:
|
||||
pct_str = "0.00%"
|
||||
|
||||
# Add emoji indicator
|
||||
if delta > 0:
|
||||
emoji = "📈"
|
||||
elif delta < 0:
|
||||
emoji = "📉"
|
||||
else:
|
||||
emoji = "➡️"
|
||||
|
||||
return f"{emoji} {delta_str} ({pct_str})"
|
||||
|
||||
|
||||
def create_comment_body(
|
||||
component: str,
|
||||
platform: str,
|
||||
target_ram: int,
|
||||
target_flash: int,
|
||||
pr_ram: int,
|
||||
pr_flash: int,
|
||||
) -> str:
|
||||
"""Create the comment body with memory impact analysis.
|
||||
|
||||
Args:
|
||||
component: Component name
|
||||
platform: Platform name
|
||||
target_ram: RAM usage in target branch
|
||||
target_flash: Flash usage in target branch
|
||||
pr_ram: RAM usage in PR branch
|
||||
pr_flash: Flash usage in PR branch
|
||||
|
||||
Returns:
|
||||
Formatted comment body
|
||||
"""
|
||||
ram_change = format_change(target_ram, pr_ram)
|
||||
flash_change = format_change(target_flash, pr_flash)
|
||||
|
||||
return f"""{COMMENT_MARKER}
|
||||
## Memory Impact Analysis
|
||||
|
||||
**Component:** `{component}`
|
||||
**Platform:** `{platform}`
|
||||
|
||||
| Metric | Target Branch | This PR | Change |
|
||||
|--------|--------------|---------|--------|
|
||||
| **RAM** | {format_bytes(target_ram)} | {format_bytes(pr_ram)} | {ram_change} |
|
||||
| **Flash** | {format_bytes(target_flash)} | {format_bytes(pr_flash)} | {flash_change} |
|
||||
|
||||
---
|
||||
*This analysis runs automatically when a single component changes. Memory usage is measured from a representative test configuration.*
|
||||
"""
|
||||
|
||||
|
||||
def find_existing_comment(pr_number: str) -> str | None:
|
||||
"""Find existing memory impact comment on the PR.
|
||||
|
||||
Args:
|
||||
pr_number: PR number
|
||||
|
||||
Returns:
|
||||
Comment ID if found, None otherwise
|
||||
"""
|
||||
try:
|
||||
# List all comments on the PR
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gh",
|
||||
"pr",
|
||||
"view",
|
||||
pr_number,
|
||||
"--json",
|
||||
"comments",
|
||||
"--jq",
|
||||
".comments[]",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Parse comments and look for our marker
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
comment = json.loads(line)
|
||||
if COMMENT_MARKER in comment.get("body", ""):
|
||||
return str(comment["id"])
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error finding existing comment: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def post_or_update_comment(pr_number: str, comment_body: str) -> bool:
|
||||
"""Post a new comment or update existing one.
|
||||
|
||||
Args:
|
||||
pr_number: PR number
|
||||
comment_body: Comment body text
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
# Look for existing comment
|
||||
existing_comment_id = find_existing_comment(pr_number)
|
||||
|
||||
try:
|
||||
if existing_comment_id:
|
||||
# Update existing comment
|
||||
print(f"Updating existing comment {existing_comment_id}", file=sys.stderr)
|
||||
subprocess.run(
|
||||
[
|
||||
"gh",
|
||||
"api",
|
||||
f"/repos/{{owner}}/{{repo}}/issues/comments/{existing_comment_id}",
|
||||
"-X",
|
||||
"PATCH",
|
||||
"-f",
|
||||
f"body={comment_body}",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
else:
|
||||
# Post new comment
|
||||
print("Posting new comment", file=sys.stderr)
|
||||
subprocess.run(
|
||||
["gh", "pr", "comment", pr_number, "--body", comment_body],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
print("Comment posted/updated successfully", file=sys.stderr)
|
||||
return True
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error posting/updating comment: {e}", file=sys.stderr)
|
||||
if e.stderr:
|
||||
print(f"stderr: {e.stderr.decode()}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
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("--platform", required=True, help="Platform name")
|
||||
parser.add_argument(
|
||||
"--target-ram", type=int, required=True, help="Target branch RAM usage"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-flash", type=int, required=True, help="Target branch flash usage"
|
||||
)
|
||||
parser.add_argument("--pr-ram", type=int, required=True, help="PR branch RAM usage")
|
||||
parser.add_argument(
|
||||
"--pr-flash", type=int, required=True, help="PR branch flash usage"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Create comment body
|
||||
comment_body = create_comment_body(
|
||||
component=args.component,
|
||||
platform=args.platform,
|
||||
target_ram=args.target_ram,
|
||||
target_flash=args.target_flash,
|
||||
pr_ram=args.pr_ram,
|
||||
pr_flash=args.pr_flash,
|
||||
)
|
||||
|
||||
# Post or update comment
|
||||
success = post_or_update_comment(args.pr_number, comment_body)
|
||||
|
||||
return 0 if success else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
134
script/ci_memory_impact_detector.py
Executable file
134
script/ci_memory_impact_detector.py
Executable file
@ -0,0 +1,134 @@
|
||||
#!/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()
|
104
script/ci_memory_impact_extract.py
Executable file
104
script/ci_memory_impact_extract.py
Executable file
@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract memory usage statistics from ESPHome build output.
|
||||
|
||||
This script parses the PlatformIO build output to extract RAM and flash
|
||||
usage statistics for a compiled component. It's used by the CI workflow to
|
||||
compare memory usage between branches.
|
||||
|
||||
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)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import re
|
||||
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
|
||||
|
||||
|
||||
def extract_from_compile_output(output_text: str) -> tuple[int | None, int | None]:
|
||||
"""Extract memory usage from PlatformIO compile output.
|
||||
|
||||
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
|
||||
|
||||
Returns:
|
||||
Tuple of (ram_bytes, flash_bytes) or (None, None) if not found
|
||||
"""
|
||||
ram_match = re.search(
|
||||
r"RAM:\s+\[.*?\]\s+\d+\.\d+%\s+\(used\s+(\d+)\s+bytes", output_text
|
||||
)
|
||||
flash_match = re.search(
|
||||
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))
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extract memory usage from ESPHome build output"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-env",
|
||||
action="store_true",
|
||||
help="Output to GITHUB_OUTPUT environment file",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Read compile output from stdin
|
||||
compile_output = sys.stdin.read()
|
||||
|
||||
# Extract memory usage
|
||||
ram_bytes, flash_bytes = extract_from_compile_output(compile_output)
|
||||
|
||||
if ram_bytes is None or flash_bytes is None:
|
||||
print("Failed to extract memory usage from compile output", file=sys.stderr)
|
||||
print("Expected lines like:", file=sys.stderr)
|
||||
print(
|
||||
" RAM: [==== ] 36.1% (used 29548 bytes from 81920 bytes)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
" Flash: [=== ] 34.0% (used 348511 bytes from 1023984 bytes)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
print(f"RAM: {ram_bytes} bytes", file=sys.stderr)
|
||||
print(f"Flash: {flash_bytes} bytes", file=sys.stderr)
|
||||
|
||||
if args.output_env:
|
||||
# Output to GitHub Actions
|
||||
write_github_output(
|
||||
{
|
||||
"ram_usage": ram_bytes,
|
||||
"flash_usage": flash_bytes,
|
||||
}
|
||||
)
|
||||
else:
|
||||
print(f"{ram_bytes},{flash_bytes}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
Loading…
x
Reference in New Issue
Block a user