1#!/usr/bin/env python3 2# 3# Determines which clang-tidy checks are "fast enough" to run in clangd. 4# This runs clangd --check --check-tidy-time and parses the output. 5# This program outputs a header fragment specifying which checks are fast: 6# FAST(bugprone-argument-comment, 5) 7# SLOW(misc-const-correctness, 200) 8# If given the old header fragment as input, we lean to preserve its choices. 9# 10# This is not deterministic or hermetic, but should be run occasionally to 11# update the list of allowed checks. From llvm-project: 12# clang-tools-extra/clangd/TidyFastChecks.py --clangd=build-opt/bin/clangd 13# Be sure to use an optimized, no-asserts, tidy-enabled build of clangd! 14 15import argparse 16import os 17import re 18import subprocess 19import sys 20 21# Checks faster than FAST_THRESHOLD are fast, slower than SLOW_THRESHOLD slow. 22# If a check is in between, we stick with our previous decision. This avoids 23# enabling/disabling checks between releases due to random measurement jitter. 24FAST_THRESHOLD = 8 # percent 25SLOW_THRESHOLD = 15 26 27parser = argparse.ArgumentParser() 28parser.add_argument( 29 "--target", 30 help="X-macro output file. " 31 "If it exists, existing contents will be used for hysteresis", 32 default="clang-tools-extra/clangd/TidyFastChecks.inc", 33) 34parser.add_argument( 35 "--source", 36 help="Source file to benchmark tidy checks", 37 default="clang/lib/Sema/Sema.cpp", 38) 39parser.add_argument( 40 "--clangd", help="clangd binary to invoke", default="build/bin/clangd" 41) 42parser.add_argument("--checks", help="check glob to run", default="*") 43parser.add_argument("--verbose", help="log clangd output", action="store_true") 44args = parser.parse_args() 45 46# Use the preprocessor to extract the list of previously-fast checks. 47def read_old_fast(path): 48 text = subprocess.check_output( 49 [ 50 "cpp", 51 "-P", # Omit GNU line markers 52 "-nostdinc", # Don't include stdc-predef.h 53 "-DFAST(C,T)=C", # Print fast checks only 54 path, 55 ] 56 ) 57 for line in text.splitlines(): 58 if line.strip(): 59 yield line.strip().decode("utf-8") 60 61 62old_fast = list(read_old_fast(args.target)) if os.path.exists(args.target) else [] 63print(f"Old fast checks: {old_fast}", file=sys.stderr) 64 65# Runs clangd --check --check-tidy-time. 66# Yields (check, percent-overhead) pairs. 67def measure(): 68 process = subprocess.Popen( 69 [ 70 args.clangd, 71 "--check=" + args.source, 72 "--check-locations=0", # Skip useless slow steps. 73 "--check-tidy-time=" + args.checks, 74 ], 75 stderr=subprocess.PIPE, 76 ) 77 recording = False 78 for line in iter(process.stderr.readline, b""): 79 if args.verbose: 80 print("clangd> ", line, file=sys.stderr) 81 if not recording: 82 if b"Timing AST build with individual clang-tidy checks" in line: 83 recording = True 84 continue 85 if b"Finished individual clang-tidy checks" in line: 86 return 87 match = re.search(rb"(\S+) = (\S+)%", line) 88 if match: 89 yield (match.group(1).decode("utf-8"), float(match.group(2))) 90 91 92with open(args.target, "w", buffering=1) as target: 93 # Produce an includable X-macros fragment with our decisions. 94 print( 95 f"""// This file is generated, do not edit it directly! 96// Deltas are percentage regression in parsing {args.source} 97#ifndef FAST 98#define FAST(CHECK, DELTA) 99#endif 100#ifndef SLOW 101#define SLOW(CHECK, DELTA) 102#endif 103""", 104 file=target, 105 ) 106 107 for check, time in measure(): 108 threshold = SLOW_THRESHOLD if check in old_fast else FAST_THRESHOLD 109 decision = "FAST" if time <= threshold else "SLOW" 110 print(f"{decision} {check} {time}% <= {threshold}%", file=sys.stderr) 111 print(f"{decision}({check}, {time})", file=target) 112 113 print( 114 """ 115#undef FAST 116#undef SLOW 117""", 118 file=target, 119 ) 120