aboutsummaryrefslogtreecommitdiff
path: root/clang-tools-extra/clang-tidy
diff options
context:
space:
mode:
authorBaranov Victor <bar.victor.2002@gmail.com>2025-08-03 11:22:13 +0300
committerGitHub <noreply@github.com>2025-08-03 11:22:13 +0300
commitf72b3ec85267af38d55cd80060415079780406a4 (patch)
treeed65b1ee2bf7e949ba552d30c7b9072abeeec4ee /clang-tools-extra/clang-tidy
parent49b5a1fa71d5af2b463c327e17017c754eb50801 (diff)
downloadllvm-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-xclang-tools-extra/clang-tidy/tool/run-clang-tidy.py150
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)