#!/usr/bin/env python3 import argparse import sys import os import shutil import subprocess benchmark_configs = { "perf32b": { "description": "Benchmark config ARM (so3) Emulated - 32 bit", "image_name": "ghcr.io/smartobjectoriented/so3-lvperf32b:main", "defaults_file": "lv_conf_perf32b.defaults", }, "perf64b": { "description": "Benchmark config ARM (so3) Emulated - 64 bit", "image_name": "ghcr.io/smartobjectoriented/so3-lvperf64b:main", "defaults_file": "lv_conf_perf64b.defaults", }, } lvgl_test_dir = os.path.dirname(os.path.realpath(__file__)) lvgl_root_dir = os.path.dirname(lvgl_test_dir) def main() -> int: epilog = """This program runs the LVGL demo benchmark In order to provide timing consistency between host computers, these runs are run in an ARM emulated environment inside QEMU. For the runtime environment, SO3 is used which is a lightweight, ARM-based operating system. Right now, this script requires a host linux computer as we depend on `losetup` which is used to set up and control loop devices. """ parser = argparse.ArgumentParser( description="Run LVGL benchmark tests.", epilog=epilog ) parser.add_argument( "--config", nargs=1, choices=benchmark_configs.keys(), help="The benchmark config to use. Available configs: " + ", ".join(benchmark_configs.keys()), required=False, ) parser.add_argument( "--pull", action="store_true", default=False, help="Pull latest images from registry before running tests", ) parser.add_argument( "--clean", action="store_true", default=False, help="Clean existing build artifacts before operation", ) parser.add_argument( "--auto-clean", action="store_true", default=False, help="Automatically clean build directories", ) parser.add_argument( "actions", nargs="*", choices=["generate", "run"], help="generate: generate the lv_conf.h from the `configs/ci/perf` folder, run: run the benchmark with the provided config", ) args = parser.parse_args() if not args.actions: print("Error: At least one action must be specified") parser.print_help() return 1 configs = [] if args.config: configs = args.config else: configs = benchmark_configs.keys() for config in configs: if args.clean: clean(config) if "generate" in args.actions: generate_benchmark_files(config) if "run" in args.actions: run_benchmark(config, args.pull) if args.auto_clean: clean(config) return 0 def get_build_dir(config_name: str) -> str: """Given the config name, return the build directory path.""" build_dir_name = f"build_benchmark_{config_name}" return os.path.join(lvgl_test_dir, build_dir_name) def get_container_name(config_name: str) -> str: """ Returns the docker container name based on the config name """ return f"lv_benchmark_{config_name}" def get_docker_volumes(config_name: str) -> list[str]: """ Returns all docker volume names that should be created """ return [get_build_cache_volume(config_name), get_disk_cache_volume(config_name)] def get_build_cache_volume(config_name: str) -> str: """ Returns the docker volume name for storing cmake generated files """ return f"{get_container_name(config_name)}_build_cache" def get_disk_cache_volume(config_name: str) -> str: """ Returns the docker volume name for storing the virtual disks """ return f"{get_container_name(config_name)}_disk_cache" def create_dir_if_not_exists(build_dir: str) -> bool: """Create directory if it doesn't exist""" created_build_dir = False if os.path.exists(build_dir): if not os.path.isdir(build_dir): raise ValueError(f"{build_dir} exists but is not a directory") else: os.makedirs(build_dir, exist_ok=True) created_build_dir = True return created_build_dir def generate_config(config_name: str) -> None: """Generate lv_conf.h from the defaults file for the given config""" print(f"Generating config for {config_name}") build_dir = get_build_dir(config_name) create_dir_if_not_exists(build_dir) defaults_file = benchmark_configs[config_name]["defaults_file"] defaults_path = os.path.join(lvgl_root_dir, "configs", "ci", "perf", defaults_file) template_path = os.path.join(lvgl_root_dir, "lv_conf_template.h") assert os.path.exists(defaults_path) assert template_path output_path = os.path.join(build_dir, "lv_conf.h") generate_script = os.path.join(lvgl_root_dir, "scripts", "generate_lv_conf.py") cmd = [ sys.executable, generate_script, "--template", template_path, "--config", output_path, "--defaults", defaults_path, build_dir, ] subprocess.check_call(cmd) print(f"Generated lv_conf.h at {output_path}") def generate_benchmark_files(config_name: str) -> None: """Generate the necessary files for running benchmark inside so3""" print() print() label = f"Generating benchmark files for: {config_name}: {benchmark_configs[config_name]['description']}" print("=" * len(label)) print(label) print("=" * len(label)) build_dir = get_build_dir(config_name) create_dir_if_not_exists(build_dir) generate_config(config_name) def clean(config_name: str) -> None: """Clean build directory and docker resources""" print(f"Cleaning resources for {config_name}") build_dir = get_build_dir(config_name) container_name = get_container_name(config_name) if os.path.exists(build_dir): shutil.rmtree(build_dir) print(f"Removed build directory: {build_dir}") subprocess.check_call( ["docker", "rm", "-f", container_name], ) for volume in get_docker_volumes(config_name): subprocess.check_call( ["docker", "volume", "remove", "-f", volume], ) def run_benchmark(config_name: str, pull: bool) -> None: """Run the benchmark using docker""" def volume(src, dst): return ["-v", f"{src}:{dst}"] def so3_usr_lib(path): return f"/so3/usr/lib/{path}" so3_usr_build = "/so3/usr/build" persistence_dir = "/persistence" container_name = get_container_name(config_name) build_dir = get_build_dir(config_name) for v in get_docker_volumes(config_name): subprocess.check_call(["docker", "volume", "create", v]) lvgl_src_path = os.path.join(lvgl_root_dir, "src") lvgl_h_path = os.path.join(lvgl_root_dir, "lvgl.h") lvgl_demos_path = os.path.join(lvgl_root_dir, "demos") lv_conf_path = os.path.join(build_dir, "lv_conf.h") if not os.path.exists(lv_conf_path): generate_config(config_name) assert os.path.exists(lv_conf_path) docker_image_name = benchmark_configs[config_name]["image_name"] volumes = [ # This is necessary to create a loop device volume("/dev", "/dev"), # Replace container's lvgl source and lv_conf volume(lvgl_h_path, so3_usr_lib("lvgl/lvgl.h")), volume(lvgl_src_path, so3_usr_lib("lvgl/src")), volume(lvgl_demos_path, so3_usr_lib("lvgl/demos")), volume(lv_conf_path, so3_usr_lib("lv_conf.h")), # Cache build and disk folders so we don't regenerate everything in consecutive runs volume(get_build_cache_volume(config_name), so3_usr_build), volume(get_disk_cache_volume(config_name), persistence_dir), ] interactive = "-it" if sys.stdout.isatty() else "-t" command = [ "docker", "run", "--rm", "--privileged", interactive, "--name", container_name, ] if pull: command.append("--pull=always") for v in volumes: command.extend(v) command.append(docker_image_name) print() print() label = f"Running benchmark: {config_name}: {benchmark_configs[config_name]['description']}" print("=" * len(label)) print(label) print("=" * len(label)) subprocess.check_call(command) if __name__ == "__main__": sys.exit(main())