49
49
import time
50
50
import traceback
51
51
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
53
53
54
54
55
55
yaml : Optional [ModuleType ] = None
@@ -105,6 +105,7 @@ def get_tidy_invocation(
105
105
warnings_as_errors : Optional [str ],
106
106
exclude_header_filter : Optional [str ],
107
107
allow_no_checks : bool ,
108
+ store_check_profile : Optional [str ],
108
109
) -> List [str ]:
109
110
"""Gets a command line for clang-tidy."""
110
111
start = [clang_tidy_binary ]
@@ -147,6 +148,9 @@ def get_tidy_invocation(
147
148
start .append (f"--warnings-as-errors={ warnings_as_errors } " )
148
149
if allow_no_checks :
149
150
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 } " )
150
154
if f :
151
155
start .append (f )
152
156
return start
@@ -178,6 +182,124 @@ def merge_replacement_files(tmpdir: str, mergefile: str) -> None:
178
182
open (mergefile , "w" ).close ()
179
183
180
184
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
+
181
303
def find_binary (arg : str , name : str , build_path : str ) -> str :
182
304
"""Get the path for a binary or exit"""
183
305
if arg :
@@ -240,6 +362,7 @@ async def run_tidy(
240
362
clang_tidy_binary : str ,
241
363
tmpdir : str ,
242
364
build_path : str ,
365
+ store_check_profile : Optional [str ],
243
366
) -> ClangTidyResult :
244
367
"""
245
368
Runs clang-tidy on a single file and returns the result.
@@ -263,6 +386,7 @@ async def run_tidy(
263
386
args .warnings_as_errors ,
264
387
args .exclude_header_filter ,
265
388
args .allow_no_checks ,
389
+ store_check_profile ,
266
390
)
267
391
268
392
try :
@@ -447,6 +571,11 @@ async def main() -> None:
447
571
action = "store_true" ,
448
572
help = "Allow empty enabled checks." ,
449
573
)
574
+ parser .add_argument (
575
+ "-enable-check-profile" ,
576
+ action = "store_true" ,
577
+ help = "Enable per-check timing profiles, and print a report" ,
578
+ )
450
579
args = parser .parse_args ()
451
580
452
581
db_path = "compile_commands.json"
@@ -489,6 +618,10 @@ async def main() -> None:
489
618
export_fixes_dir = tempfile .mkdtemp ()
490
619
delete_fixes_dir = True
491
620
621
+ profile_dir : Optional [str ] = None
622
+ if args .enable_check_profile :
623
+ profile_dir = tempfile .mkdtemp ()
624
+
492
625
try :
493
626
invocation = get_tidy_invocation (
494
627
None ,
@@ -509,6 +642,7 @@ async def main() -> None:
509
642
args .warnings_as_errors ,
510
643
args .exclude_header_filter ,
511
644
args .allow_no_checks ,
645
+ None , # No profiling for the list-checks invocation
512
646
)
513
647
invocation .append ("-list-checks" )
514
648
invocation .append ("-" )
@@ -567,6 +701,7 @@ async def main() -> None:
567
701
clang_tidy_binary ,
568
702
export_fixes_dir ,
569
703
build_path ,
704
+ profile_dir ,
570
705
)
571
706
)
572
707
for f in files
@@ -593,8 +728,19 @@ async def main() -> None:
593
728
if delete_fixes_dir :
594
729
assert export_fixes_dir
595
730
shutil .rmtree (export_fixes_dir )
731
+ if profile_dir :
732
+ shutil .rmtree (profile_dir )
596
733
return
597
734
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
+
598
744
if combine_fixes :
599
745
print (f"Writing fixes to { args .export_fixes } ..." )
600
746
try :
@@ -618,6 +764,8 @@ async def main() -> None:
618
764
if delete_fixes_dir :
619
765
assert export_fixes_dir
620
766
shutil .rmtree (export_fixes_dir )
767
+ if profile_dir :
768
+ shutil .rmtree (profile_dir )
621
769
sys .exit (returncode )
622
770
623
771
0 commit comments