#!/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)