mirror of
https://github.com/espressif/esp-idf.git
synced 2026-04-27 19:13:21 +00:00
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
|
# Place the default settings in `.gitlab/ci/common.yml` instead
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- '.gitlab/ci/danger.yml'
|
- ".gitlab/ci/danger.yml"
|
||||||
- '.gitlab/ci/common.yml'
|
- ".gitlab/ci/common.yml"
|
||||||
- '.gitlab/ci/rules.yml'
|
- ".gitlab/ci/rules.yml"
|
||||||
- '.gitlab/ci/upload_cache.yml'
|
- ".gitlab/ci/upload_cache.yml"
|
||||||
- '.gitlab/ci/docs.yml'
|
- ".gitlab/ci/docs.yml"
|
||||||
- '.gitlab/ci/static-code-analysis.yml'
|
- ".gitlab/ci/static-code-analysis.yml"
|
||||||
- '.gitlab/ci/pre_commit.yml'
|
- ".gitlab/ci/pre_commit.yml"
|
||||||
- '.gitlab/ci/pre_check.yml'
|
- ".gitlab/ci/pre_check.yml"
|
||||||
- '.gitlab/ci/build.yml'
|
- ".gitlab/ci/build.yml"
|
||||||
- '.gitlab/ci/integration_test.yml'
|
- ".gitlab/ci/integration_test.yml"
|
||||||
- '.gitlab/ci/host-test.yml'
|
- ".gitlab/ci/host-test.yml"
|
||||||
- '.gitlab/ci/deploy.yml'
|
- ".gitlab/ci/deploy.yml"
|
||||||
- '.gitlab/ci/post_deploy.yml'
|
- ".gitlab/ci/post_deploy.yml"
|
||||||
- '.gitlab/ci/retry_failed_jobs.yml'
|
- ".gitlab/ci/retry_failed_jobs.yml"
|
||||||
- '.gitlab/ci/test-win.yml'
|
- ".gitlab/ci/test-win.yml"
|
||||||
|
|||||||
@@ -3,6 +3,27 @@
|
|||||||
image: $ESP_ENV_IMAGE
|
image: $ESP_ENV_IMAGE
|
||||||
tags: [ deploy ]
|
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:
|
check_submodule_sync:
|
||||||
extends:
|
extends:
|
||||||
- .deploy_job_template
|
- .deploy_job_template
|
||||||
@@ -82,3 +103,13 @@ upload_junit_report:
|
|||||||
junit: XUNIT_RESULT_*.xml
|
junit: XUNIT_RESULT_*.xml
|
||||||
expire_in: 1 week
|
expire_in: 1 week
|
||||||
when: always
|
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_local/**/*
|
||||||
tools/ci/idf_ci_utils.py
|
tools/ci/idf_ci_utils.py
|
||||||
tools/ci/idf_pytest/**/*
|
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/mirror-submodule-update.sh
|
||||||
tools/ci/multirun_with_pyenv.sh
|
tools/ci/multirun_with_pyenv.sh
|
||||||
tools/ci/mypy_ignore_list.txt
|
tools/ci/mypy_ignore_list.txt
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ tools/ci/generate_rules.py
|
|||||||
tools/ci/get-full-sources.sh
|
tools/ci/get-full-sources.sh
|
||||||
tools/ci/get_supported_examples.sh
|
tools/ci/get_supported_examples.sh
|
||||||
tools/ci/gitlab_yaml_linter.py
|
tools/ci/gitlab_yaml_linter.py
|
||||||
|
tools/ci/metrics/examples_count/generate_metrics.py
|
||||||
tools/ci/mirror-submodule-update.sh
|
tools/ci/mirror-submodule-update.sh
|
||||||
tools/ci/multirun_with_pyenv.sh
|
tools/ci/multirun_with_pyenv.sh
|
||||||
tools/ci/push_to_github.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