Home | History | Annotate | Download | only in resources
      1 # Copyright 2014 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """The ElfSymbolizer class for symbolizing Executable and Linkable Files.
      6 
      7 Adapted for Skia's use from
      8 chromium/src/build/android/pylib/symbols/elf_symbolizer.py.
      9 
     10 Main changes:
     11 -- Added prefix_to_remove param to remove path prefix from tree data.
     12 """
     13 
     14 import collections
     15 import datetime
     16 import logging
     17 import multiprocessing
     18 import os
     19 import posixpath
     20 import Queue
     21 import re
     22 import subprocess
     23 import sys
     24 import threading
     25 
     26 
     27 # addr2line builds a possibly infinite memory cache that can exhaust
     28 # the computer's memory if allowed to grow for too long. This constant
     29 # controls how many lookups we do before restarting the process. 4000
     30 # gives near peak performance without extreme memory usage.
     31 ADDR2LINE_RECYCLE_LIMIT = 4000
     32 
     33 
     34 class ELFSymbolizer(object):
     35   """An uber-fast (multiprocessing, pipelined and asynchronous) ELF symbolizer.
     36 
     37   This class is a frontend for addr2line (part of GNU binutils), designed to
     38   symbolize batches of large numbers of symbols for a given ELF file. It
     39   supports sharding symbolization against many addr2line instances and
     40   pipelining of multiple requests per each instance (in order to hide addr2line
     41   internals and OS pipe latencies).
     42 
     43   The interface exhibited by this class is a very simple asynchronous interface,
     44   which is based on the following three methods:
     45   - SymbolizeAsync(): used to request (enqueue) resolution of a given address.
     46   - The |callback| method: used to communicated back the symbol information.
     47   - Join(): called to conclude the batch to gather the last outstanding results.
     48   In essence, before the Join method returns, this class will have issued as
     49   many callbacks as the number of SymbolizeAsync() calls. In this regard, note
     50   that due to multiprocess sharding, callbacks can be delivered out of order.
     51 
     52   Some background about addr2line:
     53   - it is invoked passing the elf path in the cmdline, piping the addresses in
     54     its stdin and getting results on its stdout.
     55   - it has pretty large response times for the first requests, but it
     56     works very well in streaming mode once it has been warmed up.
     57   - it doesn't scale by itself (on more cores). However, spawning multiple
     58     instances at the same time on the same file is pretty efficient as they
     59     keep hitting the pagecache and become mostly CPU bound.
     60   - it might hang or crash, mostly for OOM. This class deals with both of these
     61     problems.
     62 
     63   Despite the "scary" imports and the multi* words above, (almost) no multi-
     64   threading/processing is involved from the python viewpoint. Concurrency
     65   here is achieved by spawning several addr2line subprocesses and handling their
     66   output pipes asynchronously. Therefore, all the code here (with the exception
     67   of the Queue instance in Addr2Line) should be free from mind-blowing
     68   thread-safety concerns.
     69 
     70   The multiprocess sharding works as follows:
     71   The symbolizer tries to use the lowest number of addr2line instances as
     72   possible (with respect of |max_concurrent_jobs|) and enqueue all the requests
     73   in a single addr2line instance. For few symbols (i.e. dozens) sharding isn't
     74   worth the startup cost.
     75   The multiprocess logic kicks in as soon as the queues for the existing
     76   instances grow. Specifically, once all the existing instances reach the
     77   |max_queue_size| bound, a new addr2line instance is kicked in.
     78   In the case of a very eager producer (i.e. all |max_concurrent_jobs| instances
     79   have a backlog of |max_queue_size|), back-pressure is applied on the caller by
     80   blocking the SymbolizeAsync method.
     81 
     82   This module has been deliberately designed to be dependency free (w.r.t. of
     83   other modules in this project), to allow easy reuse in external projects.
     84   """
     85 
     86   def __init__(self, elf_file_path, addr2line_path, callback, inlines=False,
     87       max_concurrent_jobs=None, addr2line_timeout=30, max_queue_size=50,
     88       source_root_path=None, strip_base_path=None, prefix_to_remove=None):
     89     """Args:
     90       elf_file_path: path of the elf file to be symbolized.
     91       addr2line_path: path of the toolchain's addr2line binary.
     92       callback: a callback which will be invoked for each resolved symbol with
     93           the two args (sym_info, callback_arg). The former is an instance of
     94           |ELFSymbolInfo| and contains the symbol information. The latter is an
     95           embedder-provided argument which is passed to SymbolizeAsync().
     96       inlines: when True, the ELFSymbolInfo will contain also the details about
     97           the outer inlining functions. When False, only the innermost function
     98           will be provided.
     99       max_concurrent_jobs: Max number of addr2line instances spawned.
    100           Parallelize responsibly, addr2line is a memory and I/O monster.
    101       max_queue_size: Max number of outstanding requests per addr2line instance.
    102       addr2line_timeout: Max time (in seconds) to wait for a addr2line response.
    103           After the timeout, the instance will be considered hung and respawned.
    104       source_root_path: In some toolchains only the name of the source file is
    105           is output, without any path information; disambiguation searches
    106           through the source directory specified by |source_root_path| argument
    107           for files whose name matches, adding the full path information to the
    108           output. For example, if the toolchain outputs "unicode.cc" and there
    109           is a file called "unicode.cc" located under |source_root_path|/foo,
    110           the tool will replace "unicode.cc" with
    111           "|source_root_path|/foo/unicode.cc". If there are multiple files with
    112           the same name, disambiguation will fail because the tool cannot
    113           determine which of the files was the source of the symbol.
    114       strip_base_path: Rebases the symbols source paths onto |source_root_path|
    115           (i.e replace |strip_base_path| with |source_root_path).
    116       prefix_to_remove: Removes the prefix from ElfSymbolInfo output. Skia added
    117     """
    118     assert(os.path.isfile(addr2line_path)), 'Cannot find ' + addr2line_path
    119     self.elf_file_path = elf_file_path
    120     self.addr2line_path = addr2line_path
    121     self.callback = callback
    122     self.inlines = inlines
    123     self.max_concurrent_jobs = (max_concurrent_jobs or
    124                                 min(multiprocessing.cpu_count(), 4))
    125     self.max_queue_size = max_queue_size
    126     self.addr2line_timeout = addr2line_timeout
    127     self.requests_counter = 0  # For generating monotonic request IDs.
    128     self._a2l_instances = []  # Up to |max_concurrent_jobs| _Addr2Line inst.
    129 
    130     # Skia addition: remove the given prefix from tree paths.
    131     self.prefix_to_remove = prefix_to_remove
    132 
    133     # If necessary, create disambiguation lookup table
    134     self.disambiguate = source_root_path is not None
    135     self.disambiguation_table = {}
    136     self.strip_base_path = strip_base_path
    137     if(self.disambiguate):
    138       self.source_root_path = os.path.abspath(source_root_path)
    139       self._CreateDisambiguationTable()
    140 
    141     # Create one addr2line instance. More instances will be created on demand
    142     # (up to |max_concurrent_jobs|) depending on the rate of the requests.
    143     self._CreateNewA2LInstance()
    144 
    145   def SymbolizeAsync(self, addr, callback_arg=None):
    146     """Requests symbolization of a given address.
    147 
    148     This method is not guaranteed to return immediately. It generally does, but
    149     in some scenarios (e.g. all addr2line instances have full queues) it can
    150     block to create back-pressure.
    151 
    152     Args:
    153       addr: address to symbolize.
    154       callback_arg: optional argument which will be passed to the |callback|."""
    155     assert(isinstance(addr, int))
    156 
    157     # Process all the symbols that have been resolved in the meanwhile.
    158     # Essentially, this drains all the addr2line(s) out queues.
    159     for a2l_to_purge in self._a2l_instances:
    160       a2l_to_purge.ProcessAllResolvedSymbolsInQueue()
    161       a2l_to_purge.RecycleIfNecessary()
    162 
    163     # Find the best instance according to this logic:
    164     # 1. Find an existing instance with the shortest queue.
    165     # 2. If all of instances' queues are full, but there is room in the pool,
    166     #    (i.e. < |max_concurrent_jobs|) create a new instance.
    167     # 3. If there were already |max_concurrent_jobs| instances and all of them
    168     #    had full queues, make back-pressure.
    169 
    170     # 1.
    171     def _SortByQueueSizeAndReqID(a2l):
    172       return (a2l.queue_size, a2l.first_request_id)
    173     a2l = min(self._a2l_instances, key=_SortByQueueSizeAndReqID)
    174 
    175     # 2.
    176     if (a2l.queue_size >= self.max_queue_size and
    177         len(self._a2l_instances) < self.max_concurrent_jobs):
    178       a2l = self._CreateNewA2LInstance()
    179 
    180     # 3.
    181     if a2l.queue_size >= self.max_queue_size:
    182       a2l.WaitForNextSymbolInQueue()
    183 
    184     a2l.EnqueueRequest(addr, callback_arg)
    185 
    186   def Join(self):
    187     """Waits for all the outstanding requests to complete and terminates."""
    188     for a2l in self._a2l_instances:
    189       a2l.WaitForIdle()
    190       a2l.Terminate()
    191 
    192   def _CreateNewA2LInstance(self):
    193     assert(len(self._a2l_instances) < self.max_concurrent_jobs)
    194     a2l = ELFSymbolizer.Addr2Line(self)
    195     self._a2l_instances.append(a2l)
    196     return a2l
    197 
    198   def _CreateDisambiguationTable(self):
    199     """ Non-unique file names will result in None entries"""
    200     self.disambiguation_table = {}
    201 
    202     for root, _, filenames in os.walk(self.source_root_path):
    203       for f in filenames:
    204         self.disambiguation_table[f] = os.path.join(root, f) if (f not in
    205                                        self.disambiguation_table) else None
    206 
    207 
    208   class Addr2Line(object):
    209     """A python wrapper around an addr2line instance.
    210 
    211     The communication with the addr2line process looks as follows:
    212       [STDIN]         [STDOUT]  (from addr2line's viewpoint)
    213     > f001111
    214     > f002222
    215                     < Symbol::Name(foo, bar) for f001111
    216                     < /path/to/source/file.c:line_number
    217     > f003333
    218                     < Symbol::Name2() for f002222
    219                     < /path/to/source/file.c:line_number
    220                     < Symbol::Name3() for f003333
    221                     < /path/to/source/file.c:line_number
    222     """
    223 
    224     SYM_ADDR_RE = re.compile(r'([^:]+):(\?|\d+).*')
    225 
    226     def __init__(self, symbolizer):
    227       self._symbolizer = symbolizer
    228       self._lib_file_name = posixpath.basename(symbolizer.elf_file_path)
    229 
    230       # The request queue (i.e. addresses pushed to addr2line's stdin and not
    231       # yet retrieved on stdout)
    232       self._request_queue = collections.deque()
    233 
    234       # This is essentially len(self._request_queue). It has been optimized to a
    235       # separate field because turned out to be a perf hot-spot.
    236       self.queue_size = 0
    237 
    238       # Keep track of the number of symbols a process has processed to
    239       # avoid a single process growing too big and using all the memory.
    240       self._processed_symbols_count = 0
    241 
    242       # Objects required to handle the addr2line subprocess.
    243       self._proc = None  # Subprocess.Popen(...) instance.
    244       self._thread = None  # Threading.thread instance.
    245       self._out_queue = None  # Queue.Queue instance (for buffering a2l stdout).
    246       self._RestartAddr2LineProcess()
    247 
    248     def EnqueueRequest(self, addr, callback_arg):
    249       """Pushes an address to addr2line's stdin (and keeps track of it)."""
    250       self._symbolizer.requests_counter += 1  # For global "age" of requests.
    251       req_idx = self._symbolizer.requests_counter
    252       self._request_queue.append((addr, callback_arg, req_idx))
    253       self.queue_size += 1
    254       self._WriteToA2lStdin(addr)
    255 
    256     def WaitForIdle(self):
    257       """Waits until all the pending requests have been symbolized."""
    258       while self.queue_size > 0:
    259         self.WaitForNextSymbolInQueue()
    260 
    261     def WaitForNextSymbolInQueue(self):
    262       """Waits for the next pending request to be symbolized."""
    263       if not self.queue_size:
    264         return
    265 
    266       # This outer loop guards against a2l hanging (detecting stdout timeout).
    267       while True:
    268         start_time = datetime.datetime.now()
    269         timeout = datetime.timedelta(seconds=self._symbolizer.addr2line_timeout)
    270 
    271         # The inner loop guards against a2l crashing (checking if it exited).
    272         while (datetime.datetime.now() - start_time < timeout):
    273           # poll() returns !None if the process exited. a2l should never exit.
    274           if self._proc.poll():
    275             logging.warning('addr2line crashed, respawning (lib: %s).' %
    276                             self._lib_file_name)
    277             self._RestartAddr2LineProcess()
    278             # TODO(primiano): the best thing to do in this case would be
    279             # shrinking the pool size as, very likely, addr2line is crashed
    280             # due to low memory (and the respawned one will die again soon).
    281 
    282           try:
    283             lines = self._out_queue.get(block=True, timeout=0.25)
    284           except Queue.Empty:
    285             # On timeout (1/4 s.) repeat the inner loop and check if either the
    286             # addr2line process did crash or we waited its output for too long.
    287             continue
    288 
    289           # In nominal conditions, we get straight to this point.
    290           self._ProcessSymbolOutput(lines)
    291           return
    292 
    293         # If this point is reached, we waited more than |addr2line_timeout|.
    294         logging.warning('Hung addr2line process, respawning (lib: %s).' %
    295                         self._lib_file_name)
    296         self._RestartAddr2LineProcess()
    297 
    298     def ProcessAllResolvedSymbolsInQueue(self):
    299       """Consumes all the addr2line output lines produced (without blocking)."""
    300       if not self.queue_size:
    301         return
    302       while True:
    303         try:
    304           lines = self._out_queue.get_nowait()
    305         except Queue.Empty:
    306           break
    307         self._ProcessSymbolOutput(lines)
    308 
    309     def RecycleIfNecessary(self):
    310       """Restarts the process if it has been used for too long.
    311 
    312       A long running addr2line process will consume excessive amounts
    313       of memory without any gain in performance."""
    314       if self._processed_symbols_count >= ADDR2LINE_RECYCLE_LIMIT:
    315         self._RestartAddr2LineProcess()
    316 
    317 
    318     def Terminate(self):
    319       """Kills the underlying addr2line process.
    320 
    321       The poller |_thread| will terminate as well due to the broken pipe."""
    322       try:
    323         self._proc.kill()
    324         self._proc.communicate()  # Essentially wait() without risking deadlock.
    325       except Exception:  # An exception while terminating? How interesting.
    326         pass
    327       self._proc = None
    328 
    329     def _WriteToA2lStdin(self, addr):
    330       self._proc.stdin.write('%s\n' % hex(addr))
    331       if self._symbolizer.inlines:
    332         # In the case of inlines we output an extra blank line, which causes
    333         # addr2line to emit a (??,??:0) tuple that we use as a boundary marker.
    334         self._proc.stdin.write('\n')
    335       self._proc.stdin.flush()
    336 
    337     def _ProcessSymbolOutput(self, lines):
    338       """Parses an addr2line symbol output and triggers the client callback."""
    339       (_, callback_arg, _) = self._request_queue.popleft()
    340       self.queue_size -= 1
    341 
    342       innermost_sym_info = None
    343       sym_info = None
    344       for (line1, line2) in lines:
    345         prev_sym_info = sym_info
    346         name = line1 if not line1.startswith('?') else None
    347         source_path = None
    348         source_line = None
    349         m = ELFSymbolizer.Addr2Line.SYM_ADDR_RE.match(line2)
    350         if m:
    351           if not m.group(1).startswith('?'):
    352             source_path = m.group(1)
    353             if not m.group(2).startswith('?'):
    354               source_line = int(m.group(2))
    355         else:
    356           logging.warning('Got invalid symbol path from addr2line: %s' % line2)
    357 
    358         # In case disambiguation is on, and needed
    359         was_ambiguous = False
    360         disambiguated = False
    361         if self._symbolizer.disambiguate:
    362           if source_path and not posixpath.isabs(source_path):
    363             path = self._symbolizer.disambiguation_table.get(source_path)
    364             was_ambiguous = True
    365             disambiguated = path is not None
    366             source_path = path if disambiguated else source_path
    367 
    368           # Use absolute paths (so that paths are consistent, as disambiguation
    369           # uses absolute paths)
    370           if source_path and not was_ambiguous:
    371             source_path = os.path.abspath(source_path)
    372 
    373         if source_path and self._symbolizer.strip_base_path:
    374           # Strip the base path
    375           source_path = re.sub('^' + self._symbolizer.strip_base_path,
    376               self._symbolizer.source_root_path or '', source_path)
    377 
    378         sym_info = ELFSymbolInfo(name, source_path, source_line, was_ambiguous,
    379                                  disambiguated,
    380                                  self._symbolizer.prefix_to_remove)
    381         if prev_sym_info:
    382           prev_sym_info.inlined_by = sym_info
    383         if not innermost_sym_info:
    384           innermost_sym_info = sym_info
    385 
    386       self._processed_symbols_count += 1
    387       self._symbolizer.callback(innermost_sym_info, callback_arg)
    388 
    389     def _RestartAddr2LineProcess(self):
    390       if self._proc:
    391         self.Terminate()
    392 
    393       # The only reason of existence of this Queue (and the corresponding
    394       # Thread below) is the lack of a subprocess.stdout.poll_avail_lines().
    395       # Essentially this is a pipe able to extract a couple of lines atomically.
    396       self._out_queue = Queue.Queue()
    397 
    398       # Start the underlying addr2line process in line buffered mode.
    399 
    400       cmd = [self._symbolizer.addr2line_path, '--functions', '--demangle',
    401           '--exe=' + self._symbolizer.elf_file_path]
    402       if self._symbolizer.inlines:
    403         cmd += ['--inlines']
    404       self._proc = subprocess.Popen(cmd, bufsize=1, stdout=subprocess.PIPE,
    405           stdin=subprocess.PIPE, stderr=sys.stderr, close_fds=True)
    406 
    407       # Start the poller thread, which simply moves atomically the lines read
    408       # from the addr2line's stdout to the |_out_queue|.
    409       self._thread = threading.Thread(
    410           target=ELFSymbolizer.Addr2Line.StdoutReaderThread,
    411           args=(self._proc.stdout, self._out_queue, self._symbolizer.inlines))
    412       self._thread.daemon = True  # Don't prevent early process exit.
    413       self._thread.start()
    414 
    415       self._processed_symbols_count = 0
    416 
    417       # Replay the pending requests on the new process (only for the case
    418       # of a hung addr2line timing out during the game).
    419       for (addr, _, _) in self._request_queue:
    420         self._WriteToA2lStdin(addr)
    421 
    422     @staticmethod
    423     def StdoutReaderThread(process_pipe, queue, inlines):
    424       """The poller thread fn, which moves the addr2line stdout to the |queue|.
    425 
    426       This is the only piece of code not running on the main thread. It merely
    427       writes to a Queue, which is thread-safe. In the case of inlines, it
    428       detects the ??,??:0 marker and sends the lines atomically, such that the
    429       main thread always receives all the lines corresponding to one symbol in
    430       one shot."""
    431       try:
    432         lines_for_one_symbol = []
    433         while True:
    434           line1 = process_pipe.readline().rstrip('\r\n')
    435           line2 = process_pipe.readline().rstrip('\r\n')
    436           if not line1 or not line2:
    437             break
    438           inline_has_more_lines = inlines and (len(lines_for_one_symbol) == 0 or
    439                                   (line1 != '??' and line2 != '??:0'))
    440           if not inlines or inline_has_more_lines:
    441             lines_for_one_symbol += [(line1, line2)]
    442           if inline_has_more_lines:
    443             continue
    444           queue.put(lines_for_one_symbol)
    445           lines_for_one_symbol = []
    446         process_pipe.close()
    447 
    448       # Every addr2line processes will die at some point, please die silently.
    449       except (IOError, OSError):
    450         pass
    451 
    452     @property
    453     def first_request_id(self):
    454       """Returns the request_id of the oldest pending request in the queue."""
    455       return self._request_queue[0][2] if self._request_queue else 0
    456 
    457 
    458 class ELFSymbolInfo(object):
    459   """The result of the symbolization passed as first arg. of each callback."""
    460 
    461   def __init__(self, name, source_path, source_line, was_ambiguous=False,
    462                disambiguated=False, prefix_to_remove=None):
    463     """All the fields here can be None (if addr2line replies with '??')."""
    464     self.name = name
    465     if source_path and source_path.startswith(prefix_to_remove):
    466       source_path = source_path[len(prefix_to_remove) : ]
    467     self.source_path = source_path
    468     self.source_line = source_line
    469     # In the case of |inlines|=True, the |inlined_by| points to the outer
    470     # function inlining the current one (and so on, to form a chain).
    471     self.inlined_by = None
    472     self.disambiguated = disambiguated
    473     self.was_ambiguous = was_ambiguous
    474 
    475   def __str__(self):
    476     return '%s [%s:%d]' % (
    477         self.name or '??', self.source_path or '??', self.source_line or 0)
    478