|  | # Lint as: python3 | 
|  | # Copyright 2020 The Bazel Authors. All rights reserved. | 
|  | # | 
|  | # 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. | 
|  | """ctexplain: how does configuration affect build graphs? | 
|  |  | 
|  | This is a swiss army knife tool that tries to explain why build graphs are the | 
|  | size they are and how build flags, configuration transitions, and dependency | 
|  | structures affect that. | 
|  |  | 
|  | This can help developers use flags and transitions with minimal memory and | 
|  | maximum build speed. | 
|  |  | 
|  | Usage: | 
|  |  | 
|  | $ ctexplain [--analysis=...] -b "<targets_to_build> [build flags]" | 
|  |  | 
|  | Example: | 
|  |  | 
|  | $ ctexplain -b "//mypkg:mybinary --define MY_FEATURE=1" | 
|  |  | 
|  | Relevant terms in https://docs.bazel.build/versions/main/glossary.html: | 
|  | "target", "configuration", "analysis phase", "configured target", | 
|  | "configuration trimming", "transition" | 
|  |  | 
|  | TODO(gregce): link to proper documentation for full details. | 
|  | """ | 
|  | from typing import Callable | 
|  | from typing import Tuple | 
|  |  | 
|  | # Do not edit this line. Copybara replaces it with PY2 migration helper. | 
|  | from absl import app | 
|  | from absl import flags | 
|  | from dataclasses import dataclass | 
|  |  | 
|  | # Do not edit this line. Copybara replaces it with PY2 migration helper..third_party.bazel.tools.ctexplain.analyses.summary as summary | 
|  | from tools.ctexplain.bazel_api import BazelApi | 
|  | # Do not edit this line. Copybara replaces it with PY2 migration helper..third_party.bazel.tools.ctexplain.lib as lib | 
|  | from tools.ctexplain.types import ConfiguredTarget | 
|  | # Do not edit this line. Copybara replaces it with PY2 migration helper..third_party.bazel.tools.ctexplain.util as util | 
|  |  | 
|  | FLAGS = flags.FLAGS | 
|  |  | 
|  |  | 
|  | @dataclass(frozen=True) | 
|  | class Analysis(): | 
|  | """Supported analysis type.""" | 
|  | # The value in --analysis=<value> that triggers this analysis. | 
|  | key: str | 
|  | # The function that invokes this analysis. | 
|  | exec: Callable[[Tuple[ConfiguredTarget, ...]], None] | 
|  | # User-friendly analysis description. | 
|  | description: str | 
|  |  | 
|  | available_analyses = [ | 
|  | Analysis( | 
|  | "summary", | 
|  | lambda x: summary.report(summary.analyze(x)), | 
|  | "summarizes build graph size and how trimming could help" | 
|  | ), | 
|  | Analysis( | 
|  | "culprits", | 
|  | lambda x: print("this analysis not yet implemented"), | 
|  | "shows which flags unnecessarily fork configured targets. These\n" | 
|  | + "are conceptually mergeable." | 
|  | ), | 
|  | Analysis( | 
|  | "forked_targets", | 
|  | lambda x: print("this analysis not yet implemented"), | 
|  | "ranks targets by how many configured targets they\n" | 
|  | + "create. These may be legitimate forks (because they behave " | 
|  | + "differently with\n different flags) or identical clones that are " | 
|  | + "conceptually mergeable." | 
|  | ), | 
|  | Analysis( | 
|  | "cloned_targets", | 
|  | lambda x: print("this analysis not yet implemented"), | 
|  | "ranks targets by how many behavior-identical configured\n targets " | 
|  | + "they produce. These are conceptually mergeable." | 
|  | ) | 
|  | ] | 
|  |  | 
|  | # Available analyses, keyed by --analysis=<value> triggers. | 
|  | analyses = {analysis.key: analysis for analysis in available_analyses} | 
|  |  | 
|  |  | 
|  | # Command-line flag registration: | 
|  |  | 
|  |  | 
|  | def _render_analysis_help_text() -> str: | 
|  | """Pretty-prints help text for available analyses.""" | 
|  | return "\n".join(f'- "{name}": {analysis.description}' | 
|  | for name, analysis in analyses.items()) | 
|  |  | 
|  | flags.DEFINE_list("analysis", ["summary"], f""" | 
|  | Analyses to run. May be any comma-separated combination of | 
|  |  | 
|  | {_render_analysis_help_text()} | 
|  | """) | 
|  |  | 
|  | flags.register_validator( | 
|  | "analysis", | 
|  | lambda flag_value: all(name in analyses for name in flag_value), | 
|  | message=f'available analyses: {", ".join(analyses.keys())}') | 
|  |  | 
|  | flags.DEFINE_multi_string( | 
|  | "build", [], | 
|  | """command-line invocation of the build to analyze. For example: | 
|  | "//foo --define a=b". If listed multiple times, this is a "multi-build | 
|  | analysis" that measures how much distinct builds can share subgraphs""", | 
|  | short_name="b") | 
|  |  | 
|  |  | 
|  | # Core program logic: | 
|  |  | 
|  |  | 
|  | def _get_build_flags(cmdline: str) -> Tuple[Tuple[str, ...], Tuple[str, ...]]: | 
|  | """Parses a build invocation command line. | 
|  |  | 
|  | Args: | 
|  | cmdline: raw build invocation string. For example: "//foo --cpu=x86" | 
|  |  | 
|  | Returns: | 
|  | Tuple of ((target labels to build), (build flags)) | 
|  | """ | 
|  | cmdlist = cmdline.split() | 
|  | labels = [arg for arg in cmdlist if arg.startswith("//")] | 
|  | build_flags = [arg for arg in cmdlist if not arg.startswith("//")] | 
|  | return (tuple(labels), tuple(build_flags)) | 
|  |  | 
|  |  | 
|  | def main(argv): | 
|  | del argv  # Satisfy py linter's "unused" warning. | 
|  | if not FLAGS.build: | 
|  | exit("ctexplain: build efficiency measurement tool. Add --help " | 
|  | + "for usage.") | 
|  | elif len(FLAGS.build) > 1: | 
|  | exit("TODO(gregce): support multi-build shareability analysis") | 
|  |  | 
|  | (labels, build_flags) = _get_build_flags(FLAGS.build[0]) | 
|  | build_desc = ",".join(labels) | 
|  | with util.ProgressStep(f"Collecting configured targets for {build_desc}"): | 
|  | cts = lib.analyze_build(BazelApi(), labels, build_flags) | 
|  | for analysis in FLAGS.analysis: | 
|  | analyses[analysis].exec(cts) | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | app.run(main) |