diff options
author | Baranov Victor <bar.victor.2002@gmail.com> | 2025-08-03 11:22:13 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-08-03 11:22:13 +0300 |
commit | f72b3ec85267af38d55cd80060415079780406a4 (patch) | |
tree | ed65b1ee2bf7e949ba552d30c7b9072abeeec4ee /clang-tools-extra/clang-tidy | |
parent | 49b5a1fa71d5af2b463c327e17017c754eb50801 (diff) | |
download | llvm-f72b3ec85267af38d55cd80060415079780406a4.zip llvm-f72b3ec85267af38d55cd80060415079780406a4.tar.gz llvm-f72b3ec85267af38d55cd80060415079780406a4.tar.bz2 |
[clang-tidy] Add 'enable-check-profiling' with aggregated results to 'run-clang-tidy' (#151011)
Add new option `enable-check-profiling` to `run-clang-tidy` for seamless
integration of `clang-tidy`'s `enable-check-profiling` option.
`run-clang-tidy` will post aggregated results report in the same style
as `clang-tidy`.
This PR will help users to benchmark their `clang-tidy` runs easily.
Also, `clang-tidy` developers could build benchmark infrastructure in
the future.
Diffstat (limited to 'clang-tools-extra/clang-tidy')
-rwxr-xr-x | clang-tools-extra/clang-tidy/tool/run-clang-tidy.py | 150 |
1 files changed, 149 insertions, 1 deletions
diff --git a/clang-tools-extra/clang-tidy/tool/run-clang-tidy.py b/clang-tools-extra/clang-tidy/tool/run-clang-tidy.py index a3dca6c..d307b26 100755 --- a/clang-tools-extra/clang-tidy/tool/run-clang-tidy.py +++ b/clang-tools-extra/clang-tidy/tool/run-clang-tidy.py @@ -49,7 +49,7 @@ import tempfile import time import traceback from types import ModuleType -from typing import Any, Awaitable, Callable, List, Optional, TypeVar +from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, TypeVar yaml: Optional[ModuleType] = None @@ -105,6 +105,7 @@ def get_tidy_invocation( warnings_as_errors: Optional[str], exclude_header_filter: Optional[str], allow_no_checks: bool, + store_check_profile: Optional[str], ) -> List[str]: """Gets a command line for clang-tidy.""" start = [clang_tidy_binary] @@ -147,6 +148,9 @@ def get_tidy_invocation( start.append(f"--warnings-as-errors={warnings_as_errors}") if allow_no_checks: start.append("--allow-no-checks") + if store_check_profile: + start.append("--enable-check-profile") + start.append(f"--store-check-profile={store_check_profile}") if f: start.append(f) return start @@ -178,6 +182,124 @@ def merge_replacement_files(tmpdir: str, mergefile: str) -> None: open(mergefile, "w").close() +def aggregate_profiles(profile_dir: str) -> Dict[str, float]: + """Aggregate timing data from multiple profile JSON files""" + aggregated: Dict[str, float] = {} + + for profile_file in glob.iglob(os.path.join(profile_dir, "*.json")): + try: + with open(profile_file, "r", encoding="utf-8") as f: + data = json.load(f) + profile_data: Dict[str, float] = data.get("profile", {}) + + for key, value in profile_data.items(): + if key.startswith("time.clang-tidy."): + if key in aggregated: + aggregated[key] += value + else: + aggregated[key] = value + except (json.JSONDecodeError, KeyError, IOError) as e: + print(f"Error: invalid json file {profile_file}: {e}", file=sys.stderr) + continue + + return aggregated + + +def print_profile_data(aggregated_data: Dict[str, float]) -> None: + """Print aggregated checks profile data in the same format as clang-tidy""" + if not aggregated_data: + return + + # Extract checker names and their timing data + checkers: Dict[str, Dict[str, float]] = {} + for key, value in aggregated_data.items(): + parts = key.split(".") + if len(parts) >= 4 and parts[0] == "time" and parts[1] == "clang-tidy": + checker_name = ".".join( + parts[2:-1] + ) # Everything between "clang-tidy" and the timing type + timing_type = parts[-1] # wall, user, or sys + + if checker_name not in checkers: + checkers[checker_name] = {"wall": 0.0, "user": 0.0, "sys": 0.0} + + checkers[checker_name][timing_type] = value + + if not checkers: + return + + total_user = sum(data["user"] for data in checkers.values()) + total_sys = sum(data["sys"] for data in checkers.values()) + total_wall = sum(data["wall"] for data in checkers.values()) + + sorted_checkers: List[Tuple[str, Dict[str, float]]] = sorted( + checkers.items(), key=lambda x: x[1]["user"] + x[1]["sys"], reverse=True + ) + + def print_stderr(*args, **kwargs) -> None: + print(*args, file=sys.stderr, **kwargs) + + print_stderr( + "===-------------------------------------------------------------------------===" + ) + print_stderr(" clang-tidy checks profiling") + print_stderr( + "===-------------------------------------------------------------------------===" + ) + print_stderr( + f" Total Execution Time: {total_user + total_sys:.4f} seconds ({total_wall:.4f} wall clock)\n" + ) + + # Calculate field widths based on the Total line which has the largest values + total_combined = total_user + total_sys + user_width = len(f"{total_user:.4f}") + sys_width = len(f"{total_sys:.4f}") + combined_width = len(f"{total_combined:.4f}") + wall_width = len(f"{total_wall:.4f}") + + # Header with proper alignment + additional_width = 9 # for " (100.0%)" + user_header = "---User Time---".center(user_width + additional_width) + sys_header = "--System Time--".center(sys_width + additional_width) + combined_header = "--User+System--".center(combined_width + additional_width) + wall_header = "---Wall Time---".center(wall_width + additional_width) + + print_stderr( + f" {user_header} {sys_header} {combined_header} {wall_header} --- Name ---" + ) + + for checker_name, data in sorted_checkers: + user_time = data["user"] + sys_time = data["sys"] + wall_time = data["wall"] + combined_time = user_time + sys_time + + user_percent = (user_time / total_user * 100) if total_user > 0 else 0 + sys_percent = (sys_time / total_sys * 100) if total_sys > 0 else 0 + combined_percent = ( + (combined_time / total_combined * 100) if total_combined > 0 else 0 + ) + wall_percent = (wall_time / total_wall * 100) if total_wall > 0 else 0 + + user_str = f"{user_time:{user_width}.4f} ({user_percent:5.1f}%)" + sys_str = f"{sys_time:{sys_width}.4f} ({sys_percent:5.1f}%)" + combined_str = f"{combined_time:{combined_width}.4f} ({combined_percent:5.1f}%)" + wall_str = f"{wall_time:{wall_width}.4f} ({wall_percent:5.1f}%)" + + print_stderr( + f" {user_str} {sys_str} {combined_str} {wall_str} {checker_name}" + ) + + user_total_str = f"{total_user:{user_width}.4f} (100.0%)" + sys_total_str = f"{total_sys:{sys_width}.4f} (100.0%)" + combined_total_str = f"{total_combined:{combined_width}.4f} (100.0%)" + wall_total_str = f"{total_wall:{wall_width}.4f} (100.0%)" + + print_stderr( + f" {user_total_str} {sys_total_str} {combined_total_str} {wall_total_str} Total" + ) + + def find_binary(arg: str, name: str, build_path: str) -> str: """Get the path for a binary or exit""" if arg: @@ -240,6 +362,7 @@ async def run_tidy( clang_tidy_binary: str, tmpdir: str, build_path: str, + store_check_profile: Optional[str], ) -> ClangTidyResult: """ Runs clang-tidy on a single file and returns the result. @@ -263,6 +386,7 @@ async def run_tidy( args.warnings_as_errors, args.exclude_header_filter, args.allow_no_checks, + store_check_profile, ) try: @@ -447,6 +571,11 @@ async def main() -> None: action="store_true", help="Allow empty enabled checks.", ) + parser.add_argument( + "-enable-check-profile", + action="store_true", + help="Enable per-check timing profiles, and print a report", + ) args = parser.parse_args() db_path = "compile_commands.json" @@ -489,6 +618,10 @@ async def main() -> None: export_fixes_dir = tempfile.mkdtemp() delete_fixes_dir = True + profile_dir: Optional[str] = None + if args.enable_check_profile: + profile_dir = tempfile.mkdtemp() + try: invocation = get_tidy_invocation( None, @@ -509,6 +642,7 @@ async def main() -> None: args.warnings_as_errors, args.exclude_header_filter, args.allow_no_checks, + None, # No profiling for the list-checks invocation ) invocation.append("-list-checks") invocation.append("-") @@ -567,6 +701,7 @@ async def main() -> None: clang_tidy_binary, export_fixes_dir, build_path, + profile_dir, ) ) for f in files @@ -593,8 +728,19 @@ async def main() -> None: if delete_fixes_dir: assert export_fixes_dir shutil.rmtree(export_fixes_dir) + if profile_dir: + shutil.rmtree(profile_dir) return + if args.enable_check_profile and profile_dir: + # Ensure all clang-tidy stdout is flushed before printing profiling + sys.stdout.flush() + aggregated_data = aggregate_profiles(profile_dir) + if aggregated_data: + print_profile_data(aggregated_data) + else: + print("No profiling data found.") + if combine_fixes: print(f"Writing fixes to {args.export_fixes} ...") try: @@ -618,6 +764,8 @@ async def main() -> None: if delete_fixes_dir: assert export_fixes_dir shutil.rmtree(export_fixes_dir) + if profile_dir: + shutil.rmtree(profile_dir) sys.exit(returncode) |