4949import time
5050import traceback
5151from 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
5555yaml : 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+
181303def 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
0 commit comments