| #!/usr/bin/env python |
| # |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| # |
| """ |
| This script can be used to make the output from |
| apache::thrift::profile_print_info() more human-readable. |
| |
| It translates each executable file name and address into the corresponding |
| source file name, line number, and function name. By default, it also |
| demangles C++ symbol names. |
| """ |
| |
| import optparse |
| import os |
| import re |
| import subprocess |
| import sys |
| |
| |
| class AddressInfo(object): |
| """ |
| A class to store information about a particular address in an object file. |
| """ |
| def __init__(self, obj_file, address): |
| self.objectFile = obj_file |
| self.address = address |
| self.sourceFile = None |
| self.sourceLine = None |
| self.funtion = None |
| |
| |
| g_addrs_by_filename = {} |
| def get_address(filename, address): |
| """ |
| Retrieve an AddressInfo object for the specified object file and address. |
| |
| Keeps a global list of AddressInfo objects. Two calls to get_address() |
| with the same filename and address will always return the same AddressInfo |
| object. |
| """ |
| global g_addrs_by_filename |
| try: |
| by_address = g_addrs_by_filename[filename] |
| except KeyError: |
| by_address = {} |
| g_addrs_by_filename[filename] = by_address |
| |
| try: |
| addr_info = by_address[address] |
| except KeyError: |
| addr_info = AddressInfo(filename, address) |
| by_address[address] = addr_info |
| return addr_info |
| |
| |
| def translate_file_addresses(filename, addresses, options): |
| """ |
| Use addr2line to look up information for the specified addresses. |
| All of the addresses must belong to the same object file. |
| """ |
| # Do nothing if we can't find the file |
| if not os.path.isfile(filename): |
| return |
| |
| args = ['addr2line'] |
| if options.printFunctions: |
| args.append('-f') |
| args.extend(['-e', filename]) |
| |
| proc = subprocess.Popen(args, stdin=subprocess.PIPE, |
| stdout=subprocess.PIPE) |
| for address in addresses: |
| assert address.objectFile == filename |
| proc.stdin.write(address.address + '\n') |
| |
| if options.printFunctions: |
| function = proc.stdout.readline() |
| function = function.strip() |
| if not function: |
| raise Exception('unexpected EOF from addr2line') |
| address.function = function |
| |
| file_and_line = proc.stdout.readline() |
| file_and_line = file_and_line.strip() |
| if not file_and_line: |
| raise Exception('unexpected EOF from addr2line') |
| idx = file_and_line.rfind(':') |
| if idx < 0: |
| msg = 'expected file and line number from addr2line; got %r' % \ |
| (file_and_line,) |
| msg += '\nfile=%r, address=%r' % (filename, address.address) |
| raise Exception(msg) |
| |
| address.sourceFile = file_and_line[:idx] |
| address.sourceLine = file_and_line[idx+1:] |
| |
| (remaining_out, cmd_err) = proc.communicate() |
| retcode = proc.wait() |
| if retcode != 0: |
| raise subprocess.CalledProcessError(retcode, args) |
| |
| |
| def lookup_addresses(options): |
| """ |
| Look up source file information for all of the addresses currently stored |
| in the global list of AddressInfo objects. |
| """ |
| global g_addrs_by_filename |
| for (file, addresses) in g_addrs_by_filename.items(): |
| translate_file_addresses(file, addresses.values(), options) |
| |
| |
| class Entry(object): |
| """ |
| An entry in the thrift profile output. |
| Contains a header line, and a backtrace. |
| """ |
| def __init__(self, header): |
| self.header = header |
| self.bt = [] |
| |
| def addFrame(self, filename, address): |
| # If libc was able to determine the symbols names, the filename |
| # argument will be of the form <filename>(<function>+<offset>) |
| # So, strip off anything after the last '(' |
| idx = filename.rfind('(') |
| if idx >= 0: |
| filename = filename[:idx] |
| |
| addr = get_address(filename, address) |
| self.bt.append(addr) |
| |
| def write(self, f, options): |
| f.write(self.header) |
| f.write('\n') |
| n = 0 |
| for address in self.bt: |
| f.write(' #%-2d %s:%s\n' % (n, address.sourceFile, |
| address.sourceLine)) |
| n += 1 |
| if options.printFunctions: |
| if address.function: |
| f.write(' %s\n' % (address.function,)) |
| else: |
| f.write(' ??\n') |
| |
| |
| def process_file(in_file, out_file, options): |
| """ |
| Read thrift profile output from the specified input file, and print |
| prettier information on the output file. |
| """ |
| # |
| # A naive approach would be to read the input line by line, |
| # and each time we come to a filename and address, pass it to addr2line |
| # and print the resulting information. Unfortunately, addr2line can be |
| # quite slow, especially with large executables. |
| # |
| # This approach is much faster. We read in all of the input, storing |
| # the addresses in each file that need to be resolved. We then call |
| # addr2line just once for each file. This is much faster than calling |
| # addr2line once per address. |
| # |
| |
| virt_call_regex = re.compile(r'^\s*T_VIRTUAL_CALL: (\d+) calls on (.*):$') |
| gen_prot_regex = re.compile( |
| r'^\s*T_GENERIC_PROTOCOL: (\d+) calls to (.*) with a (.*):$') |
| bt_regex = re.compile(r'^\s*#(\d+)\s*(.*) \[(0x[0-9A-Za-z]+)\]$') |
| |
| # Parse all of the input, and store it as Entry objects |
| entries = [] |
| current_entry = None |
| while True: |
| line = in_file.readline() |
| if not line: |
| break |
| |
| if line == '\n' or line.startswith('Thrift virtual call info:'): |
| continue |
| |
| virt_call_match = virt_call_regex.match(line) |
| if virt_call_match: |
| num_calls = int(virt_call_match.group(1)) |
| type_name = virt_call_match.group(2) |
| if options.cxxfilt: |
| # Type names reported by typeid() are internal names. |
| # By default, c++filt doesn't demangle internal type names. |
| # (Some versions of c++filt have a "-t" option to enable this. |
| # Other versions don't have this argument, but demangle type |
| # names passed as an argument, but not on stdin.) |
| # |
| # If the output is being filtered through c++filt, prepend |
| # "_Z" to the type name to make it look like an external name. |
| type_name = '_Z' + type_name |
| header = 'T_VIRTUAL_CALL: %d calls on "%s"' % \ |
| (num_calls, type_name) |
| if current_entry is not None: |
| entries.append(current_entry) |
| current_entry = Entry(header) |
| continue |
| |
| gen_prot_match = gen_prot_regex.match(line) |
| if gen_prot_match: |
| num_calls = int(gen_prot_match.group(1)) |
| type_name1 = gen_prot_match.group(2) |
| type_name2 = gen_prot_match.group(3) |
| if options.cxxfilt: |
| type_name1 = '_Z' + type_name1 |
| type_name2 = '_Z' + type_name2 |
| header = 'T_GENERIC_PROTOCOL: %d calls to "%s" with a "%s"' % \ |
| (num_calls, type_name1, type_name2) |
| if current_entry is not None: |
| entries.append(current_entry) |
| current_entry = Entry(header) |
| continue |
| |
| bt_match = bt_regex.match(line) |
| if bt_match: |
| if current_entry is None: |
| raise Exception('found backtrace frame before entry header') |
| frame_num = int(bt_match.group(1)) |
| filename = bt_match.group(2) |
| address = bt_match.group(3) |
| current_entry.addFrame(filename, address) |
| continue |
| |
| raise Exception('unexpected line in input: %r' % (line,)) |
| |
| # Add the last entry we were processing to the list |
| if current_entry is not None: |
| entries.append(current_entry) |
| current_entry = None |
| |
| # Look up all of the addresses |
| lookup_addresses(options) |
| |
| # Print out the entries, now that the information has been translated |
| for entry in entries: |
| entry.write(out_file, options) |
| out_file.write('\n') |
| |
| |
| def start_cppfilt(): |
| (read_pipe, write_pipe) = os.pipe() |
| |
| # Fork. Run c++filt in the parent process, |
| # and then continue normal processing in the child. |
| pid = os.fork() |
| if pid == 0: |
| # child |
| os.dup2(write_pipe, sys.stdout.fileno()) |
| os.close(read_pipe) |
| os.close(write_pipe) |
| return |
| else: |
| # parent |
| os.dup2(read_pipe, sys.stdin.fileno()) |
| os.close(read_pipe) |
| os.close(write_pipe) |
| |
| cmd = ['c++filt'] |
| os.execvp(cmd[0], cmd) |
| |
| |
| def main(argv): |
| parser = optparse.OptionParser(usage='%prog [options] [<file>]') |
| parser.add_option('--no-functions', help='Don\'t print function names', |
| dest='printFunctions', action='store_false', |
| default=True) |
| parser.add_option('--no-demangle', |
| help='Don\'t demangle C++ symbol names', |
| dest='cxxfilt', action='store_false', |
| default=True) |
| |
| (options, args) = parser.parse_args(argv[1:]) |
| num_args = len(args) |
| if num_args == 0: |
| in_file = sys.stdin |
| elif num_args == 1: |
| in_file = open(argv[1], 'r') |
| else: |
| parser.print_usage(sys.stderr) |
| print >> sys.stderr, 'trailing arguments: %s' % (' '.join(args[1:],)) |
| return 1 |
| |
| if options.cxxfilt: |
| start_cppfilt() |
| |
| process_file(in_file, sys.stdout, options) |
| |
| |
| if __name__ == '__main__': |
| rc = main(sys.argv) |
| sys.exit(rc) |