#!/usr/bin/env python3 # # Determines which clang-tidy checks are "fast enough" to run in clangd. # This runs clangd --check --check-tidy-time and parses the output. # This program outputs a header fragment specifying which checks are fast: # FAST(bugprone-argument-comment, 5) # SLOW(misc-const-correctness, 200) # If given the old header fragment as input, we lean to preserve its choices. # # This is not deterministic or hermetic, but should be run occasionally to # update the list of allowed checks. From llvm-project: # clang-tools-extra/clangd/TidyFastChecks.py --clangd=build-opt/bin/clangd # Be sure to use an optimized, no-asserts, tidy-enabled build of clangd! import argparse import os import re import subprocess import sys # Checks faster than FAST_THRESHOLD are fast, slower than SLOW_THRESHOLD slow. # If a check is in between, we stick with our previous decision. This avoids # enabling/disabling checks between releases due to random measurement jitter. FAST_THRESHOLD = 8 # percent SLOW_THRESHOLD = 15 parser = argparse.ArgumentParser() parser.add_argument( "--target", help="X-macro output file. " "If it exists, existing contents will be used for hysteresis", default="clang-tools-extra/clangd/TidyFastChecks.inc", ) parser.add_argument( "--source", help="Source file to benchmark tidy checks", default="clang/lib/Sema/Sema.cpp", ) parser.add_argument( "--clangd", help="clangd binary to invoke", default="build/bin/clangd" ) parser.add_argument("--checks", help="check glob to run", default="*") parser.add_argument("--verbose", help="log clangd output", action="store_true") args = parser.parse_args() # Use the preprocessor to extract the list of previously-fast checks. def read_old_fast(path): text = subprocess.check_output( [ "cpp", "-P", # Omit GNU line markers "-nostdinc", # Don't include stdc-predef.h "-DFAST(C,T)=C", # Print fast checks only path, ] ) for line in text.splitlines(): if line.strip(): yield line.strip().decode("utf-8") old_fast = list(read_old_fast(args.target)) if os.path.exists(args.target) else [] print(f"Old fast checks: {old_fast}", file=sys.stderr) # Runs clangd --check --check-tidy-time. # Yields (check, percent-overhead) pairs. def measure(): process = subprocess.Popen( [ args.clangd, "--check=" + args.source, "--check-locations=0", # Skip useless slow steps. "--check-tidy-time=" + args.checks, ], stderr=subprocess.PIPE, ) recording = False for line in iter(process.stderr.readline, b""): if args.verbose: print("clangd> ", line, file=sys.stderr) if not recording: if b"Timing AST build with individual clang-tidy checks" in line: recording = True continue if b"Finished individual clang-tidy checks" in line: return match = re.search(rb"(\S+) = (\S+)%", line) if match: yield (match.group(1).decode("utf-8"), float(match.group(2))) with open(args.target, "w", buffering=1) as target: # Produce an includable X-macros fragment with our decisions. print( f"""// This file is generated, do not edit it directly! // Deltas are percentage regression in parsing {args.source} #ifndef FAST #define FAST(CHECK, DELTA) #endif #ifndef SLOW #define SLOW(CHECK, DELTA) #endif """, file=target, ) for check, time in measure(): threshold = SLOW_THRESHOLD if check in old_fast else FAST_THRESHOLD decision = "FAST" if time <= threshold else "SLOW" print(f"{decision} {check} {time}% <= {threshold}%", file=sys.stderr) print(f"{decision}({check}, {time})", file=target) print( """ #undef FAST #undef SLOW """, file=target, )