2 # ignore-tidy-linelength
4 # Compatible with Python 3.6+
18 from io import StringIO
19 from pathlib import Path
20 from typing import Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union
22 PGO_HOST = os.environ["PGO_HOST"]
24 LOGGER = logging.getLogger("stage-build")
40 "token-stream-stress",
47 LLVM_BOLT_CRATES = LLVM_PGO_CRATES
52 def checkout_path(self) -> Path:
54 The root checkout, where the source is located.
56 raise NotImplementedError
58 def downloaded_llvm_dir(self) -> Path:
60 Directory where the host LLVM is located.
62 raise NotImplementedError
64 def build_root(self) -> Path:
66 The main directory where the build occurs.
68 raise NotImplementedError
70 def build_artifacts(self) -> Path:
71 return self.build_root() / "build" / PGO_HOST
73 def rustc_stage_0(self) -> Path:
74 return self.build_artifacts() / "stage0" / "bin" / "rustc"
76 def cargo_stage_0(self) -> Path:
77 return self.build_artifacts() / "stage0" / "bin" / "cargo"
79 def rustc_stage_2(self) -> Path:
80 return self.build_artifacts() / "stage2" / "bin" / "rustc"
82 def opt_artifacts(self) -> Path:
83 raise NotImplementedError
85 def llvm_profile_dir_root(self) -> Path:
86 return self.opt_artifacts() / "llvm-pgo"
88 def llvm_profile_merged_file(self) -> Path:
89 return self.opt_artifacts() / "llvm-pgo.profdata"
91 def rustc_perf_dir(self) -> Path:
92 return self.opt_artifacts() / "rustc-perf"
94 def build_rustc_perf(self):
95 raise NotImplementedError()
97 def rustc_profile_dir_root(self) -> Path:
98 return self.opt_artifacts() / "rustc-pgo"
100 def rustc_profile_merged_file(self) -> Path:
101 return self.opt_artifacts() / "rustc-pgo.profdata"
103 def rustc_profile_template_path(self) -> Path:
105 The profile data is written into a single filepath that is being repeatedly merged when each
106 rustc invocation ends. Empirically, this can result in some profiling data being lost. That's
107 why we override the profile path to include the PID. This will produce many more profiling
108 files, but the resulting profile will produce a slightly faster rustc binary.
110 return self.rustc_profile_dir_root() / "default_%m_%p.profraw"
112 def supports_bolt(self) -> bool:
113 raise NotImplementedError
115 def llvm_bolt_profile_merged_file(self) -> Path:
116 return self.opt_artifacts() / "bolt.profdata"
119 class LinuxPipeline(Pipeline):
120 def checkout_path(self) -> Path:
121 return Path("/checkout")
123 def downloaded_llvm_dir(self) -> Path:
124 return Path("/rustroot")
126 def build_root(self) -> Path:
127 return self.checkout_path() / "obj"
129 def opt_artifacts(self) -> Path:
130 return Path("/tmp/tmp-multistage/opt-artifacts")
132 def build_rustc_perf(self):
133 # /tmp/rustc-perf comes from the Dockerfile
134 shutil.copytree("/tmp/rustc-perf", self.rustc_perf_dir())
135 cmd(["chown", "-R", f"{getpass.getuser()}:", self.rustc_perf_dir()])
137 with change_cwd(self.rustc_perf_dir()):
138 cmd([self.cargo_stage_0(), "build", "-p", "collector"], env=dict(
139 RUSTC=str(self.rustc_stage_0()),
143 def supports_bolt(self) -> bool:
147 class WindowsPipeline(Pipeline):
149 self.checkout_dir = Path(os.getcwd())
151 def checkout_path(self) -> Path:
152 return self.checkout_dir
154 def downloaded_llvm_dir(self) -> Path:
155 return self.checkout_path() / "citools" / "clang-rust"
157 def build_root(self) -> Path:
158 return self.checkout_path()
160 def opt_artifacts(self) -> Path:
161 return self.checkout_path() / "opt-artifacts"
163 def rustc_stage_0(self) -> Path:
164 return super().rustc_stage_0().with_suffix(".exe")
166 def cargo_stage_0(self) -> Path:
167 return super().cargo_stage_0().with_suffix(".exe")
169 def rustc_stage_2(self) -> Path:
170 return super().rustc_stage_2().with_suffix(".exe")
172 def build_rustc_perf(self):
173 # rustc-perf version from 2022-07-22
174 perf_commit = "3c253134664fdcba862c539d37f0de18557a9a4c"
175 rustc_perf_zip_path = self.opt_artifacts() / "perf.zip"
177 def download_rustc_perf():
179 f"https://github.com/rust-lang/rustc-perf/archive/{perf_commit}.zip",
182 with change_cwd(self.opt_artifacts()):
183 unpack_archive(rustc_perf_zip_path)
184 move_path(Path(f"rustc-perf-{perf_commit}"), self.rustc_perf_dir())
185 delete_file(rustc_perf_zip_path)
187 retry_action(download_rustc_perf, "Download rustc-perf")
189 with change_cwd(self.rustc_perf_dir()):
190 cmd([self.cargo_stage_0(), "build", "-p", "collector"], env=dict(
191 RUSTC=str(self.rustc_stage_0()),
195 def rustc_profile_template_path(self) -> Path:
197 On Windows, we don't have enough space to use separate files for each rustc invocation.
198 Therefore, we use a single file for the generated profiles.
200 return self.rustc_profile_dir_root() / "default_%m.profraw"
202 def supports_bolt(self) -> bool:
206 def get_timestamp() -> float:
211 TimerSection = Union[Duration, "Timer"]
214 def iterate_sections(section: TimerSection, name: str, level: int = 0) -> Iterator[Tuple[int, str, Duration]]:
216 Hierarchically iterate the sections of a timer, in a depth-first order.
218 if isinstance(section, Duration):
219 yield (level, name, section)
220 elif isinstance(section, Timer):
221 yield (level, name, section.total_duration())
222 for (child_name, child_section) in section.sections:
223 yield from iterate_sections(child_section, child_name, level=level + 1)
229 def __init__(self, parent_names: Tuple[str, ...] = ()):
230 self.sections: List[Tuple[str, TimerSection]] = []
231 self.section_active = False
232 self.parent_names = parent_names
234 @contextlib.contextmanager
235 def section(self, name: str) -> "Timer":
236 assert not self.section_active
237 self.section_active = True
239 start = get_timestamp()
242 child_timer = Timer(parent_names=self.parent_names + (name, ))
243 full_name = " > ".join(child_timer.parent_names)
245 LOGGER.info(f"Section `{full_name}` starts")
247 except BaseException as exception:
251 end = get_timestamp()
252 duration = end - start
254 if child_timer.has_children():
255 self.sections.append((name, child_timer))
257 self.sections.append((name, duration))
259 LOGGER.info(f"Section `{full_name}` ended: OK ({duration:.2f}s)")
261 LOGGER.info(f"Section `{full_name}` ended: FAIL ({duration:.2f}s)")
262 self.section_active = False
264 def total_duration(self) -> Duration:
266 for (_, section) in self.sections:
267 if isinstance(section, Duration):
270 duration += section.total_duration()
273 def has_children(self) -> bool:
274 return len(self.sections) > 0
276 def print_stats(self):
278 for (child_name, child_section) in self.sections:
279 for (level, name, duration) in iterate_sections(child_section, child_name, level=0):
280 label = f"{' ' * level}{name}:"
281 rows.append((label, duration))
284 rows.append(("", ""))
286 total_duration_label = "Total duration:"
287 total_duration = self.total_duration()
288 rows.append((total_duration_label, humantime(total_duration)))
290 space_after_label = 2
291 max_label_length = max(16, max(len(label) for (label, _) in rows)) + space_after_label
293 table_width = max_label_length + 23
294 divider = "-" * table_width
296 with StringIO() as output:
297 print(divider, file=output)
298 for (label, duration) in rows:
299 if isinstance(duration, Duration):
300 pct = (duration / total_duration) * 100
301 value = f"{duration:>12.2f}s ({pct:>5.2f}%)"
303 value = f"{duration:>{len(total_duration_label) + 7}}"
304 print(f"{label:<{max_label_length}} {value}", file=output)
305 print(divider, file=output, end="")
306 LOGGER.info(f"Timer results\n{output.getvalue()}")
309 @contextlib.contextmanager
310 def change_cwd(dir: Path):
312 Temporarily change working directory to `dir`.
315 LOGGER.debug(f"Changing working dir from `{cwd}` to `{dir}`")
320 LOGGER.debug(f"Reverting working dir to `{cwd}`")
324 def humantime(time_s: float) -> str:
325 hours = time_s // 3600
326 time_s = time_s % 3600
327 minutes = time_s // 60
328 seconds = time_s % 60
332 result += f"{int(hours)}h "
334 result += f"{int(minutes)}m "
335 result += f"{round(seconds)}s"
339 def move_path(src: Path, dst: Path):
340 LOGGER.info(f"Moving `{src}` to `{dst}`")
341 shutil.move(src, dst)
344 def delete_file(path: Path):
345 LOGGER.info(f"Deleting file `{path}`")
349 def delete_directory(path: Path):
350 LOGGER.info(f"Deleting directory `{path}`")
354 def unpack_archive(archive: Path):
355 LOGGER.info(f"Unpacking archive `{archive}`")
356 shutil.unpack_archive(archive)
359 def download_file(src: str, target: Path):
360 LOGGER.info(f"Downloading `{src}` into `{target}`")
361 urllib.request.urlretrieve(src, str(target))
364 def retry_action(action, name: str, max_fails: int = 5):
365 LOGGER.info(f"Attempting to perform action `{name}` with retry")
366 for iteration in range(max_fails):
367 LOGGER.info(f"Attempt {iteration + 1}/{max_fails}")
372 LOGGER.error(f"Action `{name}` has failed\n{traceback.format_exc()}")
374 raise Exception(f"Action `{name}` has failed after {max_fails} attempts")
378 args: List[Union[str, Path]],
379 env: Optional[Dict[str, str]] = None,
380 output_path: Optional[Path] = None
382 args = [str(arg) for arg in args]
384 environment = os.environ.copy()
388 environment.update(env)
389 cmd_str += " ".join(f"{k}={v}" for (k, v) in (env or {}).items())
391 cmd_str += " ".join(args)
392 if output_path is not None:
393 cmd_str += f" > {output_path}"
394 LOGGER.info(f"Executing `{cmd_str}`")
396 if output_path is not None:
397 with open(output_path, "w") as f:
398 return subprocess.run(
404 return subprocess.run(args, env=environment, check=True)
407 def run_compiler_benchmarks(
410 scenarios: List[str],
412 env: Optional[Dict[str, str]] = None
414 env = env if env is not None else {}
416 # Compile libcore, both in opt-level=0 and opt-level=3
417 with change_cwd(pipeline.build_root()):
419 pipeline.rustc_stage_2(),
421 "--crate-type", "lib",
422 str(pipeline.checkout_path() / "library/core/src/lib.rs"),
423 "--out-dir", pipeline.opt_artifacts()
424 ], env=dict(RUSTC_BOOTSTRAP="1", **env))
427 pipeline.rustc_stage_2(),
429 "--crate-type", "lib",
431 str(pipeline.checkout_path() / "library/core/src/lib.rs"),
432 "--out-dir", pipeline.opt_artifacts()
433 ], env=dict(RUSTC_BOOTSTRAP="1", **env))
435 # Run rustc-perf benchmarks
436 # Benchmark using profile_local with eprintln, which essentially just means
437 # don't actually benchmark -- just make sure we run rustc a bunch of times.
438 with change_cwd(pipeline.rustc_perf_dir()):
440 pipeline.cargo_stage_0(),
442 "-p", "collector", "--bin", "collector", "--",
443 "profile_local", "eprintln",
444 pipeline.rustc_stage_2(),
446 "--cargo", pipeline.cargo_stage_0(),
447 "--profiles", ",".join(profiles),
448 "--scenarios", ",".join(scenarios),
449 "--include", ",".join(crates)
451 RUST_LOG="collector=debug",
452 RUSTC=str(pipeline.rustc_stage_0()),
458 # https://stackoverflow.com/a/31631711/1107768
459 def format_bytes(size: int) -> str:
460 """Return the given bytes as a human friendly KiB, MiB or GiB string."""
462 MB = KB ** 2 # 1,048,576
463 GB = KB ** 3 # 1,073,741,824
464 TB = KB ** 4 # 1,099,511,627,776
468 elif KB <= size < MB:
469 return f"{size / KB:.2f} KiB"
470 elif MB <= size < GB:
471 return f"{size / MB:.2f} MiB"
472 elif GB <= size < TB:
473 return f"{size / GB:.2f} GiB"
478 # https://stackoverflow.com/a/63307131/1107768
479 def count_files(path: Path) -> int:
480 return sum(1 for p in path.rglob("*") if p.is_file())
483 def count_files_with_prefix(path: Path) -> int:
484 return sum(1 for p in glob.glob(f"{path}*") if Path(p).is_file())
487 # https://stackoverflow.com/a/55659577/1107768
488 def get_path_size(path: Path) -> int:
490 return sum(p.stat().st_size for p in path.rglob("*"))
491 return path.stat().st_size
494 def get_path_prefix_size(path: Path) -> int:
496 Get size of all files beginning with the prefix `path`.
497 Alternative to shell `du -sh <path>*`.
499 return sum(Path(p).stat().st_size for p in glob.glob(f"{path}*"))
502 def get_files(directory: Path, filter: Optional[Callable[[Path], bool]] = None) -> Iterable[Path]:
503 for file in os.listdir(directory):
504 path = directory / file
505 if filter is None or filter(path):
512 env: Optional[Dict[str, str]] = None
516 pipeline.checkout_path() / "x.py",
518 "--target", PGO_HOST,
523 cmd(arguments, env=env)
526 def create_pipeline() -> Pipeline:
527 if sys.platform == "linux":
528 return LinuxPipeline()
529 elif sys.platform in ("cygwin", "win32"):
530 return WindowsPipeline()
532 raise Exception(f"Optimized build is not supported for platform {sys.platform}")
535 def gather_llvm_profiles(pipeline: Pipeline):
536 LOGGER.info("Running benchmarks with PGO instrumented LLVM")
537 run_compiler_benchmarks(
539 profiles=["Debug", "Opt"],
541 crates=LLVM_PGO_CRATES
544 profile_path = pipeline.llvm_profile_merged_file()
545 LOGGER.info(f"Merging LLVM PGO profiles to {profile_path}")
547 pipeline.downloaded_llvm_dir() / "bin" / "llvm-profdata",
550 pipeline.llvm_profile_dir_root()
553 LOGGER.info("LLVM PGO statistics")
554 LOGGER.info(f"{profile_path}: {format_bytes(get_path_size(profile_path))}")
556 f"{pipeline.llvm_profile_dir_root()}: {format_bytes(get_path_size(pipeline.llvm_profile_dir_root()))}")
557 LOGGER.info(f"Profile file count: {count_files(pipeline.llvm_profile_dir_root())}")
559 # We don't need the individual .profraw files now that they have been merged
560 # into a final .profdata
561 delete_directory(pipeline.llvm_profile_dir_root())
564 def gather_rustc_profiles(pipeline: Pipeline):
565 LOGGER.info("Running benchmarks with PGO instrumented rustc")
567 # Here we're profiling the `rustc` frontend, so we also include `Check`.
568 # The benchmark set includes various stress tests that put the frontend under pressure.
569 run_compiler_benchmarks(
571 profiles=["Check", "Debug", "Opt"],
573 crates=RUSTC_PGO_CRATES,
575 LLVM_PROFILE_FILE=str(pipeline.rustc_profile_template_path())
579 profile_path = pipeline.rustc_profile_merged_file()
580 LOGGER.info(f"Merging Rustc PGO profiles to {profile_path}")
582 pipeline.build_artifacts() / "llvm" / "bin" / "llvm-profdata",
585 pipeline.rustc_profile_dir_root()
588 LOGGER.info("Rustc PGO statistics")
589 LOGGER.info(f"{profile_path}: {format_bytes(get_path_size(profile_path))}")
591 f"{pipeline.rustc_profile_dir_root()}: {format_bytes(get_path_size(pipeline.rustc_profile_dir_root()))}")
592 LOGGER.info(f"Profile file count: {count_files(pipeline.rustc_profile_dir_root())}")
594 # We don't need the individual .profraw files now that they have been merged
595 # into a final .profdata
596 delete_directory(pipeline.rustc_profile_dir_root())
599 def gather_llvm_bolt_profiles(pipeline: Pipeline):
600 LOGGER.info("Running benchmarks with BOLT instrumented LLVM")
601 run_compiler_benchmarks(
603 profiles=["Check", "Debug", "Opt"],
605 crates=LLVM_BOLT_CRATES
608 merged_profile_path = pipeline.llvm_bolt_profile_merged_file()
609 profile_files_path = Path("/tmp/prof.fdata")
610 LOGGER.info(f"Merging LLVM BOLT profiles to {merged_profile_path}")
612 profile_files = sorted(glob.glob(f"{profile_files_path}*"))
616 ], output_path=merged_profile_path)
618 LOGGER.info("LLVM BOLT statistics")
619 LOGGER.info(f"{merged_profile_path}: {format_bytes(get_path_size(merged_profile_path))}")
621 f"{profile_files_path}: {format_bytes(get_path_prefix_size(profile_files_path))}")
622 LOGGER.info(f"Profile file count: {count_files_with_prefix(profile_files_path)}")
625 def clear_llvm_files(pipeline: Pipeline):
627 Rustbuild currently doesn't support rebuilding LLVM when PGO options
628 change (or any other llvm-related options); so just clear out the relevant
629 directories ourselves.
631 LOGGER.info("Clearing LLVM build files")
632 delete_directory(pipeline.build_artifacts() / "llvm")
633 delete_directory(pipeline.build_artifacts() / "lld")
636 def print_binary_sizes(pipeline: Pipeline):
637 bin_dir = pipeline.build_artifacts() / "stage2" / "bin"
638 binaries = get_files(bin_dir)
640 lib_dir = pipeline.build_artifacts() / "stage2" / "lib"
641 libraries = get_files(lib_dir, lambda p: p.suffix == ".so")
643 paths = sorted(binaries) + sorted(libraries)
644 with StringIO() as output:
646 path_str = f"{path.name}:"
647 print(f"{path_str:<30}{format_bytes(path.stat().st_size):>14}", file=output)
648 LOGGER.info(f"Rustc binary size\n{output.getvalue()}")
651 def execute_build_pipeline(timer: Timer, pipeline: Pipeline, final_build_args: List[str]):
652 # Clear and prepare tmp directory
653 shutil.rmtree(pipeline.opt_artifacts(), ignore_errors=True)
654 os.makedirs(pipeline.opt_artifacts(), exist_ok=True)
656 pipeline.build_rustc_perf()
658 # Stage 1: Build rustc + PGO instrumented LLVM
659 with timer.section("Stage 1 (LLVM PGO)") as stage1:
660 with stage1.section("Build rustc and LLVM"):
661 build_rustc(pipeline, args=[
662 "--llvm-profile-generate"
664 LLVM_PROFILE_DIR=str(pipeline.llvm_profile_dir_root() / "prof-%p")
667 with stage1.section("Gather profiles"):
668 gather_llvm_profiles(pipeline)
670 clear_llvm_files(pipeline)
671 final_build_args += [
672 "--llvm-profile-use",
673 pipeline.llvm_profile_merged_file()
676 # Stage 2: Build PGO instrumented rustc + LLVM
677 with timer.section("Stage 2 (rustc PGO)") as stage2:
678 with stage2.section("Build rustc and LLVM"):
679 build_rustc(pipeline, args=[
680 "--rust-profile-generate",
681 pipeline.rustc_profile_dir_root()
684 with stage2.section("Gather profiles"):
685 gather_rustc_profiles(pipeline)
687 clear_llvm_files(pipeline)
688 final_build_args += [
689 "--rust-profile-use",
690 pipeline.rustc_profile_merged_file()
693 # Stage 3: Build rustc + BOLT instrumented LLVM
694 if pipeline.supports_bolt():
695 with timer.section("Stage 3 (LLVM BOLT)") as stage3:
696 with stage3.section("Build rustc and LLVM"):
697 build_rustc(pipeline, args=[
698 "--llvm-profile-use",
699 pipeline.llvm_profile_merged_file(),
700 "--llvm-bolt-profile-generate",
702 with stage3.section("Gather profiles"):
703 gather_llvm_bolt_profiles(pipeline)
705 clear_llvm_files(pipeline)
706 final_build_args += [
707 "--llvm-bolt-profile-use",
708 pipeline.llvm_bolt_profile_merged_file()
711 # Stage 4: Build PGO optimized rustc + PGO/BOLT optimized LLVM
712 with timer.section("Stage 4 (final build)"):
713 cmd(final_build_args)
716 if __name__ == "__main__":
719 format="%(name)s %(levelname)-4s: %(message)s",
722 LOGGER.info(f"Running multi-stage build using Python {sys.version}")
723 LOGGER.info(f"Environment values\n{pprint.pformat(dict(os.environ), indent=2)}")
725 build_args = sys.argv[1:]
728 pipeline = create_pipeline()
730 execute_build_pipeline(timer, pipeline, build_args)
731 except BaseException as e:
732 LOGGER.error("The multi-stage build has failed")
737 print_binary_sizes(pipeline)