• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
7Adapted for Skia's use from
8chromium/src/build/android/pylib/symbols/elf_symbolizer.py.
9
10Main changes:
11-- Added prefix_to_remove param to remove path prefix from tree data.
12"""
13
14import collections
15import datetime
16import logging
17import multiprocessing
18import os
19import posixpath
20import Queue
21import re
22import subprocess
23import sys
24import 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.
31ADDR2LINE_RECYCLE_LIMIT = 4000
32
33
34class 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
458class 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