native_heap.py revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
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
5from memory_inspector.core import memory_map
6from memory_inspector.core import stacktrace
7from memory_inspector.core import symbol
8
9from memory_inspector.core.memory_map import PAGE_SIZE
10
11
12class NativeHeap(object):
13  """A snapshot of outstanding (i.e. not freed) native allocations.
14
15  This is typically obtained by calling |backends.Process|.DumpNativeHeap()
16  """
17
18  def __init__(self):
19    self.allocations = []  # list of individual |Allocation|s.
20    self.stack_frames = {}  # absolute_address (int) -> |stacktrace.Frame|.
21
22  def Add(self, allocation):
23    assert(isinstance(allocation, Allocation))
24    self.allocations += [allocation]
25
26  def GetStackFrame(self, absolute_addr):
27    """Guarantees that multiple calls with the same addr return the same obj."""
28    assert(isinstance(absolute_addr, (long, int)))
29    stack_frame = self.stack_frames.get(absolute_addr)
30    if not stack_frame:
31      stack_frame = stacktrace.Frame(absolute_addr)
32      self.stack_frames[absolute_addr] = stack_frame
33    return stack_frame
34
35  def SymbolizeUsingSymbolDB(self, symbols):
36    assert(isinstance(symbols, symbol.Symbols))
37    for stack_frame in self.stack_frames.itervalues():
38      if not stack_frame.exec_file_rel_path:
39        continue
40      sym = symbols.Lookup(stack_frame.exec_file_rel_path, stack_frame.offset)
41      if sym:
42        stack_frame.SetSymbolInfo(sym)
43
44  def RelativizeStackFrames(self, mmap):
45    """Turns stack frames' absolute addresses into mmap relative addresses.
46
47    For each absolute address, the containing mmap is looked up and the frame
48    is decorated with the mapped file + relative address in the file."""
49    assert(isinstance(mmap, memory_map.Map))
50    for abs_addr, stack_frame in self.stack_frames.iteritems():
51      assert(abs_addr == stack_frame.address)
52      map_entry = mmap.Lookup(abs_addr)
53      if not map_entry:
54        continue
55      stack_frame.SetExecFileInfo(map_entry.mapped_file,
56                                  map_entry.GetRelativeFileOffset(abs_addr))
57
58  def CalculateResidentSize(self, mmap):
59    """Updates the |Allocation|.|resident_size|s by looking at mmap stats.
60
61    Not all the allocated memory is always used (read: resident). This function
62    estimates the resident size of an allocation intersecting the mmaps dump.
63    """
64    assert(isinstance(mmap, memory_map.Map))
65    for alloc in self.allocations:
66      # This function loops over all the memory pages that intersect, partially
67      # or fully, with each allocation. For each of them, the allocation  is
68      # attributed a resident size equal to the size of intersecting range iff
69      # the page is resident.
70      # The tricky part is that, in the general case, an allocation can span
71      # over multiple (contiguous) mmaps. See the chart below for a reference:
72      #
73      # VA space:  |0    |4k   |8k   |12k  |16k  |20k  |24k  |28k  |32k  |
74      # Mmaps:     [   mm 1   ][ mm2 ]           [          map 3        ]
75      # Allocs:      <a1>  <  a2  >                       <      a3      >
76      #
77      # Note: this accounting technique is not fully correct but is generally a
78      # good tradeoff between accuracy and speed of profiling. The OS provides
79      # resident information with the page granularity (typ. 4k). Finer values
80      # would require more fancy techniques based, for instance, on run-time
81      # instrumentation tools like Valgrind or *sanitizer.
82      cur_start = alloc.start
83      mm = None
84      while cur_start < alloc.end:
85        if not mm or not mm.Contains(cur_start):
86          mm = mmap.Lookup(cur_start)
87        if mm:
88          page, page_off = mm.GetRelativeMMOffset(cur_start)
89          if mm.IsPageResident(page):
90            page_end = mm.start + page * PAGE_SIZE + PAGE_SIZE - 1
91            alloc_memory_in_current_page = PAGE_SIZE - page_off
92            if alloc.end < page_end:
93              alloc_memory_in_current_page -= page_end - alloc.end
94            alloc.resident_size += alloc_memory_in_current_page
95        # Move to the next page boundary.
96        cur_start = (cur_start + PAGE_SIZE) & ~(PAGE_SIZE - 1)
97
98
99class Allocation(object):
100  """Records profiling information about a native heap allocation.
101
102  Args:
103      size: size of the allocation, in bytes.
104      stack_trace: the allocation call-site. See |stacktrace.Stacktrace|.
105      start: (Optional) Absolute start address in the process VMA. It is
106          required only for |CalculateResidentSize|.
107      flags: (Optional) More details about the call site (e.g., mmap vs malloc).
108      resident_size: this is normally obtained through |CalculateResidentSize|
109          and is part of the initializer just for deserialization purposes.
110  """
111
112  def __init__(self, size, stack_trace, start=0, flags=0, resident_size=0):
113    assert(size > 0)
114    assert(isinstance(stack_trace, stacktrace.Stacktrace))
115    self.size = size  # in bytes.
116    self.stack_trace = stack_trace
117    self.start = start  # Optional, for using the resident size logic.
118    self.flags = flags
119    self.resident_size = resident_size  #  see |CalculateResidentSize|.
120
121  @property
122  def end(self):
123    return self.start + self.size - 1
124
125  def __str__(self):
126    return '%d : %s' % (self.size, self.stack_trace)
127