Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / tools / memory_inspector / memory_inspector / backends / android / android_backend.py
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 """Android-specific implementation of the core backend interfaces.
6
7 See core/backends.py for more docs.
8 """
9
10 import datetime
11 import glob
12 import hashlib
13 import json
14 import os
15 import posixpath
16
17 from memory_inspector import constants
18 from memory_inspector.backends import prebuilts_fetcher
19 from memory_inspector.backends.android import dumpheap_native_parser
20 from memory_inspector.backends.android import memdump_parser
21 from memory_inspector.core import backends
22 from memory_inspector.core import exceptions
23 from memory_inspector.core import native_heap
24 from memory_inspector.core import symbol
25
26 # The memory_inspector/__init__ module will add the <CHROME_SRC>/build/android
27 # deps to the PYTHONPATH for pylib.
28 from pylib import android_commands
29 from pylib.device import device_utils
30 from pylib.symbols import elf_symbolizer
31
32
33 _MEMDUMP_PREBUILT_PATH = os.path.join(constants.PROJECT_SRC,
34                                       'prebuilts', 'memdump-android-arm')
35 _MEMDUMP_PATH_ON_DEVICE = '/data/local/tmp/memdump'
36 _PSEXT_PREBUILT_PATH = os.path.join(constants.PROJECT_SRC,
37                                     'prebuilts', 'ps_ext-android-arm')
38 _PSEXT_PATH_ON_DEVICE = '/data/local/tmp/ps_ext'
39 _DLMALLOC_DEBUG_SYSPROP = 'libc.debug.malloc'
40 _DUMPHEAP_OUT_FILE_PATH = '/data/local/tmp/heap-%d-native.dump'
41
42
43 class AndroidBackend(backends.Backend):
44   """Android-specific implementation of the core |Backend| interface."""
45
46   _SETTINGS_KEYS = {
47       'adb_path': 'Path of directory containing the adb binary',
48       'toolchain_path': 'Path of toolchain (for addr2line)'}
49
50   def __init__(self):
51     super(AndroidBackend, self).__init__(
52         settings=backends.Settings(AndroidBackend._SETTINGS_KEYS))
53     self._devices = {}  # 'device id' -> |Device|.
54
55   def EnumerateDevices(self):
56     # If a custom adb_path has been setup through settings, prepend that to the
57     # PATH. The android_commands module will use that to locate adb.
58     if (self.settings['adb_path'] and
59         not os.environ['PATH'].startswith(self.settings['adb_path'])):
60       os.environ['PATH'] = os.pathsep.join([self.settings['adb_path'],
61                                            os.environ['PATH']])
62     for device_id in android_commands.GetAttachedDevices():
63       device = self._devices.get(device_id)
64       if not device:
65         device = AndroidDevice(
66           self, device_utils.DeviceUtils(device_id))
67         self._devices[device_id] = device
68       yield device
69
70   def ExtractSymbols(self, native_heaps, sym_paths):
71     """Performs symbolization. Returns a |symbol.Symbols| from |NativeHeap|s.
72
73     This method performs the symbolization but does NOT decorate (i.e. add
74     symbol/source info) to the stack frames of |native_heaps|. The heaps
75     can be decorated as needed using the native_heap.SymbolizeUsingSymbolDB()
76     method. Rationale: the most common use case in this application is:
77     symbolize-and-store-symbols and load-symbols-and-decorate-heaps (in two
78     different stages at two different times).
79
80     Args:
81       native_heaps: a collection of native_heap.NativeHeap instances.
82       sym_paths: either a list of or a string of comma-separated symbol paths.
83     """
84     assert(all(isinstance(x, native_heap.NativeHeap) for x in native_heaps))
85     symbols = symbol.Symbols()
86
87     # Find addr2line in toolchain_path.
88     if isinstance(sym_paths, basestring):
89       sym_paths = sym_paths.split(',')
90     matches = glob.glob(os.path.join(self.settings['toolchain_path'],
91                                      '*addr2line'))
92     if not matches:
93       raise exceptions.MemoryInspectorException('Cannot find addr2line')
94     addr2line_path = matches[0]
95
96     # First group all the stack frames together by lib path.
97     frames_by_lib = {}
98     for nheap in native_heaps:
99       for stack_frame in nheap.stack_frames.itervalues():
100         frames = frames_by_lib.setdefault(stack_frame.exec_file_rel_path, set())
101         frames.add(stack_frame)
102
103     # The symbolization process is asynchronous (but yet single-threaded). This
104     # callback is invoked every time the symbol info for a stack frame is ready.
105     def SymbolizeAsyncCallback(sym_info, stack_frame):
106       if not sym_info.name:
107         return
108       sym = symbol.Symbol(name=sym_info.name,
109                           source_file_path=sym_info.source_path,
110                           line_number=sym_info.source_line)
111       symbols.Add(stack_frame.exec_file_rel_path, stack_frame.offset, sym)
112       # TODO(primiano): support inline sym info (i.e. |sym_info.inlined_by|).
113
114     # Perform the actual symbolization (ordered by lib).
115     for exec_file_rel_path, frames in frames_by_lib.iteritems():
116       # Look up the full path of the symbol in the sym paths.
117       exec_file_name = posixpath.basename(exec_file_rel_path)
118       if exec_file_rel_path.startswith('/'):
119         exec_file_rel_path = exec_file_rel_path[1:]
120       exec_file_abs_path = ''
121       for sym_path in sym_paths:
122         # First try to locate the symbol file following the full relative path
123         # e.g. /host/syms/ + /system/lib/foo.so => /host/syms/system/lib/foo.so.
124         exec_file_abs_path = os.path.join(sym_path, exec_file_rel_path)
125         if os.path.exists(exec_file_abs_path):
126           break
127
128         # If no luck, try looking just for the file name in the sym path,
129         # e.g. /host/syms/ + (/system/lib/)foo.so => /host/syms/foo.so
130         exec_file_abs_path = os.path.join(sym_path, exec_file_name)
131         if os.path.exists(exec_file_abs_path):
132           break
133
134       if not os.path.exists(exec_file_abs_path):
135         continue
136
137       symbolizer = elf_symbolizer.ELFSymbolizer(
138           elf_file_path=exec_file_abs_path,
139           addr2line_path=addr2line_path,
140           callback=SymbolizeAsyncCallback,
141           inlines=False)
142
143       # Kick off the symbolizer and then wait that all callbacks are issued.
144       for stack_frame in sorted(frames, key=lambda x: x.offset):
145         symbolizer.SymbolizeAsync(stack_frame.offset, stack_frame)
146       symbolizer.Join()
147
148     return symbols
149
150   @property
151   def name(self):
152     return 'Android'
153
154
155 class AndroidDevice(backends.Device):
156   """Android-specific implementation of the core |Device| interface."""
157
158   _SETTINGS_KEYS = {
159       'native_symbol_paths': 'Comma-separated list of native libs search path'}
160
161   def __init__(self, backend, underlying_device):
162     super(AndroidDevice, self).__init__(
163         backend=backend,
164         settings=backends.Settings(AndroidDevice._SETTINGS_KEYS))
165     self.underlying_device = underlying_device
166     self._id = underlying_device.old_interface.GetDevice()
167     self._name = underlying_device.old_interface.GetProductModel()
168     self._sys_stats = None
169     self._last_device_stats = None
170     self._sys_stats_last_update = None
171     self._processes = {}  # pid (int) -> |Process|
172     self._initialized = False
173
174   def Initialize(self):
175     """Starts adb root and deploys the prebuilt binaries on initialization."""
176     self.underlying_device.old_interface.EnableAdbRoot()
177
178     # Download (from GCS) and deploy prebuilt helper binaries on the device.
179     self._DeployPrebuiltOnDeviceIfNeeded(_MEMDUMP_PREBUILT_PATH,
180                                          _MEMDUMP_PATH_ON_DEVICE)
181     self._DeployPrebuiltOnDeviceIfNeeded(_PSEXT_PREBUILT_PATH,
182                                          _PSEXT_PATH_ON_DEVICE)
183     self._initialized = True
184
185   def IsNativeTracingEnabled(self):
186     """Checks for the libc.debug.malloc system property."""
187     return bool(self.underlying_device.old_interface.system_properties[
188         _DLMALLOC_DEBUG_SYSPROP])
189
190   def EnableNativeTracing(self, enabled):
191     """Enables libc.debug.malloc and restarts the shell."""
192     assert(self._initialized)
193     prop_value = '1' if enabled else ''
194     self.underlying_device.old_interface.system_properties[
195         _DLMALLOC_DEBUG_SYSPROP] = prop_value
196     assert(self.IsNativeTracingEnabled())
197     # The libc.debug property takes effect only after restarting the Zygote.
198     self.underlying_device.old_interface.RestartShell()
199
200   def ListProcesses(self):
201     """Returns a sequence of |AndroidProcess|."""
202     self._RefreshProcessesList()
203     return self._processes.itervalues()
204
205   def GetProcess(self, pid):
206     """Returns an instance of |AndroidProcess| (None if not found)."""
207     assert(isinstance(pid, int))
208     self._RefreshProcessesList()
209     return self._processes.get(pid)
210
211   def GetStats(self):
212     """Returns an instance of |DeviceStats| with the OS CPU/Memory stats."""
213     cur = self.UpdateAndGetSystemStats()
214     old = self._last_device_stats or cur  # Handle 1st call case.
215     uptime = cur['time']['ticks'] / cur['time']['rate']
216     ticks = max(1, cur['time']['ticks'] - old['time']['ticks'])
217
218     cpu_times = []
219     for i in xrange(len(cur['cpu'])):
220       cpu_time = {
221           'usr': 100 * (cur['cpu'][i]['usr'] - old['cpu'][i]['usr']) / ticks,
222           'sys': 100 * (cur['cpu'][i]['sys'] - old['cpu'][i]['sys']) / ticks,
223           'idle': 100 * (cur['cpu'][i]['idle'] - old['cpu'][i]['idle']) / ticks}
224       # The idle tick count on many Linux kernels is frozen when the CPU is
225       # offline, and bumps up (compensating all the offline period) when it
226       # reactivates. For this reason it needs to be saturated at [0, 100].
227       cpu_time['idle'] = max(0, min(cpu_time['idle'],
228                                     100 - cpu_time['usr'] - cpu_time['sys']))
229
230       cpu_times.append(cpu_time)
231
232     memory_stats = {'Free': cur['mem']['MemFree:'],
233                     'Cache': cur['mem']['Buffers:'] + cur['mem']['Cached:'],
234                     'Swap': cur['mem']['SwapCached:'],
235                     'Anonymous': cur['mem']['AnonPages:'],
236                     'Kernel': cur['mem']['VmallocUsed:']}
237     self._last_device_stats = cur
238
239     return backends.DeviceStats(uptime=uptime,
240                                 cpu_times=cpu_times,
241                                 memory_stats=memory_stats)
242
243   def UpdateAndGetSystemStats(self):
244     """Grabs and caches system stats through ps_ext (max cache TTL = 0.5s).
245
246     Rationale of caching: avoid invoking adb too often, it is slow.
247     """
248     assert(self._initialized)
249     max_ttl = datetime.timedelta(seconds=0.5)
250     if (self._sys_stats_last_update and
251         datetime.datetime.now() - self._sys_stats_last_update <= max_ttl):
252       return self._sys_stats
253
254     dump_out = '\n'.join(
255         self.underlying_device.old_interface.RunShellCommand(
256             _PSEXT_PATH_ON_DEVICE))
257     stats = json.loads(dump_out)
258     assert(all([x in stats for x in ['cpu', 'processes', 'time', 'mem']])), (
259         'ps_ext returned a malformed JSON dictionary.')
260     self._sys_stats = stats
261     self._sys_stats_last_update = datetime.datetime.now()
262     return self._sys_stats
263
264   def _RefreshProcessesList(self):
265     sys_stats = self.UpdateAndGetSystemStats()
266     processes_to_delete = set(self._processes.keys())
267     for pid, proc in sys_stats['processes'].iteritems():
268       pid = int(pid)
269       process = self._processes.get(pid)
270       if not process or process.name != proc['name']:
271         process = AndroidProcess(self, int(pid), proc['name'])
272         self._processes[pid] = process
273       processes_to_delete.discard(pid)
274     for pid in processes_to_delete:
275       del self._processes[pid]
276
277   def _DeployPrebuiltOnDeviceIfNeeded(self, local_path, path_on_device):
278     # TODO(primiano): check that the md5 binary is built-in also on pre-KK.
279     # Alternatively add tools/android/md5sum to prebuilts and use that one.
280     prebuilts_fetcher.GetIfChanged(local_path)
281     with open(local_path, 'rb') as f:
282       local_hash = hashlib.md5(f.read()).hexdigest()
283     device_md5_out = self.underlying_device.old_interface.RunShellCommand(
284         'md5 "%s"' % path_on_device)
285     if local_hash in device_md5_out:
286       return
287     self.underlying_device.old_interface.Adb().Push(local_path, path_on_device)
288     self.underlying_device.old_interface.RunShellCommand(
289         'chmod 755 "%s"' % path_on_device)
290
291   @property
292   def name(self):
293     """Device name, as defined in the |backends.Device| interface."""
294     return self._name
295
296   @property
297   def id(self):
298     """Device id, as defined in the |backends.Device| interface."""
299     return self._id
300
301
302 class AndroidProcess(backends.Process):
303   """Android-specific implementation of the core |Process| interface."""
304
305   def __init__(self, device, pid, name):
306     super(AndroidProcess, self).__init__(device, pid, name)
307     self._last_sys_stats = None
308
309   def DumpMemoryMaps(self):
310     """Grabs and parses memory maps through memdump."""
311     cmd = '%s %d' % (_MEMDUMP_PATH_ON_DEVICE, self.pid)
312     dump_out = self.device.underlying_device.old_interface.RunShellCommand(cmd)
313     return memdump_parser.Parse(dump_out)
314
315   def DumpNativeHeap(self):
316     """Grabs and parses malloc traces through am dumpheap -n."""
317     # TODO(primiano): grab also mmap bt (depends on pending framework change).
318     dump_file_path = _DUMPHEAP_OUT_FILE_PATH % self.pid
319     cmd = 'am dumpheap -n %d %s' % (self.pid, dump_file_path)
320     self.device.underlying_device.old_interface.RunShellCommand(cmd)
321     # TODO(primiano): Some pre-KK versions of Android might need a sleep here
322     # as, IIRC, 'am dumpheap' did not wait for the dump to be completed before
323     # returning. Double check this and either add a sleep or remove this TODO.
324     dump_out = self.device.underlying_device.old_interface.GetFileContents(
325         dump_file_path)
326     self.device.underlying_device.old_interface.RunShellCommand(
327         'rm %s' % dump_file_path)
328     return dumpheap_native_parser.Parse(dump_out)
329
330   def GetStats(self):
331     """Calculate process CPU/VM stats (CPU stats are relative to last call)."""
332     # Process must retain its own copy of _last_sys_stats because CPU times
333     # are calculated relatively to the last GetStats() call (for the process).
334     cur_sys_stats = self.device.UpdateAndGetSystemStats()
335     old_sys_stats = self._last_sys_stats or cur_sys_stats
336     cur_proc_stats = cur_sys_stats['processes'].get(str(self.pid))
337     old_proc_stats = old_sys_stats['processes'].get(str(self.pid))
338
339     # The process might have gone in the meanwhile.
340     if (not cur_proc_stats or not old_proc_stats):
341       return None
342
343     run_time = (((cur_sys_stats['time']['ticks'] -
344                 cur_proc_stats['start_time']) / cur_sys_stats['time']['rate']))
345     ticks = max(1, cur_sys_stats['time']['ticks'] -
346                 old_sys_stats['time']['ticks'])
347     cpu_usage = (100 *
348                  ((cur_proc_stats['user_time'] + cur_proc_stats['sys_time']) -
349                  (old_proc_stats['user_time'] + old_proc_stats['sys_time'])) /
350                  ticks) / len(cur_sys_stats['cpu'])
351     proc_stats = backends.ProcessStats(
352         threads=cur_proc_stats['n_threads'],
353         run_time=run_time,
354         cpu_usage=cpu_usage,
355         vm_rss=cur_proc_stats['vm_rss'],
356         page_faults=(
357             (cur_proc_stats['maj_faults'] + cur_proc_stats['min_faults']) -
358             (old_proc_stats['maj_faults'] + old_proc_stats['min_faults'])))
359     self._last_sys_stats = cur_sys_stats
360     return proc_stats