blob: 11c7b2ad76d4cb7aca61b1f4ef994746778fa2ea [file] [edit]
"""Build Ruff with profile-guided optimization using pinned ecosystem projects."""
# /// script
# requires-python = ">=3.11"
# dependencies = []
#
# [tool.ty.rules]
# blanket-ignore-comment = "warn"
# missing-type-argument = "warn"
# possibly-unresolved-reference = "warn"
# unsound-return-statement = "warn"
# unsound-yield = "warn"
# unsupported-dynamic-base = "warn"
# division-by-zero = "warn"
#
# [tool.uv]
# no-build = true
# exclude-newer = "P7D"
# ///
from __future__ import annotations
import argparse
import os
import re
import shlex
import subprocess
import sys
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path
REPOSITORY_ROOT = Path(__file__).resolve().parent.parent
EXCLUDED_DIRECTORIES = frozenset({"_tests", "_vendor", "test", "tests"})
@dataclass(frozen=True, slots=True)
class EcosystemProject:
name: str
repository: str
revision: str
source_directories: tuple[str, ...]
def __post_init__(self) -> None:
if re.fullmatch(r"[0-9a-f]{40}", self.revision) is None:
raise ValueError(
f"{self.repository} must be pinned to a full Git commit SHA, "
f"got {self.revision!r}"
)
@property
def url(self) -> str:
return f"https://github.com/{self.repository}.git"
# Train on a subset of the pinned ecosystem projects that we already use for
# linting, formatting, or type checking. The goal is to create a representative
# corpus that includes scientific computing, synchronous and asynchronous code,
# applications, libraries, and type stubs.
#
# But it wasn't a highly optimized selection process. (For example, during
# development, we added Zulip and Warehouse, which reduced Ruff's CPU time by
# 0.35% while increasing its wheel size by 0.44%.)
CORPUS_PROJECTS = (
EcosystemProject(
name="pytest",
repository="pytest-dev/pytest",
revision="28e86a6c2ae0173831e4925a4af89b02a2936d09",
source_directories=("src/_pytest",),
),
EcosystemProject(
name="httpx",
repository="encode/httpx",
revision="b5addb64f0161ff6bfe94c124ef76f6a1fba5254",
source_directories=("httpx",),
),
EcosystemProject(
name="fastapi",
repository="fastapi/fastapi",
revision="a375f6b948b99fa4260129856bbf11d037f363ef",
source_directories=("fastapi",),
),
EcosystemProject(
name="anyio",
repository="agronholm/anyio",
revision="ffe91331adb912c5d150f5d373f7cd28a0e96a62",
source_directories=("src/anyio",),
),
EcosystemProject(
name="zulip",
repository="zulip/zulip",
revision="ccddbba7a3074283ccaac3bde35fd32b19faf042",
source_directories=("zerver/views", "zerver/models"),
),
EcosystemProject(
name="warehouse",
repository="pypi/warehouse",
revision="5a4d2cadec641b5d6a6847d0127940e0f532f184",
source_directories=(
"warehouse/accounts",
"warehouse/oidc",
"warehouse/forklift",
),
),
EcosystemProject(
name="pip",
repository="pypa/pip",
revision="d1fd55753405fd728a0751a578e27c1054acdf48",
source_directories=("src/pip/_internal",),
),
EcosystemProject(
name="sphinx",
repository="sphinx-doc/sphinx",
revision="b06d92e80eed130e1dd4e67cac4afa1267424f1a",
source_directories=(
"sphinx/builders",
"sphinx/ext/autodoc",
"sphinx/domains/python",
),
),
EcosystemProject(
name="astropy",
repository="astropy/astropy",
revision="b779108c7cec25c840c0f744fdf2a1550441e309",
source_directories=("astropy/units",),
),
EcosystemProject(
name="typeshed",
repository="python/typeshed",
revision="e0efbeef901e9b6998d016e1ab9352678f09ae77",
source_directories=(
"stdlib/asyncio",
"stdlib/collections",
"stubs/requests",
),
),
)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--target", help="Host-native Rust target triple")
parser.add_argument(
"--debug",
action="store_true",
help="Use debug builds to validate the complete PGO pipeline",
)
parser.add_argument(
"--target-dir",
type=Path,
help="Cargo target directory (default: CARGO_TARGET_DIR or target/ruff-pgo)",
)
parser.add_argument(
"--profile-dir",
type=Path,
help="Raw profile directory (default: <target-dir>/profiles)",
)
parser.add_argument(
"--llvm-profdata",
type=Path,
help="Override the active Rust toolchain's llvm-profdata executable",
)
mode = parser.add_mutually_exclusive_group()
mode.add_argument(
"--train-only",
action="store_true",
help="Only produce <target-dir>/ruff.profdata for a subsequent release build",
)
mode.add_argument(
"--prepare-corpus",
action="store_true",
help="Only download and prepare the pinned ecosystem training corpus",
)
args = parser.parse_args()
target_dir = (
args.target_dir
or Path(
os.environ.get("CARGO_TARGET_DIR", REPOSITORY_ROOT / "target" / "ruff-pgo")
)
).resolve()
profile_dir = (args.profile_dir or target_dir / "profiles").resolve()
merged_profile = target_dir / "ruff.profdata"
environment = os.environ.copy()
if args.prepare_corpus:
corpus = ecosystem_python_files(target_dir / "corpus", environment=environment)
write_corpus_arguments(target_dir, corpus)
print(f"Prepared {len(corpus)} ecosystem Python files", flush=True)
return
host = rustc_host()
target = args.target or host
if target != host:
parser.error(
f"PGO training requires the host-native target {host}, got {target}"
)
profiler = find_llvm_profdata(host, args.llvm_profdata)
corpus = ecosystem_python_files(target_dir / "corpus", environment=environment)
corpus_arguments = write_corpus_arguments(target_dir, corpus)
profile_dir.mkdir(parents=True, exist_ok=True)
for profile in profile_dir.glob("ruff-*.profraw"):
profile.unlink()
environment["CARGO_INCREMENTAL"] = "0"
if target.endswith("-apple-darwin"):
for variable in ("CFLAGS", "CXXFLAGS"):
environment[variable] = append_flags(
environment.get(variable), "-fno-profile-generate -fno-profile-use"
)
instrumented_target_dir = target_dir / "instrumented"
instrumented_environment = environment | {
"CARGO_TARGET_DIR": str(instrumented_target_dir),
"RUSTFLAGS": append_flags(
environment.get("RUSTFLAGS"), f"-Cprofile-generate={profile_dir}"
),
}
profile = "debug" if args.debug else "release"
print(f"Building instrumented {profile} Ruff", flush=True)
run(cargo_command(target, debug=args.debug), environment=instrumented_environment)
binary_name = "ruff.exe" if "windows" in target else "ruff"
instrumented_binary = instrumented_target_dir / target / profile / binary_name
if not instrumented_binary.is_file():
raise RuntimeError(f"Instrumented Ruff binary not found: {instrumented_binary}")
profiles = train_ruff(
instrumented_binary,
corpus_arguments,
profile_dir,
corpus_size=len(corpus),
environment=instrumented_environment,
)
merge_profiles(profiler, profiles, merged_profile, environment=environment)
if args.train_only:
return
optimized_environment = environment | {
"CARGO_TARGET_DIR": str(target_dir),
"RUSTFLAGS": append_flags(
environment.get("RUSTFLAGS"), f"-Cprofile-use={merged_profile}"
),
}
print(f"Building profile-guided {profile} Ruff", flush=True)
run(cargo_command(target, debug=args.debug), environment=optimized_environment)
print(
f"Profile-guided Ruff: {target_dir / target / profile / binary_name}",
flush=True,
)
def train_ruff(
binary: Path,
corpus_arguments: Path,
profile_directory: Path,
*,
corpus_size: int,
environment: dict[str, str],
) -> list[Path]:
common_arguments = [
"--isolated",
"--target-version",
"py314",
"--no-cache",
"--silent",
]
workloads = (
("check", "--exit-zero", (0,)),
("format", "--check", (0, 1)),
)
print(f"Training on {corpus_size} ecosystem Python files", flush=True)
profiles = []
for mode, mode_argument, allowed_exit_codes in workloads:
run(
[
str(binary),
mode,
*common_arguments,
mode_argument,
f"@{corpus_arguments}",
],
environment=environment
| {
"LLVM_PROFILE_FILE": str(
profile_directory / f"ruff-{mode}-%m-%p.profraw"
)
},
allowed_exit_codes=allowed_exit_codes,
)
workload_profiles = sorted(profile_directory.glob(f"ruff-{mode}-*.profraw"))
if not workload_profiles or any(
profile.stat().st_size == 0 for profile in workload_profiles
):
raise RuntimeError(
f"No complete Ruff {mode} profiling data found in {profile_directory}"
)
profiles.extend(workload_profiles)
return profiles
def merge_profiles(
profiler: Path,
profiles: list[Path],
destination: Path,
*,
environment: dict[str, str],
) -> None:
profile_size = sum(profile.stat().st_size for profile in profiles)
with tempfile.NamedTemporaryFile(
dir=destination.parent, prefix="ruff-", suffix=".profdata", delete=False
) as temporary_file:
temporary_profile = Path(temporary_file.name)
try:
run(
[
str(profiler),
"merge",
"--output",
str(temporary_profile),
*map(str, profiles),
],
environment=environment,
)
temporary_profile.replace(destination)
finally:
temporary_profile.unlink(missing_ok=True)
print(
f"Merged {len(profiles)} PGO profiles ({profile_size:,} bytes): {destination}",
flush=True,
)
def rustc_host() -> str:
version = subprocess.run(
["rustc", "--version", "--verbose"],
cwd=REPOSITORY_ROOT,
check=True,
capture_output=True,
text=True,
).stdout
for line in version.splitlines():
if line.startswith("host: "):
return line.removeprefix("host: ")
raise RuntimeError("Could not determine the active Rust compiler's host target")
def find_llvm_profdata(host: str, override: Path | None) -> Path:
if override is not None:
profiler = override.resolve()
else:
sysroot = subprocess.run(
["rustc", "--print", "sysroot"],
cwd=REPOSITORY_ROOT,
check=True,
capture_output=True,
text=True,
).stdout.strip()
binary_name = "llvm-profdata.exe" if "windows" in host else "llvm-profdata"
profiler = Path(sysroot) / "lib" / "rustlib" / host / "bin" / binary_name
if not profiler.is_file() or not os.access(profiler, os.X_OK):
raise RuntimeError(
f"Rust toolchain llvm-profdata not found: {profiler}; "
"run `rustup component add llvm-tools-preview`"
)
return profiler
def ecosystem_python_files(
corpus_directory: Path, *, environment: dict[str, str]
) -> list[str]:
corpus_directory.mkdir(parents=True, exist_ok=True)
git_environment = environment | {
"GIT_CONFIG_GLOBAL": os.devnull,
"GIT_TERMINAL_PROMPT": "0",
"GIT_LFS_SKIP_SMUDGE": "1",
}
paths: list[str] = []
for project in CORPUS_PROJECTS:
checkout = corpus_directory / project.name
checkout.mkdir(parents=True, exist_ok=True)
git = ["git", "-c", f"core.hooksPath={os.devnull}", "-C", str(checkout)]
if not (checkout / ".git").is_dir():
print(f"Preparing {project.repository}@{project.revision}", flush=True)
run([*git, "init", "--quiet"], environment=git_environment)
run(
[
*git,
"remote",
"add",
"origin",
project.url,
],
environment=git_environment,
)
remote = subprocess.run(
[*git, "config", "--local", "--get", "remote.origin.url"],
cwd=REPOSITORY_ROOT,
env=git_environment,
check=True,
capture_output=True,
text=True,
).stdout.strip()
if remote != project.url:
raise RuntimeError(
f"Unexpected origin for cached {project.name} checkout: "
f"expected {project.url}, got {remote}"
)
run(
[*git, "sparse-checkout", "set", "--cone", *project.source_directories],
environment=git_environment,
)
current_revision = subprocess.run(
[*git, "rev-parse", "--verify", "HEAD"],
cwd=REPOSITORY_ROOT,
env=git_environment,
check=False,
capture_output=True,
text=True,
)
if (
current_revision.returncode != 0
or current_revision.stdout.strip() != project.revision
):
run_git_with_retry(
[
*git,
"fetch",
"--quiet",
"--no-tags",
"--no-recurse-submodules",
"--depth=1",
"--filter=blob:none",
"origin",
project.revision,
],
environment=git_environment,
)
run_git_with_retry(
[
*git,
"checkout",
"--quiet",
"--detach",
"--force",
"--no-recurse-submodules",
project.revision,
],
environment=git_environment,
)
for source_directory in project.source_directories:
source = checkout / source_directory
if not source.is_dir():
raise RuntimeError(
f"Missing training source directory {source_directory!r} "
f"in {project.repository}@{project.revision}"
)
tracked_files = subprocess.run(
[*git, "ls-files", "-z", "--", *project.source_directories],
cwd=REPOSITORY_ROOT,
env=git_environment,
check=True,
capture_output=True,
).stdout.split(b"\0")
project_paths = [
str(path)
for tracked_file in tracked_files
if tracked_file
and (path := checkout / os.fsdecode(tracked_file)).suffix in {".py", ".pyi"}
and path.is_file()
and not path.is_symlink()
and not EXCLUDED_DIRECTORIES.intersection(
path.relative_to(checkout).parts[:-1]
)
]
if not project_paths:
raise RuntimeError(
f"No Python training files found in {project.repository}"
)
paths.extend(sorted(project_paths))
print(f" {project.name}: {len(project_paths)} Python files", flush=True)
return paths
def run_git_with_retry(command: list[str], *, environment: dict[str, str]) -> None:
for attempt in range(3):
try:
run(command, environment=environment)
return
except subprocess.CalledProcessError:
if attempt == 2:
raise
delay = 2**attempt
print(
f"Git command failed; retrying in {delay}s (attempt {attempt + 2} of 3)",
file=sys.stderr,
flush=True,
)
time.sleep(delay)
def write_corpus_arguments(target_directory: Path, corpus: list[str]) -> Path:
arguments = target_directory / "ruff-pgo.args"
arguments.write_text("\n".join(corpus) + "\n", encoding="utf-8", newline="\n")
return arguments
def cargo_command(target: str, *, debug: bool = False) -> list[str]:
return [
"cargo",
"rustc",
*(() if debug else ("--release",)),
"--locked",
"--package",
"ruff",
"--bin",
"ruff",
"--target",
target,
"--",
"-C",
"strip=symbols",
]
def append_flags(existing: str | None, additional: str) -> str:
return " ".join(flag for flag in (existing, additional) if flag)
def run(
command: list[str],
*,
environment: dict[str, str],
allowed_exit_codes: tuple[int, ...] = (0,),
) -> None:
logged_arguments = 16
displayed_command = shlex.join(command[:logged_arguments])
if len(command) > logged_arguments:
displayed_command += (
f" ... ({len(command) - logged_arguments} arguments omitted)"
)
print(f"> {displayed_command}", flush=True)
completed = subprocess.run(
command, cwd=REPOSITORY_ROOT, env=environment, check=False
)
if completed.returncode not in allowed_exit_codes:
raise subprocess.CalledProcessError(completed.returncode, command)
if __name__ == "__main__":
try:
main()
except (OSError, RuntimeError, subprocess.CalledProcessError) as error:
print(f"error: {error}", file=sys.stderr)
raise SystemExit(1) from error