diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3eb4c8d3f1..554a49bd13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -331,9 +331,8 @@ jobs: id: build run: | . venv/bin/activate - BENCHMARK_LIB=$(python script/setup_codspeed_lib.py) - BENCHMARK_LIB="$BENCHMARK_LIB" \ - script/cpp_benchmark.py --all --build-only 2>&1 | tee /tmp/bench-build.log + export BENCHMARK_LIB_CONFIG=$(python script/setup_codspeed_lib.py) + script/cpp_benchmark.py --all --build-only 2>&1 | tee /tmp/bench-build.log BINARY=$(tail -1 /tmp/bench-build.log) echo "binary=$BINARY" >> $GITHUB_OUTPUT diff --git a/script/cpp_benchmark.py b/script/cpp_benchmark.py index 06377ae51f..36980fff88 100755 --- a/script/cpp_benchmark.py +++ b/script/cpp_benchmark.py @@ -26,12 +26,26 @@ PLATFORMIO_OPTIONS = { "-DUSE_TIME_TIMEZONE", # enable timezone code paths "-g", # debug symbols for profiling ], + # Use deep+ LDF mode to ensure PlatformIO detects the benchmark + # library dependency from nested includes. + "lib_ldf_mode": "deep+", } def run_benchmarks(selected_components: list[str], build_only: bool = False) -> int: - # Allow CI to override the benchmark library (e.g. with CodSpeed's fork) - benchmark_lib = os.environ.get("BENCHMARK_LIB", PLATFORMIO_GOOGLE_BENCHMARK_LIB) + # Allow CI to override the benchmark library (e.g. with CodSpeed's fork). + # BENCHMARK_LIB_CONFIG is a JSON string from setup_codspeed_lib.py + # containing {"lib_path": "/path/to/google_benchmark"}. + lib_config_json = os.environ.get("BENCHMARK_LIB_CONFIG") + + if lib_config_json: + import json + + lib_config = json.loads(lib_config_json) + benchmark_lib = f"benchmark=symlink://{lib_config['lib_path']}" + else: + benchmark_lib = PLATFORMIO_GOOGLE_BENCHMARK_LIB + return build_and_run( selected_components=selected_components, tests_dir=BENCHMARKS_DIR, diff --git a/script/setup_codspeed_lib.py b/script/setup_codspeed_lib.py index 611abb4525..322a8069a6 100755 --- a/script/setup_codspeed_lib.py +++ b/script/setup_codspeed_lib.py @@ -2,13 +2,14 @@ """Set up CodSpeed's google_benchmark fork as a PlatformIO library. CodSpeed requires their codspeed-cpp fork for CPU simulation instrumentation. -This script clones the repo and creates a PlatformIO-compatible library.json -that combines the google_benchmark and codspeed core sources. +This script clones the repo and creates a flat PlatformIO-compatible library +by combining google_benchmark sources and codspeed core sources. Usage: python script/setup_codspeed_lib.py [--output-dir DIR] -Prints the PlatformIO library path (symlink:// URL) to stdout. +Prints JSON to stdout with lib_path and build_flags for cpp_benchmark.py. +Git output goes to stderr. """ from __future__ import annotations @@ -16,7 +17,9 @@ from __future__ import annotations import argparse import json from pathlib import Path +import shutil import subprocess +import sys # Pin to a specific commit for reproducibility CODSPEED_CPP_REPO = "https://github.com/CodSpeedHQ/codspeed-cpp.git" @@ -25,44 +28,99 @@ CODSPEED_CPP_SHA = "d6b4111428ae1f1667fec9bff009522378d5d347" DEFAULT_OUTPUT_DIR = "/tmp/codspeed-cpp" -def setup_codspeed_lib(output_dir: Path) -> str: - """Clone codspeed-cpp and create PlatformIO library layout. +def setup_codspeed_lib(output_dir: Path) -> None: + """Clone codspeed-cpp and create a flat PlatformIO library. Args: output_dir: Directory to clone into - - Returns: - PlatformIO library path (symlink:// URL) """ if not (output_dir / ".git").exists(): subprocess.run( ["git", "clone", CODSPEED_CPP_REPO, str(output_dir)], check=True, + stdout=sys.stderr, + stderr=sys.stderr, + ) + # Checkout pinned SHA and init submodules in one pass + subprocess.run( + ["git", "-C", str(output_dir), "checkout", CODSPEED_CPP_SHA], + check=True, + capture_output=True, + ) + subprocess.run( + [ + "git", + "-C", + str(output_dir), + "submodule", + "update", + "--init", + "--recursive", + ], + check=True, + stdout=sys.stderr, + stderr=sys.stderr, ) - subprocess.run( - ["git", "-C", str(output_dir), "checkout", CODSPEED_CPP_SHA], - check=True, - capture_output=True, - ) benchmark_dir = output_dir / "google_benchmark" core_dir = output_dir / "core" + instrument_hooks_dir = core_dir / "instrument-hooks" / "includes" - # Create library.json combining google_benchmark + codspeed core + # Read version from core/CMakeLists.txt (needed by walltime.cpp) + version = "0.0.0" + cmake_file = core_dir / "CMakeLists.txt" + if cmake_file.exists(): + for line in cmake_file.read_text().splitlines(): + if line.startswith("set(CODSPEED_VERSION"): + version = line.split()[1].rstrip(")") + break + + # PlatformIO doesn't compile .cc files — rename to .cpp + for cc_file in (benchmark_dir / "src").glob("*.cc"): + cpp_file = cc_file.with_suffix(".cpp") + if not cpp_file.exists(): + cc_file.rename(cpp_file) + + # Copy codspeed core sources and headers into google_benchmark/src/ + # so PlatformIO compiles everything as one library. + # .cpp files get a codspeed_ prefix to avoid name collisions. + # .h files keep their original names since they're referenced by includes. + for src_file in (core_dir / "src").glob("*"): + if src_file.suffix == ".cpp": + dest = benchmark_dir / "src" / f"codspeed_{src_file.name}" + elif src_file.suffix == ".h": + dest = benchmark_dir / "src" / src_file.name + else: + continue + if not dest.exists(): + shutil.copy2(src_file, dest) + + # Copy instrument-hooks C source (provides instrument_hooks_* symbols) + hooks_c = instrument_hooks_dir.parent / "dist" / "core.c" + if hooks_c.exists(): + dest = benchmark_dir / "src" / "instrument_hooks.c" + if not dest.exists(): + shutil.copy2(hooks_c, dest) + + # Resolve the ESPHome project root for CODSPEED_ROOT_DIR + project_root = Path(__file__).resolve().parent.parent + + # Create library.json library_json = { "name": "benchmark", "version": "0.0.0", "build": { "flags": [ f"-I{core_dir / 'include'}", - f"-I{core_dir / 'instrument-hooks'}", + f"-I{instrument_hooks_dir}", "-DHAVE_STD_REGEX", "-DHAVE_STEADY_CLOCK", "-DBENCHMARK_STATIC_DEFINE", - ], - "srcFilter": [ - "+", - f"+<{core_dir / 'src' / '*.cpp'}>", + "-DCODSPEED_ENABLED", + "-DCODSPEED_SIMULATION", + f'-DCODSPEED_VERSION=\\"{version}\\"', + f'-DCODSPEED_ROOT_DIR=\\"{project_root}\\"', + '-DCODSPEED_MODE_DISPLAY=\\"simulation\\"', ], "includeDir": "include", }, @@ -72,7 +130,11 @@ def setup_codspeed_lib(output_dir: Path) -> str: json.dumps(library_json, indent=2) + "\n" ) - return f"symlink://{benchmark_dir}" + # Output JSON config for cpp_benchmark.py + result = { + "lib_path": str(benchmark_dir), + } + print(json.dumps(result)) def main() -> None: @@ -85,8 +147,7 @@ def main() -> None: ) args = parser.parse_args() - lib_path = setup_codspeed_lib(args.output_dir) - print(lib_path) + setup_codspeed_lib(args.output_dir) if __name__ == "__main__":