Tools/scripts/jitlist_bisect.py (98 lines of code) (raw):
#!/usr/bin/env python3
# Copyright (c) Facebook, Inc. and its affiliates. (http://www.facebook.com)
import argparse
import logging
import os
import re
import shlex
import subprocess
import sys
import tempfile
JITLIST_FILENAME = "jitlist.txt"
def write_jitlist(jitlist):
with open(JITLIST_FILENAME, "w") as file:
for func in jitlist:
print(func, file=file)
def run_with_jitlist(command, jitlist):
write_jitlist(jitlist)
environ = dict(os.environ)
environ.update({"PYTHONJITLISTFILE": JITLIST_FILENAME})
logging.debug(
f"Running '{shlex.join(command)}' with jitlist of size {len(jitlist)}"
)
proc = subprocess.run(
command,
env=environ,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding=sys.stdout.encoding,
)
logging.debug(
f"Finished with exit status {proc.returncode}\n\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}"
)
return proc.returncode == 0
COMPILED_FUNC_RE = re.compile(r" -- Compiling ([^ ]+) @ 0x[0-9a-f]+$")
def get_compiled_funcs(command):
environ = dict(os.environ)
environ.update({"PYTHONJITDEBUG": "1"})
logging.info("Generating initial jit-list")
proc = subprocess.run(
command,
env=environ,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
encoding=sys.stderr.encoding,
)
if proc.returncode == 0:
sys.exit(f"Command succeeded during jit-list generation")
funcs = set()
for line in proc.stderr.splitlines():
m = COMPILED_FUNC_RE.search(line)
if m is None:
continue
funcs.add(m[1])
if len(funcs) == 0:
sys.exit(f"No compiled functions found")
# We want a deterministic jitlist, unaffected by the order functions happen
# to be compiled in.
return sorted(funcs)
# Return two halves of the given list. For odd-length lists, the second half
# will be larger.
def split_list(l):
half = len(l) // 2
return l[0:half], l[half:]
# Attempt to reduce the `jitlist` argument as much as possible, returning the
# shorter version. `fixed` will always be used as part of the jitlist when
# running `command`.
def bisect_impl(command, fixed, jitlist, indent=""):
logging.info(f"{indent}step fixed[{len(fixed)}] and jitlist[{len(jitlist)}]")
while len(jitlist) > 1:
logging.info(f"{indent}{len(fixed) + len(jitlist)} candidates")
left, right = split_list(jitlist)
if not run_with_jitlist(command, fixed + left):
jitlist = left
continue
if not run_with_jitlist(command, fixed + right):
jitlist = right
continue
# We need something from both halves to trigger the failure. Try
# holding each half fixed and bisecting the other half to reduce the
# candidates.
new_right = bisect_impl(command, fixed + left, right, indent + "< ")
new_left = bisect_impl(command, fixed + new_right, left, indent + "> ")
return new_left + new_right
return jitlist
def run_bisect(command):
jitlist = get_compiled_funcs(command)
logging.info("Verifying jit-list")
if run_with_jitlist(command, jitlist):
sys.exit(f"Command succeeded with full jit-list")
if not run_with_jitlist(command, []):
sys.exit(f"Command failed with empty jit-list")
jitlist = bisect_impl(command, [], jitlist)
write_jitlist(jitlist)
print(f"Bisect finished with {len(jitlist)} functions in {JITLIST_FILENAME}")
def parse_args():
parser = argparse.ArgumentParser(
description="When given a command that fails with the jit enabled (including -X jit as appropriate), bisects to find a minimal jit-list that preserves the failure"
)
parser.add_argument(
"--verbose", "-v", action="store_true", help="Enable verbose logging"
)
parser.add_argument("command", nargs=argparse.REMAINDER)
return parser.parse_args()
def main():
args = parse_args()
log_level = logging.DEBUG if args.verbose else logging.INFO
logging.basicConfig(level=log_level)
run_bisect(args.command)
if __name__ == "__main__":
sys.exit(main())