forked from espressif/esp-idf
ci: add generate metrics of the target examples count
This commit is contained in:
+15
-15
@@ -16,18 +16,18 @@ workflow:
|
||||
# Place the default settings in `.gitlab/ci/common.yml` instead
|
||||
|
||||
include:
|
||||
- '.gitlab/ci/danger.yml'
|
||||
- '.gitlab/ci/common.yml'
|
||||
- '.gitlab/ci/rules.yml'
|
||||
- '.gitlab/ci/upload_cache.yml'
|
||||
- '.gitlab/ci/docs.yml'
|
||||
- '.gitlab/ci/static-code-analysis.yml'
|
||||
- '.gitlab/ci/pre_commit.yml'
|
||||
- '.gitlab/ci/pre_check.yml'
|
||||
- '.gitlab/ci/build.yml'
|
||||
- '.gitlab/ci/integration_test.yml'
|
||||
- '.gitlab/ci/host-test.yml'
|
||||
- '.gitlab/ci/deploy.yml'
|
||||
- '.gitlab/ci/post_deploy.yml'
|
||||
- '.gitlab/ci/retry_failed_jobs.yml'
|
||||
- '.gitlab/ci/test-win.yml'
|
||||
- ".gitlab/ci/danger.yml"
|
||||
- ".gitlab/ci/common.yml"
|
||||
- ".gitlab/ci/rules.yml"
|
||||
- ".gitlab/ci/upload_cache.yml"
|
||||
- ".gitlab/ci/docs.yml"
|
||||
- ".gitlab/ci/static-code-analysis.yml"
|
||||
- ".gitlab/ci/pre_commit.yml"
|
||||
- ".gitlab/ci/pre_check.yml"
|
||||
- ".gitlab/ci/build.yml"
|
||||
- ".gitlab/ci/integration_test.yml"
|
||||
- ".gitlab/ci/host-test.yml"
|
||||
- ".gitlab/ci/deploy.yml"
|
||||
- ".gitlab/ci/post_deploy.yml"
|
||||
- ".gitlab/ci/retry_failed_jobs.yml"
|
||||
- ".gitlab/ci/test-win.yml"
|
||||
|
||||
@@ -3,6 +3,27 @@
|
||||
image: $ESP_ENV_IMAGE
|
||||
tags: [ deploy ]
|
||||
|
||||
.metrics_template:
|
||||
stage: deploy
|
||||
tags: [ fast_run, shiny ]
|
||||
image: python:3.13-slim
|
||||
dependencies: []
|
||||
needs: []
|
||||
variables:
|
||||
PIP_CACHE_DIR: ".cache/pip"
|
||||
# Metrics - related env vars
|
||||
ESP_METRICS_PROJECT_URL: "$CI_PROJECT_URL"
|
||||
ESP_METRICS_PROJECT_ID: "$CI_PROJECT_ID"
|
||||
ESP_METRICS_COMMIT_SHA: "$PIPELINE_COMMIT_SHA"
|
||||
ESP_METRICS_BRANCH_NAME: "$CI_COMMIT_REF_NAME"
|
||||
cache:
|
||||
key: metrics-pip
|
||||
paths:
|
||||
- .cache/pip
|
||||
before_script:
|
||||
- echo "Installing esp-metrics-cli tool"
|
||||
- pip install "esp-metrics-cli>=0.3,<1"
|
||||
|
||||
check_submodule_sync:
|
||||
extends:
|
||||
- .deploy_job_template
|
||||
@@ -82,3 +103,13 @@ upload_junit_report:
|
||||
junit: XUNIT_RESULT_*.xml
|
||||
expire_in: 1 week
|
||||
when: always
|
||||
|
||||
target-examples-count-metrics:
|
||||
extends:
|
||||
- .metrics_template
|
||||
allow_failure: true
|
||||
script:
|
||||
- echo "Generating ESP-IDF examples count metrics"
|
||||
- cd tools/ci/metrics/examples_count
|
||||
- python3 generate_metrics.py
|
||||
- esp-metrics-cli upload -d schema.yaml -i metrics.json
|
||||
|
||||
@@ -27,6 +27,9 @@ tools/ci/idf_build_apps_dump_soc_caps.py
|
||||
tools/ci/idf_ci_local/**/*
|
||||
tools/ci/idf_ci_utils.py
|
||||
tools/ci/idf_pytest/**/*
|
||||
tools/ci/metrics/examples_count/example.metrics.json
|
||||
tools/ci/metrics/examples_count/generate_metrics.py
|
||||
tools/ci/metrics/examples_count/schema.yaml
|
||||
tools/ci/mirror-submodule-update.sh
|
||||
tools/ci/multirun_with_pyenv.sh
|
||||
tools/ci/mypy_ignore_list.txt
|
||||
|
||||
@@ -77,6 +77,7 @@ tools/ci/generate_rules.py
|
||||
tools/ci/get-full-sources.sh
|
||||
tools/ci/get_supported_examples.sh
|
||||
tools/ci/gitlab_yaml_linter.py
|
||||
tools/ci/metrics/examples_count/generate_metrics.py
|
||||
tools/ci/mirror-submodule-update.sh
|
||||
tools/ci/multirun_with_pyenv.sh
|
||||
tools/ci/push_to_github.sh
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"examples_count": {
|
||||
"data": [
|
||||
{
|
||||
"component": "storage",
|
||||
"target": "ESP32-C3",
|
||||
"count": 7
|
||||
},
|
||||
{
|
||||
"component": "peripherals",
|
||||
"target": "ESP32",
|
||||
"count": 25
|
||||
},
|
||||
{
|
||||
"component": "peripherals",
|
||||
"target": "ESP32-C2",
|
||||
"count": 18
|
||||
},
|
||||
{
|
||||
"component": "peripherals",
|
||||
"target": "ESP32-C3",
|
||||
"count": 22
|
||||
}
|
||||
],
|
||||
"total_examples_count": 153
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
ESP-IDF Examples Count Metrics Generator
|
||||
|
||||
This script scans all ESP-IDF examples and generates a list of records,
|
||||
where each record contains:
|
||||
- component: the component name (e.g., 'wifi', 'bluetooth', 'storage')
|
||||
- target: the supported ESP32 chip variant (e.g., 'ESP32', 'ESP32-C3', 'ESP32-S3')
|
||||
- count: the number of examples for that component-target combination
|
||||
|
||||
The script parses README.md files in example directories to extract supported targets
|
||||
from the "Supported Targets" table format.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def extract_supported_targets_from_readme(readme_path: Path) -> list[str]:
|
||||
"""
|
||||
Extract supported targets from a README.md file.
|
||||
|
||||
Looks for the "Supported Targets" table in the README header
|
||||
and parses the list of supported ESP32 variants.
|
||||
|
||||
Expected table format::
|
||||
|
||||
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ... |
|
||||
| ----------------- | ----- | -------- | -------- | ... |
|
||||
|
||||
:param readme_path: Path to a README.md file
|
||||
:return: List of supported targets (e.g. ``["ESP32", "ESP32-C3", "ESP32-S3"]``)
|
||||
"""
|
||||
if not readme_path.exists():
|
||||
return []
|
||||
|
||||
try:
|
||||
for line in readme_path.read_text(encoding='utf-8').splitlines():
|
||||
if not line.strip().startswith('| Supported Targets'):
|
||||
continue
|
||||
parts = [p.strip() for p in line.split('|')[2:]]
|
||||
return [p for p in parts if p and p not in ('-', '--')]
|
||||
except Exception as e:
|
||||
print(f'Warning: Could not parse {readme_path}: {e}')
|
||||
return []
|
||||
|
||||
|
||||
def process_example_dir(component: str, example_dir: Path, results: dict[str, dict[str, list[str]]]) -> None:
|
||||
"""Process a single example directory (with README)."""
|
||||
readme = example_dir / 'README.md'
|
||||
if not readme.exists():
|
||||
return
|
||||
targets = extract_supported_targets_from_readme(readme)
|
||||
if targets:
|
||||
path = f'{component}/{example_dir.name}'
|
||||
results[component][path] = targets
|
||||
print(f' Found example: {path} ({len(targets)} targets)')
|
||||
|
||||
|
||||
def process_nested_example(component: str, parent_dir: Path, results: dict[str, dict[str, list[str]]]) -> None:
|
||||
"""Process nested sub-examples inside a parent directory."""
|
||||
for sub in parent_dir.iterdir():
|
||||
if not sub.is_dir() or sub.name.startswith('.'):
|
||||
continue
|
||||
readme = sub / 'README.md'
|
||||
if not readme.exists():
|
||||
continue
|
||||
targets = extract_supported_targets_from_readme(readme)
|
||||
if targets:
|
||||
path = f'{component}/{parent_dir.name}/{sub.name}'
|
||||
results[component][path] = targets
|
||||
|
||||
|
||||
def find_examples(examples_root: Path) -> dict[str, dict[str, list[str]]]:
|
||||
"""
|
||||
Traverse the ESP-IDF examples directory and collect supported targets.
|
||||
|
||||
:param examples_root: Path to the ``examples`` directory
|
||||
:return: Nested dictionary of the form::
|
||||
|
||||
{
|
||||
"component": {
|
||||
"component/example": ["ESP32", "ESP32-C3"],
|
||||
"component/nested/example": ["ESP32-S3"],
|
||||
},
|
||||
...
|
||||
}
|
||||
"""
|
||||
results: dict[str, dict[str, list[str]]] = defaultdict(dict)
|
||||
if not examples_root.exists():
|
||||
print(f'Error: Examples directory not found: {examples_root}')
|
||||
return {}
|
||||
|
||||
for component_dir in examples_root.iterdir():
|
||||
if not component_dir.is_dir() or component_dir.name.startswith('.'):
|
||||
continue
|
||||
component = component_dir.name
|
||||
print(f'Analyzing component: {component}')
|
||||
|
||||
for item in component_dir.iterdir():
|
||||
if not item.is_dir() or item.name.startswith('.'):
|
||||
continue
|
||||
if (item / 'README.md').exists():
|
||||
process_example_dir(component, item, results)
|
||||
else:
|
||||
process_nested_example(component, item, results)
|
||||
|
||||
return dict(results)
|
||||
|
||||
|
||||
def generate_metrics(esp_idf_path: str) -> dict:
|
||||
"""
|
||||
Generate ESP-IDF examples count metrics.
|
||||
|
||||
:param esp_idf_path: Path to the ESP-IDF repository root
|
||||
:return: Dictionary with examples count data in a list format
|
||||
"""
|
||||
examples_data = find_examples(Path(esp_idf_path) / 'examples')
|
||||
if not examples_data:
|
||||
return {'examples_count': {'data': [], 'total_examples_count': 0}}
|
||||
|
||||
counts: dict[tuple[str, str], int] = defaultdict(int)
|
||||
total_examples = 0
|
||||
|
||||
for component, examples in examples_data.items():
|
||||
for example_path, targets in examples.items():
|
||||
for target in targets:
|
||||
counts[(component, target)] += 1
|
||||
total_examples += len(examples)
|
||||
|
||||
examples_count_list = []
|
||||
for (component, target), count in sorted(counts.items()):
|
||||
examples_count_list.append(
|
||||
{
|
||||
'component': component,
|
||||
'target': target,
|
||||
'count': count,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
'examples_count': {
|
||||
'data': examples_count_list,
|
||||
'total_examples_count': total_examples,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""CLI to generate metrics."""
|
||||
esp_idf_path = os.environ.get('IDF_PATH', os.getcwd())
|
||||
|
||||
if not Path(esp_idf_path, 'examples').exists():
|
||||
print(f'Error: ESP-IDF root not found at {esp_idf_path}')
|
||||
sys.exit(1)
|
||||
|
||||
print(f'Using ESP-IDF path: {esp_idf_path}')
|
||||
metrics = generate_metrics(esp_idf_path)
|
||||
|
||||
output_file = Path('metrics.json')
|
||||
output_file.write_text(json.dumps(metrics, indent=2))
|
||||
print(f'\nMetrics generated successfully and saved to {output_file}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
# ESP-IDF Examples Count Metrics Schema Definitions
|
||||
# This file defines the schema and validation rules for ESP-IDF examples count metrics
|
||||
# We use JSON Schema for annotating and validating JSON documents' structure.
|
||||
# Read more about the JSON Schema: https://json-schema.org/understanding-json-schema/about#what-is-a-schema
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema"
|
||||
# Note: Each key under 'properties' will correspond to an independent document in the DB.
|
||||
# For example, 'examples_count' will be stored as a separate document with its data. See the example.metrics.json how the output will look like.
|
||||
properties:
|
||||
examples_count:
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
title: "ESP-IDF Examples Count Data"
|
||||
description: "List of example counts for each component-target combination"
|
||||
items:
|
||||
properties:
|
||||
component:
|
||||
type: string
|
||||
title: "Component Name"
|
||||
description: "The component name (e.g., 'wifi', 'bluetooth', 'storage')"
|
||||
target:
|
||||
type: string
|
||||
title: "Target Name"
|
||||
description: "The ESP32 target (e.g., 'ESP32', 'ESP32-C3', 'ESP32-S3')"
|
||||
count:
|
||||
type: integer
|
||||
title: "Examples Count"
|
||||
description: "Number of examples for this component-target combination"
|
||||
total_examples_count:
|
||||
type: integer
|
||||
title: "Total Examples Count"
|
||||
description: "Total number of ESP-IDF examples across all components"
|
||||
Reference in New Issue
Block a user