Skip to content

Commit 5c6f8f9

Browse files
committed
ci(runner): Add benchmark example and print benchmark results
1 parent 740a580 commit 5c6f8f9

19 files changed

+814
-6
lines changed

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

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,20 @@ 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
922

1023
jobs:
1124
build:
@@ -89,7 +102,7 @@ jobs:
89102
90103
run-target:
91104
name: Run apps
92-
if: github.repository_owner == 'espressif' && needs.prepare.outputs.build_only != '1'
105+
if: github.repository_owner == 'espressif' && !contains(github.event.pull_request.labels.*.name, 'Build only')
93106
needs: build
94107
strategy:
95108
fail-fast: false
@@ -151,6 +164,7 @@ jobs:
151164
target: "esp32s3"
152165
env:
153166
TEST_RESULT_NAME: test_results_${{ matrix.runner.target }}_${{ matrix.runner.marker }}_${{ matrix.idf_ver }}
167+
BENCHMARK_RESULT_NAME: benchmark_${{ matrix.runner.target }}_${{ matrix.runner.marker }}_${{ matrix.idf_ver }}
154168
TEST_RESULT_FILE: test_results_${{ matrix.runner.target }}_${{ matrix.runner.marker }}_${{ matrix.idf_ver }}.xml
155169
runs-on: [self-hosted, Linux, bspwall]
156170
container:
@@ -165,22 +179,58 @@ jobs:
165179
- name: Install Python packages
166180
env:
167181
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
182+
run: |
183+
echo "PYTEST_BENCHMARK_IGNORE=--ignore='examples/display_lvgl_benchmark'" >> $GITHUB_ENV
184+
pip install --prefer-binary cryptography pytest-embedded pytest-embedded-serial-esp pytest-embedded-idf pytest-custom_exit_code
185+
- name: Download latest results
186+
uses: actions/download-artifact@v4
187+
with:
188+
pattern: benchmark_*
189+
path: benchmark/
190+
- name: Set ignores
191+
if: contains(github.event.pull_request.labels.*.name, 'Run benchmark') || contains(inputs.WFType, 'Build + Tests + Benchmark') || github.ref_name == 'master'
192+
id: set_ignores
193+
run: |
194+
echo "PYTEST_BENCHMARK_IGNORE=" >> $GITHUB_ENV
195+
- name: Pull
196+
run: |
197+
git pull --rebase origin ${{ github.head_ref }} || echo "No remote changes to rebase"
169198
- name: Run apps
170199
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 }}
200+
pytest --suppress-no-test-exit-code --ignore-glob '*/managed_components/*' --ignore=.github ${{ env.PYTEST_BENCHMARK_IGNORE }} --junit-xml=${{ env.TEST_RESULT_FILE }} --target=${{ matrix.runner.target }} -m ${{ matrix.runner.marker }} --build-dir=build_${{ matrix.runner.runs-on }}
172201
- name: Upload test results
173202
uses: actions/upload-artifact@v4
174203
if: always()
175204
with:
176205
name: ${{ env.TEST_RESULT_NAME }}
177-
path: ${{ env.TEST_RESULT_FILE }}
206+
path: |
207+
${{ env.TEST_RESULT_FILE }}
208+
benchmark_*.md
209+
benchmark_*.json
210+
benchmark.json
211+
- name: Upload test results
212+
uses: actions/upload-artifact@v4
213+
if: github.ref_name == 'master'
214+
with:
215+
name: ${{ env.BENCHMARK_RESULT_NAME }}
216+
path: |
217+
benchmark_*.md
218+
benchmark_*.json
219+
- name: Update benchmark release
220+
uses: pyTooling/Actions/releaser@r0
221+
if: github.ref_name == 'master'
222+
with:
223+
token: ${{ secrets.GITHUB_TOKEN }}
224+
files: |
225+
benchmark_*.json
226+
benchmark_*.md
227+
tag: benchmark-latest
178228

179229
publish-results:
180230
name: Publish Test results
181231
needs:
182232
- run-target
183-
if: github.repository_owner == 'espressif' && always() && github.event_name == 'pull_request' && needs.prepare.outputs.build_only == '0'
233+
if: github.repository_owner == 'espressif' && always() && github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'Build only')
184234
runs-on: ubuntu-22.04
185235
steps:
186236
- name: Download Test results
@@ -192,3 +242,29 @@ jobs:
192242
uses: EnricoMi/publish-unit-test-result-action@v2
193243
with:
194244
files: test_results/**/*.xml
245+
- name: Find test result files
246+
id: find_files
247+
run: |
248+
OUTPUT_FILE="combined_benchmarks.md"
249+
echo "" > $OUTPUT_FILE
250+
python <<EOF
251+
import glob
252+
253+
files = sorted(glob.glob("test_results/**/benchmark_*.md"))
254+
print(files)
255+
output_file = "combined_benchmarks.md"
256+
257+
with open(output_file, "w", encoding="utf-8") as outfile:
258+
for file in files:
259+
with open(file, "r", encoding="utf-8") as infile:
260+
outfile.write(infile.read() + "\n\n")
261+
262+
print(f"Merged {len(files)} files into {output_file}")
263+
EOF
264+
265+
echo "output_file=$OUTPUT_FILE" >> "$GITHUB_ENV"
266+
- name: Comment PR
267+
uses: thollander/actions-comment-pull-request@v3
268+
with:
269+
comment-tag: benchmark_results
270+
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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Display LVGL Benchmark
2+
3+
This example shows LVGL internal benchmark demo.
4+
5+
## How to use the example
6+
7+
### Hardware Required
8+
9+
* ESP32-S3-LCD-EV-Board or ESP32-S3-LCD-EV-Board-2
10+
* USB-C Cable
11+
12+
### Compile and flash
13+
14+
```
15+
idf.py -p COMx build flash monitor
16+
```
17+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
set(LV_DEMO_DIR "")
2+
set(LV_DEMOS_SOURCES "")
3+
if(CONFIG_LV_USE_DEMO_BENCHMARK)
4+
list(APPEND LV_DEMO_DIR ../managed_components/lvgl__lvgl/demos)
5+
file(GLOB_RECURSE LV_DEMOS_SOURCES ${LV_DEMO_DIR}/*.c)
6+
endif()
7+
8+
idf_component_register(
9+
SRCS "main.c" ${LV_DEMOS_SOURCES}
10+
INCLUDE_DIRS "." ${LV_DEMO_DIR})
11+
12+
if(CONFIG_LV_USE_DEMO_BENCHMARK)
13+
set_source_files_properties(
14+
${LV_DEMOS_SOURCES}
15+
PROPERTIES COMPILE_OPTIONS
16+
-DLV_LVGL_H_INCLUDE_SIMPLE)
17+
endif()
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: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
2+
# SPDX-License-Identifier: CC0-1.0
3+
4+
import os
5+
import datetime
6+
import json
7+
from pathlib import Path
8+
import pytest
9+
from pytest_embedded import Dut
10+
import urllib.request
11+
12+
BENCHMARK_RELEASES_URL = "https://github.com/espressif/esp-bsp/releases/download/benchmark-latest"
13+
14+
15+
def write_to_file(board, ext, text):
16+
with open("benchmark_" + board + ext, "a") as file:
17+
file.write(text)
18+
19+
20+
def read_json_file(board):
21+
try:
22+
url = f"{BENCHMARK_RELEASES_URL}/benchmark_{board}.json"
23+
with urllib.request.urlopen(url) as file:
24+
return json.load(file)
25+
except json.JSONDecodeError:
26+
return []
27+
28+
29+
def write_json_file(board, data):
30+
repo_root = Path(__file__).resolve().parent
31+
while repo_root.name != "esp-bsp" and repo_root.parent != repo_root:
32+
repo_root = repo_root.parent
33+
file_path = f"{repo_root}/bsp/{board}/benchmark.json"
34+
try:
35+
os.remove(file_path)
36+
except OSError:
37+
pass
38+
try:
39+
with open(file_path, "a") as file:
40+
file.write(data)
41+
except OSError:
42+
pass
43+
44+
45+
def find_test_results(json_obj, test):
46+
if json_obj:
47+
for t in json_obj["tests"]:
48+
if t["Name"] == test:
49+
return t
50+
51+
52+
def get_test_diff(test1, test2, name, positive):
53+
if not test1 or not test2 or not test1[name] or not test2[name]:
54+
return ""
55+
test1[name] = test1[name].replace("%", "")
56+
test2[name] = test2[name].replace("%", "")
57+
diff = int(test1[name]) - int(test2[name])
58+
if diff == 0:
59+
return ""
60+
else:
61+
if positive:
62+
color = "red" if diff < 0 else "green"
63+
else:
64+
color = "green" if diff < 0 else "red"
65+
sign = "+" if diff > 0 else ""
66+
return f"*<span style=\"color:{color}\"><sub>({sign}{diff})</sub></span>*"
67+
68+
69+
@pytest.mark.esp_box_3
70+
@pytest.mark.esp32_p4_function_ev_board
71+
@pytest.mark.esp32_s3_eye
72+
@pytest.mark.esp32_s3_lcd_ev_board
73+
@pytest.mark.esp32_s3_lcd_ev_board_2
74+
@pytest.mark.m5dial
75+
@pytest.mark.m5stack_core_s3
76+
@pytest.mark.m5stack_core_s3_se
77+
def test_example(dut: Dut, request) -> None:
78+
date = datetime.datetime.now()
79+
board = request.node.callspec.id
80+
81+
# Wait for start benchmark
82+
dut.expect_exact('app_main: Display LVGL demo')
83+
dut.expect_exact('main_task: Returned from app_main()')
84+
85+
try:
86+
os.remove("benchmark_" + board + ".md")
87+
os.remove("benchmark_" + board + ".json")
88+
except OSError:
89+
pass
90+
91+
output = {
92+
"date": date.strftime('%d.%m.%Y %H:%M'),
93+
"board": board
94+
}
95+
96+
# Write board into file
97+
write_to_file(board, ".md", f"# Benchmark for BOARD " + board + "\n\n")
98+
write_to_file(board, ".md", f"**DATE:** " + date.strftime('%d.%m.%Y %H:%M') + "\n\n")
99+
# Get LVGL version write it into file
100+
outdata = dut.expect(r'Benchmark Summary \((.*) \)', timeout=200)
101+
output["LVGL"] = outdata[1].decode()
102+
write_to_file(board, ".md", f"**LVGL version:** " + outdata[1].decode() + "\n\n")
103+
outdata = dut.expect(r'Name, Avg. CPU, Avg. FPS, Avg. time, render time, flush time', timeout=200)
104+
write_to_file(board, ".md", f"| Name | Avg. CPU | Avg. FPS | Avg. time | render time | flush time |\n")
105+
write_to_file(board, ".md", f"| ---- | :------: | :------: | :-------: | :---------: | :--------: |\n") # noqa: E203
106+
107+
last_results = read_json_file(board)
108+
109+
# Benchmark lines
110+
output["tests"] = []
111+
for x in range(17):
112+
outdata = dut.expect(r'([\w \.]+),[ ]?(\d+%),[ ]?(\d+),[ ]?(\d+),[ ]?(\d+),[ ]?(\d+)', timeout=200)
113+
test_entry = {
114+
"Name": outdata[1].decode(),
115+
"Avg. CPU": outdata[2].decode(),
116+
"Avg. FPS": outdata[3].decode(),
117+
"Avg. time": outdata[4].decode(),
118+
"Render time": outdata[5].decode(),
119+
"Flush time": outdata[6].decode()
120+
}
121+
output["tests"].append(test_entry)
122+
123+
last_test_result = find_test_results(last_results, test_entry["Name"])
124+
write_to_file(board, ".md", f"| " +
125+
test_entry["Name"] + " | " +
126+
test_entry["Avg. CPU"] + " " + get_test_diff(test_entry, last_test_result, "Avg. CPU", False) + " | " +
127+
test_entry["Avg. FPS"] + " " + get_test_diff(test_entry, last_test_result, "Avg. FPS", True) + " | " +
128+
test_entry["Avg. time"] + " " + get_test_diff(test_entry, last_test_result, "Avg. time", False) + " | " +
129+
test_entry["Render time"] + " " + get_test_diff(test_entry, last_test_result, "Render time", False) + " | " +
130+
test_entry["Flush time"] + " " + get_test_diff(test_entry, last_test_result, "Flush time", False) + " |\n")
131+
132+
write_to_file(board, ".md", "\n")
133+
write_to_file(board, ".md", "***")
134+
write_to_file(board, ".md", "\n\n")
135+
136+
# Save JSON to file
137+
json_output = json.dumps(output, indent=4)
138+
write_to_file(board, ".json", json_output)
139+
write_json_file(board, json_output)

0 commit comments

Comments
 (0)