Skip to content

Commit 731fa5e

Browse files
authored
Merge pull request #512 from espressif/ci/bsp_wall_benchmark
ci(runner): Add benchmark example and print benchmark results
2 parents 531ad57 + 183916d commit 731fa5e

19 files changed

+789
-6
lines changed

.github/workflows/build-run-applications.yml

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,25 @@ name: Build ESP-BSP apps
55

66
on:
77
pull_request:
8-
types: [opened, reopened, synchronize]
8+
types: [opened, reopened, synchronize, labeled]
9+
push:
10+
branches:
11+
- master
12+
workflow_dispatch:
13+
inputs:
14+
WFType:
15+
description: 'Workflow type'
16+
required: true
17+
default: 'Build + Tests'
18+
type: choice
19+
options:
20+
- Build + Tests
21+
- Build + Tests + Benchmark
22+
23+
# Cancel previous CI, if running and changed label or pushed PR (Prevent to wait for runners)
24+
concurrency:
25+
group: pr-${{ github.event.pull_request.number }}
26+
cancel-in-progress: true
927

1028
jobs:
1129
build:
@@ -89,7 +107,7 @@ jobs:
89107
90108
run-target:
91109
name: Run apps
92-
if: github.repository_owner == 'espressif' && needs.prepare.outputs.build_only != '1'
110+
if: github.repository_owner == 'espressif' && !contains(github.event.pull_request.labels.*.name, 'Build only')
93111
needs: build
94112
strategy:
95113
fail-fast: false
@@ -151,7 +169,9 @@ jobs:
151169
target: "esp32s3"
152170
env:
153171
TEST_RESULT_NAME: test_results_${{ matrix.runner.target }}_${{ matrix.runner.marker }}_${{ matrix.idf_ver }}
172+
BENCHMARK_RESULT_NAME: benchmark_${{ matrix.runner.target }}_${{ matrix.runner.marker }}_${{ matrix.idf_ver }}
154173
TEST_RESULT_FILE: test_results_${{ matrix.runner.target }}_${{ matrix.runner.marker }}_${{ matrix.idf_ver }}.xml
174+
PYTEST_BENCHMARK_IGNORE: ${{ (contains(github.event.pull_request.labels.*.name, 'Run benchmark') || contains(inputs.WFType, 'Build + Tests + Benchmark') || github.ref_name == 'master') && format(' ') || format('--ignore=examples/display_lvgl_benchmark') }}
155175
runs-on: [self-hosted, Linux, bspwall]
156176
container:
157177
image: python:3.11-bookworm
@@ -165,22 +185,49 @@ jobs:
165185
- name: Install Python packages
166186
env:
167187
PIP_EXTRA_INDEX_URL: "https://dl.espressif.com/pypi/"
168-
run: pip install --prefer-binary cryptography pytest-embedded pytest-embedded-serial-esp pytest-embedded-idf pytest-custom_exit_code
188+
run: |
189+
pip install --prefer-binary cryptography pytest-embedded pytest-embedded-serial-esp pytest-embedded-idf pytest-custom_exit_code
190+
- name: Download latest results
191+
uses: actions/download-artifact@v4
192+
with:
193+
pattern: benchmark_*
194+
path: benchmark/
169195
- name: Run apps
170196
run: |
171-
pytest --suppress-no-test-exit-code --ignore-glob '*/managed_components/*' --ignore=.github --junit-xml=${{ env.TEST_RESULT_FILE }} --target=${{ matrix.runner.target }} -m ${{ matrix.runner.marker }} --build-dir=build_${{ matrix.runner.runs-on }}
197+
pytest --suppress-no-test-exit-code --ignore-glob '*/managed_components/*' --ignore=.github --junit-xml=${{ env.TEST_RESULT_FILE }} --target=${{ matrix.runner.target }} -m ${{ matrix.runner.marker }} --build-dir=build_${{ matrix.runner.runs-on }} ${{ env.PYTEST_BENCHMARK_IGNORE }}
172198
- name: Upload test results
173199
uses: actions/upload-artifact@v4
174200
if: always()
175201
with:
176202
name: ${{ env.TEST_RESULT_NAME }}
177-
path: ${{ env.TEST_RESULT_FILE }}
203+
path: |
204+
${{ env.TEST_RESULT_FILE }}
205+
benchmark_*.md
206+
benchmark_*.json
207+
benchmark.json
208+
- name: Upload test results
209+
uses: actions/upload-artifact@v4
210+
if: github.ref_name == 'master'
211+
with:
212+
name: ${{ env.BENCHMARK_RESULT_NAME }}
213+
path: |
214+
benchmark_*.md
215+
benchmark_*.json
216+
- name: Update benchmark release
217+
uses: pyTooling/Actions/releaser@r0
218+
if: github.ref_name == 'master'
219+
with:
220+
token: ${{ secrets.GITHUB_TOKEN }}
221+
files: |
222+
benchmark_*.json
223+
benchmark_*.md
224+
tag: benchmark-latest
178225

179226
publish-results:
180227
name: Publish Test results
181228
needs:
182229
- run-target
183-
if: github.repository_owner == 'espressif' && always() && github.event_name == 'pull_request' && needs.prepare.outputs.build_only == '0'
230+
if: github.repository_owner == 'espressif' && always() && github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'Build only')
184231
runs-on: ubuntu-22.04
185232
steps:
186233
- name: Download Test results
@@ -192,3 +239,31 @@ jobs:
192239
uses: EnricoMi/publish-unit-test-result-action@v2
193240
with:
194241
files: test_results/**/*.xml
242+
- name: Find benchmark result files
243+
if: (contains(github.event.pull_request.labels.*.name, 'Run benchmark') || contains(inputs.WFType, 'Build + Tests + Benchmark') || github.ref_name == 'master')
244+
id: find_files
245+
run: |
246+
OUTPUT_FILE="combined_benchmarks.md"
247+
echo "" > $OUTPUT_FILE
248+
python <<EOF
249+
import glob
250+
251+
files = sorted(glob.glob("test_results/**/benchmark_*.md"))
252+
print(files)
253+
output_file = "combined_benchmarks.md"
254+
255+
with open(output_file, "w", encoding="utf-8") as outfile:
256+
for file in files:
257+
with open(file, "r", encoding="utf-8") as infile:
258+
outfile.write(infile.read() + "\n\n")
259+
260+
print(f"Merged {len(files)} files into {output_file}")
261+
EOF
262+
263+
echo "output_file=$OUTPUT_FILE" >> "$GITHUB_ENV"
264+
- name: Comment PR
265+
if: (contains(github.event.pull_request.labels.*.name, 'Run benchmark') || contains(inputs.WFType, 'Build + Tests + Benchmark') || github.ref_name == 'master')
266+
uses: thollander/actions-comment-pull-request@v3
267+
with:
268+
comment-tag: benchmark_results
269+
file-path: ${{ env.output_file }}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# For more information about build system see
2+
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
3+
# The following five lines of boilerplate have to be in your project's
4+
# CMakeLists in this exact order for cmake to work correctly
5+
cmake_minimum_required(VERSION 3.5)
6+
7+
set(COMPONENTS main) # "Trim" the build. Include the minimal set of components; main and anything it depends on.
8+
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
9+
add_compile_options("-Wno-attributes") # For LVGL code
10+
project(display_lvgl_benchmark)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Display LVGL Benchmark
2+
3+
This example runs the LVGL benchmark demo to measure graphical performance on Espressif and M5Stack boards. It is used in CI for selected pull requests (based on labels) and after merging changes into the master branch.
4+
5+
## Main Features
6+
- Can be triggered by adding the "Run benchmark" label to a PR.
7+
- The measured values in a PR are compared against the master branch and posted as a comment, highlighting any differences.
8+
- Benchmark results for the master branch are stored in BSP releases.
9+
10+
## How to use the example
11+
12+
This example can be used as standalone example too.
13+
14+
### Hardware Required
15+
16+
* ESP32-S3-LCD-EV-Board or ESP32-S3-LCD-EV-Board-2
17+
* USB-C Cable
18+
19+
### Compile and flash
20+
21+
```
22+
idf.py -p COMx build flash monitor
23+
```
24+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
idf_component_register(
2+
SRCS "main.c"
3+
INCLUDE_DIRS ".")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
description: BSP Display rotation example
2+
dependencies:
3+
esp32_p4_function_ev_board:
4+
version: '*'
5+
override_path: ../../../bsp/esp32_p4_function_ev_board
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
3+
*
4+
* SPDX-License-Identifier: CC0-1.0
5+
*/
6+
7+
#include "freertos/FreeRTOS.h"
8+
#include "freertos/task.h"
9+
#include "esp_log.h"
10+
11+
#include "lv_demos.h"
12+
#include "bsp/esp-bsp.h"
13+
14+
static char *TAG = "app_main";
15+
16+
#define LOG_MEM_INFO (0)
17+
18+
void app_main(void)
19+
{
20+
/* Initialize display and LVGL */
21+
#if defined(BSP_LCD_SUB_BOARD_2_H_RES)
22+
/* Only for esp32_s3_lcd_ev_board */
23+
bsp_display_cfg_t cfg = {
24+
.lvgl_port_cfg = ESP_LVGL_PORT_INIT_CONFIG(),
25+
};
26+
cfg.lvgl_port_cfg.task_stack = 10000;
27+
bsp_display_start_with_config(&cfg);
28+
#else
29+
bsp_display_start();
30+
#endif
31+
32+
/* Set display brightness to 100% */
33+
bsp_display_backlight_on();
34+
35+
ESP_LOGI(TAG, "Display LVGL demo");
36+
bsp_display_lock(0);
37+
lv_demo_benchmark(); /* A demo to measure the performance of LVGL or to compare different settings. */
38+
bsp_display_unlock();
39+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Name, Type, SubType, Offset, Size, Flags
2+
# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild
3+
nvs, data, nvs, 0x9000, 0x6000,
4+
phy_init, data, phy, 0xf000, 0x1000,
5+
factory, app, factory, 0x10000, 0x160000,
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
2+
# SPDX-License-Identifier: CC0-1.0
3+
4+
import datetime
5+
import json
6+
from pathlib import Path
7+
import pytest
8+
from pytest_embedded import Dut
9+
import urllib.request
10+
11+
BENCHMARK_RELEASES_URL = "https://github.com/espressif/esp-bsp/releases/download/benchmark-latest"
12+
13+
14+
def write_to_file(board, ext, text):
15+
with open("benchmark_" + board + ext, "a") as file:
16+
file.write(text)
17+
18+
19+
def read_json_file(board):
20+
try:
21+
url = f"{BENCHMARK_RELEASES_URL}/benchmark_{board}.json"
22+
with urllib.request.urlopen(url) as file:
23+
return json.load(file)
24+
except urllib.error.HTTPError:
25+
return []
26+
except json.JSONDecodeError:
27+
return []
28+
29+
30+
def find_test_results(json_obj, test):
31+
if json_obj:
32+
for t in json_obj["tests"]:
33+
if t["Name"] == test:
34+
return t
35+
36+
37+
def get_test_diff(test1, test2, name, positive):
38+
if not test1 or not test2 or not test1[name] or not test2[name]:
39+
return ""
40+
test1[name] = test1[name].replace("%", "")
41+
test2[name] = test2[name].replace("%", "")
42+
diff = int(test1[name]) - int(test2[name])
43+
if diff == 0:
44+
return ""
45+
else:
46+
if positive:
47+
color = "red" if diff < 0 else "green"
48+
else:
49+
color = "green" if diff < 0 else "red"
50+
sign = "+" if diff > 0 else ""
51+
return f"*<span style=\"color:{color}\"><sub>({sign}{diff})</sub></span>*"
52+
53+
54+
@pytest.mark.esp_box_3
55+
@pytest.mark.esp32_p4_function_ev_board
56+
@pytest.mark.esp32_s3_eye
57+
@pytest.mark.esp32_s3_lcd_ev_board
58+
@pytest.mark.esp32_s3_lcd_ev_board_2
59+
@pytest.mark.m5dial
60+
@pytest.mark.m5stack_core_s3
61+
@pytest.mark.m5stack_core_s3_se
62+
def test_example(dut: Dut, request) -> None:
63+
date = datetime.datetime.now()
64+
board = request.node.callspec.id
65+
66+
# Wait for start benchmark
67+
dut.expect_exact('app_main: Display LVGL demo')
68+
dut.expect_exact('main_task: Returned from app_main()')
69+
70+
file_path = Path(f"benchmark_" + board + ".md")
71+
file_path.unlink(missing_ok=True)
72+
file_path = Path(f"benchmark_" + board + ".json")
73+
file_path.unlink(missing_ok=True)
74+
75+
output = {
76+
"date": date.strftime('%d.%m.%Y %H:%M'),
77+
"board": board
78+
}
79+
80+
# Write board into file
81+
write_to_file(board, ".md", f"# Benchmark for BOARD " + board + "\n\n")
82+
write_to_file(board, ".md", f"**DATE:** " + date.strftime('%d.%m.%Y %H:%M') + "\n\n")
83+
# Get LVGL version write it into file
84+
outdata = dut.expect(r'Benchmark Summary \((.*) \)', timeout=200)
85+
output["LVGL"] = outdata[1].decode()
86+
write_to_file(board, ".md", f"**LVGL version:** " + outdata[1].decode() + "\n\n")
87+
outdata = dut.expect(r'Name, Avg. CPU, Avg. FPS, Avg. time, render time, flush time', timeout=200)
88+
write_to_file(board, ".md", f"| Name | Avg. CPU | Avg. FPS | Avg. time | render time | flush time |\n")
89+
write_to_file(board, ".md", f"| ---- | :------: | :------: | :-------: | :---------: | :--------: |\n") # noqa: E203
90+
91+
last_results = read_json_file(board)
92+
93+
# Benchmark lines
94+
output["tests"] = []
95+
for x in range(17):
96+
outdata = dut.expect(r'([\w \.]+),[ ]?(\d+%),[ ]?(\d+),[ ]?(\d+),[ ]?(\d+),[ ]?(\d+)', timeout=200)
97+
test_entry = {
98+
"Name": outdata[1].decode(),
99+
"Avg. CPU": outdata[2].decode(),
100+
"Avg. FPS": outdata[3].decode(),
101+
"Avg. time": outdata[4].decode(),
102+
"Render time": outdata[5].decode(),
103+
"Flush time": outdata[6].decode()
104+
}
105+
output["tests"].append(test_entry)
106+
107+
last_test_result = find_test_results(last_results, test_entry["Name"])
108+
write_to_file(board, ".md", f"| " +
109+
test_entry["Name"] + " | " +
110+
test_entry["Avg. CPU"] + " " + get_test_diff(test_entry, last_test_result, "Avg. CPU", False) + " | " +
111+
test_entry["Avg. FPS"] + " " + get_test_diff(test_entry, last_test_result, "Avg. FPS", True) + " | " +
112+
test_entry["Avg. time"] + " " + get_test_diff(test_entry, last_test_result, "Avg. time", False) + " | " +
113+
test_entry["Render time"] + " " + get_test_diff(test_entry, last_test_result, "Render time", False) + " | " +
114+
test_entry["Flush time"] + " " + get_test_diff(test_entry, last_test_result, "Flush time", False) + " |\n")
115+
116+
write_to_file(board, ".md", "\n")
117+
write_to_file(board, ".md", "***")
118+
write_to_file(board, ".md", "\n\n")
119+
120+
# Save JSON to file
121+
json_output = json.dumps(output, indent=4)
122+
write_to_file(board, ".json", json_output)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# This file was generated using idf.py save-defconfig. It can be edited manually.
2+
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
3+
#
4+
CONFIG_IDF_TARGET="esp32s3"
5+
CONFIG_ESPTOOLPY_FLASHMODE_QIO=y
6+
CONFIG_COMPILER_OPTIMIZATION_PERF=y
7+
CONFIG_PARTITION_TABLE_CUSTOM=y
8+
CONFIG_SPIRAM=y
9+
CONFIG_SPIRAM_MODE_OCT=y
10+
CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y
11+
CONFIG_SPIRAM_RODATA=y
12+
CONFIG_SPIRAM_SPEED_80M=y
13+
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
14+
CONFIG_FREERTOS_HZ=1000
15+
CONFIG_LV_MEM_SIZE_KILOBYTES=48
16+
CONFIG_LV_ATTRIBUTE_FAST_MEM_USE_IRAM=y
17+
CONFIG_LV_FONT_MONTSERRAT_12=y
18+
CONFIG_LV_FONT_MONTSERRAT_16=y
19+
CONFIG_LV_USE_DEMO_WIDGETS=y
20+
CONFIG_LV_USE_DEMO_BENCHMARK=y
21+
22+
# Enable logging
23+
CONFIG_LV_USE_LOG=y
24+
CONFIG_LV_LOG_PRINTF=y
25+
26+
## LVGL8 ##
27+
CONFIG_LV_USE_PERF_MONITOR=y
28+
CONFIG_LV_COLOR_16_SWAP=y
29+
CONFIG_LV_MEM_CUSTOM=y
30+
CONFIG_LV_MEMCPY_MEMSET_STD=y
31+
32+
## LVGL9 ##
33+
CONFIG_LV_CONF_SKIP=y
34+
35+
#CLIB default
36+
CONFIG_LV_USE_CLIB_MALLOC=y
37+
CONFIG_LV_USE_CLIB_SPRINTF=y
38+
CONFIG_LV_USE_CLIB_STRING=y
39+
40+
# Performance monitor
41+
CONFIG_LV_USE_OBSERVER=y
42+
CONFIG_LV_USE_SYSMON=y
43+
CONFIG_LV_USE_PERF_MONITOR=y

0 commit comments

Comments
 (0)