blob: 2132a960793dab285765cf50e1b39640bb94c5b1 [file] [log] [blame]
#!/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)