| # Given a path to llvm-objdump and a directory tree, spider the directory tree | |
| # dumping every object file encountered with correct options needed to demangle | |
| # symbols in the object file, and collect statistics about failed / crashed | |
| # demanglings. Useful for stress testing the demangler against a large corpus | |
| # of inputs. | |
| from __future__ import print_function | |
| import argparse | |
| import functools | |
| import os | |
| import re | |
| import sys | |
| import subprocess | |
| import traceback | |
| from multiprocessing import Pool | |
| import multiprocessing | |
| args = None | |
| def parse_line(line): | |
| question = line.find("?") | |
| if question == -1: | |
| return None, None | |
| open_paren = line.find("(", question) | |
| if open_paren == -1: | |
| return None, None | |
| close_paren = line.rfind(")", open_paren) | |
| if close_paren == -1: | |
| return None, None | |
| mangled = line[question:open_paren] | |
| demangled = line[open_paren + 1 : close_paren] | |
| return mangled.strip(), demangled.strip() | |
| class Result(object): | |
| def __init__(self): | |
| self.crashed = [] | |
| self.file = None | |
| self.nsymbols = 0 | |
| self.errors = set() | |
| self.nfiles = 0 | |
| class MapContext(object): | |
| def __init__(self): | |
| self.rincomplete = None | |
| self.rcumulative = Result() | |
| self.pending_objs = [] | |
| self.npending = 0 | |
| def process_file(path, objdump): | |
| r = Result() | |
| r.file = path | |
| popen_args = [objdump, "-t", "-demangle", path] | |
| p = subprocess.Popen(popen_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| stdout, stderr = p.communicate() | |
| if p.returncode != 0: | |
| r.crashed = [r.file] | |
| return r | |
| output = stdout.decode("utf-8") | |
| for line in output.splitlines(): | |
| mangled, demangled = parse_line(line) | |
| if mangled is None: | |
| continue | |
| r.nsymbols += 1 | |
| if "invalid mangled name" in demangled: | |
| r.errors.add(mangled) | |
| return r | |
| def add_results(r1, r2): | |
| r1.crashed.extend(r2.crashed) | |
| r1.errors.update(r2.errors) | |
| r1.nsymbols += r2.nsymbols | |
| r1.nfiles += r2.nfiles | |
| def print_result_row(directory, result): | |
| print( | |
| "[{0} files, {1} crashes, {2} errors, {3} symbols]: '{4}'".format( | |
| result.nfiles, | |
| len(result.crashed), | |
| len(result.errors), | |
| result.nsymbols, | |
| directory, | |
| ) | |
| ) | |
| def process_one_chunk(pool, chunk_size, objdump, context): | |
| objs = [] | |
| incomplete = False | |
| dir_results = {} | |
| ordered_dirs = [] | |
| while context.npending > 0 and len(objs) < chunk_size: | |
| this_dir = context.pending_objs[0][0] | |
| ordered_dirs.append(this_dir) | |
| re = Result() | |
| if context.rincomplete is not None: | |
| re = context.rincomplete | |
| context.rincomplete = None | |
| dir_results[this_dir] = re | |
| re.file = this_dir | |
| nneeded = chunk_size - len(objs) | |
| objs_this_dir = context.pending_objs[0][1] | |
| navail = len(objs_this_dir) | |
| ntaken = min(nneeded, navail) | |
| objs.extend(objs_this_dir[0:ntaken]) | |
| remaining_objs_this_dir = objs_this_dir[ntaken:] | |
| context.pending_objs[0] = (context.pending_objs[0][0], remaining_objs_this_dir) | |
| context.npending -= ntaken | |
| if ntaken == navail: | |
| context.pending_objs.pop(0) | |
| else: | |
| incomplete = True | |
| re.nfiles += ntaken | |
| assert len(objs) == chunk_size or context.npending == 0 | |
| copier = functools.partial(process_file, objdump=objdump) | |
| mapped_results = list(pool.map(copier, objs)) | |
| for mr in mapped_results: | |
| result_dir = os.path.dirname(mr.file) | |
| result_entry = dir_results[result_dir] | |
| add_results(result_entry, mr) | |
| # It's only possible that a single item is incomplete, and it has to be the | |
| # last item. | |
| if incomplete: | |
| context.rincomplete = dir_results[ordered_dirs[-1]] | |
| ordered_dirs.pop() | |
| # Now ordered_dirs contains a list of all directories which *did* complete. | |
| for c in ordered_dirs: | |
| re = dir_results[c] | |
| add_results(context.rcumulative, re) | |
| print_result_row(c, re) | |
| def process_pending_files(pool, chunk_size, objdump, context): | |
| while context.npending >= chunk_size: | |
| process_one_chunk(pool, chunk_size, objdump, context) | |
| def go(): | |
| global args | |
| obj_dir = args.dir | |
| extensions = args.extensions.split(",") | |
| extensions = [x if x[0] == "." else "." + x for x in extensions] | |
| pool_size = 48 | |
| pool = Pool(processes=pool_size) | |
| try: | |
| nfiles = 0 | |
| context = MapContext() | |
| for root, dirs, files in os.walk(obj_dir): | |
| root = os.path.normpath(root) | |
| pending = [] | |
| for f in files: | |
| file, ext = os.path.splitext(f) | |
| if not ext in extensions: | |
| continue | |
| nfiles += 1 | |
| full_path = os.path.join(root, f) | |
| full_path = os.path.normpath(full_path) | |
| pending.append(full_path) | |
| # If this directory had no object files, just print a default | |
| # status line and continue with the next dir | |
| if len(pending) == 0: | |
| print_result_row(root, Result()) | |
| continue | |
| context.npending += len(pending) | |
| context.pending_objs.append((root, pending)) | |
| # Drain the tasks, `pool_size` at a time, until we have less than | |
| # `pool_size` tasks remaining. | |
| process_pending_files(pool, pool_size, args.objdump, context) | |
| assert context.npending < pool_size | |
| process_one_chunk(pool, pool_size, args.objdump, context) | |
| total = context.rcumulative | |
| nfailed = len(total.errors) | |
| nsuccess = total.nsymbols - nfailed | |
| ncrashed = len(total.crashed) | |
| if nfailed > 0: | |
| print("Failures:") | |
| for m in sorted(total.errors): | |
| print(" " + m) | |
| if ncrashed > 0: | |
| print("Crashes:") | |
| for f in sorted(total.crashed): | |
| print(" " + f) | |
| print("Summary:") | |
| spct = float(nsuccess) / float(total.nsymbols) | |
| fpct = float(nfailed) / float(total.nsymbols) | |
| cpct = float(ncrashed) / float(nfiles) | |
| print("Processed {0} object files.".format(nfiles)) | |
| print( | |
| "{0}/{1} symbols successfully demangled ({2:.4%})".format( | |
| nsuccess, total.nsymbols, spct | |
| ) | |
| ) | |
| print("{0} symbols could not be demangled ({1:.4%})".format(nfailed, fpct)) | |
| print("{0} files crashed while demangling ({1:.4%})".format(ncrashed, cpct)) | |
| except: | |
| traceback.print_exc() | |
| pool.close() | |
| pool.join() | |
| if __name__ == "__main__": | |
| def_obj = "obj" if sys.platform == "win32" else "o" | |
| parser = argparse.ArgumentParser( | |
| description="Demangle all symbols in a tree of object files, looking for failures." | |
| ) | |
| parser.add_argument( | |
| "dir", type=str, help="the root directory at which to start crawling" | |
| ) | |
| parser.add_argument( | |
| "--objdump", | |
| type=str, | |
| help="path to llvm-objdump. If not specified " | |
| + "the tool is located as if by `which llvm-objdump`.", | |
| ) | |
| parser.add_argument( | |
| "--extensions", | |
| type=str, | |
| default=def_obj, | |
| help="comma separated list of extensions to demangle (e.g. `o,obj`). " | |
| + "By default this will be `obj` on Windows and `o` otherwise.", | |
| ) | |
| args = parser.parse_args() | |
| multiprocessing.freeze_support() | |
| go() |