mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 17:05:36 +00:00
dd52c91290
Sub-microsecond benchmarks are dominated by the ~60ns per-iteration valgrind start/stop cost in CodSpeed simulation mode. Add kInnerIterations (1000) inner loops to all fast benchmarks so the actual work dominates. Move DoNotOptimize calls outside inner loops to prevent artificial overhead. Also address review feedback: - Use tokenless CodSpeed (public repo, no CODSPEED_TOKEN needed) - Fix warning message to show component-specific path - Fix stray ". :" in error message - Verify pinned SHA on re-runs to prevent stale checkouts
232 lines
7.7 KiB
Python
Executable File
232 lines
7.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""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 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 for cpp_benchmark.py.
|
|
Git output goes to stderr.
|
|
|
|
See https://codspeed.io/docs/benchmarks/cpp#custom-build-systems
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
# Pin to a specific release for reproducibility
|
|
CODSPEED_CPP_REPO = "https://github.com/CodSpeedHQ/codspeed-cpp.git"
|
|
CODSPEED_CPP_SHA = "e633aca00da3d0ad14e7bf424d9cb47165a29028" # v2.1.0
|
|
|
|
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 _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,
|
|
)
|
|
|
|
|
|
def _clone_repo(output_dir: Path) -> None:
|
|
"""Shallow-clone codspeed-cpp at the pinned SHA with submodules."""
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
_git(["init", str(output_dir)])
|
|
_git(["-C", str(output_dir), "remote", "add", "origin", CODSPEED_CPP_REPO])
|
|
_git(["-C", str(output_dir), "fetch", "--depth", "1", "origin", CODSPEED_CPP_SHA])
|
|
_git(
|
|
["-C", str(output_dir), "checkout", "FETCH_HEAD"],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
_git(
|
|
[
|
|
"-C",
|
|
str(output_dir),
|
|
"submodule",
|
|
"update",
|
|
"--init",
|
|
"--recursive",
|
|
"--depth",
|
|
"1",
|
|
]
|
|
)
|
|
|
|
|
|
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)
|
|
|
|
|
|
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":
|
|
_copy_if_missing(src_file, lib_src / f"codspeed_{src_file.name}")
|
|
elif src_file.suffix == ".h":
|
|
_copy_if_missing(src_file, lib_src / src_file.name)
|
|
|
|
|
|
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_include}",
|
|
f"-I{hooks_include}",
|
|
# google benchmark build flags
|
|
# -O2 is critical: without it, instrument_hooks_start_benchmark_inline
|
|
# doesn't get inlined and shows up as overhead in profiles
|
|
"-O2",
|
|
"-DNDEBUG",
|
|
"-DHAVE_STD_REGEX",
|
|
"-DHAVE_STEADY_CLOCK",
|
|
"-DBENCHMARK_STATIC_DEFINE",
|
|
# CodSpeed instrumentation flags
|
|
# https://codspeed.io/docs/benchmarks/cpp#custom-build-systems
|
|
"-DCODSPEED_ENABLED",
|
|
"-DCODSPEED_ANALYSIS",
|
|
f'-DCODSPEED_VERSION=\\"{version}\\"',
|
|
f'-DCODSPEED_ROOT_DIR=\\"{project_root}\\"',
|
|
'-DCODSPEED_MODE_DISPLAY=\\"simulation\\"',
|
|
],
|
|
"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)
|
|
else:
|
|
# Verify the existing checkout matches the pinned SHA
|
|
result = subprocess.run(
|
|
["git", "-C", str(output_dir), "rev-parse", "HEAD"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if result.returncode != 0 or result.stdout.strip() != CODSPEED_CPP_SHA:
|
|
print(
|
|
f"Stale codspeed-cpp checkout, re-cloning at {CODSPEED_CPP_SHA}",
|
|
file=sys.stderr,
|
|
)
|
|
shutil.rmtree(output_dir)
|
|
_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
|
|
print(json.dumps({"lib_path": str(benchmark_dir)}))
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--output-dir",
|
|
type=Path,
|
|
default=Path(DEFAULT_OUTPUT_DIR),
|
|
help=f"Directory to clone codspeed-cpp into (default: {DEFAULT_OUTPUT_DIR})",
|
|
)
|
|
args = parser.parse_args()
|
|
setup_codspeed_lib(args.output_dir)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|