blob: 6211b5670aa693a0faf18eb1048ccc371b7487f7 [file] [log] [blame]
use anyhow::Context;
use camino::{Utf8Path, Utf8PathBuf};
use clap::Parser;
use log::LevelFilter;
use utils::io;
use crate::bolt::{bolt_optimize, with_bolt_instrumented};
use crate::environment::{Environment, EnvironmentBuilder};
use crate::exec::{Bootstrap, cmd};
use crate::tests::run_tests;
use crate::timer::Timer;
use crate::training::{
gather_bolt_profiles, gather_llvm_profiles, gather_rustc_profiles, llvm_benchmarks,
rustc_benchmarks,
};
use crate::utils::artifact_size::print_binary_sizes;
use crate::utils::io::{copy_directory, reset_directory};
use crate::utils::{
clear_llvm_files, format_env_variables, print_free_disk_space, with_log_group,
write_timer_to_summary,
};
mod bolt;
mod environment;
mod exec;
mod metrics;
mod tests;
mod timer;
mod training;
mod utils;
#[derive(clap::Parser, Debug)]
struct Args {
#[clap(subcommand)]
env: EnvironmentCmd,
}
#[derive(clap::Parser, Clone, Debug)]
struct SharedArgs {
// Arguments passed to `x` to perform the final (dist) build.
build_args: Vec<String>,
}
#[derive(clap::Parser, Clone, Debug)]
enum EnvironmentCmd {
/// Perform a custom local PGO/BOLT optimized build.
Local {
/// Target triple of the host.
#[arg(long)]
target_triple: String,
/// Checkout directory of `rustc`.
#[arg(long)]
checkout_dir: Utf8PathBuf,
/// Host LLVM installation directory.
#[arg(long)]
llvm_dir: Utf8PathBuf,
/// Python binary to use in bootstrap invocations.
#[arg(long, default_value = "python3")]
python: String,
/// Directory where artifacts (like PGO profiles or rustc-perf) of this workflow
/// will be stored.
#[arg(long, default_value = "opt-artifacts")]
artifact_dir: Utf8PathBuf,
/// Checkout directory of `rustc-perf`.
///
/// If unspecified, defaults to the rustc-perf submodule in the rustc checkout dir
/// (`src/tools/rustc-perf`), which should have been initialized when building this tool.
// FIXME: Move update_submodule into build_helper, that way we can also ensure the submodule
// is updated when _running_ opt-dist, rather than building.
#[arg(long)]
rustc_perf_checkout_dir: Option<Utf8PathBuf>,
/// Is LLVM for `rustc` built in shared library mode?
#[arg(long, default_value_t = true)]
llvm_shared: bool,
/// Should BOLT optimization be used? If yes, host LLVM must have BOLT binaries
/// (`llvm-bolt` and `merge-fdata`) available.
#[arg(long, default_value_t = false)]
use_bolt: bool,
/// Tests that should be skipped when testing the optimized compiler.
#[arg(long)]
skipped_tests: Vec<String>,
#[clap(flatten)]
shared: SharedArgs,
/// Arguments passed to `rustc-perf --cargo-config <value>` when running benchmarks.
#[arg(long)]
benchmark_cargo_config: Vec<String>,
},
/// Perform an optimized build on Linux CI, from inside Docker.
LinuxCi {
#[clap(flatten)]
shared: SharedArgs,
},
/// Perform an optimized build on Windows CI, directly inside Github Actions.
WindowsCi {
#[clap(flatten)]
shared: SharedArgs,
},
}
fn is_try_build() -> bool {
std::env::var("DIST_TRY_BUILD").unwrap_or_else(|_| "0".to_string()) != "0"
}
fn create_environment(args: Args) -> anyhow::Result<(Environment, Vec<String>)> {
let (env, args) = match args.env {
EnvironmentCmd::Local {
target_triple,
checkout_dir,
llvm_dir,
python,
artifact_dir,
rustc_perf_checkout_dir,
llvm_shared,
use_bolt,
skipped_tests,
benchmark_cargo_config,
shared,
} => {
let env = EnvironmentBuilder::default()
.host_triple(target_triple)
.python_binary(python)
.checkout_dir(checkout_dir.clone())
.host_llvm_dir(llvm_dir)
.artifact_dir(artifact_dir)
.build_dir(checkout_dir)
.prebuilt_rustc_perf(rustc_perf_checkout_dir)
.shared_llvm(llvm_shared)
.use_bolt(use_bolt)
.skipped_tests(skipped_tests)
.benchmark_cargo_config(benchmark_cargo_config)
.build()?;
(env, shared.build_args)
}
EnvironmentCmd::LinuxCi { shared } => {
let target_triple =
std::env::var("PGO_HOST").expect("PGO_HOST environment variable missing");
let checkout_dir = Utf8PathBuf::from("/checkout");
let env = EnvironmentBuilder::default()
.host_triple(target_triple)
.python_binary("python3".to_string())
.checkout_dir(checkout_dir.clone())
.host_llvm_dir(Utf8PathBuf::from("/rustroot"))
.artifact_dir(Utf8PathBuf::from("/tmp/tmp-multistage/opt-artifacts"))
.build_dir(checkout_dir.join("obj"))
.shared_llvm(true)
.use_bolt(true)
.skipped_tests(vec![
// Fails because of linker errors, as of June 2023.
"tests/ui/process/nofile-limit.rs".to_string(),
])
.build()?;
(env, shared.build_args)
}
EnvironmentCmd::WindowsCi { shared } => {
let target_triple =
std::env::var("PGO_HOST").expect("PGO_HOST environment variable missing");
let checkout_dir: Utf8PathBuf = std::env::current_dir()?.try_into()?;
let env = EnvironmentBuilder::default()
.host_triple(target_triple)
.python_binary("python".to_string())
.checkout_dir(checkout_dir.clone())
.host_llvm_dir(checkout_dir.join("citools").join("clang-rust"))
.artifact_dir(checkout_dir.join("opt-artifacts"))
.build_dir(checkout_dir)
.shared_llvm(false)
.use_bolt(false)
.skipped_tests(vec![
// Fails as of June 2023.
"tests\\codegen\\vec-shrink-panik.rs".to_string(),
])
.build()?;
(env, shared.build_args)
}
};
Ok((env, args))
}
fn execute_pipeline(
env: &Environment,
timer: &mut Timer,
dist_args: Vec<String>,
) -> anyhow::Result<()> {
reset_directory(&env.artifact_dir())?;
with_log_group("Building rustc-perf", || {
let rustc_perf_checkout_dir = match env.prebuilt_rustc_perf() {
Some(dir) => dir,
None => env.checkout_path().join("src").join("tools").join("rustc-perf"),
};
copy_rustc_perf(env, &rustc_perf_checkout_dir)
})?;
// Stage 1: Build PGO instrumented rustc
// We use a normal build of LLVM, because gathering PGO profiles for LLVM and `rustc` at the
// same time can cause issues, because the host and in-tree LLVM versions can diverge.
let rustc_pgo_profile = timer.section("Stage 1 (Rustc PGO)", |stage| {
let rustc_profile_dir_root = env.artifact_dir().join("rustc-pgo");
stage.section("Build PGO instrumented rustc and LLVM", |section| {
let mut builder = Bootstrap::build(env).rustc_pgo_instrument(&rustc_profile_dir_root);
if env.supports_shared_llvm() {
// This first LLVM that we build will be thrown away after this stage, and it
// doesn't really need LTO. Without LTO, it builds in ~1 minute thanks to sccache,
// with LTO it takes almost 10 minutes. It makes the followup Rustc PGO
// instrumented/optimized build a bit slower, but it seems to be worth it.
builder = builder.without_llvm_lto();
}
builder.run(section)
})?;
let profile = stage
.section("Gather profiles", |_| gather_rustc_profiles(env, &rustc_profile_dir_root))?;
print_free_disk_space()?;
stage.section("Build PGO optimized rustc", |section| {
let mut cmd = Bootstrap::build(env).rustc_pgo_optimize(&profile);
if env.use_bolt() {
cmd = cmd.with_rustc_bolt_ldflags();
}
cmd.run(section)
})?;
Ok(profile)
})?;
// Stage 2: Gather LLVM PGO profiles
// Here we build a PGO instrumented LLVM, reusing the previously PGO optimized rustc.
// Then we use the instrumented LLVM to gather LLVM PGO profiles.
let llvm_pgo_profile = timer.section("Stage 2 (LLVM PGO)", |stage| {
// Remove the previous, uninstrumented build of LLVM.
clear_llvm_files(env)?;
let llvm_profile_dir_root = env.artifact_dir().join("llvm-pgo");
stage.section("Build PGO instrumented LLVM", |section| {
Bootstrap::build(env)
.llvm_pgo_instrument(&llvm_profile_dir_root)
.avoid_rustc_rebuild()
.run(section)
})?;
let profile = stage
.section("Gather profiles", |_| gather_llvm_profiles(env, &llvm_profile_dir_root))?;
print_free_disk_space()?;
// Proactively delete the instrumented artifacts, to avoid using them by accident in
// follow-up stages.
clear_llvm_files(env)?;
Ok(profile)
})?;
let bolt_profiles = if env.use_bolt() {
// Stage 3: Build BOLT instrumented LLVM
// We build a PGO optimized LLVM in this step, then instrument it with BOLT and gather BOLT profiles.
// Note that we don't remove LLVM artifacts after this step, so that they are reused in the final dist build.
// BOLT instrumentation is performed "on-the-fly" when the LLVM library is copied to the sysroot of rustc,
// therefore the LLVM artifacts on disk are not "tainted" with BOLT instrumentation and they can be reused.
timer.section("Stage 3 (BOLT)", |stage| {
stage.section("Build PGO optimized LLVM", |stage| {
Bootstrap::build(env)
.with_llvm_bolt_ldflags()
.llvm_pgo_optimize(&llvm_pgo_profile)
.avoid_rustc_rebuild()
.run(stage)
})?;
let libdir = env.build_artifacts().join("stage2").join("lib");
// The actual name will be something like libLLVM.so.18.1-rust-dev.
let llvm_lib = io::find_file_in_dir(&libdir, "libLLVM.so", "")?;
log::info!("Optimizing {llvm_lib} with BOLT");
// FIXME(kobzol): try gather profiles together, at once for LLVM and rustc
// Instrument the libraries and gather profiles
let llvm_profile = with_bolt_instrumented(&llvm_lib, |llvm_profile_dir| {
stage.section("Gather profiles", |_| {
gather_bolt_profiles(env, "LLVM", llvm_benchmarks(env), llvm_profile_dir)
})
})?;
print_free_disk_space()?;
// Now optimize the library with BOLT. The `libLLVM-XXX.so` library is actually hard-linked
// from several places, and this specific path (`llvm_lib`) will *not* be packaged into
// the final dist build. However, when BOLT optimizes an artifact, it does so *in-place*,
// therefore it will actually optimize all the hard links, which means that the final
// packaged `libLLVM.so` file *will* be BOLT optimized.
bolt_optimize(&llvm_lib, &llvm_profile).context("Could not optimize LLVM with BOLT")?;
let rustc_lib = io::find_file_in_dir(&libdir, "librustc_driver", ".so")?;
log::info!("Optimizing {rustc_lib} with BOLT");
// Instrument it and gather profiles
let rustc_profile = with_bolt_instrumented(&rustc_lib, |rustc_profile_dir| {
stage.section("Gather profiles", |_| {
gather_bolt_profiles(env, "rustc", rustc_benchmarks(env), rustc_profile_dir)
})
})?;
print_free_disk_space()?;
// Now optimize the library with BOLT.
bolt_optimize(&rustc_lib, &rustc_profile)
.context("Could not optimize rustc with BOLT")?;
// LLVM is not being cleared here, we want to use the BOLT-optimized LLVM
Ok(vec![llvm_profile, rustc_profile])
})?
} else {
vec![]
};
let mut dist = Bootstrap::dist(env, &dist_args)
.llvm_pgo_optimize(&llvm_pgo_profile)
.rustc_pgo_optimize(&rustc_pgo_profile)
.avoid_rustc_rebuild();
for bolt_profile in bolt_profiles {
dist = dist.with_bolt_profile(bolt_profile);
}
// Final stage: Assemble the dist artifacts
// The previous PGO optimized rustc build and PGO optimized LLVM builds should be reused.
timer.section("Stage 5 (final build)", |stage| dist.run(stage))?;
// After dist has finished, run a subset of the test suite on the optimized artifacts to discover
// possible regressions.
// The tests are not executed for try builds, which can be in various broken states, so we don't
// want to gatekeep them with tests.
if !is_try_build() {
timer.section("Run tests", |_| run_tests(env))?;
}
Ok(())
}
fn main() -> anyhow::Result<()> {
// Make sure that we get backtraces for easier debugging in CI
std::env::set_var("RUST_BACKTRACE", "1");
env_logger::builder()
.filter_level(LevelFilter::Info)
.format_timestamp_millis()
.parse_default_env()
.init();
let args = Args::parse();
println!("Running optimized build pipeline with args `{:?}`", args);
with_log_group("Environment values", || {
println!("Environment values\n{}", format_env_variables());
});
with_log_group("Printing config.toml", || {
if let Ok(config) = std::fs::read_to_string("config.toml") {
println!("Contents of `config.toml`:\n{config}");
}
});
let (env, mut build_args) = create_environment(args).context("Cannot create environment")?;
// Skip components that are not needed for try builds to speed them up
if is_try_build() {
log::info!("Skipping building of unimportant components for a try build");
for target in [
"rust-docs",
"rustc-docs",
"rust-docs-json",
"rust-analyzer",
"rustc-src",
"clippy",
"miri",
"rustfmt",
] {
build_args.extend(["--skip".to_string(), target.to_string()]);
}
}
let mut timer = Timer::new();
let result = execute_pipeline(&env, &mut timer, build_args);
log::info!("Timer results\n{}", timer.format_stats());
if let Ok(summary_path) = std::env::var("GITHUB_STEP_SUMMARY") {
write_timer_to_summary(&summary_path, &timer)?;
}
print_free_disk_space()?;
result.context("Optimized build pipeline has failed")?;
print_binary_sizes(&env)?;
Ok(())
}
// Copy rustc-perf from the given path into the environment and build it.
fn copy_rustc_perf(env: &Environment, dir: &Utf8Path) -> anyhow::Result<()> {
copy_directory(dir, &env.rustc_perf_dir())?;
build_rustc_perf(env)
}
fn build_rustc_perf(env: &Environment) -> anyhow::Result<()> {
cmd(&[env.cargo_stage_0().as_str(), "build", "-p", "collector"])
.workdir(&env.rustc_perf_dir())
.env("RUSTC", &env.rustc_stage_0().into_string())
.env("RUSTC_BOOTSTRAP", "1")
.run()?;
Ok(())
}