Upstream version 5.34.104.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 hashlib
12 import json
13 import os
14
15 from memory_inspector import constants
16 from memory_inspector.backends import prebuilts_fetcher
17 from memory_inspector.backends.android import dumpheap_native_parser
18 from memory_inspector.backends.android import memdump_parser
19 from memory_inspector.core import backends
20
21 # The embedder of this module (unittest runner, web server, ...) is expected
22 # to add the <CHROME_SRC>/build/android to the PYTHONPATH for pylib.
23 from pylib import android_commands  # pylint: disable=F0401
24
25
26 _MEMDUMP_PREBUILT_PATH = os.path.join(constants.PROJECT_SRC,
27                                       'prebuilts', 'memdump-android-arm')
28 _MEMDUMP_PATH_ON_DEVICE = '/data/local/tmp/memdump'
29 _PSEXT_PREBUILT_PATH = os.path.join(constants.PROJECT_SRC,
30                                     'prebuilts', 'ps_ext-android-arm')
31 _PSEXT_PATH_ON_DEVICE = '/data/local/tmp/ps_ext'
32 _DLMALLOC_DEBUG_SYSPROP = 'libc.debug.malloc'
33 _DUMPHEAP_OUT_FILE_PATH = '/data/local/tmp/heap-%d-native.dump'
34
35
36 class AndroidBackend(backends.Backend):
37   """Android-specific implementation of the core |Backend| interface."""
38
39   _SETTINGS_KEYS = {
40       'adb_path': 'Path of directory containing the adb binary',
41       'toolchain_path': 'Path of toolchain (for addr2line)'}
42
43   def __init__(self, settings=None):
44     super(AndroidBackend, self).__init__(
45         settings=backends.Settings(AndroidBackend._SETTINGS_KEYS))
46
47   def EnumerateDevices(self):
48     # If a custom adb_path has been setup through settings, prepend that to the
49     # PATH. The android_commands module will use that to locate adb.
50     if (self.settings['adb_path'] and
51         not os.environ['PATH'].startswith(self.settings['adb_path'])):
52       os.environ['PATH'] = os.pathsep.join([self.settings['adb_path'],
53                                            os.environ['PATH']])
54     for device in android_commands.GetAttachedDevices():
55       yield AndroidDevice(
56           self, android_commands.AndroidCommands(device, api_strict_mode=True))
57
58   @property
59   def name(self):
60     return 'Android'
61
62
63 class AndroidDevice(backends.Device):
64   """Android-specific implementation of the core |Device| interface."""
65
66   _SETTINGS_KEYS = {
67       'native_symbol_paths': 'Comma-separated list of native libs search path'}
68
69   def __init__(self, backend, adb):
70     super(AndroidDevice, self).__init__(
71         backend=backend,
72         settings=backends.Settings(AndroidDevice._SETTINGS_KEYS))
73     self.adb = adb
74     self._id = adb.GetDevice()
75     self._name = adb.GetProductModel()
76     self._sys_stats = None
77     self._sys_stats_last_update = None
78
79   def Initialize(self):
80     """Starts adb root and deploys the prebuilt binaries on initialization."""
81     self.adb.EnableAdbRoot()
82
83     # Download (from GCS) and deploy prebuilt helper binaries on the device.
84     self._DeployPrebuiltOnDeviceIfNeeded(_MEMDUMP_PREBUILT_PATH,
85                                          _MEMDUMP_PATH_ON_DEVICE)
86     self._DeployPrebuiltOnDeviceIfNeeded(_PSEXT_PREBUILT_PATH,
87                                          _PSEXT_PATH_ON_DEVICE)
88
89   def EnableMmapTracing(self, enabled):
90     """Nothing to do here. memdump is already deployed in Initialize()."""
91     pass
92
93   def IsMmapTracingEnabled(self):
94     return True
95
96   def IsNativeAllocTracingEnabled(self):
97     """Checks for the libc.debug.malloc system property."""
98     return self.adb.system_properties[_DLMALLOC_DEBUG_SYSPROP]
99
100   def EnableNativeAllocTracing(self, enabled):
101     """Enables libc.debug.malloc and restarts the shell."""
102     prop_value = '1' if enabled else ''
103     self.adb.system_properties[_DLMALLOC_DEBUG_SYSPROP] = prop_value
104     assert(self.IsNativeAllocTracingEnabled())
105     # The libc.debug property takes effect only after restarting the Zygote.
106     self.adb.RestartShell()
107
108   def ListProcesses(self):
109     """Returns a sequence of |AndroidProcess|."""
110     sys_stats = self.UpdateAndGetSystemStats()
111     for pid, proc in sys_stats['processes'].iteritems():
112       yield AndroidProcess(self, int(pid), proc['name'])
113
114   def GetProcess(self, pid):
115     """Returns an instance of |AndroidProcess| (None if not found)."""
116     assert(isinstance(pid, int))
117     sys_stats = self.UpdateAndGetSystemStats()
118     proc = sys_stats['processes'].get(str(pid))
119     if not proc:
120       return None
121     return AndroidProcess(self, pid, proc['name'])
122
123   def GetStats(self):
124     """Returns an instance of |DeviceStats| with the OS CPU/Memory stats."""
125     old = self._sys_stats
126     cur = self.UpdateAndGetSystemStats()
127     old = old or cur  # Handle 1st call case.
128     uptime = cur['time']['ticks'] / cur['time']['rate']
129     ticks = max(1, cur['time']['ticks'] - old['time']['ticks'])
130
131     cpu_times = []
132     for i in xrange(len(cur['cpu'])):
133       cpu_time = {
134           'usr': 100 * (cur['cpu'][i]['usr'] - old['cpu'][i]['usr']) / ticks,
135           'sys': 100 * (cur['cpu'][i]['sys'] - old['cpu'][i]['sys']) / ticks,
136           'idle': 100 * (cur['cpu'][i]['idle'] - old['cpu'][i]['idle']) / ticks}
137       # The idle tick count on many Linux kernels stays frozen when the CPU is
138       # offline, and bumps up (compensating all the offline period) when it
139       # reactivates. For this reason it needs to be saturated at 100.
140       cpu_time['idle'] = min(cpu_time['idle'],
141                              100 - cpu_time['usr'] - cpu_time['sys'])
142       cpu_times.append(cpu_time)
143     return backends.DeviceStats(uptime=uptime,
144                                cpu_times=cpu_times,
145                                memory_stats=cur['mem'])
146
147   def UpdateAndGetSystemStats(self):
148     """Grabs and caches system stats through ps_ext (max cache TTL = 1s).
149
150     Rationale of caching: avoid invoking adb too often, it is slow.
151     """
152     max_ttl = datetime.timedelta(seconds=1)
153     if (self._sys_stats_last_update and
154         datetime.datetime.now() - self._sys_stats_last_update <= max_ttl):
155       return self._sys_stats
156
157     dump_out = '\n'.join(self.adb.RunShellCommand(_PSEXT_PATH_ON_DEVICE))
158     stats = json.loads(dump_out)
159     assert(all([x in stats for x in ['cpu', 'processes', 'time', 'mem']])), (
160         'ps_ext returned a malformed JSON dictionary.')
161     self._sys_stats = stats
162     self._sys_stats_last_update = datetime.datetime.now()
163     return self._sys_stats
164
165   def _DeployPrebuiltOnDeviceIfNeeded(self, local_path, path_on_device):
166     # TODO(primiano): check that the md5 binary is built-in also on pre-KK.
167     # Alternatively add tools/android/md5sum to prebuilts and use that one.
168     prebuilts_fetcher.GetIfChanged(local_path)
169     with open(local_path, 'rb') as f:
170       local_hash = hashlib.md5(f.read()).hexdigest()
171     device_md5_out = self.adb.RunShellCommand('md5 "%s"' % path_on_device)
172     if local_hash in device_md5_out:
173       return
174     self.adb.Adb().Push(local_path, path_on_device)
175     self.adb.RunShellCommand('chmod 755 "%s"' % path_on_device)
176
177   @property
178   def name(self):
179     """Device name, as defined in the |backends.Device| interface."""
180     return self._name
181
182   @property
183   def id(self):
184     """Device id, as defined in the |backends.Device| interface."""
185     return self._id
186
187
188 class AndroidProcess(backends.Process):
189   """Android-specific implementation of the core |Process| interface."""
190
191   def __init__(self, device, pid, name):
192     super(AndroidProcess, self).__init__(device, pid, name)
193     self._last_sys_stats = None
194
195   def DumpMemoryMaps(self):
196     """Grabs and parses memory maps through memdump."""
197     cmd = '%s %d' % (_MEMDUMP_PATH_ON_DEVICE, self.pid)
198     dump_out = self.device.adb.RunShellCommand(cmd)
199     return memdump_parser.Parse(dump_out)
200
201   def DumpNativeHeap(self):
202     """Grabs and parses malloc traces through am dumpheap -n."""
203     # TODO(primiano): grab also mmap bt (depends on pending framework change).
204     dump_file_path = _DUMPHEAP_OUT_FILE_PATH % self.pid
205     cmd = 'am dumpheap -n %d %s' % (self.pid, dump_file_path)
206     self.device.adb.RunShellCommand(cmd)
207     # TODO(primiano): Some pre-KK versions of Android might need a sleep here
208     # as, IIRC, 'am dumpheap' did not wait for the dump to be completed before
209     # returning. Double check this and either add a sleep or remove this TODO.
210     dump_out = self.device.adb.GetFileContents(dump_file_path)
211     self.device.adb.RunShellCommand('rm %s' % dump_file_path)
212     return dumpheap_native_parser.Parse(dump_out)
213
214   def GetStats(self):
215     """Calculate process CPU/VM stats (CPU stats are relative to last call)."""
216     # Process must retain its own copy of _last_sys_stats because CPU times
217     # are calculated relatively to the last GetStats() call (for the process).
218     cur_sys_stats = self.device.UpdateAndGetSystemStats()
219     old_sys_stats = self._last_sys_stats or cur_sys_stats
220     cur_proc_stats = cur_sys_stats['processes'].get(str(self.pid))
221     old_proc_stats = old_sys_stats['processes'].get(str(self.pid))
222
223     # The process might have gone in the meanwhile.
224     if (not cur_proc_stats or not old_proc_stats):
225       return None
226
227     run_time = (((cur_sys_stats['time']['ticks'] -
228                 cur_proc_stats['start_time']) / cur_sys_stats['time']['rate']))
229     ticks = max(1, cur_sys_stats['time']['ticks'] -
230                 old_sys_stats['time']['ticks'])
231     cpu_usage = (100 *
232                  ((cur_proc_stats['user_time'] + cur_proc_stats['sys_time']) -
233                  (old_proc_stats['user_time'] + old_proc_stats['sys_time'])) /
234                  ticks) / len(cur_sys_stats['cpu'])
235     proc_stats = backends.ProcessStats(
236         threads=cur_proc_stats['n_threads'],
237         run_time=run_time,
238         cpu_usage=cpu_usage,
239         vm_rss=cur_proc_stats['vm_rss'],
240         page_faults=cur_proc_stats['maj_faults'] + cur_proc_stats['min_faults'])
241     self._last_sys_stats = cur_sys_stats
242     return proc_stats