Clean up shared constants and setup script

- Move BASE_CODEGEN_COMPONENTS and USE_TIME_TIMEZONE_FLAG to test_helpers.py
- Use shared constants in both cpp_unit_test.py and cpp_benchmark.py
- Move json import to top level in cpp_benchmark.py
- Refactor setup_codspeed_lib.py into focused helper functions
- Combine clone + submodule init, use --shallow-submodules
This commit is contained in:
J. Nick Koston
2026-03-16 21:25:25 -10:00
parent f13513239d
commit 0fc3fc2776
4 changed files with 164 additions and 96 deletions
+12 -9
View File
@@ -2,12 +2,18 @@
"""Build and run C++ benchmarks for ESPHome components using Google Benchmark."""
import argparse
import json
import os
from pathlib import Path
import sys
from helpers import root_path
from test_helpers import PLATFORMIO_GOOGLE_BENCHMARK_LIB, build_and_run
from test_helpers import (
BASE_CODEGEN_COMPONENTS,
PLATFORMIO_GOOGLE_BENCHMARK_LIB,
USE_TIME_TIMEZONE_FLAG,
build_and_run,
)
# Path to /tests/benchmarks/components
BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "components"
@@ -15,15 +21,14 @@ BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "components"
# Path to /tests/benchmarks/core (always included, not a component)
CORE_BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "core"
# Components whose to_code should run during benchmark builds.
# core/host/logger are infrastructure. json is needed because its
# to_code adds the ArduinoJson library (it's auto-loaded by api but
# cpp_testing suppresses to_code for components not in this set).
BENCHMARK_CODEGEN_COMPONENTS = {"core", "host", "logger", "json"}
# Additional codegen components beyond the base set.
# json is needed because its to_code adds the ArduinoJson library
# (auto-loaded by api, but cpp_testing suppresses to_code unless listed).
BENCHMARK_CODEGEN_COMPONENTS = BASE_CODEGEN_COMPONENTS | {"json"}
PLATFORMIO_OPTIONS = {
"build_flags": [
"-DUSE_TIME_TIMEZONE", # enable timezone code paths
USE_TIME_TIMEZONE_FLAG,
"-g", # debug symbols for profiling
],
# Use deep+ LDF mode to ensure PlatformIO detects the benchmark
@@ -39,8 +44,6 @@ def run_benchmarks(selected_components: list[str], build_only: bool = False) ->
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:
+8 -10
View File
@@ -4,18 +4,16 @@ from pathlib import Path
import sys
from helpers import get_all_components, root_path
from test_helpers import PLATFORMIO_GOOGLE_TEST_LIB, build_and_run
from test_helpers import (
BASE_CODEGEN_COMPONENTS,
PLATFORMIO_GOOGLE_TEST_LIB,
USE_TIME_TIMEZONE_FLAG,
build_and_run,
)
# Path to /tests/components
COMPONENTS_TESTS_DIR: Path = Path(root_path) / "tests" / "components"
# Components whose to_code should run during C++ test builds.
# Most components don't need code generation for tests; only these
# essential ones (platform setup, logging, core config) are needed.
# Note: "core" is the esphome core config module (esphome/core/config.py),
# which registers under package name "core" not "esphome".
CPP_TESTING_CODEGEN_COMPONENTS = {"core", "host", "logger"}
PLATFORMIO_OPTIONS = {
"build_type": "debug",
"build_unflags": [
@@ -23,7 +21,7 @@ PLATFORMIO_OPTIONS = {
],
"build_flags": [
"-Og", # optimize for debug
"-DUSE_TIME_TIMEZONE", # enable timezone code paths for testing
USE_TIME_TIMEZONE_FLAG,
"-DESPHOME_DEBUG", # enable debug assertions
# Enable the address and undefined behavior sanitizers
"-fsanitize=address",
@@ -41,7 +39,7 @@ def run_tests(selected_components: list[str]) -> int:
return build_and_run(
selected_components=selected_components,
tests_dir=COMPONENTS_TESTS_DIR,
codegen_components=CPP_TESTING_CODEGEN_COMPONENTS,
codegen_components=BASE_CODEGEN_COMPONENTS,
config_prefix="cpptests",
friendly_name="CPP Unit Tests",
libraries=PLATFORMIO_GOOGLE_TEST_LIB,
+135 -77
View File
@@ -2,14 +2,22 @@
"""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 flat PlatformIO-compatible library
by combining google_benchmark sources and codspeed core sources.
This script clones the repo and assembles a flat PlatformIO-compatible library
by combining google_benchmark sources, codspeed core, and instrument-hooks.
PlatformIO quirks addressed:
- .cc files renamed to .cpp (PlatformIO ignores .cc)
- All sources merged into one src/ dir (PlatformIO can't compile from
multiple source directories in a single library)
- library.json created with required CodSpeed preprocessor defines
Usage:
python script/setup_codspeed_lib.py [--output-dir DIR]
Prints JSON to stdout with lib_path and build_flags for cpp_benchmark.py.
Prints JSON to stdout with lib_path for cpp_benchmark.py.
Git output goes to stderr.
See https://codspeed.io/docs/benchmarks/cpp#custom-build-systems
"""
from __future__ import annotations
@@ -27,95 +35,110 @@ CODSPEED_CPP_SHA = "d6b4111428ae1f1667fec9bff009522378d5d347"
DEFAULT_OUTPUT_DIR = "/tmp/codspeed-cpp"
# Well-known paths within the codspeed-cpp repository
GOOGLE_BENCHMARK_SUBDIR = "google_benchmark"
CORE_SUBDIR = "core"
INSTRUMENT_HOOKS_SUBDIR = Path(CORE_SUBDIR) / "instrument-hooks"
INSTRUMENT_HOOKS_INCLUDES = INSTRUMENT_HOOKS_SUBDIR / "includes"
INSTRUMENT_HOOKS_DIST = INSTRUMENT_HOOKS_SUBDIR / "dist" / "core.c"
CORE_CMAKE = Path(CORE_SUBDIR) / "CMakeLists.txt"
def setup_codspeed_lib(output_dir: Path) -> None:
"""Clone codspeed-cpp and create a flat PlatformIO library.
Args:
output_dir: Directory to clone into
"""
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,
)
def _git(args: list[str], **kwargs: object) -> None:
"""Run a git command, sending output to stderr."""
subprocess.run(
["git", *args],
check=True,
stdout=kwargs.pop("stdout", sys.stderr),
stderr=kwargs.pop("stderr", sys.stderr),
**kwargs,
)
benchmark_dir = output_dir / "google_benchmark"
core_dir = output_dir / "core"
instrument_hooks_dir = core_dir / "instrument-hooks" / "includes"
# 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
def _clone_repo(output_dir: Path) -> None:
"""Clone codspeed-cpp at the pinned SHA with submodules."""
_git(
[
"clone",
"--recurse-submodules",
"--shallow-submodules",
CODSPEED_CPP_REPO,
str(output_dir),
]
)
_git(
[
"-C",
str(output_dir),
"-c",
"advice.detachedHead=false",
"checkout",
CODSPEED_CPP_SHA,
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
# PlatformIO doesn't compile .cc files — rename to .cpp
for cc_file in (benchmark_dir / "src").glob("*.cc"):
def _read_codspeed_version(cmake_path: Path) -> str:
"""Extract CODSPEED_VERSION from core/CMakeLists.txt."""
if not cmake_path.exists():
return "0.0.0"
for line in cmake_path.read_text().splitlines():
if line.startswith("set(CODSPEED_VERSION"):
return line.split()[1].rstrip(")")
return "0.0.0"
def _rename_cc_to_cpp(src_dir: Path) -> None:
"""Rename .cc files to .cpp so PlatformIO compiles them."""
for cc_file in src_dir.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("*"):
def _copy_if_missing(src: Path, dest: Path) -> None:
"""Copy a file only if the destination doesn't already exist."""
if not dest.exists():
shutil.copy2(src, dest)
def _merge_codspeed_core_into_lib(core_src: Path, lib_src: Path) -> None:
"""Copy codspeed core sources into the benchmark library src/.
.cpp files get a ``codspeed_`` prefix to avoid name collisions with
google_benchmark's own sources. .h files keep their original names
since they're referenced by ``#include "walltime.h"`` etc.
"""
for src_file in core_src.iterdir():
if src_file.suffix == ".cpp":
dest = benchmark_dir / "src" / f"codspeed_{src_file.name}"
_copy_if_missing(src_file, lib_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_if_missing(src_file, lib_src / src_file.name)
# 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
def _write_library_json(
benchmark_dir: Path,
core_include: Path,
hooks_include: Path,
version: str,
project_root: Path,
) -> None:
"""Write a PlatformIO library.json with CodSpeed build flags."""
library_json = {
"name": "benchmark",
"version": "0.0.0",
"build": {
"flags": [
f"-I{core_dir / 'include'}",
f"-I{instrument_hooks_dir}",
f"-I{core_include}",
f"-I{hooks_include}",
# google benchmark build flags
"-DHAVE_STD_REGEX",
"-DHAVE_STEADY_CLOCK",
"-DBENCHMARK_STATIC_DEFINE",
# CodSpeed instrumentation flags
# https://codspeed.io/docs/benchmarks/cpp#custom-build-systems
"-DCODSPEED_ENABLED",
"-DCODSPEED_SIMULATION",
f'-DCODSPEED_VERSION=\\"{version}\\"',
@@ -125,16 +148,52 @@ def setup_codspeed_lib(output_dir: Path) -> None:
"includeDir": "include",
},
}
(benchmark_dir / "library.json").write_text(
json.dumps(library_json, indent=2) + "\n"
)
def setup_codspeed_lib(output_dir: Path) -> None:
"""Clone codspeed-cpp and assemble a flat PlatformIO library.
The resulting library at ``output_dir/google_benchmark/`` contains:
- google_benchmark sources (.cc renamed to .cpp)
- codspeed core sources (prefixed ``codspeed_``)
- instrument-hooks C source (as ``instrument_hooks.c``)
- library.json with all required CodSpeed defines
Args:
output_dir: Directory to clone the repository into
"""
if not (output_dir / ".git").exists():
_clone_repo(output_dir)
benchmark_dir = output_dir / GOOGLE_BENCHMARK_SUBDIR
lib_src = benchmark_dir / "src"
core_dir = output_dir / CORE_SUBDIR
core_include = core_dir / "include"
hooks_include = output_dir / INSTRUMENT_HOOKS_INCLUDES
hooks_dist_c = output_dir / INSTRUMENT_HOOKS_DIST
project_root = Path(__file__).resolve().parent.parent
# 1. Rename .cc → .cpp (PlatformIO doesn't compile .cc)
_rename_cc_to_cpp(lib_src)
# 2. Merge codspeed core sources into the library
_merge_codspeed_core_into_lib(core_dir / "src", lib_src)
# 3. Copy instrument-hooks C source (provides instrument_hooks_* symbols)
if hooks_dist_c.exists():
_copy_if_missing(hooks_dist_c, lib_src / "instrument_hooks.c")
# 4. Write library.json
version = _read_codspeed_version(output_dir / CORE_CMAKE)
_write_library_json(
benchmark_dir, core_include, hooks_include, version, project_root
)
# Output JSON config for cpp_benchmark.py
result = {
"lib_path": str(benchmark_dir),
}
print(json.dumps(result))
print(json.dumps({"lib_path": str(benchmark_dir)}))
def main() -> None:
@@ -146,7 +205,6 @@ def main() -> None:
help=f"Directory to clone codspeed-cpp into (default: {DEFAULT_OUTPUT_DIR})",
)
args = parser.parse_args()
setup_codspeed_lib(args.output_dir)
+9
View File
@@ -36,6 +36,15 @@ LOGGER_KEY = "logger"
# by component benchmark.yaml files. esphome: allows sub-key merging.
BASE_CONFIG_KEYS = frozenset({ESPHOME_KEY, HOST_KEY, LOGGER_KEY})
# Shared build flag — enables timezone code paths for testing/benchmarking.
USE_TIME_TIMEZONE_FLAG = "-DUSE_TIME_TIMEZONE"
# Components whose to_code should always run during C++ test/benchmark builds.
# These are the minimal infrastructure components needed for host compilation.
# Note: "core" is the esphome core config module (esphome/core/config.py),
# which registers under package name "core" not "esphome".
BASE_CODEGEN_COMPONENTS = {"core", "host", "logger"}
# Exit codes
EXIT_OK = 0
EXIT_SKIPPED = 1