|  | # Copyright 2014 The Chromium Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | import collections | 
|  | import datetime | 
|  | import logging | 
|  | import multiprocessing | 
|  | import os | 
|  | import posixpath | 
|  | import Queue | 
|  | import re | 
|  | import subprocess | 
|  | import sys | 
|  | import threading | 
|  | import time | 
|  |  | 
|  |  | 
|  | # addr2line builds a possibly infinite memory cache that can exhaust | 
|  | # the computer's memory if allowed to grow for too long. This constant | 
|  | # controls how many lookups we do before restarting the process. 4000 | 
|  | # gives near peak performance without extreme memory usage. | 
|  | ADDR2LINE_RECYCLE_LIMIT = 4000 | 
|  |  | 
|  |  | 
|  | class ELFSymbolizer(object): | 
|  | """An uber-fast (multiprocessing, pipelined and asynchronous) ELF symbolizer. | 
|  |  | 
|  | This class is a frontend for addr2line (part of GNU binutils), designed to | 
|  | symbolize batches of large numbers of symbols for a given ELF file. It | 
|  | supports sharding symbolization against many addr2line instances and | 
|  | pipelining of multiple requests per each instance (in order to hide addr2line | 
|  | internals and OS pipe latencies). | 
|  |  | 
|  | The interface exhibited by this class is a very simple asynchronous interface, | 
|  | which is based on the following three methods: | 
|  | - SymbolizeAsync(): used to request (enqueue) resolution of a given address. | 
|  | - The |callback| method: used to communicated back the symbol information. | 
|  | - Join(): called to conclude the batch to gather the last outstanding results. | 
|  | In essence, before the Join method returns, this class will have issued as | 
|  | many callbacks as the number of SymbolizeAsync() calls. In this regard, note | 
|  | that due to multiprocess sharding, callbacks can be delivered out of order. | 
|  |  | 
|  | Some background about addr2line: | 
|  | - it is invoked passing the elf path in the cmdline, piping the addresses in | 
|  | its stdin and getting results on its stdout. | 
|  | - it has pretty large response times for the first requests, but it | 
|  | works very well in streaming mode once it has been warmed up. | 
|  | - it doesn't scale by itself (on more cores). However, spawning multiple | 
|  | instances at the same time on the same file is pretty efficient as they | 
|  | keep hitting the pagecache and become mostly CPU bound. | 
|  | - it might hang or crash, mostly for OOM. This class deals with both of these | 
|  | problems. | 
|  |  | 
|  | Despite the "scary" imports and the multi* words above, (almost) no multi- | 
|  | threading/processing is involved from the python viewpoint. Concurrency | 
|  | here is achieved by spawning several addr2line subprocesses and handling their | 
|  | output pipes asynchronously. Therefore, all the code here (with the exception | 
|  | of the Queue instance in Addr2Line) should be free from mind-blowing | 
|  | thread-safety concerns. | 
|  |  | 
|  | The multiprocess sharding works as follows: | 
|  | The symbolizer tries to use the lowest number of addr2line instances as | 
|  | possible (with respect of |max_concurrent_jobs|) and enqueue all the requests | 
|  | in a single addr2line instance. For few symbols (i.e. dozens) sharding isn't | 
|  | worth the startup cost. | 
|  | The multiprocess logic kicks in as soon as the queues for the existing | 
|  | instances grow. Specifically, once all the existing instances reach the | 
|  | |max_queue_size| bound, a new addr2line instance is kicked in. | 
|  | In the case of a very eager producer (i.e. all |max_concurrent_jobs| instances | 
|  | have a backlog of |max_queue_size|), back-pressure is applied on the caller by | 
|  | blocking the SymbolizeAsync method. | 
|  |  | 
|  | This module has been deliberately designed to be dependency free (w.r.t. of | 
|  | other modules in this project), to allow easy reuse in external projects. | 
|  | """ | 
|  |  | 
|  | def __init__(self, elf_file_path, addr2line_path, callback, inlines=False, | 
|  | max_concurrent_jobs=None, addr2line_timeout=30, max_queue_size=50, | 
|  | source_root_path=None, strip_base_path=None): | 
|  | """Args: | 
|  | elf_file_path: path of the elf file to be symbolized. | 
|  | addr2line_path: path of the toolchain's addr2line binary. | 
|  | callback: a callback which will be invoked for each resolved symbol with | 
|  | the two args (sym_info, callback_arg). The former is an instance of | 
|  | |ELFSymbolInfo| and contains the symbol information. The latter is an | 
|  | embedder-provided argument which is passed to SymbolizeAsync(). | 
|  | inlines: when True, the ELFSymbolInfo will contain also the details about | 
|  | the outer inlining functions. When False, only the innermost function | 
|  | will be provided. | 
|  | max_concurrent_jobs: Max number of addr2line instances spawned. | 
|  | Parallelize responsibly, addr2line is a memory and I/O monster. | 
|  | max_queue_size: Max number of outstanding requests per addr2line instance. | 
|  | addr2line_timeout: Max time (in seconds) to wait for a addr2line response. | 
|  | After the timeout, the instance will be considered hung and respawned. | 
|  | source_root_path: In some toolchains only the name of the source file is | 
|  | is output, without any path information; disambiguation searches | 
|  | through the source directory specified by |source_root_path| argument | 
|  | for files whose name matches, adding the full path information to the | 
|  | output. For example, if the toolchain outputs "unicode.cc" and there | 
|  | is a file called "unicode.cc" located under |source_root_path|/foo, | 
|  | the tool will replace "unicode.cc" with | 
|  | "|source_root_path|/foo/unicode.cc". If there are multiple files with | 
|  | the same name, disambiguation will fail because the tool cannot | 
|  | determine which of the files was the source of the symbol. | 
|  | strip_base_path: Rebases the symbols source paths onto |source_root_path| | 
|  | (i.e replace |strip_base_path| with |source_root_path). | 
|  | """ | 
|  | assert(os.path.isfile(addr2line_path)), 'Cannot find ' + addr2line_path | 
|  | self.elf_file_path = elf_file_path | 
|  | self.addr2line_path = addr2line_path | 
|  | self.callback = callback | 
|  | self.inlines = inlines | 
|  | self.max_concurrent_jobs = (max_concurrent_jobs or | 
|  | min(multiprocessing.cpu_count(), 4)) | 
|  | self.max_queue_size = max_queue_size | 
|  | self.addr2line_timeout = addr2line_timeout | 
|  | self.requests_counter = 0  # For generating monotonic request IDs. | 
|  | self._a2l_instances = []  # Up to |max_concurrent_jobs| _Addr2Line inst. | 
|  |  | 
|  | # If necessary, create disambiguation lookup table | 
|  | self.disambiguate = source_root_path is not None | 
|  | self.disambiguation_table = {} | 
|  | self.strip_base_path = strip_base_path | 
|  | if(self.disambiguate): | 
|  | self.source_root_path = os.path.abspath(source_root_path) | 
|  | self._CreateDisambiguationTable() | 
|  |  | 
|  | # Create one addr2line instance. More instances will be created on demand | 
|  | # (up to |max_concurrent_jobs|) depending on the rate of the requests. | 
|  | self._CreateNewA2LInstance() | 
|  |  | 
|  | def SymbolizeAsync(self, addr, callback_arg=None): | 
|  | """Requests symbolization of a given address. | 
|  |  | 
|  | This method is not guaranteed to return immediately. It generally does, but | 
|  | in some scenarios (e.g. all addr2line instances have full queues) it can | 
|  | block to create back-pressure. | 
|  |  | 
|  | Args: | 
|  | addr: address to symbolize. | 
|  | callback_arg: optional argument which will be passed to the |callback|.""" | 
|  | assert(isinstance(addr, int)) | 
|  |  | 
|  | # Process all the symbols that have been resolved in the meanwhile. | 
|  | # Essentially, this drains all the addr2line(s) out queues. | 
|  | for a2l_to_purge in self._a2l_instances: | 
|  | a2l_to_purge.ProcessAllResolvedSymbolsInQueue() | 
|  | a2l_to_purge.RecycleIfNecessary() | 
|  |  | 
|  | # Find the best instance according to this logic: | 
|  | # 1. Find an existing instance with the shortest queue. | 
|  | # 2. If all of instances' queues are full, but there is room in the pool, | 
|  | #    (i.e. < |max_concurrent_jobs|) create a new instance. | 
|  | # 3. If there were already |max_concurrent_jobs| instances and all of them | 
|  | #    had full queues, make back-pressure. | 
|  |  | 
|  | # 1. | 
|  | def _SortByQueueSizeAndReqID(a2l): | 
|  | return (a2l.queue_size, a2l.first_request_id) | 
|  | a2l = min(self._a2l_instances, key=_SortByQueueSizeAndReqID) | 
|  |  | 
|  | # 2. | 
|  | if (a2l.queue_size >= self.max_queue_size and | 
|  | len(self._a2l_instances) < self.max_concurrent_jobs): | 
|  | a2l = self._CreateNewA2LInstance() | 
|  |  | 
|  | # 3. | 
|  | if a2l.queue_size >= self.max_queue_size: | 
|  | a2l.WaitForNextSymbolInQueue() | 
|  |  | 
|  | a2l.EnqueueRequest(addr, callback_arg) | 
|  |  | 
|  | def Join(self): | 
|  | """Waits for all the outstanding requests to complete and terminates.""" | 
|  | for a2l in self._a2l_instances: | 
|  | a2l.WaitForIdle() | 
|  | a2l.Terminate() | 
|  |  | 
|  | def _CreateNewA2LInstance(self): | 
|  | assert(len(self._a2l_instances) < self.max_concurrent_jobs) | 
|  | a2l = ELFSymbolizer.Addr2Line(self) | 
|  | self._a2l_instances.append(a2l) | 
|  | return a2l | 
|  |  | 
|  | def _CreateDisambiguationTable(self): | 
|  | """ Non-unique file names will result in None entries""" | 
|  | start_time = time.time() | 
|  | logging.info('Collecting information about available source files...') | 
|  | self.disambiguation_table = {} | 
|  |  | 
|  | for root, _, filenames in os.walk(self.source_root_path): | 
|  | for f in filenames: | 
|  | self.disambiguation_table[f] = os.path.join(root, f) if (f not in | 
|  | self.disambiguation_table) else None | 
|  | logging.info('Finished collecting information about ' | 
|  | 'possible files (took %.1f s).', | 
|  | (time.time() - start_time)) | 
|  |  | 
|  |  | 
|  | class Addr2Line(object): | 
|  | """A python wrapper around an addr2line instance. | 
|  |  | 
|  | The communication with the addr2line process looks as follows: | 
|  | [STDIN]         [STDOUT]  (from addr2line's viewpoint) | 
|  | > f001111 | 
|  | > f002222 | 
|  | < Symbol::Name(foo, bar) for f001111 | 
|  | < /path/to/source/file.c:line_number | 
|  | > f003333 | 
|  | < Symbol::Name2() for f002222 | 
|  | < /path/to/source/file.c:line_number | 
|  | < Symbol::Name3() for f003333 | 
|  | < /path/to/source/file.c:line_number | 
|  | """ | 
|  |  | 
|  | SYM_ADDR_RE = re.compile(r'([^:]+):(\?|\d+).*') | 
|  |  | 
|  | def __init__(self, symbolizer): | 
|  | self._symbolizer = symbolizer | 
|  | self._lib_file_name = posixpath.basename(symbolizer.elf_file_path) | 
|  |  | 
|  | # The request queue (i.e. addresses pushed to addr2line's stdin and not | 
|  | # yet retrieved on stdout) | 
|  | self._request_queue = collections.deque() | 
|  |  | 
|  | # This is essentially len(self._request_queue). It has been optimized to a | 
|  | # separate field because turned out to be a perf hot-spot. | 
|  | self.queue_size = 0 | 
|  |  | 
|  | # Keep track of the number of symbols a process has processed to | 
|  | # avoid a single process growing too big and using all the memory. | 
|  | self._processed_symbols_count = 0 | 
|  |  | 
|  | # Objects required to handle the addr2line subprocess. | 
|  | self._proc = None  # Subprocess.Popen(...) instance. | 
|  | self._thread = None  # Threading.thread instance. | 
|  | self._out_queue = None  # Queue.Queue instance (for buffering a2l stdout). | 
|  | self._RestartAddr2LineProcess() | 
|  |  | 
|  | def EnqueueRequest(self, addr, callback_arg): | 
|  | """Pushes an address to addr2line's stdin (and keeps track of it).""" | 
|  | self._symbolizer.requests_counter += 1  # For global "age" of requests. | 
|  | req_idx = self._symbolizer.requests_counter | 
|  | self._request_queue.append((addr, callback_arg, req_idx)) | 
|  | self.queue_size += 1 | 
|  | self._WriteToA2lStdin(addr) | 
|  |  | 
|  | def WaitForIdle(self): | 
|  | """Waits until all the pending requests have been symbolized.""" | 
|  | while self.queue_size > 0: | 
|  | self.WaitForNextSymbolInQueue() | 
|  |  | 
|  | def WaitForNextSymbolInQueue(self): | 
|  | """Waits for the next pending request to be symbolized.""" | 
|  | if not self.queue_size: | 
|  | return | 
|  |  | 
|  | # This outer loop guards against a2l hanging (detecting stdout timeout). | 
|  | while True: | 
|  | start_time = datetime.datetime.now() | 
|  | timeout = datetime.timedelta(seconds=self._symbolizer.addr2line_timeout) | 
|  |  | 
|  | # The inner loop guards against a2l crashing (checking if it exited). | 
|  | while (datetime.datetime.now() - start_time < timeout): | 
|  | # poll() returns !None if the process exited. a2l should never exit. | 
|  | if self._proc.poll(): | 
|  | logging.warning('addr2line crashed, respawning (lib: %s).' % | 
|  | self._lib_file_name) | 
|  | self._RestartAddr2LineProcess() | 
|  | # TODO(primiano): the best thing to do in this case would be | 
|  | # shrinking the pool size as, very likely, addr2line is crashed | 
|  | # due to low memory (and the respawned one will die again soon). | 
|  |  | 
|  | try: | 
|  | lines = self._out_queue.get(block=True, timeout=0.25) | 
|  | except Queue.Empty: | 
|  | # On timeout (1/4 s.) repeat the inner loop and check if either the | 
|  | # addr2line process did crash or we waited its output for too long. | 
|  | continue | 
|  |  | 
|  | # In nominal conditions, we get straight to this point. | 
|  | self._ProcessSymbolOutput(lines) | 
|  | return | 
|  |  | 
|  | # If this point is reached, we waited more than |addr2line_timeout|. | 
|  | logging.warning('Hung addr2line process, respawning (lib: %s).' % | 
|  | self._lib_file_name) | 
|  | self._RestartAddr2LineProcess() | 
|  |  | 
|  | def ProcessAllResolvedSymbolsInQueue(self): | 
|  | """Consumes all the addr2line output lines produced (without blocking).""" | 
|  | if not self.queue_size: | 
|  | return | 
|  | while True: | 
|  | try: | 
|  | lines = self._out_queue.get_nowait() | 
|  | except Queue.Empty: | 
|  | break | 
|  | self._ProcessSymbolOutput(lines) | 
|  |  | 
|  | def RecycleIfNecessary(self): | 
|  | """Restarts the process if it has been used for too long. | 
|  |  | 
|  | A long running addr2line process will consume excessive amounts | 
|  | of memory without any gain in performance.""" | 
|  | if self._processed_symbols_count >= ADDR2LINE_RECYCLE_LIMIT: | 
|  | self._RestartAddr2LineProcess() | 
|  |  | 
|  |  | 
|  | def Terminate(self): | 
|  | """Kills the underlying addr2line process. | 
|  |  | 
|  | The poller |_thread| will terminate as well due to the broken pipe.""" | 
|  | try: | 
|  | self._proc.kill() | 
|  | self._proc.communicate()  # Essentially wait() without risking deadlock. | 
|  | except Exception:  # An exception while terminating? How interesting. | 
|  | pass | 
|  | self._proc = None | 
|  |  | 
|  | def _WriteToA2lStdin(self, addr): | 
|  | self._proc.stdin.write('%s\n' % hex(addr)) | 
|  | if self._symbolizer.inlines: | 
|  | # In the case of inlines we output an extra blank line, which causes | 
|  | # addr2line to emit a (??,??:0) tuple that we use as a boundary marker. | 
|  | self._proc.stdin.write('\n') | 
|  | self._proc.stdin.flush() | 
|  |  | 
|  | def _ProcessSymbolOutput(self, lines): | 
|  | """Parses an addr2line symbol output and triggers the client callback.""" | 
|  | (_, callback_arg, _) = self._request_queue.popleft() | 
|  | self.queue_size -= 1 | 
|  |  | 
|  | innermost_sym_info = None | 
|  | sym_info = None | 
|  | for (line1, line2) in lines: | 
|  | prev_sym_info = sym_info | 
|  | name = line1 if not line1.startswith('?') else None | 
|  | source_path = None | 
|  | source_line = None | 
|  | m = ELFSymbolizer.Addr2Line.SYM_ADDR_RE.match(line2) | 
|  | if m: | 
|  | if not m.group(1).startswith('?'): | 
|  | source_path = m.group(1) | 
|  | if not m.group(2).startswith('?'): | 
|  | source_line = int(m.group(2)) | 
|  | else: | 
|  | logging.warning('Got invalid symbol path from addr2line: %s' % line2) | 
|  |  | 
|  | # In case disambiguation is on, and needed | 
|  | was_ambiguous = False | 
|  | disambiguated = False | 
|  | if self._symbolizer.disambiguate: | 
|  | if source_path and not posixpath.isabs(source_path): | 
|  | path = self._symbolizer.disambiguation_table.get(source_path) | 
|  | was_ambiguous = True | 
|  | disambiguated = path is not None | 
|  | source_path = path if disambiguated else source_path | 
|  |  | 
|  | # Use absolute paths (so that paths are consistent, as disambiguation | 
|  | # uses absolute paths) | 
|  | if source_path and not was_ambiguous: | 
|  | source_path = os.path.abspath(source_path) | 
|  |  | 
|  | if source_path and self._symbolizer.strip_base_path: | 
|  | # Strip the base path | 
|  | source_path = re.sub('^' + self._symbolizer.strip_base_path, | 
|  | self._symbolizer.source_root_path or '', source_path) | 
|  |  | 
|  | sym_info = ELFSymbolInfo(name, source_path, source_line, was_ambiguous, | 
|  | disambiguated) | 
|  | if prev_sym_info: | 
|  | prev_sym_info.inlined_by = sym_info | 
|  | if not innermost_sym_info: | 
|  | innermost_sym_info = sym_info | 
|  |  | 
|  | self._processed_symbols_count += 1 | 
|  | self._symbolizer.callback(innermost_sym_info, callback_arg) | 
|  |  | 
|  | def _RestartAddr2LineProcess(self): | 
|  | if self._proc: | 
|  | self.Terminate() | 
|  |  | 
|  | # The only reason of existence of this Queue (and the corresponding | 
|  | # Thread below) is the lack of a subprocess.stdout.poll_avail_lines(). | 
|  | # Essentially this is a pipe able to extract a couple of lines atomically. | 
|  | self._out_queue = Queue.Queue() | 
|  |  | 
|  | # Start the underlying addr2line process in line buffered mode. | 
|  |  | 
|  | cmd = [self._symbolizer.addr2line_path, '--functions', '--demangle', | 
|  | '--exe=' + self._symbolizer.elf_file_path] | 
|  | if self._symbolizer.inlines: | 
|  | cmd += ['--inlines'] | 
|  | self._proc = subprocess.Popen(cmd, bufsize=1, stdout=subprocess.PIPE, | 
|  | stdin=subprocess.PIPE, stderr=sys.stderr, close_fds=True) | 
|  |  | 
|  | # Start the poller thread, which simply moves atomically the lines read | 
|  | # from the addr2line's stdout to the |_out_queue|. | 
|  | self._thread = threading.Thread( | 
|  | target=ELFSymbolizer.Addr2Line.StdoutReaderThread, | 
|  | args=(self._proc.stdout, self._out_queue, self._symbolizer.inlines)) | 
|  | self._thread.daemon = True  # Don't prevent early process exit. | 
|  | self._thread.start() | 
|  |  | 
|  | self._processed_symbols_count = 0 | 
|  |  | 
|  | # Replay the pending requests on the new process (only for the case | 
|  | # of a hung addr2line timing out during the game). | 
|  | for (addr, _, _) in self._request_queue: | 
|  | self._WriteToA2lStdin(addr) | 
|  |  | 
|  | @staticmethod | 
|  | def StdoutReaderThread(process_pipe, queue, inlines): | 
|  | """The poller thread fn, which moves the addr2line stdout to the |queue|. | 
|  |  | 
|  | This is the only piece of code not running on the main thread. It merely | 
|  | writes to a Queue, which is thread-safe. In the case of inlines, it | 
|  | detects the ??,??:0 marker and sends the lines atomically, such that the | 
|  | main thread always receives all the lines corresponding to one symbol in | 
|  | one shot.""" | 
|  | try: | 
|  | lines_for_one_symbol = [] | 
|  | while True: | 
|  | line1 = process_pipe.readline().rstrip('\r\n') | 
|  | line2 = process_pipe.readline().rstrip('\r\n') | 
|  | if not line1 or not line2: | 
|  | break | 
|  | inline_has_more_lines = inlines and (len(lines_for_one_symbol) == 0 or | 
|  | (line1 != '??' and line2 != '??:0')) | 
|  | if not inlines or inline_has_more_lines: | 
|  | lines_for_one_symbol += [(line1, line2)] | 
|  | if inline_has_more_lines: | 
|  | continue | 
|  | queue.put(lines_for_one_symbol) | 
|  | lines_for_one_symbol = [] | 
|  | process_pipe.close() | 
|  |  | 
|  | # Every addr2line processes will die at some point, please die silently. | 
|  | except (IOError, OSError): | 
|  | pass | 
|  |  | 
|  | @property | 
|  | def first_request_id(self): | 
|  | """Returns the request_id of the oldest pending request in the queue.""" | 
|  | return self._request_queue[0][2] if self._request_queue else 0 | 
|  |  | 
|  |  | 
|  | class ELFSymbolInfo(object): | 
|  | """The result of the symbolization passed as first arg. of each callback.""" | 
|  |  | 
|  | def __init__(self, name, source_path, source_line, was_ambiguous=False, | 
|  | disambiguated=False): | 
|  | """All the fields here can be None (if addr2line replies with '??').""" | 
|  | self.name = name | 
|  | self.source_path = source_path | 
|  | self.source_line = source_line | 
|  | # In the case of |inlines|=True, the |inlined_by| points to the outer | 
|  | # function inlining the current one (and so on, to form a chain). | 
|  | self.inlined_by = None | 
|  | self.disambiguated = disambiguated | 
|  | self.was_ambiguous = was_ambiguous | 
|  |  | 
|  | def __str__(self): | 
|  | return '%s [%s:%d]' % ( | 
|  | self.name or '??', self.source_path or '??', self.source_line or 0) |