blob: d8118106a0d48a48e79a9d4356927c882f24cec0 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright (C) 2022 The Android Open Source Project
#
# Licensed 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.
import argparse
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
import shutil
import subprocess
import sys
from tempfile import TemporaryDirectory
from typing import Optional, TextIO
import context
from android_rust import build_platform
from android_rust.paths import *
from android_rust.toolchains import CLANG_TOOLCHAIN_HOST, ClangToolchain, RustToolchain
from android_rust.utils import (
TERM_RED,
ExtantPath,
ResolvedPath,
ScriptException,
archive_create,
archive_extract,
extend_suffix,
get_prebuilt_binary_paths,
print_colored,
print_fs_tree,
is_archive,
reify_singleton_patterns)
from test_compiler import initialize_clang_prebuilt
#
# Constants
#
BOLT_INSTRUMENTATION_SUBJECTS: list[str] = [
"lib/librustc_driver-*.so",
"lib/libstd-*.so",
"lib/libLLVM.so.*",
]
BOLT_SKIP_FUNCS: dict[str, list[str]] = {
"librustc_driver": ["blake3_hash_many_sse2/1",],
}
#
# Program logic
#
def parse_args() -> argparse.Namespace:
"""Parses arguments and returns the parsed structure."""
parser = argparse.ArgumentParser("Run BOLT on a Rust toolchain archive")
parser.add_argument(
"archive_path", type=ExtantPath, help="Path to the prebuilt archive to instrument/optimize")
parser.add_argument(
"--build-name",
default="bolted",
help="Desired filename for the output archive (w/o extension)")
parser.add_argument(
"--dist",
"-d",
dest="dist_path",
type=ResolvedPath,
default=DIST_PATH_DEFAULT,
help="Where to place distributable artifacts")
parser.add_argument(
"--clang-prebuilt",
type=ExtantPath,
default=CLANG_TOOLCHAIN_HOST,
help="Path to Clang toolchain that will be used to merge profile data")
parser.add_argument(
"--strip-only", action="store_true", help="Don't invoke BOLT, just strip the binaries")
action_group = parser.add_mutually_exclusive_group()
action_group.add_argument(
"--profile-generate",
type=ExtantPath,
nargs="?",
const=OUT_PATH_PROFILES,
help="Instrument the toolchain for BOLT profile generation with an absolute path")
action_group.add_argument(
"--profile-generate-relative",
type=Path,
nargs="?",
const=Path("out/profiles"),
help="Instrument the toolchain for BOLT profile generation with a relative path")
action_group.add_argument(
"--profile-use",
type=ExtantPath,
nargs="?",
const=OUT_PATH_PROFILES,
help="Path to archive or directory with a bolt/ subdir containing BOLT profiles")
return parser.parse_args()
@contextmanager
def handle_input_path(input_path: Path) -> Iterator[Path]:
if is_archive(input_path):
with TemporaryDirectory() as temp_dir:
temp_dir_path = Path(temp_dir)
print(f"Unpacking archive into {temp_dir}")
archive_extract(input_path, temp_dir_path)
yield temp_dir_path
else:
yield input_path
@contextmanager
def expand_profiles(profile_path: Path | None) -> Iterator[Path | None]:
if profile_path is not None:
if is_archive(profile_path):
with TemporaryDirectory() as temp_dir:
temp_dir_path = Path(temp_dir)
print(f"Unpacking profiles into {temp_dir}")
archive_extract(profile_path, temp_dir_path)
yield temp_dir_path
else:
yield profile_path
else:
yield None
def invoke_bolt(
toolchain: ClangToolchain,
obj_path: Path,
bolt_log: TextIO,
options: list[str] = []) -> None:
bolt_path = toolchain.bolt()
assert (bolt_path is not None)
obj_path_bolted = extend_suffix(obj_path, ".bolt")
bolt_log.write(f"BOLTing {str(obj_path)}\n")
print(f"BOLTing {str(obj_path)}\n")
bolt_log.flush()
for (lib_prefix, func_names) in BOLT_SKIP_FUNCS.items():
if obj_path.name.startswith(lib_prefix):
options = options.copy()
options.append(f"--skip-funcs={','.join(func_names)}")
try:
subprocess.run([bolt_path] + options + ["-o", obj_path_bolted, obj_path],
check=True,
stdout=bolt_log,
stderr=bolt_log)
except subprocess.CalledProcessError as err:
print(f"Failed to process object {str(obj_path)}")
raise err
bolt_log.write("\n")
bolt_log.flush()
obj_path.unlink()
obj_path_bolted.rename(obj_path)
def process_objects(
toolchain: ClangToolchain,
root: Path,
strip_only: bool,
profile_generate: Optional[Path],
profile_use: Optional[Path],
bolt_log: TextIO) -> None:
print("Processing objects")
instrumentation_subjects = reify_singleton_patterns(root, BOLT_INSTRUMENTATION_SUBJECTS)
optimization_subjects = get_prebuilt_binary_paths(root, toplevel_only=True)
for obj_path in get_prebuilt_binary_paths(root, subdirs=["bin", "lib"]):
print(f"Boltifyer is looking at object {str(obj_path)}")
if not strip_only and obj_path in optimization_subjects:
print(f"Object {str(obj_path)} is an optimization subject")
options = ["--peepholes=all"]
if obj_path in instrumentation_subjects:
if profile_generate:
fdata_path = profile_generate / PROFILE_SUBDIR_BOLT / obj_path.name
options += [
"--instrument",
f"--instrumentation-file={fdata_path}",
"--instrumentation-file-append-pid",
]
elif profile_use:
fdata_path = profile_use / PROFILE_SUBDIR_BOLT / (obj_path.name + ".fdata")
if fdata_path.exists():
options += [
f"--data={fdata_path}",
"--reorder-blocks=ext-tsp",
"--reorder-functions=hfsort",
"--split-functions",
"--split-all-cold",
"--split-eh",
"--dyno-stats",
]
else:
print(f"Profile data missing for {obj_path.relative_to(root)}")
invoke_bolt(toolchain, obj_path, bolt_log, options)
else:
print(f"Object {str(obj_path)} is NOT an optimization subject\n")
toolchain.strip_symbols(obj_path)
def boltify_toolchain(
input_path: Path,
dist_path: Path,
build_name: str,
strip_only: bool = False,
profile_generate: Path | None = None,
profile_use: Path | None = None,
toolchain: ClangToolchain = CLANG_TOOLCHAIN_HOST) -> None:
with (dist_path / BOLT_LOG_NAME).open("w") as bolt_log:
with handle_input_path(input_path) as input_dir_path:
print_fs_tree(input_dir_path)
with expand_profiles(profile_use) as profile_use_path:
if profile_use_path is not None:
print_fs_tree(profile_use_path)
version_file_path = profile_use_path / PROFILE_SUBDIR_BOLT / PROFILED_VERSION_FILENAME
if version_file_path.exists():
with open(version_file_path) as version_file:
profiled_version = version_file.read()
input_toolchain = RustToolchain(
input_dir_path, host=build_platform.system())
if input_toolchain.get_version() != profiled_version:
raise ScriptException(
"Profiled compiler and input compiler versions do not match")
try:
process_objects(
toolchain,
input_dir_path,
strip_only,
profile_generate,
profile_use_path,
bolt_log)
bolt_log.flush()
except subprocess.CalledProcessError as err:
print("Error BOLTing prebuilt objects")
raise err
print(f"Creating BOLTed archive")
try:
archive_create(dist_path / f"rust-{build_name}", input_dir_path, overwrite=True)
except subprocess.CalledProcessError as err:
print("Error creating the BOLTed archive")
raise err
def main() -> None:
if not build_platform.is_linux():
raise ScriptException("BOLT tools are only available on Linux hosts")
args = parse_args()
initialize_clang_prebuilt(args)
boltify_toolchain(
args.archive_path,
args.dist_path,
args.build_name,
args.strip_only,
args.profile_generate or args.profile_generate_relative,
args.profile_use)
if args.profile_use is not None and args.profile_use.name.endswith(
".tar.xz") and args.profile_use.parent != args.dist_path:
shutil.copy2(args.profile_use, args.dist_path)
if __name__ == "__main__":
try:
main()
except (ScriptException, argparse.ArgumentTypeError) as err:
print_colored(str(err), TERM_RED)
sys.exit(1)