ci: add generate metrics of the target examples count

This commit is contained in:
Aleksei Apaseev
2025-08-28 15:57:53 +08:00
parent 8f1e7bc4e0
commit fb1fae9627
7 changed files with 281 additions and 15 deletions
+15 -15
View File
@@ -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"
+31
View File
@@ -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
+3
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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"