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 |
index 50d905eb79426f6e4f0d6370827af4e2fbd8c7aa..e35e12d55fbd1fa82ecccaaa98d640b492ed5efe 100644 |
--- a/tools/memory_inspector/memory_inspector/backends/android/android_backend.py |
+++ b/tools/memory_inspector/memory_inspector/backends/android/android_backend.py |
@@ -16,7 +16,7 @@ import posixpath |
from memory_inspector import constants |
from memory_inspector.backends import prebuilts_fetcher |
-from memory_inspector.backends.android import dumpheap_native_parser |
+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 |
@@ -31,14 +31,20 @@ 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-arm') |
+ 'memdump-android-%(arch)s') |
_MEMDUMP_PATH_ON_DEVICE = '/data/local/tmp/memdump' |
_PSEXT_PREBUILT_PATH = os.path.join(constants.PREBUILTS_PATH, |
- 'ps_ext-android-arm') |
+ 'ps_ext-android-%(arch)s') |
_PSEXT_PATH_ON_DEVICE = '/data/local/tmp/ps_ext' |
-_DLMALLOC_DEBUG_SYSPROP = 'libc.debug.malloc' |
-_DUMPHEAP_OUT_FILE_PATH = '/data/local/tmp/heap-%d-native.dump' |
+_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): |
@@ -179,6 +185,22 @@ class AndroidDevice(backends.Device): |
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: |
@@ -190,26 +212,89 @@ class AndroidDevice(backends.Device): |
'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, |
- _MEMDUMP_PATH_ON_DEVICE) |
- self._DeployPrebuiltOnDeviceIfNeeded(_PSEXT_PREBUILT_PATH, |
- _PSEXT_PATH_ON_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 for the libc.debug.malloc system property.""" |
- return bool(self.adb.GetProp(_DLMALLOC_DEBUG_SYSPROP)) |
+ """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): |
- """Enables libc.debug.malloc and restarts the shell.""" |
+ """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) |
- prop_value = '1' if enabled else '' |
- self.adb.SetProp(_DLMALLOC_DEBUG_SYSPROP, prop_value) |
- assert(self.IsNativeTracingEnabled()) |
- # The libc.debug property takes effect only after restarting the Zygote. |
+ 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() |
@@ -324,17 +409,16 @@ class AndroidProcess(backends.Process): |
return memdump_parser.Parse(dump_out) |
def DumpNativeHeap(self): |
- """Grabs and parses malloc traces through am dumpheap -n.""" |
- # TODO(primiano): grab also mmap bt (depends on pending framework change). |
- dump_file_path = _DUMPHEAP_OUT_FILE_PATH % self.pid |
- cmd = 'am dumpheap -n %d %s' % (self.pid, dump_file_path) |
- self.device.adb.RunShellCommand(cmd) |
- # TODO(primiano): Some pre-KK versions of Android might need a sleep here |
- # as, IIRC, 'am dumpheap' did not wait for the dump to be completed before |
- # returning. Double check this and either add a sleep or remove this TODO. |
- dump_out = self.device.adb.ReadFile(dump_file_path) |
- self.device.adb.RunShellCommand('rm %s' % dump_file_path) |
- return dumpheap_native_parser.Parse(dump_out) |
+ """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).""" |