| #!/usr/bin/env python3 | |
| # Licensed to the Apache Software Foundation (ASF) under one | |
| # or more contributor license agreements. See the NOTICE file | |
| # distributed with this work for additional information | |
| # regarding copyright ownership. The ASF licenses this file | |
| # to you under the Apache License, Version 2.0 (the | |
| # "License"); you may not use this file except in compliance | |
| # with the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, | |
| # software distributed under the License is distributed on an | |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| # KIND, either express or implied. See the License for the | |
| # specific language governing permissions and limitations | |
| # under the License. | |
| """Compile profile benchmark runner for DataFusion. | |
| Builds the `dfbench` benchmark binary with several Cargo profiles | |
| (e.g. `--release` or `--profile ci`), runs the full TPC-H suite against | |
| the Parquet data under `benchmarks/data/tpch_sf1`, and reports compile | |
| time, execution time, and resulting binary size. | |
| See `benchmarks/README.md` for usages. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import os | |
| import subprocess | |
| import sys | |
| import time | |
| from pathlib import Path | |
| from typing import Iterable, NamedTuple | |
| REPO_ROOT = Path(__file__).resolve().parents[1] | |
| DEFAULT_DATA_DIR = REPO_ROOT / "benchmarks" / "data" / "tpch_sf1" | |
| DEFAULT_ITERATIONS = 1 | |
| DEFAULT_FORMAT = "parquet" | |
| DEFAULT_PARTITIONS: int | None = None | |
| BENCHMARK_PACKAGE = "datafusion-benchmarks" | |
| BENCHMARK_BINARY = "dfbench.exe" if os.name == "nt" else "dfbench" | |
| PROFILE_TARGET_DIR = { | |
| "dev": "debug", | |
| "release": "release", | |
| "ci": "ci", | |
| "ci-optimized": "ci-optimized", | |
| "release-nonlto": "release-nonlto", | |
| "profiling": "profiling", | |
| } | |
| class ProfileResult(NamedTuple): | |
| profile: str | |
| compile_seconds: float | |
| run_seconds: float | |
| binary_bytes: int | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument( | |
| "--profiles", | |
| nargs="+", | |
| default=list(PROFILE_TARGET_DIR.keys()), | |
| help=( | |
| "Cargo profiles to test " | |
| "(default: dev release ci ci-optimized release-nonlto profiling)" | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--data", | |
| type=Path, | |
| default=DEFAULT_DATA_DIR, | |
| help="Path to TPCH dataset (default: benchmarks/data/tpch_sf1)", | |
| ) | |
| return parser.parse_args() | |
| def timed_run(command: Iterable[str]) -> float: | |
| start = time.perf_counter() | |
| try: | |
| subprocess.run(command, cwd=REPO_ROOT, check=True) | |
| except subprocess.CalledProcessError as exc: | |
| raise RuntimeError(f"command failed: {' '.join(command)}") from exc | |
| return time.perf_counter() - start | |
| def cargo_build(profile: str) -> float: | |
| if profile == "dev": | |
| command = [ | |
| "cargo", | |
| "build", | |
| "--package", | |
| BENCHMARK_PACKAGE, | |
| "--bin", | |
| "dfbench", | |
| ] | |
| else: | |
| command = [ | |
| "cargo", | |
| "build", | |
| "--profile", | |
| profile, | |
| "--package", | |
| BENCHMARK_PACKAGE, | |
| "--bin", | |
| "dfbench", | |
| ] | |
| return timed_run(command) | |
| def cargo_clean(profile: str) -> None: | |
| command = ["cargo", "clean", "--profile", profile] | |
| try: | |
| subprocess.run(command, cwd=REPO_ROOT, check=True) | |
| except subprocess.CalledProcessError as exc: | |
| raise RuntimeError(f"failed to clean cargo artifacts for profile '{profile}'") from exc | |
| def run_benchmark(profile: str, data_path: Path) -> float: | |
| binary_dir = PROFILE_TARGET_DIR.get(profile) | |
| if not binary_dir: | |
| raise ValueError(f"unknown profile '{profile}'") | |
| binary_path = REPO_ROOT / "target" / binary_dir / BENCHMARK_BINARY | |
| if not binary_path.exists(): | |
| raise FileNotFoundError(f"compiled binary not found at {binary_path}") | |
| command = [ | |
| str(binary_path), | |
| "tpch", | |
| "--iterations", | |
| str(DEFAULT_ITERATIONS), | |
| "--path", | |
| str(data_path), | |
| "--format", | |
| DEFAULT_FORMAT, | |
| ] | |
| if DEFAULT_PARTITIONS is not None: | |
| command.extend(["--partitions", str(DEFAULT_PARTITIONS)]) | |
| env = os.environ.copy() | |
| env.setdefault("RUST_LOG", "warn") | |
| start = time.perf_counter() | |
| try: | |
| subprocess.run(command, cwd=REPO_ROOT, env=env, check=True) | |
| except subprocess.CalledProcessError as exc: | |
| raise RuntimeError(f"benchmark failed for profile '{profile}'") from exc | |
| return time.perf_counter() - start | |
| def binary_size(profile: str) -> int: | |
| binary_dir = PROFILE_TARGET_DIR[profile] | |
| binary_path = REPO_ROOT / "target" / binary_dir / BENCHMARK_BINARY | |
| return binary_path.stat().st_size | |
| def human_time(seconds: float) -> str: | |
| return f"{seconds:6.2f}s" | |
| def human_size(size: int) -> str: | |
| value = float(size) | |
| for unit in ("B", "KB", "MB", "GB", "TB"): | |
| if value < 1024 or unit == "TB": | |
| return f"{value:6.1f}{unit}" | |
| value /= 1024 | |
| return f"{value:6.1f}TB" | |
| def main() -> None: | |
| args = parse_args() | |
| data_path = args.data.resolve() | |
| if not data_path.exists(): | |
| print(f"Data directory not found: {data_path}", file=sys.stderr) | |
| sys.exit(1) | |
| results: list[ProfileResult] = [] | |
| for profile in args.profiles: | |
| print(f"\n=== Profile: {profile} ===") | |
| print("Cleaning previous build artifacts...") | |
| cargo_clean(profile) | |
| compile_seconds = cargo_build(profile) | |
| run_seconds = run_benchmark(profile, data_path) | |
| size_bytes = binary_size(profile) | |
| results.append(ProfileResult(profile, compile_seconds, run_seconds, size_bytes)) | |
| print("\nSummary") | |
| header = f"{'Profile':<15}{'Compile':>12}{'Run':>12}{'Size':>12}" | |
| print(header) | |
| print("-" * len(header)) | |
| for result in results: | |
| print( | |
| f"{result.profile:<15}{human_time(result.compile_seconds):>12}" | |
| f"{human_time(result.run_seconds):>12}{human_size(result.binary_bytes):>12}" | |
| ) | |
| if __name__ == "__main__": | |
| main() |