Index: tools/memory_inspector/memory_inspector/backends/android/android_backend.py |
diff --git a/tools/memory_inspector/memory_inspector/backends/android/android_backend.py b/tools/memory_inspector/memory_inspector/backends/android/android_backend.py |
deleted file mode 100644 |
index c7cfe816d3f7e4f747120f735bcdd5fbd4902d47..0000000000000000000000000000000000000000 |
--- a/tools/memory_inspector/memory_inspector/backends/android/android_backend.py |
+++ /dev/null |
@@ -1,456 +0,0 @@ |
-# 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. |
- |
-"""Android-specific implementation of the core backend interfaces. |
- |
-See core/backends.py for more docs. |
-""" |
- |
-import datetime |
-import glob |
-import hashlib |
-import json |
-import os |
-import posixpath |
- |
-from memory_inspector import constants |
-from memory_inspector.backends import prebuilts_fetcher |
-from memory_inspector.backends.android import native_heap_dump_parser |
-from memory_inspector.backends.android import memdump_parser |
-from memory_inspector.core import backends |
-from memory_inspector.core import exceptions |
-from memory_inspector.core import native_heap |
-from memory_inspector.core import symbol |
- |
-# The memory_inspector/__init__ module will add the <CHROME_SRC>/build/android |
-# deps to the PYTHONPATH for pylib. |
-from pylib import android_commands |
-from pylib.device import device_errors |
-from pylib.device import device_utils |
-from pylib.symbols import elf_symbolizer |
- |
- |
-_SUPPORTED_32BIT_ABIS = {'armeabi': 'arm', 'armeabi-v7a': 'arm'} |
-_SUPPORTED_64BIT_ABIS = {'arm64-v8a': 'arm64'} |
-_MEMDUMP_PREBUILT_PATH = os.path.join(constants.PREBUILTS_PATH, |
- 'memdump-android-%(arch)s') |
-_MEMDUMP_PATH_ON_DEVICE = '/data/local/tmp/memdump' |
-_PSEXT_PREBUILT_PATH = os.path.join(constants.PREBUILTS_PATH, |
- 'ps_ext-android-%(arch)s') |
-_PSEXT_PATH_ON_DEVICE = '/data/local/tmp/ps_ext' |
-_HEAP_DUMP_PREBUILT_PATH = os.path.join(constants.PREBUILTS_PATH, |
- 'heap_dump-android-%(arch)s') |
-_HEAP_DUMP_PATH_ON_DEVICE = '/data/local/tmp/heap_dump' |
-_LIBHEAPPROF_PREBUILT_PATH = os.path.join(constants.PREBUILTS_PATH, |
- 'libheap_profiler-android-%(arch)s') |
-_LIBHEAPPROF_FILE_NAME = 'libheap_profiler.so' |
- |
- |
-class AndroidBackend(backends.Backend): |
- """Android-specific implementation of the core |Backend| interface.""" |
- |
- _SETTINGS_KEYS = { |
- 'adb_path': 'Path of directory containing the adb binary', |
- 'toolchain_path': 'Path of toolchain (for addr2line)'} |
- |
- def __init__(self): |
- super(AndroidBackend, self).__init__( |
- settings=backends.Settings(AndroidBackend._SETTINGS_KEYS)) |
- self._devices = {} # 'device id' -> |Device|. |
- |
- def EnumerateDevices(self): |
- # If a custom adb_path has been setup through settings, prepend that to the |
- # PATH. The android_commands module will use that to locate adb. |
- if (self.settings['adb_path'] and |
- not os.environ['PATH'].startswith(self.settings['adb_path'])): |
- os.environ['PATH'] = os.pathsep.join([self.settings['adb_path'], |
- os.environ['PATH']]) |
- for device_id in android_commands.GetAttachedDevices(): |
- device = self._devices.get(device_id) |
- if not device: |
- device = AndroidDevice( |
- self, device_utils.DeviceUtils(device_id)) |
- self._devices[device_id] = device |
- yield device |
- |
- def ExtractSymbols(self, native_heaps, sym_paths): |
- """Performs symbolization. Returns a |symbol.Symbols| from |NativeHeap|s. |
- |
- This method performs the symbolization but does NOT decorate (i.e. add |
- symbol/source info) to the stack frames of |native_heaps|. The heaps |
- can be decorated as needed using the native_heap.SymbolizeUsingSymbolDB() |
- method. Rationale: the most common use case in this application is: |
- symbolize-and-store-symbols and load-symbols-and-decorate-heaps (in two |
- different stages at two different times). |
- |
- Args: |
- native_heaps: a collection of native_heap.NativeHeap instances. |
- sym_paths: either a list of or a string of semicolon-sep. symbol paths. |
- """ |
- assert(all(isinstance(x, native_heap.NativeHeap) for x in native_heaps)) |
- symbols = symbol.Symbols() |
- |
- # Find addr2line in toolchain_path. |
- if isinstance(sym_paths, basestring): |
- sym_paths = sym_paths.split(';') |
- matches = glob.glob(os.path.join(self.settings['toolchain_path'], |
- '*addr2line')) |
- if not matches: |
- raise exceptions.MemoryInspectorException('Cannot find addr2line') |
- addr2line_path = matches[0] |
- |
- # First group all the stack frames together by lib path. |
- frames_by_lib = {} |
- for nheap in native_heaps: |
- for stack_frame in nheap.stack_frames.itervalues(): |
- frames = frames_by_lib.setdefault(stack_frame.exec_file_rel_path, set()) |
- frames.add(stack_frame) |
- |
- # The symbolization process is asynchronous (but yet single-threaded). This |
- # callback is invoked every time the symbol info for a stack frame is ready. |
- def SymbolizeAsyncCallback(sym_info, stack_frame): |
- if not sym_info.name: |
- return |
- sym = symbol.Symbol(name=sym_info.name, |
- source_file_path=sym_info.source_path, |
- line_number=sym_info.source_line) |
- symbols.Add(stack_frame.exec_file_rel_path, stack_frame.offset, sym) |
- # TODO(primiano): support inline sym info (i.e. |sym_info.inlined_by|). |
- |
- # Perform the actual symbolization (ordered by lib). |
- for exec_file_rel_path, frames in frames_by_lib.iteritems(): |
- # Look up the full path of the symbol in the sym paths. |
- exec_file_name = posixpath.basename(exec_file_rel_path) |
- if exec_file_rel_path.startswith('/'): |
- exec_file_rel_path = exec_file_rel_path[1:] |
- if not exec_file_rel_path: |
- continue |
- exec_file_abs_path = '' |
- for sym_path in sym_paths: |
- # First try to locate the symbol file following the full relative path |
- # e.g. /host/syms/ + /system/lib/foo.so => /host/syms/system/lib/foo.so. |
- exec_file_abs_path = os.path.join(sym_path, exec_file_rel_path) |
- if os.path.exists(exec_file_abs_path): |
- break |
- |
- # If no luck, try looking just for the file name in the sym path, |
- # e.g. /host/syms/ + (/system/lib/)foo.so => /host/syms/foo.so. |
- exec_file_abs_path = os.path.join(sym_path, exec_file_name) |
- if os.path.exists(exec_file_abs_path): |
- break |
- |
- # In the case of a Chrome component=shared_library build, the libs are |
- # renamed to .cr.so. Look for foo.so => foo.cr.so. |
- exec_file_abs_path = os.path.join( |
- sym_path, exec_file_name.replace('.so', '.cr.so')) |
- if os.path.exists(exec_file_abs_path): |
- break |
- |
- if not os.path.isfile(exec_file_abs_path): |
- continue |
- |
- symbolizer = elf_symbolizer.ELFSymbolizer( |
- elf_file_path=exec_file_abs_path, |
- addr2line_path=addr2line_path, |
- callback=SymbolizeAsyncCallback, |
- inlines=False) |
- |
- # Kick off the symbolizer and then wait that all callbacks are issued. |
- for stack_frame in sorted(frames, key=lambda x: x.offset): |
- symbolizer.SymbolizeAsync(stack_frame.offset, stack_frame) |
- symbolizer.Join() |
- |
- return symbols |
- |
- @property |
- def name(self): |
- return 'Android' |
- |
- |
-class AndroidDevice(backends.Device): |
- """Android-specific implementation of the core |Device| interface.""" |
- |
- _SETTINGS_KEYS = { |
- 'native_symbol_paths': 'Semicolon-sep. list of native libs search path'} |
- |
- def __init__(self, backend, adb): |
- super(AndroidDevice, self).__init__( |
- backend=backend, |
- settings=backends.Settings(AndroidDevice._SETTINGS_KEYS)) |
- self.adb = adb |
- self._name = '%s %s' % (adb.GetProp('ro.product.model'), |
- adb.GetProp('ro.build.id')) |
- self._id = str(adb) |
- self._sys_stats = None |
- self._last_device_stats = None |
- self._sys_stats_last_update = None |
- self._processes = {} # pid (int) -> |Process| |
- self._initialized = False |
- |
- # Determine the available ABIs, |_arch| will contain the primary ABI. |
- # TODO(primiano): For the moment we support only one ABI per device (i.e. we |
- # assume that all processes are 64 bit on 64 bit device, failing to profile |
- # 32 bit ones). Dealing properly with multi-ABIs requires work on ps_ext and |
- # at the moment is not an interesting use case. |
- self._arch = None |
- self._arch32 = None |
- self._arch64 = None |
- abi = adb.GetProp('ro.product.cpu.abi') |
- if abi in _SUPPORTED_64BIT_ABIS: |
- self._arch = self._arch64 = _SUPPORTED_64BIT_ABIS[abi] |
- elif abi in _SUPPORTED_32BIT_ABIS: |
- self._arch = self._arch32 = _SUPPORTED_32BIT_ABIS[abi] |
- else: |
- raise exceptions.MemoryInspectorException('ABI %s not supported' % abi) |
- |
- def Initialize(self): |
- """Starts adb root and deploys the prebuilt binaries on initialization.""" |
- try: |
- self.adb.EnableRoot() |
- except device_errors.CommandFailedError: |
- # TODO(jbudorick): Handle this exception appropriately after interface |
- # conversions are finished. |
- raise exceptions.MemoryInspectorException( |
- 'The device must be adb root-able in order to use memory_inspector') |
- |
- # Download (from GCS) and deploy prebuilt helper binaries on the device. |
- self._DeployPrebuiltOnDeviceIfNeeded( |
- _MEMDUMP_PREBUILT_PATH % {'arch': self._arch}, _MEMDUMP_PATH_ON_DEVICE) |
- self._DeployPrebuiltOnDeviceIfNeeded( |
- _PSEXT_PREBUILT_PATH % {'arch': self._arch}, _PSEXT_PATH_ON_DEVICE) |
- self._DeployPrebuiltOnDeviceIfNeeded( |
- _HEAP_DUMP_PREBUILT_PATH % {'arch': self._arch}, |
- _HEAP_DUMP_PATH_ON_DEVICE) |
- |
- self._initialized = True |
- |
- def IsNativeTracingEnabled(self): |
- """Checks whether the libheap_profiler is preloaded in the zygote.""" |
- zygote_name = 'zygote64' if self._arch64 else 'zygote' |
- zygote_process = [p for p in self.ListProcesses() if p.name == zygote_name] |
- if not zygote_process: |
- raise exceptions.MemoryInspectorException('Zygote process not found') |
- zygote_pid = zygote_process[0].pid |
- zygote_maps = self.adb.RunShellCommand('cat /proc/%d/maps' % zygote_pid) |
- return any(('libheap_profiler' in line for line in zygote_maps)) |
- |
- def EnableNativeTracing(self, enabled): |
- """Installs libheap_profiler in and injects it in the Zygote.""" |
- |
- def WrapZygote(app_process): |
- WRAPPER_SCRIPT = ('#!/system/bin/sh\n' |
- 'LD_PRELOAD="libheap_profiler.so:$LD_PRELOAD" ' |
- 'exec %s.real "$@"\n' % app_process) |
- self.adb.RunShellCommand('mv %(0)s %(0)s.real' % {'0': app_process}) |
- self.adb.WriteFile(app_process, WRAPPER_SCRIPT) |
- self.adb.RunShellCommand('chown root.shell ' + app_process) |
- self.adb.RunShellCommand('chmod 755 ' + app_process) |
- |
- def UnwrapZygote(): |
- for suffix in ('', '32', '64'): |
- # We don't really care if app_processX.real doesn't exists and mv fails. |
- # If app_processX.real doesn't exists, either app_processX is already |
- # unwrapped or it doesn't exists for the current arch. |
- app_process = '/system/bin/app_process' + suffix |
- self.adb.RunShellCommand('mv %(0)s.real %(0)s' % {'0': app_process}) |
- |
- assert(self._initialized) |
- self.adb.old_interface.MakeSystemFolderWritable() |
- |
- # Start restoring the original state in any case. |
- UnwrapZygote() |
- |
- if enabled: |
- # Temporarily disable SELinux (until next reboot). |
- self.adb.RunShellCommand('setenforce 0') |
- |
- # Wrap the Zygote startup binary (app_process) with a script which |
- # LD_PRELOADs libheap_profiler and invokes the original Zygote process. |
- if self._arch64: |
- app_process = '/system/bin/app_process64' |
- assert(self.adb.FileExists(app_process)) |
- self._DeployPrebuiltOnDeviceIfNeeded( |
- _LIBHEAPPROF_PREBUILT_PATH % {'arch': self._arch64}, |
- '/system/lib64/' + _LIBHEAPPROF_FILE_NAME) |
- WrapZygote(app_process) |
- |
- if self._arch32: |
- # Path is app_process32 for Android >= L, app_process when < L. |
- app_process = '/system/bin/app_process32' |
- if not self.adb.FileExists(app_process): |
- app_process = '/system/bin/app_process' |
- assert(self.adb.FileExists(app_process)) |
- self._DeployPrebuiltOnDeviceIfNeeded( |
- _LIBHEAPPROF_PREBUILT_PATH % {'arch': self._arch32}, |
- '/system/lib/' + _LIBHEAPPROF_FILE_NAME) |
- WrapZygote(app_process) |
- |
- # Respawn the zygote (the device will kind of reboot at this point). |
- self.adb.old_interface.RestartShell() |
- self.adb.old_interface.Adb().WaitForDevicePm(wait_time=30) |
- |
- # Remove the wrapper. This won't have effect until the next reboot, when |
- # the profiler will be automatically disarmed. |
- UnwrapZygote() |
- |
- # We can also unlink the lib files at this point. Once the Zygote has |
- # started it will keep the inodes refcounted anyways through its lifetime. |
- self.adb.RunShellCommand('rm /system/lib*/' + _LIBHEAPPROF_FILE_NAME) |
- |
- def ListProcesses(self): |
- """Returns a sequence of |AndroidProcess|.""" |
- self._RefreshProcessesList() |
- return self._processes.itervalues() |
- |
- def GetProcess(self, pid): |
- """Returns an instance of |AndroidProcess| (None if not found).""" |
- assert(isinstance(pid, int)) |
- self._RefreshProcessesList() |
- return self._processes.get(pid) |
- |
- def GetStats(self): |
- """Returns an instance of |DeviceStats| with the OS CPU/Memory stats.""" |
- cur = self.UpdateAndGetSystemStats() |
- old = self._last_device_stats or cur # Handle 1st call case. |
- uptime = cur['time']['ticks'] / cur['time']['rate'] |
- ticks = max(1, cur['time']['ticks'] - old['time']['ticks']) |
- |
- cpu_times = [] |
- for i in xrange(len(cur['cpu'])): |
- cpu_time = { |
- 'usr': 100 * (cur['cpu'][i]['usr'] - old['cpu'][i]['usr']) / ticks, |
- 'sys': 100 * (cur['cpu'][i]['sys'] - old['cpu'][i]['sys']) / ticks, |
- 'idle': 100 * (cur['cpu'][i]['idle'] - old['cpu'][i]['idle']) / ticks} |
- # The idle tick count on many Linux kernels is frozen when the CPU is |
- # offline, and bumps up (compensating all the offline period) when it |
- # reactivates. For this reason it needs to be saturated at [0, 100]. |
- cpu_time['idle'] = max(0, min(cpu_time['idle'], |
- 100 - cpu_time['usr'] - cpu_time['sys'])) |
- |
- cpu_times.append(cpu_time) |
- |
- memory_stats = {'Free': cur['mem']['MemFree:'], |
- 'Cache': cur['mem']['Buffers:'] + cur['mem']['Cached:'], |
- 'Swap': cur['mem']['SwapCached:'], |
- 'Anonymous': cur['mem']['AnonPages:'], |
- 'Kernel': cur['mem']['VmallocUsed:']} |
- self._last_device_stats = cur |
- |
- return backends.DeviceStats(uptime=uptime, |
- cpu_times=cpu_times, |
- memory_stats=memory_stats) |
- |
- def UpdateAndGetSystemStats(self): |
- """Grabs and caches system stats through ps_ext (max cache TTL = 0.5s). |
- |
- Rationale of caching: avoid invoking adb too often, it is slow. |
- """ |
- assert(self._initialized) |
- max_ttl = datetime.timedelta(seconds=0.5) |
- if (self._sys_stats_last_update and |
- datetime.datetime.now() - self._sys_stats_last_update <= max_ttl): |
- return self._sys_stats |
- |
- dump_out = '\n'.join( |
- self.adb.RunShellCommand(_PSEXT_PATH_ON_DEVICE)) |
- stats = json.loads(dump_out) |
- assert(all([x in stats for x in ['cpu', 'processes', 'time', 'mem']])), ( |
- 'ps_ext returned a malformed JSON dictionary.') |
- self._sys_stats = stats |
- self._sys_stats_last_update = datetime.datetime.now() |
- return self._sys_stats |
- |
- def _RefreshProcessesList(self): |
- sys_stats = self.UpdateAndGetSystemStats() |
- processes_to_delete = set(self._processes.keys()) |
- for pid, proc in sys_stats['processes'].iteritems(): |
- pid = int(pid) |
- process = self._processes.get(pid) |
- if not process or process.name != proc['name']: |
- process = AndroidProcess(self, int(pid), proc['name']) |
- self._processes[pid] = process |
- processes_to_delete.discard(pid) |
- for pid in processes_to_delete: |
- del self._processes[pid] |
- |
- def _DeployPrebuiltOnDeviceIfNeeded(self, local_path, path_on_device): |
- # TODO(primiano): check that the md5 binary is built-in also on pre-KK. |
- # Alternatively add tools/android/md5sum to prebuilts and use that one. |
- prebuilts_fetcher.GetIfChanged(local_path) |
- with open(local_path, 'rb') as f: |
- local_hash = hashlib.md5(f.read()).hexdigest() |
- device_md5_out = self.adb.RunShellCommand( |
- 'md5 "%s"' % path_on_device) |
- if local_hash in device_md5_out: |
- return |
- self.adb.old_interface.Adb().Push(local_path, path_on_device) |
- self.adb.RunShellCommand('chmod 755 "%s"' % path_on_device) |
- |
- @property |
- def name(self): |
- """Device name, as defined in the |backends.Device| interface.""" |
- return self._name |
- |
- @property |
- def id(self): |
- """Device id, as defined in the |backends.Device| interface.""" |
- return self._id |
- |
- |
-class AndroidProcess(backends.Process): |
- """Android-specific implementation of the core |Process| interface.""" |
- |
- def __init__(self, device, pid, name): |
- super(AndroidProcess, self).__init__(device, pid, name) |
- self._last_sys_stats = None |
- |
- def DumpMemoryMaps(self): |
- """Grabs and parses memory maps through memdump.""" |
- cmd = '%s %d' % (_MEMDUMP_PATH_ON_DEVICE, self.pid) |
- dump_out = self.device.adb.RunShellCommand(cmd) |
- return memdump_parser.Parse(dump_out) |
- |
- def DumpNativeHeap(self): |
- """Grabs and parses native heap traces using heap_dump.""" |
- cmd = '%s -n -x %d' % (_HEAP_DUMP_PATH_ON_DEVICE, self.pid) |
- out_lines = self.device.adb.RunShellCommand(cmd) |
- return native_heap_dump_parser.Parse('\n'.join(out_lines)) |
- |
- def Freeze(self): |
- self.device.adb.RunShellCommand('kill -STOP %d' % self.pid) |
- |
- def Unfreeze(self): |
- self.device.adb.RunShellCommand('kill -CONT %d' % self.pid) |
- |
- def GetStats(self): |
- """Calculate process CPU/VM stats (CPU stats are relative to last call).""" |
- # Process must retain its own copy of _last_sys_stats because CPU times |
- # are calculated relatively to the last GetStats() call (for the process). |
- cur_sys_stats = self.device.UpdateAndGetSystemStats() |
- old_sys_stats = self._last_sys_stats or cur_sys_stats |
- cur_proc_stats = cur_sys_stats['processes'].get(str(self.pid)) |
- old_proc_stats = old_sys_stats['processes'].get(str(self.pid)) |
- |
- # The process might have gone in the meanwhile. |
- if (not cur_proc_stats or not old_proc_stats): |
- return None |
- |
- run_time = (((cur_sys_stats['time']['ticks'] - |
- cur_proc_stats['start_time']) / cur_sys_stats['time']['rate'])) |
- ticks = max(1, cur_sys_stats['time']['ticks'] - |
- old_sys_stats['time']['ticks']) |
- cpu_usage = (100 * |
- ((cur_proc_stats['user_time'] + cur_proc_stats['sys_time']) - |
- (old_proc_stats['user_time'] + old_proc_stats['sys_time'])) / |
- ticks) / len(cur_sys_stats['cpu']) |
- proc_stats = backends.ProcessStats( |
- threads=cur_proc_stats['n_threads'], |
- run_time=run_time, |
- cpu_usage=cpu_usage, |
- vm_rss=cur_proc_stats['vm_rss'], |
- page_faults=( |
- (cur_proc_stats['maj_faults'] + cur_proc_stats['min_faults']) - |
- (old_proc_stats['maj_faults'] + old_proc_stats['min_faults']))) |
- self._last_sys_stats = cur_sys_stats |
- return proc_stats |