| # 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) |