Source code for pgsi_analyzer.benchmark.builder

"""
Benchmark build system for Cython and ctypes benchmarks.

Handles compilation of Cython extensions and C shared libraries
before benchmark execution. Compilation is done separately from
measurement to ensure only execution time/energy is measured.
"""

import subprocess
import sys
import platform
from pathlib import Path
from typing import Optional

from ..utils import ConfigurationError, PlatformError
from ..config import ToolPaths


[docs] def requires_build(method: str) -> bool: """ Check if a method requires compilation before execution. Args: method: Execution method name Returns: True if method requires build step """ return method in ("cython", "ctypes")
[docs] def build_cython( benchmark_path: Path, build_dir: Optional[Path] = None, tool_paths: Optional[ToolPaths] = None, ) -> Path: """ Build Cython extension module. Runs: python setup.py build_ext --inplace Args: benchmark_path: Path to Cython benchmark directory (contains setup.py) build_dir: Optional directory for build artifacts (defaults to benchmark_path) tool_paths: Optional ToolPaths configuration for Python executable Returns: Path to benchmark directory (build artifacts are in-place) Raises: ConfigurationError: If setup.py not found or build fails """ if build_dir is None: build_dir = benchmark_path setup_py = benchmark_path / "setup.py" if not setup_py.exists(): raise ConfigurationError(f"setup.py not found in {benchmark_path}") # Use configured Python or default python_exe = str(tool_paths.python) if tool_paths else sys.executable # Change to benchmark directory for build original_cwd = Path.cwd() try: # Build extension in-place result = subprocess.run( [python_exe, "setup.py", "build_ext", "--inplace"], cwd=str(benchmark_path), capture_output=True, text=True, timeout=300, # 5 minute timeout for compilation ) if result.returncode != 0: raise ConfigurationError( f"Cython build failed for {benchmark_path}:\n" f"stdout: {result.stdout}\n" f"stderr: {result.stderr}" ) return benchmark_path except subprocess.TimeoutExpired: raise ConfigurationError(f"Cython build timed out for {benchmark_path}") except Exception as e: raise ConfigurationError(f"Cython build error for {benchmark_path}: {e}") finally: # Restore original working directory import os os.chdir(str(original_cwd))
[docs] def build_ctypes( benchmark_path: Path, build_dir: Optional[Path] = None, tool_paths: Optional[ToolPaths] = None, ) -> Path: """ Build C shared library for ctypes benchmark. Compiles .c files to .so (Linux) or .dll (Windows). Args: benchmark_path: Path to ctypes benchmark directory (contains .c files) build_dir: Optional directory for build artifacts (defaults to benchmark_path) tool_paths: Optional ToolPaths configuration for C compiler Returns: Path to benchmark directory (shared library is in-place) Raises: ConfigurationError: If compilation fails PlatformError: If C compiler not available """ if build_dir is None: build_dir = benchmark_path # Find .c files (sorted for deterministic library name) c_files = sorted(benchmark_path.glob("*.c")) if not c_files: raise ConfigurationError(f"No .c files found in {benchmark_path}") # Determine output library name and extension if platform.system() == "Windows": lib_ext = ".dll" # Use first .c file name as base lib_name = c_files[0].stem else: lib_ext = ".so" lib_name = f"lib{c_files[0].stem}" lib_path = benchmark_path / f"{lib_name}{lib_ext}" # Check if already compiled and up-to-date if lib_path.exists(): # Check if .c files are newer than library c_newer = any(c.stat().st_mtime > lib_path.stat().st_mtime for c in c_files) if not c_newer: return benchmark_path # Already built and up-to-date # Determine compiler if tool_paths and tool_paths.c_compiler: compiler_exe = str(tool_paths.c_compiler) else: # Fallback to auto-detection if platform.system() == "Windows": compiler_exe = "gcc" # Will try cl.exe if gcc fails else: compiler_exe = "gcc" # Compile command if platform.system() == "Windows": # Windows: use configured compiler or try gcc first, then cl.exe compile_cmd = [ compiler_exe, "-shared", "-o", str(lib_path), *[str(c) for c in c_files], "-fPIC", ] else: # Linux/macOS: use gcc compile_cmd = [ compiler_exe, "-shared", "-fPIC", "-o", str(lib_path), *[str(c) for c in c_files], ] try: result = subprocess.run( compile_cmd, cwd=str(benchmark_path), capture_output=True, text=True, timeout=300, # 5 minute timeout ) if result.returncode != 0: # If configured compiler failed, try fallback detection if tool_paths and tool_paths.c_compiler: # User configured compiler failed raise ConfigurationError( f"ctypes compilation failed with configured compiler '{compiler_exe}':\n" f"Command: {' '.join(compile_cmd)}\n" f"stdout: {result.stdout}\n" f"stderr: {result.stderr}" ) # Try alternative: check if gcc is available gcc_check = subprocess.run( ["gcc", "--version"], capture_output=True, text=True, ) if gcc_check.returncode != 0: raise PlatformError( "C compiler not found. Configure PGSI_CC_PATH, use --cc-path, " "or install gcc/cl.exe to build ctypes benchmarks." ) raise ConfigurationError( f"ctypes compilation failed for {benchmark_path}:\n" f"Command: {' '.join(compile_cmd)}\n" f"stdout: {result.stdout}\n" f"stderr: {result.stderr}" ) if not lib_path.exists(): raise ConfigurationError( f"Compilation succeeded but library not found: {lib_path}" ) return benchmark_path except subprocess.TimeoutExpired: raise ConfigurationError(f"ctypes compilation timed out for {benchmark_path}") except FileNotFoundError: raise PlatformError( "C compiler not found. Configure PGSI_CC_PATH, use --cc-path, " "or install gcc/cl.exe to build ctypes benchmarks." ) except Exception as e: raise ConfigurationError(f"ctypes compilation error for {benchmark_path}: {e}")
[docs] def build_benchmark( algorithm: str, method: str, benchmark_path: Path, tool_paths: Optional[ToolPaths] = None, ) -> Path: """ Build a benchmark if it requires compilation. Args: algorithm: Algorithm name (for error messages) method: Execution method benchmark_path: Path to benchmark directory tool_paths: Optional ToolPaths configuration Returns: Path to benchmark directory (ready for execution) Raises: ConfigurationError: If build fails PlatformError: If build tools not available """ if not requires_build(method): return benchmark_path # No build needed # Compatibility guard: older registries/discovery may resolve build-based # methods to .../main.py. Build routines require the method directory. if benchmark_path.is_file() and benchmark_path.name == "main.py": benchmark_path = benchmark_path.parent if method == "cython": return build_cython(benchmark_path, tool_paths=tool_paths) elif method == "ctypes": return build_ctypes(benchmark_path, tool_paths=tool_paths) else: raise ValueError(f"Unknown build method: {method}")