Skip to content

Commit f72b3ec

Browse files
authored
[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.
1 parent 49b5a1f commit f72b3ec

File tree

3 files changed

+209
-1
lines changed

3 files changed

+209
-1
lines changed

clang-tools-extra/clang-tidy/tool/run-clang-tidy.py

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
import time
5050
import traceback
5151
from types import ModuleType
52-
from typing import Any, Awaitable, Callable, List, Optional, TypeVar
52+
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, TypeVar
5353

5454

5555
yaml: Optional[ModuleType] = None
@@ -105,6 +105,7 @@ def get_tidy_invocation(
105105
warnings_as_errors: Optional[str],
106106
exclude_header_filter: Optional[str],
107107
allow_no_checks: bool,
108+
store_check_profile: Optional[str],
108109
) -> List[str]:
109110
"""Gets a command line for clang-tidy."""
110111
start = [clang_tidy_binary]
@@ -147,6 +148,9 @@ def get_tidy_invocation(
147148
start.append(f"--warnings-as-errors={warnings_as_errors}")
148149
if allow_no_checks:
149150
start.append("--allow-no-checks")
151+
if store_check_profile:
152+
start.append("--enable-check-profile")
153+
start.append(f"--store-check-profile={store_check_profile}")
150154
if f:
151155
start.append(f)
152156
return start
@@ -178,6 +182,124 @@ def merge_replacement_files(tmpdir: str, mergefile: str) -> None:
178182
open(mergefile, "w").close()
179183

180184

185+
def aggregate_profiles(profile_dir: str) -> Dict[str, float]:
186+
"""Aggregate timing data from multiple profile JSON files"""
187+
aggregated: Dict[str, float] = {}
188+
189+
for profile_file in glob.iglob(os.path.join(profile_dir, "*.json")):
190+
try:
191+
with open(profile_file, "r", encoding="utf-8") as f:
192+
data = json.load(f)
193+
profile_data: Dict[str, float] = data.get("profile", {})
194+
195+
for key, value in profile_data.items():
196+
if key.startswith("time.clang-tidy."):
197+
if key in aggregated:
198+
aggregated[key] += value
199+
else:
200+
aggregated[key] = value
201+
except (json.JSONDecodeError, KeyError, IOError) as e:
202+
print(f"Error: invalid json file {profile_file}: {e}", file=sys.stderr)
203+
continue
204+
205+
return aggregated
206+
207+
208+
def print_profile_data(aggregated_data: Dict[str, float]) -> None:
209+
"""Print aggregated checks profile data in the same format as clang-tidy"""
210+
if not aggregated_data:
211+
return
212+
213+
# Extract checker names and their timing data
214+
checkers: Dict[str, Dict[str, float]] = {}
215+
for key, value in aggregated_data.items():
216+
parts = key.split(".")
217+
if len(parts) >= 4 and parts[0] == "time" and parts[1] == "clang-tidy":
218+
checker_name = ".".join(
219+
parts[2:-1]
220+
) # Everything between "clang-tidy" and the timing type
221+
timing_type = parts[-1] # wall, user, or sys
222+
223+
if checker_name not in checkers:
224+
checkers[checker_name] = {"wall": 0.0, "user": 0.0, "sys": 0.0}
225+
226+
checkers[checker_name][timing_type] = value
227+
228+
if not checkers:
229+
return
230+
231+
total_user = sum(data["user"] for data in checkers.values())
232+
total_sys = sum(data["sys"] for data in checkers.values())
233+
total_wall = sum(data["wall"] for data in checkers.values())
234+
235+
sorted_checkers: List[Tuple[str, Dict[str, float]]] = sorted(
236+
checkers.items(), key=lambda x: x[1]["user"] + x[1]["sys"], reverse=True
237+
)
238+
239+
def print_stderr(*args, **kwargs) -> None:
240+
print(*args, file=sys.stderr, **kwargs)
241+
242+
print_stderr(
243+
"===-------------------------------------------------------------------------==="
244+
)
245+
print_stderr(" clang-tidy checks profiling")
246+
print_stderr(
247+
"===-------------------------------------------------------------------------==="
248+
)
249+
print_stderr(
250+
f" Total Execution Time: {total_user + total_sys:.4f} seconds ({total_wall:.4f} wall clock)\n"
251+
)
252+
253+
# Calculate field widths based on the Total line which has the largest values
254+
total_combined = total_user + total_sys
255+
user_width = len(f"{total_user:.4f}")
256+
sys_width = len(f"{total_sys:.4f}")
257+
combined_width = len(f"{total_combined:.4f}")
258+
wall_width = len(f"{total_wall:.4f}")
259+
260+
# Header with proper alignment
261+
additional_width = 9 # for " (100.0%)"
262+
user_header = "---User Time---".center(user_width + additional_width)
263+
sys_header = "--System Time--".center(sys_width + additional_width)
264+
combined_header = "--User+System--".center(combined_width + additional_width)
265+
wall_header = "---Wall Time---".center(wall_width + additional_width)
266+
267+
print_stderr(
268+
f" {user_header} {sys_header} {combined_header} {wall_header} --- Name ---"
269+
)
270+
271+
for checker_name, data in sorted_checkers:
272+
user_time = data["user"]
273+
sys_time = data["sys"]
274+
wall_time = data["wall"]
275+
combined_time = user_time + sys_time
276+
277+
user_percent = (user_time / total_user * 100) if total_user > 0 else 0
278+
sys_percent = (sys_time / total_sys * 100) if total_sys > 0 else 0
279+
combined_percent = (
280+
(combined_time / total_combined * 100) if total_combined > 0 else 0
281+
)
282+
wall_percent = (wall_time / total_wall * 100) if total_wall > 0 else 0
283+
284+
user_str = f"{user_time:{user_width}.4f} ({user_percent:5.1f}%)"
285+
sys_str = f"{sys_time:{sys_width}.4f} ({sys_percent:5.1f}%)"
286+
combined_str = f"{combined_time:{combined_width}.4f} ({combined_percent:5.1f}%)"
287+
wall_str = f"{wall_time:{wall_width}.4f} ({wall_percent:5.1f}%)"
288+
289+
print_stderr(
290+
f" {user_str} {sys_str} {combined_str} {wall_str} {checker_name}"
291+
)
292+
293+
user_total_str = f"{total_user:{user_width}.4f} (100.0%)"
294+
sys_total_str = f"{total_sys:{sys_width}.4f} (100.0%)"
295+
combined_total_str = f"{total_combined:{combined_width}.4f} (100.0%)"
296+
wall_total_str = f"{total_wall:{wall_width}.4f} (100.0%)"
297+
298+
print_stderr(
299+
f" {user_total_str} {sys_total_str} {combined_total_str} {wall_total_str} Total"
300+
)
301+
302+
181303
def find_binary(arg: str, name: str, build_path: str) -> str:
182304
"""Get the path for a binary or exit"""
183305
if arg:
@@ -240,6 +362,7 @@ async def run_tidy(
240362
clang_tidy_binary: str,
241363
tmpdir: str,
242364
build_path: str,
365+
store_check_profile: Optional[str],
243366
) -> ClangTidyResult:
244367
"""
245368
Runs clang-tidy on a single file and returns the result.
@@ -263,6 +386,7 @@ async def run_tidy(
263386
args.warnings_as_errors,
264387
args.exclude_header_filter,
265388
args.allow_no_checks,
389+
store_check_profile,
266390
)
267391

268392
try:
@@ -447,6 +571,11 @@ async def main() -> None:
447571
action="store_true",
448572
help="Allow empty enabled checks.",
449573
)
574+
parser.add_argument(
575+
"-enable-check-profile",
576+
action="store_true",
577+
help="Enable per-check timing profiles, and print a report",
578+
)
450579
args = parser.parse_args()
451580

452581
db_path = "compile_commands.json"
@@ -489,6 +618,10 @@ async def main() -> None:
489618
export_fixes_dir = tempfile.mkdtemp()
490619
delete_fixes_dir = True
491620

621+
profile_dir: Optional[str] = None
622+
if args.enable_check_profile:
623+
profile_dir = tempfile.mkdtemp()
624+
492625
try:
493626
invocation = get_tidy_invocation(
494627
None,
@@ -509,6 +642,7 @@ async def main() -> None:
509642
args.warnings_as_errors,
510643
args.exclude_header_filter,
511644
args.allow_no_checks,
645+
None, # No profiling for the list-checks invocation
512646
)
513647
invocation.append("-list-checks")
514648
invocation.append("-")
@@ -567,6 +701,7 @@ async def main() -> None:
567701
clang_tidy_binary,
568702
export_fixes_dir,
569703
build_path,
704+
profile_dir,
570705
)
571706
)
572707
for f in files
@@ -593,8 +728,19 @@ async def main() -> None:
593728
if delete_fixes_dir:
594729
assert export_fixes_dir
595730
shutil.rmtree(export_fixes_dir)
731+
if profile_dir:
732+
shutil.rmtree(profile_dir)
596733
return
597734

735+
if args.enable_check_profile and profile_dir:
736+
# Ensure all clang-tidy stdout is flushed before printing profiling
737+
sys.stdout.flush()
738+
aggregated_data = aggregate_profiles(profile_dir)
739+
if aggregated_data:
740+
print_profile_data(aggregated_data)
741+
else:
742+
print("No profiling data found.")
743+
598744
if combine_fixes:
599745
print(f"Writing fixes to {args.export_fixes} ...")
600746
try:
@@ -618,6 +764,8 @@ async def main() -> None:
618764
if delete_fixes_dir:
619765
assert export_fixes_dir
620766
shutil.rmtree(export_fixes_dir)
767+
if profile_dir:
768+
shutil.rmtree(profile_dir)
621769
sys.exit(returncode)
622770

623771

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ Improvements to clang-tidy
105105
now run checks in parallel by default using all available hardware threads.
106106
Both scripts display the number of threads being used in their output.
107107

108+
- Improved :program:`run-clang-tidy.py` by adding a new option
109+
`enable-check-profile` to enable per-check timing profiles and print a
110+
report based on all analyzed files.
111+
108112
New checks
109113
^^^^^^^^^^
110114

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Test profiling functionality with single file
2+
// RUN: rm -rf %t
3+
// RUN: mkdir %t
4+
// RUN: echo "[{\"directory\":\".\",\"command\":\"clang++ -c %/t/test.cpp\",\"file\":\"%/t/test.cpp\"}]" | sed -e 's/\\/\\\\/g' > %t/compile_commands.json
5+
// RUN: echo "Checks: '-*,readability-function-size'" > %t/.clang-tidy
6+
// RUN: cp "%s" "%t/test.cpp"
7+
// RUN: cd "%t"
8+
// RUN: %run_clang_tidy -enable-check-profile "test.cpp" 2>&1 | FileCheck %s --check-prefix=CHECK-SINGLE
9+
10+
// CHECK-SINGLE: Running clang-tidy in {{[1-9][0-9]*}} threads for 1 files out of 1 in compilation database
11+
// CHECK-SINGLE: ===-------------------------------------------------------------------------===
12+
// CHECK-SINGLE-NEXT: clang-tidy checks profiling
13+
// CHECK-SINGLE-NEXT: ===-------------------------------------------------------------------------===
14+
// CHECK-SINGLE-NEXT: Total Execution Time: {{.*}} seconds ({{.*}} wall clock)
15+
// CHECK-SINGLE-EMPTY:
16+
// CHECK-SINGLE-NEXT: ---User Time--- --System Time-- --User+System-- ---Wall Time--- --- Name ---
17+
// CHECK-SINGLE: {{[[:space:]]*[0-9]+\.[0-9]+.*%.*readability-function-size}}
18+
// CHECK-SINGLE: {{[[:space:]]*[0-9]+\.[0-9]+.*100\.0%.*Total}}
19+
20+
// Test profiling functionality with multiple files and multiple checks
21+
// RUN: rm -rf %t-multi
22+
// RUN: mkdir %t-multi
23+
// RUN: echo "[{\"directory\":\".\",\"command\":\"clang++ -c %/t-multi/test1.cpp\",\"file\":\"%/t-multi/test1.cpp\"},{\"directory\":\".\",\"command\":\"clang++ -c %/t-multi/test2.cpp\",\"file\":\"%/t-multi/test2.cpp\"}]" | sed -e 's/\\/\\\\/g' > %t-multi/compile_commands.json
24+
// RUN: echo "Checks: '-*,readability-function-size,misc-unused-using-decls,llvm-qualified-auto'" > %t-multi/.clang-tidy
25+
// RUN: cp "%s" "%t-multi/test1.cpp"
26+
// RUN: cp "%s" "%t-multi/test2.cpp"
27+
// RUN: cd "%t-multi"
28+
// RUN: %run_clang_tidy -enable-check-profile -j 2 "test1.cpp" "test2.cpp" 2>&1 | FileCheck %s --check-prefix=CHECK-MULTIPLE
29+
30+
// CHECK-MULTIPLE: Running clang-tidy in 2 threads for 2 files out of 2 in compilation database
31+
// CHECK-MULTIPLE: ===-------------------------------------------------------------------------===
32+
// CHECK-MULTIPLE-NEXT: clang-tidy checks profiling
33+
// CHECK-MULTIPLE-NEXT: ===-------------------------------------------------------------------------===
34+
// CHECK-MULTIPLE-NEXT: Total Execution Time: {{.*}} seconds ({{.*}} wall clock)
35+
// CHECK-MULTIPLE-EMPTY:
36+
// CHECK-MULTIPLE-NEXT: ---User Time--- --System Time-- --User+System-- ---Wall Time--- --- Name ---
37+
// CHECK-MULTIPLE-DAG: {{[[:space:]]*[0-9]+\.[0-9]+.*%.*readability-function-size}}
38+
// CHECK-MULTIPLE-DAG: {{[[:space:]]*[0-9]+\.[0-9]+.*%.*misc-unused-using-decls}}
39+
// CHECK-MULTIPLE-DAG: {{[[:space:]]*[0-9]+\.[0-9]+.*%.*llvm-qualified-auto}}
40+
// CHECK-MULTIPLE: {{[[:space:]]*[0-9]+\.[0-9]+.*100\.0%.*Total}}
41+
42+
// Test profiling functionality with no files (empty database)
43+
// RUN: rm -rf %t-empty
44+
// RUN: mkdir %t-empty
45+
// RUN: echo "[]" > %t-empty/compile_commands.json
46+
// RUN: echo "Checks: '-*'" > %t-empty/.clang-tidy
47+
// RUN: cd "%t-empty"
48+
// RUN: %run_clang_tidy -enable-check-profile -allow-no-checks 2>&1 | FileCheck %s --check-prefix=CHECK-EMPTY
49+
50+
// CHECK-EMPTY: Running clang-tidy in {{[1-9][0-9]*}} threads for 0 files out of 0 in compilation database
51+
// CHECK-EMPTY: No profiling data found.
52+
53+
class A {
54+
A() {}
55+
~A() {}
56+
};

0 commit comments

Comments
 (0)